diff --git a/crates/registry-relay/docs/configuration.md b/crates/registry-relay/docs/configuration.md index 7757bc58..9e14eb91 100644 --- a/crates/registry-relay/docs/configuration.md +++ b/crates/registry-relay/docs/configuration.md @@ -270,7 +270,7 @@ Keep standard-facing meaning in the manifest: catalog, datasets, entities, fields, constraints, vocabularies, codelists, profiles, conformance claims, and descriptive ODRL policy metadata. -See [metadata.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/metadata.md) for the manifest schema, static publication, and +See [metadata.md](metadata.md) for the manifest schema, static publication, and the `metadata.manifest.*` / `runtime.binding.*` startup error codes. ODRL policy belongs in the portable metadata manifest, not in runtime dataset @@ -499,7 +499,7 @@ Token verification failures map to specific `auth.*` codes so audit pipelines ca | `auth.invalid_credential` | 401 | JWT decode failure not covered by a more specific variant | | `auth.jwks_unavailable` | 503 | JWKS fetch failed; Registry Relay cannot verify any token | -For a worked example of running Registry Relay against a local OIDC provider (using the project's dev Zitadel stack), see [development.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/development.md). +For a worked example of running Registry Relay against a local OIDC provider (using the project's dev Zitadel stack), see [development.md](development.md). ## Audit @@ -977,7 +977,7 @@ publicschema: | `schema_url` | `https://publicschema.org/schemas/{target}.schema.json` | `credentialSchema.id` in the issued VC | | `credential_type` | `{target}` | `type[1]` value in the issued VC | -See [provenance.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/provenance.md) for CEL context variables, issuance behavior, audit records, and the build and test commands for this feature. +See [provenance.md](provenance.md) for CEL context variables, issuance behavior, audit records, and the build and test commands for this feature. ## Aggregates @@ -1058,7 +1058,7 @@ The `provenance` block is optional. When absent or `enabled: false`, the gateway The key is named `provenance` for compatibility; it governs the response-credential issuer (DID, signing key, claim validity, and accepted media types). These credentials are W3C VCDM 2.0 VC-JWT with a Registry Relay JSON-LD context; they are not W3C PROV-O. -See [provenance.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/provenance.md) for the full signer, DID, schema, context, and rotation contract. +See [provenance.md](provenance.md) for the full signer, DID, schema, context, and rotation contract. ## Production checklist diff --git a/crates/registryctl/CHANGELOG.md b/crates/registryctl/CHANGELOG.md index 67cbba2a..1b7f0d1d 100644 --- a/crates/registryctl/CHANGELOG.md +++ b/crates/registryctl/CHANGELOG.md @@ -22,6 +22,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). `fingerprint.commitment` in YAML. Generated configs reference fingerprint env vars only; local raw keys and matching fingerprint values remain in `secrets/local.env`. +- The generated benefits sample now uses a richer three-sheet workbook + (`Households`, `Persons`, `Applications`) and a broader Bruno collection covering + discovery, row reads, relationship expansion, purpose-header failures, and aggregates. +- The generated Relay sample config now includes focused YAML comments that explain auth + fingerprints, source tables, public entities, relationships, filters, and aggregates. +- `registryctl init relay ` no longer generates a duplicate split `relay/metadata.yaml` + manifest for the local sample; Relay derives standards metadata from `relay/config.yaml` + unless a project explicitly opts into split metadata. + +### Fixed + +- The generated Relay sample no longer binds `person.id` to the API-key principal id, + which made the Bruno "Read sample people" request return an empty result set. ## [0.1.0] - 2026-06-12 diff --git a/crates/registryctl/src/lib.rs b/crates/registryctl/src/lib.rs index fbc72fa9..1e2ef019 100644 --- a/crates/registryctl/src/lib.rs +++ b/crates/registryctl/src/lib.rs @@ -1353,7 +1353,6 @@ fn init_benefits_project(dir: &Path) -> Result<()> { write_text(dir.join("README.md"), project_readme())?; write_text(dir.join(".gitignore"), include_str!("templates/gitignore"))?; write_text(dir.join("relay/config.yaml"), &relay_config(&credentials))?; - write_text(dir.join("relay/metadata.yaml"), relay_metadata())?; write_text(dir.join("secrets/local.env"), &credentials.env_file())?; write_text(dir.join("output/.gitkeep"), "")?; sample::write_benefits_workbook(&dir.join("data/benefits_casework.xlsx"))?; @@ -2554,8 +2553,22 @@ fn generated_file(path: &str, contents: &str) -> GeneratedFile { } fn bruno_relay_files(relay_base_url: &str, _secrets: &LocalEnv) -> Vec { + let application_query_body = r#"{ + "measures": ["application_count"], + "group_by": ["program", "application_status"], + "filters": { + "program": "cash_transfer" + } +}"#; + vec![ - bruno_get("Relay/Health.bru", "Relay health", 1, "{{relay_base_url}}/healthz", &[]), + bruno_get( + "Relay/Health.bru", + "Relay health", + 1, + "{{relay_base_url}}/healthz", + &[], + ), bruno_get("Relay/Ready.bru", "Relay ready", 2, "{{relay_base_url}}/ready", &[]), bruno_get( "Relay/OpenAPI.bru", @@ -2578,16 +2591,163 @@ fn bruno_relay_files(relay_base_url: &str, _secrets: &LocalEnv) -> Vec Result { #[derive(Serialize)] struct RelaySection<'a> { config: &'a str, - metadata: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option<&'a str>, data: Vec<&'a str>, } @@ -3511,7 +3676,7 @@ fn registryctl_manifest(dir: &Path, kind: ProjectManifestKind<'_>) -> Result &'static str { - include_str!("templates/relay_metadata.yaml") -} - #[derive(Debug, Deserialize, Serialize)] struct SmokeReport { base_url: String, @@ -3681,12 +3842,11 @@ fn run_smoke_checks(base_url: &str, secrets: &LocalEnv) -> SmokeReport { 400, &[bearer_header(secrets.value("ROW_READER_RAW"))], ); - record_smoke_check( + record_row_data_smoke_check( &mut checks, base_url, "row reader can read filtered records", "/v1/datasets/benefits_casework/entities/person/records?household_id=hh-1001", - 200, &[ bearer_header(secrets.value("ROW_READER_RAW")), ( @@ -3835,6 +3995,44 @@ fn record_smoke_check( } } +fn record_row_data_smoke_check( + checks: &mut Vec, + base_url: &str, + name: &'static str, + path: &'static str, + headers: &[(String, String)], +) { + let url = format!("{base_url}{path}"); + match http_get(&url, headers) { + Ok(response) => { + let has_rows = response.status == 200 + && serde_json::from_str::(&response.body) + .ok() + .and_then(|value| value["data"].as_array().map(|data| !data.is_empty())) + .unwrap_or(false); + checks.push(SmokeCheck { + name: name.to_string(), + method: "GET".to_string(), + path: path.to_string(), + expected_status: 200, + actual_status: Some(response.status), + passed: has_rows, + error: (!has_rows) + .then(|| "row response did not include any sample records".to_string()), + }); + } + Err(err) => checks.push(SmokeCheck { + name: name.to_string(), + method: "GET".to_string(), + path: path.to_string(), + expected_status: 200, + actual_status: None, + passed: false, + error: Some(redact_error(&err.to_string())), + }), + } +} + fn record_notary_evaluation_check( checks: &mut Vec, base_url: &str, @@ -4523,7 +4721,6 @@ workflows: "README.md", ".gitignore", "relay/config.yaml", - "relay/metadata.yaml", "data/benefits_casework.xlsx", "secrets/local.env", "output/.gitkeep", @@ -4535,6 +4732,38 @@ workflows: ] { assert!(project.join(path).exists(), "{path} should exist"); } + assert!(!project.join("relay/metadata.yaml").exists()); + + let config_text = fs::read_to_string(project.join("relay/config.yaml")).unwrap(); + assert!(config_text.contains("# This file is the Relay contract")); + assert!(config_text.contains("# The raw bearer keys live in secrets/local.env.")); + assert!(config_text.contains("# Tables describe the source workbook.")); + assert!(config_text.contains("# Aggregates expose predeclared grouped statistics.")); + assert!(config_text.contains("# Entities are the public API surface.")); + let config: Value = serde_yaml::from_str(&config_text).unwrap(); + let manifest: Value = + serde_yaml::from_str(&fs::read_to_string(project.join("registryctl.yaml")).unwrap()) + .unwrap(); + let compose = fs::read_to_string(project.join("compose.yaml")).unwrap(); + assert!(config.get("metadata").is_none()); + assert!(manifest["relay"].get("metadata").is_none()); + assert!(!compose.contains("metadata.yaml")); + assert_eq!( + config["datasets"][0]["aggregates"][0]["access"]["aggregate_only_execution"], + true + ); + assert_eq!( + config["datasets"][0]["aggregates"][0]["disclosure_control"]["min_group_size"], + 2 + ); + assert_eq!( + config["datasets"][0]["aggregates"][1]["access"]["aggregate_only_execution"], + true + ); + assert_eq!( + config["datasets"][0]["aggregates"][1]["disclosure_control"]["min_group_size"], + 2 + ); let readme = fs::read_to_string(project.join("README.md")).unwrap(); assert!(readme.contains("registryctl doctor --profile local --format json")); @@ -4556,15 +4785,28 @@ workflows: let request = fs::read_to_string(project.join("bruno/registry-api/Relay/Read sample people.bru")) .unwrap(); + let aggregate_request = fs::read_to_string( + project.join("bruno/registry-api/Relay/Run households by district aggregate.bru"), + ) + .unwrap(); + let application_aggregate_request = fs::read_to_string( + project.join("bruno/registry-api/Relay/Query applications aggregate.bru"), + ) + .unwrap(); let openapi_request = fs::read_to_string(project.join("bruno/registry-api/Relay/OpenAPI.bru")).unwrap(); assert!(local_bru.contains(&env_value(&env, "METADATA_READER_RAW"))); assert!(local_bru.contains(&env_value(&env, "ROW_READER_RAW"))); + assert!(local_bru.contains(&env_value(&env, "AGGREGATE_READER_RAW"))); assert!(example_bru.contains("replace-with-metadata_reader_raw")); + assert!(example_bru.contains("replace-with-aggregate_reader_raw")); assert!(!request.contains(&env_value(&env, "METADATA_READER_RAW"))); assert!(!request.contains(&env_value(&env, "ROW_READER_RAW"))); + assert!(!aggregate_request.contains(&env_value(&env, "AGGREGATE_READER_RAW"))); assert!(request.contains("{{relay_row_key}}")); + assert!(aggregate_request.contains("{{relay_aggregate_key}}")); + assert!(application_aggregate_request.contains("Data-Purpose")); assert!(!openapi_request.contains("Authorization")); assert!(!openapi_request.contains("{{relay_metadata_key}}")); } @@ -4927,7 +5169,9 @@ workflows: "ghcr.io/registrystack/registry-relay", ); assert_eq!(manifest["runtime"]["relay_base_url"], RELAY_BASE_URL); + assert!(manifest["relay"].get("metadata").is_none()); assert!(compose.contains(&format!("image: {RELAY_IMAGE}"))); + assert!(!compose.contains("metadata.yaml")); assert!(!compose.contains("registry-relay:snapshot")); assert!(!compose.contains("registry-relay:latest")); } @@ -5050,6 +5294,8 @@ workflows: "bruno/registry-api/environments/local.example.bru", "bruno/registry-api/Relay/List datasets.bru", "bruno/registry-api/Relay/Read sample people.bru", + "bruno/registry-api/Relay/Read approved applications.bru", + "bruno/registry-api/Relay/List aggregates.bru", "bruno/registry-api/Notary/List claims.bru", "bruno/registry-api/Notary/Evaluate person exists.bru", ] { @@ -5362,7 +5608,6 @@ workflows: "compose.yaml", "README.md", "relay/config.yaml", - "relay/metadata.yaml", ] { let contents = fs::read_to_string(project.join(path)).unwrap(); for secret in &secrets { @@ -5385,7 +5630,9 @@ workflows: let lossy = String::from_utf8_lossy(&workbook); assert!(lossy.contains("Households")); assert!(lossy.contains("Persons")); + assert!(lossy.contains("Applications")); assert!(lossy.contains("hh-1001")); + assert!(lossy.contains("app-3001")); } #[test] @@ -5506,12 +5753,7 @@ workflows: ) ); let rendered = fs::read_to_string(&doctor_config).unwrap(); - assert!(rendered.contains( - &project_dir - .join("relay/metadata.yaml") - .display() - .to_string() - )); + assert!(!rendered.contains("metadata.yaml")); assert!(rendered.contains( &project_dir .join("data/benefits_casework.xlsx") diff --git a/crates/registryctl/src/sample.rs b/crates/registryctl/src/sample.rs index a3dd75ae..5bf4a16e 100644 --- a/crates/registryctl/src/sample.rs +++ b/crates/registryctl/src/sample.rs @@ -10,18 +10,495 @@ pub enum Sample { Benefits, } +#[derive(Clone, Copy)] +enum Cell { + Text(&'static str), + Integer(i64), + Bool(bool), +} + pub fn write_benefits_workbook(path: &Path) -> Result<()> { + let households_sheet = sheet_xml(&[ + &[ + Cell::Text("household_id"), + Cell::Text("district"), + Cell::Text("ward"), + Cell::Text("poverty_band"), + Cell::Text("household_status"), + Cell::Text("registered_on"), + Cell::Text("declared_member_count"), + Cell::Text("address_line"), + ], + &[ + Cell::Text("hh-1001"), + Cell::Text("south"), + Cell::Text("ward_7"), + Cell::Text("band_1"), + Cell::Text("active"), + Cell::Text("2024-01-12"), + Cell::Integer(4), + Cell::Text("595 River Rd, Southvale"), + ], + &[ + Cell::Text("hh-1002"), + Cell::Text("north"), + Cell::Text("ward_2"), + Cell::Text("band_3"), + Cell::Text("active"), + Cell::Text("2023-11-03"), + Cell::Integer(2), + Cell::Text("524 Hill St, Northvale"), + ], + &[ + Cell::Text("hh-1003"), + Cell::Text("east"), + Cell::Text("ward_5"), + Cell::Text("band_2"), + Cell::Text("review_hold"), + Cell::Text("2024-02-18"), + Cell::Integer(3), + Cell::Text("81 Market Ln, Eastport"), + ], + &[ + Cell::Text("hh-1004"), + Cell::Text("west"), + Cell::Text("ward_1"), + Cell::Text("band_1"), + Cell::Text("active"), + Cell::Text("2022-08-30"), + Cell::Integer(1), + Cell::Text("140 Lakeside Ave, Westhaven"), + ], + &[ + Cell::Text("hh-1005"), + Cell::Text("south"), + Cell::Text("ward_8"), + Cell::Text("band_2"), + Cell::Text("closed"), + Cell::Text("2021-04-22"), + Cell::Integer(1), + Cell::Text("22 Orchard Ct, Southvale"), + ], + &[ + Cell::Text("hh-1006"), + Cell::Text("north"), + Cell::Text("ward_3"), + Cell::Text("band_1"), + Cell::Text("active"), + Cell::Text("2024-05-09"), + Cell::Integer(1), + Cell::Text("9 Cedar Loop, Northvale"), + ], + ]); + let persons_sheet = sheet_xml(&[ + &[ + Cell::Text("person_id"), + Cell::Text("household_id"), + Cell::Text("given_name"), + Cell::Text("family_name"), + Cell::Text("date_of_birth"), + Cell::Text("age_band"), + Cell::Text("relationship_to_head"), + Cell::Text("registration_status"), + Cell::Text("eligibility_status"), + Cell::Text("is_primary_applicant"), + Cell::Text("national_id"), + ], + &[ + Cell::Text("per-2001"), + Cell::Text("hh-1001"), + Cell::Text("Fae"), + Cell::Text("Elm"), + Cell::Text("1989-05-14"), + Cell::Text("35-49"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-856648"), + ], + &[ + Cell::Text("per-2002"), + Cell::Text("hh-1001"), + Cell::Text("Jo"), + Cell::Text("Elm"), + Cell::Text("2019-02-03"), + Cell::Text("5-17"), + Cell::Text("child"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(false), + Cell::Text("FAKE-806707"), + ], + &[ + Cell::Text("per-2003"), + Cell::Text("hh-1001"), + Cell::Text("Kai"), + Cell::Text("Elm"), + Cell::Text("1954-09-21"), + Cell::Text("65+"), + Cell::Text("parent"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(false), + Cell::Text("FAKE-219346"), + ], + &[ + Cell::Text("per-2004"), + Cell::Text("hh-1001"), + Cell::Text("Mina"), + Cell::Text("Elm"), + Cell::Text("1991-11-10"), + Cell::Text("35-49"), + Cell::Text("spouse"), + Cell::Text("active"), + Cell::Text("pending_review"), + Cell::Bool(false), + Cell::Text("FAKE-331902"), + ], + &[ + Cell::Text("per-2005"), + Cell::Text("hh-1002"), + Cell::Text("Dee"), + Cell::Text("Iron"), + Cell::Text("1984-01-28"), + Cell::Text("35-49"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-748201"), + ], + &[ + Cell::Text("per-2006"), + Cell::Text("hh-1002"), + Cell::Text("Ari"), + Cell::Text("Iron"), + Cell::Text("2016-07-18"), + Cell::Text("5-17"), + Cell::Text("child"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(false), + Cell::Text("FAKE-671240"), + ], + &[ + Cell::Text("per-2007"), + Cell::Text("hh-1003"), + Cell::Text("Nia"), + Cell::Text("Stone"), + Cell::Text("1998-03-05"), + Cell::Text("18-34"), + Cell::Text("head"), + Cell::Text("pending"), + Cell::Text("pending_review"), + Cell::Bool(true), + Cell::Text("FAKE-503118"), + ], + &[ + Cell::Text("per-2008"), + Cell::Text("hh-1003"), + Cell::Text("Sol"), + Cell::Text("Stone"), + Cell::Text("2022-12-12"), + Cell::Text("0-4"), + Cell::Text("child"), + Cell::Text("pending"), + Cell::Text("pending_review"), + Cell::Bool(false), + Cell::Text("FAKE-663910"), + ], + &[ + Cell::Text("per-2009"), + Cell::Text("hh-1003"), + Cell::Text("Ren"), + Cell::Text("Stone"), + Cell::Text("1970-06-30"), + Cell::Text("50-64"), + Cell::Text("parent"), + Cell::Text("active"), + Cell::Text("ineligible"), + Cell::Bool(false), + Cell::Text("FAKE-447120"), + ], + &[ + Cell::Text("per-2010"), + Cell::Text("hh-1004"), + Cell::Text("Ivo"), + Cell::Text("Reed"), + Cell::Text("1957-04-02"), + Cell::Text("65+"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-990231"), + ], + &[ + Cell::Text("per-2011"), + Cell::Text("hh-1005"), + Cell::Text("Uma"), + Cell::Text("Vale"), + Cell::Text("1993-08-16"), + Cell::Text("18-34"), + Cell::Text("head"), + Cell::Text("closed"), + Cell::Text("ineligible"), + Cell::Bool(true), + Cell::Text("FAKE-125904"), + ], + &[ + Cell::Text("per-2012"), + Cell::Text("hh-1006"), + Cell::Text("Lina"), + Cell::Text("Moss"), + Cell::Text("1982-10-25"), + Cell::Text("35-49"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-775120"), + ], + ]); + let applications_sheet = sheet_xml(&[ + &[ + Cell::Text("application_id"), + Cell::Text("household_id"), + Cell::Text("applicant_person_id"), + Cell::Text("program"), + Cell::Text("application_date"), + Cell::Text("intake_channel"), + Cell::Text("office_code"), + Cell::Text("application_status"), + Cell::Text("decision"), + Cell::Text("benefit_level"), + Cell::Text("review_due_on"), + Cell::Text("identity_verified"), + Cell::Text("residence_verified"), + Cell::Text("consent_reference"), + ], + &[ + Cell::Text("app-3001"), + Cell::Text("hh-1001"), + Cell::Text("per-2001"), + Cell::Text("cash_transfer"), + Cell::Text("2024-01-20"), + Cell::Text("office"), + Cell::Text("SOUTH-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("enhanced"), + Cell::Text("2026-01-20"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9001"), + ], + &[ + Cell::Text("app-3002"), + Cell::Text("hh-1002"), + Cell::Text("per-2005"), + Cell::Text("food_support"), + Cell::Text("2024-02-10"), + Cell::Text("mobile_team"), + Cell::Text("NORTH-02"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2025-08-10"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9002"), + ], + &[ + Cell::Text("app-3003"), + Cell::Text("hh-1003"), + Cell::Text("per-2007"), + Cell::Text("cash_transfer"), + Cell::Text("2024-03-05"), + Cell::Text("partner_referral"), + Cell::Text("EAST-01"), + Cell::Text("under_review"), + Cell::Text("pending_review"), + Cell::Text("none"), + Cell::Text("2024-06-30"), + Cell::Bool(true), + Cell::Bool(false), + Cell::Text("consent-9003"), + ], + &[ + Cell::Text("app-3004"), + Cell::Text("hh-1004"), + Cell::Text("per-2010"), + Cell::Text("disability_support"), + Cell::Text("2023-09-15"), + Cell::Text("office"), + Cell::Text("WEST-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("enhanced"), + Cell::Text("2025-09-15"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9004"), + ], + &[ + Cell::Text("app-3005"), + Cell::Text("hh-1005"), + Cell::Text("per-2011"), + Cell::Text("emergency_grant"), + Cell::Text("2023-04-25"), + Cell::Text("online"), + Cell::Text("SOUTH-01"), + Cell::Text("closed"), + Cell::Text("ineligible"), + Cell::Text("none"), + Cell::Text("2023-07-25"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9005"), + ], + &[ + Cell::Text("app-3006"), + Cell::Text("hh-1006"), + Cell::Text("per-2012"), + Cell::Text("cash_transfer"), + Cell::Text("2024-05-12"), + Cell::Text("office"), + Cell::Text("NORTH-02"), + Cell::Text("submitted"), + Cell::Text("pending_review"), + Cell::Text("none"), + Cell::Text("2024-08-12"), + Cell::Bool(false), + Cell::Bool(true), + Cell::Text("consent-9006"), + ], + &[ + Cell::Text("app-3007"), + Cell::Text("hh-1001"), + Cell::Text("per-2001"), + Cell::Text("school_meals"), + Cell::Text("2024-06-01"), + Cell::Text("office"), + Cell::Text("SOUTH-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2026-06-01"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9007"), + ], + &[ + Cell::Text("app-3008"), + Cell::Text("hh-1002"), + Cell::Text("per-2005"), + Cell::Text("cash_transfer"), + Cell::Text("2024-06-15"), + Cell::Text("mobile_team"), + Cell::Text("NORTH-02"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2026-06-15"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9008"), + ], + &[ + Cell::Text("app-3009"), + Cell::Text("hh-1003"), + Cell::Text("per-2007"), + Cell::Text("food_support"), + Cell::Text("2024-06-18"), + Cell::Text("partner_referral"), + Cell::Text("EAST-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2026-06-18"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9009"), + ], + ]); + let entries = [ ZipEntry::new("[Content_Types].xml", CONTENT_TYPES.as_bytes()), ZipEntry::new("_rels/.rels", ROOT_RELS.as_bytes()), ZipEntry::new("xl/workbook.xml", WORKBOOK.as_bytes()), ZipEntry::new("xl/_rels/workbook.xml.rels", WORKBOOK_RELS.as_bytes()), - ZipEntry::new("xl/worksheets/sheet1.xml", HOUSEHOLDS_SHEET.as_bytes()), - ZipEntry::new("xl/worksheets/sheet2.xml", PERSONS_SHEET.as_bytes()), + ZipEntry::new("xl/worksheets/sheet1.xml", households_sheet.as_bytes()), + ZipEntry::new("xl/worksheets/sheet2.xml", persons_sheet.as_bytes()), + ZipEntry::new("xl/worksheets/sheet3.xml", applications_sheet.as_bytes()), ]; ZipStoreWriter::write(path, &entries) } +fn sheet_xml(rows: &[&[Cell]]) -> String { + let mut xml = String::from( + r#" + + +"#, + ); + for (row_idx, row) in rows.iter().enumerate() { + let row_number = row_idx + 1; + xml.push_str(&format!(" \n")); + for (col_idx, cell) in row.iter().enumerate() { + let reference = cell_reference(col_idx, row_number); + write_cell(&mut xml, &reference, *cell); + } + xml.push_str(" \n"); + } + xml.push_str(" \n"); + xml +} + +fn write_cell(xml: &mut String, reference: &str, cell: Cell) { + match cell { + Cell::Text(value) => { + xml.push_str(&format!( + " {}\n", + escape_xml(value) + )); + } + Cell::Integer(value) => { + xml.push_str(&format!(" {value}\n")); + } + Cell::Bool(value) => { + let value = if value { 1 } else { 0 }; + xml.push_str(&format!( + " {value}\n" + )); + } + } +} + +fn cell_reference(mut col_idx: usize, row_number: usize) -> String { + let mut letters = Vec::new(); + loop { + let remainder = col_idx % 26; + letters.push((b'A' + remainder as u8) as char); + col_idx /= 26; + if col_idx == 0 { + break; + } + col_idx -= 1; + } + letters.iter().rev().collect::() + &row_number.to_string() +} + +fn escape_xml(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + const CONTENT_TYPES: &str = r#" @@ -29,6 +506,7 @@ const CONTENT_TYPES: &str = r#" + "#; const ROOT_RELS: &str = r#" @@ -41,6 +519,7 @@ const WORKBOOK: &str = r#" + "#; @@ -48,66 +527,5 @@ const WORKBOOK_RELS: &str = r#" + "#; - -const HOUSEHOLDS_SHEET: &str = r#" - - - - household_id - district - poverty_band - address_line - - - hh-1001 - south - band_1 - 595 Fake St, southvale - - - hh-1002 - north - band_3 - 524 Fake St, northvale - - -"#; - -const PERSONS_SHEET: &str = r#" - - - - person_id - household_id - age_band - eligibility_status - full_name - national_id - - - per-2001 - hh-1001 - 0-4 - eligible - Fae Elm - FAKE-856648 - - - per-2002 - hh-1001 - 65+ - eligible - Jo Apple - FAKE-806707 - - - per-2003 - hh-1002 - 18-64 - eligible - Dee Iron - FAKE-219346 - - -"#; diff --git a/crates/registryctl/src/templates/compose-with-notary.yaml b/crates/registryctl/src/templates/compose-with-notary.yaml index 834a0a8a..968ffea6 100644 --- a/crates/registryctl/src/templates/compose-with-notary.yaml +++ b/crates/registryctl/src/templates/compose-with-notary.yaml @@ -8,7 +8,6 @@ services: - "4242:8080" volumes: - ./relay/config.yaml:/etc/registry-relay/config.yaml:ro - - ./relay/metadata.yaml:/etc/registry-relay/metadata.yaml:ro - ./data:/var/lib/registry-relay/data:ro registry-notary: diff --git a/crates/registryctl/src/templates/compose.yaml b/crates/registryctl/src/templates/compose.yaml index 63fd3cf4..1f79ac6b 100644 --- a/crates/registryctl/src/templates/compose.yaml +++ b/crates/registryctl/src/templates/compose.yaml @@ -8,5 +8,4 @@ services: - "4242:8080" volumes: - ./relay/config.yaml:/etc/registry-relay/config.yaml:ro - - ./relay/metadata.yaml:/etc/registry-relay/metadata.yaml:ro - ./data:/var/lib/registry-relay/data:ro diff --git a/crates/registryctl/src/templates/notary_project_readme.md b/crates/registryctl/src/templates/notary_project_readme.md index a9734a92..95ff0cff 100644 --- a/crates/registryctl/src/templates/notary_project_readme.md +++ b/crates/registryctl/src/templates/notary_project_readme.md @@ -5,10 +5,23 @@ This project was generated by `registryctl`. ## Start ```sh -registryctl doctor --profile local --format json registryctl start registryctl notary smoke +``` + +Expected local URL: + +```text +Notary API: http://127.0.0.1:4255 +API docs: http://127.0.0.1:4255/docs +``` + +## Inspect + +```sh registryctl notary open +sed -n '1,220p' notary/config.yaml +registryctl doctor --profile local --format json ``` The generated local demo credentials live in `secrets/local.env`. They are for diff --git a/crates/registryctl/src/templates/project_readme.md b/crates/registryctl/src/templates/project_readme.md index 10049304..9020aea3 100644 --- a/crates/registryctl/src/templates/project_readme.md +++ b/crates/registryctl/src/templates/project_readme.md @@ -5,10 +5,40 @@ This project was generated by `registryctl`. ## Start ```sh -registryctl doctor --profile local --format json registryctl start registryctl smoke +``` + +Expected local URL: + +```text +Relay API: http://127.0.0.1:4242 +API docs: http://127.0.0.1:4242/docs +``` + +The sample workbook at `data/benefits_casework.xlsx` has three sheets: +`Households`, `Persons`, and `Applications`. + +## Try one protected read + +```sh +set -a +. secrets/local.env +set +a + +curl -sS -G \ + -H "Authorization: Bearer $ROW_READER_RAW" \ + -H "Data-Purpose: https://example.local/purpose/tutorial" \ + --data-urlencode "household_id=hh-1001" \ + http://127.0.0.1:4242/v1/datasets/benefits_casework/entities/person/records +``` + +## Inspect + +```sh registryctl open +sed -n '1,180p' relay/config.yaml +registryctl doctor --profile local --format json ``` The generated local demo credentials live in `secrets/local.env`. They are for diff --git a/crates/registryctl/src/templates/relay_config.yaml.tmpl b/crates/registryctl/src/templates/relay_config.yaml.tmpl index df8902a4..fc7fefc2 100644 --- a/crates/registryctl/src/templates/relay_config.yaml.tmpl +++ b/crates/registryctl/src/templates/relay_config.yaml.tmpl @@ -1,17 +1,23 @@ # Generated by registryctl. +# This file is the Relay contract for the local sample: service metadata, +# API-key auth, source spreadsheet tables, aggregates, and public entities. +# Edit tables, entities, and aggregates when your workbook or API shape changes. + +# server controls the listener inside the container. The generated compose file +# maps it to http://127.0.0.1:4242 on your workstation. server: bind: 0.0.0.0:8080 openapi_requires_auth: false -metadata: - source: - path: /etc/registry-relay/metadata.yaml - +# catalog values appear in dataset discovery, metadata responses, and OpenAPI. catalog: title: Benefits Casework Gateway (registryctl sample) base_url: http://127.0.0.1:4242 publisher: Registryctl Local Sample +# The raw bearer keys live in secrets/local.env. Public config keeps only the +# fingerprint env var names, so it can be shared without leaking credentials. +# If you rotate a generated key, update the raw key and fingerprint together. auth: mode: api_key api_keys: @@ -38,11 +44,14 @@ auth: - benefits_casework:metadata - benefits_casework:aggregate +# Audit entries are emitted to container stdout as JSON Lines. The hash secret +# lets Relay pseudonymize sensitive audit values without storing the secret here. audit: sink: stdout format: jsonl hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET +# A dataset is the registry product exposed under /v1/datasets/. datasets: - id: benefits_casework title: Benefits Casework @@ -55,6 +64,9 @@ datasets: refresh: mode: manual + # Tables describe the source workbook. The sample uses one Excel file with + # three sheets; primary_key names the source column that uniquely identifies + # each row. tables: - id: households_table source: @@ -65,6 +77,9 @@ datasets: sheet: Households primary_key: household_id schema: + # strict catches workbook drift by rejecting unexpected source + # columns. Mark source fields sensitive when they contain personal + # data. The sample maps only the public-safe subset into entities below. strict: true fields: - name: household_id @@ -73,9 +88,21 @@ datasets: - name: district type: string nullable: false + - name: ward + type: string + nullable: false - name: poverty_band type: string nullable: false + - name: household_status + type: string + nullable: false + - name: registered_on + type: date + nullable: false + - name: declared_member_count + type: integer + nullable: false - name: address_line type: string nullable: true @@ -98,21 +125,153 @@ datasets: - name: household_id type: string nullable: false + - name: given_name + type: string + nullable: false + sensitive: true + - name: family_name + type: string + nullable: false + sensitive: true + - name: date_of_birth + type: date + nullable: false + sensitive: true - name: age_band type: string nullable: false + - name: relationship_to_head + type: string + nullable: false + - name: registration_status + type: string + nullable: false - name: eligibility_status type: string nullable: false - - name: full_name + - name: is_primary_applicant + type: boolean + nullable: false + - name: national_id type: string nullable: true sensitive: true - - name: national_id + + - id: applications_table + source: + type: file + path: /var/lib/registry-relay/data/benefits_casework.xlsx + format: + xlsx: + sheet: Applications + primary_key: application_id + schema: + strict: true + fields: + - name: application_id type: string - nullable: true + nullable: false + - name: household_id + type: string + nullable: false + - name: applicant_person_id + type: string + nullable: false + - name: program + type: string + nullable: false + - name: application_date + type: date + nullable: false + - name: intake_channel + type: string + nullable: false + - name: office_code + type: string + nullable: false + - name: application_status + type: string + nullable: false + - name: decision + type: string + nullable: false + - name: benefit_level + type: string + nullable: false + - name: review_due_on + type: date + nullable: false + - name: identity_verified + type: boolean + nullable: false + - name: residence_verified + type: boolean + nullable: false + - name: consent_reference + type: string + nullable: false sensitive: true + # Aggregates expose predeclared grouped statistics. aggregate_only_execution + # allows aggregate readers to run these definitions without granting row + # access. disclosure_control omits small groups from the response. + aggregates: + - id: by_district + title: Households by district + description: Count of households by district + source_entity: household + access: + aggregate_only_execution: true + default_group_by: + - district + dimensions: + - id: district + label: District + field: district + indicators: + - id: household_count + label: Households + function: count + column: id + unit_measure: households + disclosure_control: + min_group_size: 2 + suppression: omit + + - id: applications_by_program_status + title: Applications by program and status + description: Count of applications by program and application status + source_entity: application + access: + aggregate_only_execution: true + default_group_by: + - program + - application_status + dimensions: + - id: program + label: Program + field: program + - id: application_status + label: Application status + field: application_status + indicators: + - id: application_count + label: Applications + function: count + column: id + unit_measure: applications + allowed_filters: + - field: program + ops: [eq, in] + - field: application_status + ops: [eq, in] + disclosure_control: + min_group_size: 2 + suppression: omit + + # Entities are the public API surface. They choose which table fields are + # returned, define relationships for expand=..., attach access scopes, and + # restrict filters so collection reads stay intentional. entities: - name: household title: Household @@ -123,13 +282,25 @@ datasets: from: household_id - name: district from: district + - name: ward + from: ward - name: poverty_band from: poverty_band + - name: household_status + from: household_status + - name: registered_on + from: registered_on + - name: declared_member_count + from: declared_member_count relationships: - name: members kind: has_many target: person foreign_key: household_id + - name: applications + kind: has_many + target: application + foreign_key: household_id access: metadata_scope: benefits_casework:metadata aggregate_scope: benefits_casework:aggregate @@ -144,19 +315,9 @@ datasets: ops: [eq, in] - field: poverty_band ops: [eq, in] - allowed_expansions: [members] - aggregates: - - id: by_district - description: Count of households by district - group_by: - - district - measures: - - name: household_count - function: count - column: id - disclosure_control: - min_group_size: 1 - suppression: omit + - field: household_status + ops: [eq, in] + allowed_expansions: [members, applications] - name: person title: Person @@ -169,13 +330,23 @@ datasets: from: household_id - name: age_band from: age_band + - name: relationship_to_head + from: relationship_to_head + - name: registration_status + from: registration_status - name: eligibility_status from: eligibility_status + - name: is_primary_applicant + from: is_primary_applicant relationships: - name: household kind: belongs_to target: household foreign_key: household_id + - name: applications + kind: has_many + target: application + foreign_key: applicant_person_id access: metadata_scope: benefits_casework:metadata aggregate_scope: benefits_casework:aggregate @@ -184,13 +355,6 @@ datasets: default_limit: 100 max_limit: 1000 require_purpose_header: true - required_filters: - - id - - household_id - - eligibility_status - required_filter_bindings: - - field: id - source: principal_id allowed_filters: - field: id ops: [eq, in] @@ -198,4 +362,73 @@ datasets: ops: [eq, in] - field: eligibility_status ops: [eq, in] - allowed_expansions: [household] + - field: registration_status + ops: [eq, in] + allowed_expansions: [household, applications] + + - name: application + title: Application + description: A synthetic benefit application + table: applications_table + fields: + - name: id + from: application_id + - name: household_id + from: household_id + - name: applicant_person_id + from: applicant_person_id + - name: program + from: program + - name: application_date + from: application_date + - name: intake_channel + from: intake_channel + - name: office_code + from: office_code + - name: application_status + from: application_status + - name: decision + from: decision + - name: benefit_level + from: benefit_level + - name: review_due_on + from: review_due_on + - name: identity_verified + from: identity_verified + - name: residence_verified + from: residence_verified + relationships: + - name: household + kind: belongs_to + target: household + foreign_key: household_id + - name: applicant + kind: belongs_to + target: person + foreign_key: applicant_person_id + access: + metadata_scope: benefits_casework:metadata + aggregate_scope: benefits_casework:aggregate + read_scope: benefits_casework:rows + api: + default_limit: 100 + max_limit: 1000 + require_purpose_header: true + allowed_filters: + - field: id + ops: [eq, in] + - field: household_id + ops: [eq, in] + - field: applicant_person_id + ops: [eq, in] + - field: program + ops: [eq, in] + - field: application_status + ops: [eq, in] + - field: decision + ops: [eq, in] + - field: benefit_level + ops: [eq, in] + - field: review_due_on + ops: [gte, lte, between] + allowed_expansions: [household, applicant] diff --git a/crates/registryctl/src/templates/relay_metadata.yaml b/crates/registryctl/src/templates/relay_metadata.yaml deleted file mode 100644 index 8504ec48..00000000 --- a/crates/registryctl/src/templates/relay_metadata.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by registryctl. -schema_version: registry-manifest/v1 -catalog: - id: registryctl-benefits-sample - base_url: http://127.0.0.1:4242 - title: Benefits Casework Gateway (registryctl sample) - publisher: - name: Registryctl Local Sample - standards: - dcat: '3.0' - shacl: '1.1' - json_schema: 2020-12 -datasets: - - id: benefits_casework - title: Benefits Casework - description: Synthetic benefits casework registry for local registryctl evaluation. - owner: Registryctl Local Sample - sensitivity: personal - access_rights: restricted - update_frequency: monthly - entities: - - name: household - title: Household - description: A synthetic benefits household - identifiers: - - name: id - kind: primary - fields: - - name: id - type: string - required: true - - name: district - type: string - required: true - - name: poverty_band - type: string - required: true - relationships: - - name: members - target_entity: person - cardinality: many - - name: person - title: Person - description: A synthetic person in a benefits household - identifiers: - - name: id - kind: primary - fields: - - name: id - type: string - required: true - - name: household_id - type: string - required: true - - name: age_band - type: string - required: true - - name: eligibility_status - type: string - required: true - relationships: - - name: household - target_entity: household - cardinality: zero_or_one diff --git a/docs/site/AGENTS.md b/docs/site/AGENTS.md index c56809d5..3d303d3e 100644 --- a/docs/site/AGENTS.md +++ b/docs/site/AGENTS.md @@ -18,7 +18,9 @@ This repo is an Astro and Starlight documentation site. Read `docs/style-guide.md` before drafting or editing any page. It covers voice, structure, frontmatter, page types, the banned-word list, claim levels for standards, and the GitLab rules we adopt, adapt, or skip. The visual design -language is recorded separately in `../design-registry-docs.md`. +language is recorded separately in `design-registry-docs.md`, maintained +alongside the repository, not published in it; the binding visual rules for +diagrams are summarized in the style guide's "Images and diagrams" section. Every factual claim about a source repo must be anchored in code, tests, fixtures, OpenAPI, or an upstream standard. When evidence is missing, mark the diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index fa60de1d..091062b7 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -41,6 +41,16 @@ const base = process.env.DOCS_BASE || undefined; const basePath = base?.replace(/\/$/, ''); const isArchivedBuild = Boolean(basePath); const productSidebar = loadProductSidebar(); + +// Lift a generated per-product group to the top level of the sidebar. +// Fails the build loudly if the generator's labels change, so the nav can +// never silently lose a product section. +/** @param {string} label */ +function generatedProduct(label) { + const group = productSidebar.find((/** @type {{ label: string }} */ entry) => entry.label === label); + if (!group) throw new Error(`generated sidebar group "${label}" not found`); + return group; +} const disabledSitemap = { name: '@astrojs/sitemap', hooks: {}, @@ -64,11 +74,14 @@ export default defineConfig({ // to their new homes so old links and search results keep resolving. redirects: { '/start/': internalRedirect('/'), + '/start/see-it-live/': internalRedirect('/start/quickstart/'), + '/explanation/trust-posture-and-security-guarantees/': internalRedirect('/security/'), + '/reference/security-self-assessment/': internalRedirect('/security/self-assessment/'), + '/reference/openssf-evidence/': internalRedirect('/security/openssf-evidence/'), // quickstart's "Choose by question" router merged into the homepage (2026-06). - '/start/quickstart/': internalRedirect('/'), '/start/your-first-call/': internalRedirect('/tutorials/first-run-with-registry-lab/'), - // verify-claim-own-api merged into the claim-verification tutorial (2026-06). - '/tutorials/verify-claim-own-api/': internalRedirect('/tutorials/verify-claim-registry-api/'), + // verify-claim-own-api moved into the Apply to your stack path (2026-06). + '/tutorials/verify-claim-own-api/': internalRedirect('/tutorials/run-notary-standalone-for-api/'), '/tutorials/verify-opencrvs-dci-claims/': internalRedirect('/tutorials/verify-opencrvs-claims/'), // Problems -> marketing /why '/problems/': `${marketing}/why/`, @@ -109,7 +122,7 @@ export default defineConfig({ '/products/registry-notary/opencrvs-dci-onboarding/': 'https://docs.registrystack.org/products/registry-notary/opencrvs-onboarding/', // registry-manifest, registry-atlas, registry-platform, registry-lab projects/* // redirects removed: targets are deferred from the MVP docs cut. - '/projects/registry-lab/demo-flow/': internalRedirect('/start/see-it-live/'), + '/projects/registry-lab/demo-flow/': internalRedirect('/start/quickstart/'), // The API reference moved from static Redoc HTML to native, theme-aware, // searchable pages. Keep the old shareable links working. '/api/registry-relay.html': internalRedirect('/reference/apis/relay/'), @@ -193,19 +206,19 @@ export default defineConfig({ }, ], // Diataxis IA: Get started, Tutorials, Products, Explanation, Reference. - // The per-product groups inside Products are generated from - // src/data/repo-docs.yaml by scripts/generate-sidebar.mjs (the - // productSidebar array), so the menu follows each product's - // doc_type/nav_order and never drifts from the manifest. Within a - // product, pages are sub-grouped by Diataxis type once the product grows - // past a threshold; smaller products stay flat. Product labels drop the - // shared "Registry" prefix (Relay, Notary, ...) since the site title and - // the Products group already supply that context. + // The per-product groups are generated from src/data/repo-docs.yaml by + // scripts/generate-sidebar.mjs (the productSidebar array), so each + // product menu follows its doc_type/nav_order and never drifts from the + // manifest. Within a product, pages are sub-grouped by Diataxis type + // once the product grows past a threshold; smaller products stay flat. + // generatedProduct() lifts each group into its own top-level product + // section; hand-authored operator tutorials append after the generated + // items. // - // "Get started" is orientation only: Overview (which carries the - // "Choose by question" router), the zero-install demo, and the - // evaluation page. The hands-on pages live under Tutorials, ordered by - // weight: the lightest local run first, the full multi-service lab last. + // "Get started" is the first-run journey in reading order: the hosted + // quickstart and credential tour, then the two local first-run + // tutorials, then the evaluation and lab pages. Named source-system + // paths live under Integrations. sidebar: [ { label: 'Get started', @@ -213,38 +226,75 @@ export default defineConfig({ // Short nav labels to avoid wrapping in the narrow sidebar; page // titles keep the full wording. { label: 'Overview', link: '/' }, - { label: 'See it live', slug: 'start/see-it-live' }, + { label: 'Quickstart', slug: 'start/quickstart' }, + { label: 'Credential tour', slug: 'start/credential-tour' }, + { label: 'Your first Relay', slug: 'tutorials/publish-spreadsheet-secured-registry-api' }, + { label: 'Your first Notary', slug: 'tutorials/verify-claim-registry-api' }, { label: 'When to use', slug: 'start/when-to-use' }, + { label: 'Run the lab', slug: 'tutorials/first-run-with-registry-lab' }, ], }, { - label: 'Tutorials', + label: 'Registry Relay', + collapsed: true, items: [ - { label: 'Publish a spreadsheet', slug: 'tutorials/publish-spreadsheet-secured-registry-api' }, - { label: 'Verify a claim', slug: 'tutorials/verify-claim-registry-api' }, - { label: 'OpenCRVS claims', slug: 'tutorials/verify-opencrvs-claims' }, + ...generatedProduct('Relay').items, { label: 'Deploy with own data', slug: 'tutorials/deploy-standalone-with-own-data' }, - { label: 'Run the lab', slug: 'tutorials/first-run-with-registry-lab' }, - { label: 'FHIR evidence', slug: 'tutorials/getting-started-fhir-evidence' }, ], }, { - label: 'Products', - items: productSidebar, + label: 'Registry Notary', + collapsed: true, + items: [ + ...generatedProduct('Notary').items, + { label: 'Notary for a Registry Data API', slug: 'tutorials/run-notary-standalone-for-api' }, + ], + }, + { + label: 'Registry Manifest', + collapsed: true, + items: generatedProduct('Manifest').items, + }, + { + label: 'Integrations', + items: [ + { label: 'OpenCRVS claims', slug: 'tutorials/verify-opencrvs-claims' }, + { label: 'DHIS2 claim checks', slug: 'tutorials/configure-dhis2-claim-checks' }, + { label: 'FHIR evidence', slug: 'tutorials/getting-started-fhir-evidence' }, + ], }, { - label: 'Explanation', + label: 'Concepts', collapsed: true, items: [ { label: 'Architecture', slug: 'explanation/architecture' }, + { label: 'Records stay home', slug: 'explanation/records-stay-home' }, { label: 'Boundaries and map', slug: 'map/boundaries-and-map' }, { label: 'Consultation flow', slug: 'explanation/consultation-flow' }, { label: 'Evidence issuance', slug: 'explanation/evidence-issuance' }, + { label: 'Disclosure modes', slug: 'explanation/disclosure-modes-and-computed-answers' }, + { label: 'Data minimization', slug: 'explanation/data-minimization-and-purpose-limitation' }, { label: 'Trusted context', slug: 'explanation/trusted-context-constraints' }, { label: 'Integration patterns', slug: 'explanation/integration-patterns' }, { label: 'DPI safeguards', slug: 'explanation/dpi-safeguards-alignment' }, ], }, + { + // The reviewer/auditor-facing rail: the enforced model, the threat + // model, the canonical limits hub, and the public evidence a + // security reviewer can check. + label: 'Security', + collapsed: true, + items: [ + { label: 'Overview', slug: 'security' }, + { label: 'Threat model', slug: 'explanation/threat-model' }, + { label: 'Known limitations', slug: 'explanation/known-limitations' }, + { label: 'Harden a deployment', slug: 'security/hardening-checklist' }, + { label: 'Report a vulnerability', slug: 'security/report-a-vulnerability' }, + { label: 'Self-assessment', slug: 'security/self-assessment' }, + { label: 'OpenSSF evidence', slug: 'security/openssf-evidence' }, + ], + }, { label: 'Reference', collapsed: true, @@ -265,8 +315,6 @@ export default defineConfig({ { label: 'Environment variables', slug: 'reference/environment-variables' }, { label: 'Contracts', slug: 'reference/contracts' }, { label: 'Standards', slug: 'reference/standards' }, - { label: 'OpenSSF evidence', slug: 'reference/openssf-evidence' }, - { label: 'Security self-assessment', slug: 'reference/security-self-assessment' }, { label: 'ITB and SEMIC evidence', slug: 'reference/itb-semic-evidence' }, { label: 'Glossary', slug: 'reference/glossary' }, ], diff --git a/docs/site/docs/style-guide.md b/docs/site/docs/style-guide.md index e770bac7..d1a952e3 100644 --- a/docs/site/docs/style-guide.md +++ b/docs/site/docs/style-guide.md @@ -128,7 +128,7 @@ Preferred terms. - Do not capitalize the target page's title inside link text unless it is a proper noun. - Cap one paragraph at three links. Cap one page at fifteen. If you need more, the page should be a list. - Link to upstream standards bodies first, then to mirrors or summaries. -- Link to a commit SHA, not a branch, when linking to code that may move. +- Pin links to code to a release tag (`v0.8.3`) or a commit SHA, never a branch, when the claim depends on the code state. ## Tables @@ -200,7 +200,7 @@ This applies to every page that touches a standard or a contract. - No animated GIFs or videos in v0. - No screenshots as the only source of instructions. - No real user data, real production hostnames, or real tokens, even in `example` blocks. -- No relative links into source repos. Use full URLs with a commit SHA. +- No relative links into source repos. Use full URLs pinned to a release tag or commit SHA. - No nested admonitions. No admonition immediately under H1. - No anchor links into other pages; link to the page and use the sidebar's on-this-page index. - No `should` as a promise. Either it does or it does not. diff --git a/docs/site/package.json b/docs/site/package.json index 44146a5d..85aade7c 100644 --- a/docs/site/package.json +++ b/docs/site/package.json @@ -19,7 +19,7 @@ "check:content": "node scripts/check-doc-frontmatter.mjs", "check:svg": "node scripts/check-svg-a11y.mjs", "check:markdown": "markdownlint-cli2", - "check:style": "node scripts/run-vale.mjs --glob=!**/products/** src/content/docs README.md", + "check:style": "node scripts/run-vale.mjs src/content/docs README.md", "check:style:fixtures": "node scripts/check-vale-fixtures.mjs", "check:openapi": "redocly lint registry-relay registry-notary", "check:config-vocabulary": "scripts/check-stale-config-vocabulary.sh", diff --git a/docs/site/scripts/check-stale-config-vocabulary.sh b/docs/site/scripts/check-stale-config-vocabulary.sh index d0a5639d..9f3726cf 100755 --- a/docs/site/scripts/check-stale-config-vocabulary.sh +++ b/docs/site/scripts/check-stale-config-vocabulary.sh @@ -12,7 +12,7 @@ PATHS=( status=0 if command -v rg >/dev/null 2>&1; then - rg -n --glob '!scripts/check-stale-config-vocabulary.sh' "$PATTERN" "${PATHS[@]}" || status=$? + rg -n --no-ignore --glob '!scripts/check-stale-config-vocabulary.sh' "$PATTERN" "${PATHS[@]}" || status=$? else grep -RInE --exclude='check-stale-config-vocabulary.sh' "$PATTERN" "${PATHS[@]}" || status=$? fi diff --git a/docs/site/scripts/check-tutorial.sh b/docs/site/scripts/check-tutorial.sh index eff1a67d..b5cde043 100755 --- a/docs/site/scripts/check-tutorial.sh +++ b/docs/site/scripts/check-tutorial.sh @@ -150,8 +150,8 @@ done # inside `sh` fences. Bump the expected count when you intentionally add or # remove a documented command. REGISTRYCTL_TUTORIALS=( - "publish-spreadsheet-secured-registry-api:31" - "verify-claim-registry-api:74" + "publish-spreadsheet-secured-registry-api:29" + "verify-claim-registry-api:55" ) count_sh_command_lines() { diff --git a/docs/site/scripts/generate-data.mjs b/docs/site/scripts/generate-data.mjs index 3370cf9b..322d5c78 100644 --- a/docs/site/scripts/generate-data.mjs +++ b/docs/site/scripts/generate-data.mjs @@ -42,6 +42,9 @@ async function loadYaml(name) { throw new Error(`${name}.yaml entry ${index + 1} is missing ${key}`); } } + if (name === 'standards' && 'evidence_gap' in item && typeof item.evidence_gap !== 'boolean') { + throw new Error(`standards.yaml entry ${index + 1} evidence_gap must be true or false`); + } } return parsed; } diff --git a/docs/site/src/components/HomeLanding.astro b/docs/site/src/components/HomeLanding.astro deleted file mode 100644 index 5d19dbac..00000000 --- a/docs/site/src/components/HomeLanding.astro +++ /dev/null @@ -1,156 +0,0 @@ ---- -// The homepage section index. Renders the "Choose by question" router as a -// stack of register cards plus a primary-action row, reusing the shared card -// classes (.card-header, .card-meta, .card-footer) so it inherits the -// civic-print look and dark mode automatically. Editorial routing data lives -// here (like DocsList.astro's descriptions map), so index.mdx stays prose. -const base = import.meta.env.BASE_URL === '/' ? '' : import.meta.env.BASE_URL.replace(/\/$/, ''); - -function withBase(path: string) { - return path.startsWith('/') ? `${base}${path}` : path; -} - -interface Cta { - label: string; - href: string; -} - -interface RouteCard { - id: string; - question: string; - start: Cta; - then: Cta[]; -} - -const primary: Cta = { label: 'See it live', href: '/start/see-it-live/' }; -const secondary: Cta[] = [ - { label: 'When to use', href: '/start/when-to-use/' }, - { label: 'API reference', href: '/reference/apis/' }, -]; - -const cards: RouteCard[] = [ - { - id: 'see-it-work', - question: 'I want to see it work right now.', - start: { label: 'See it live', href: '/start/see-it-live/' }, - then: [{ label: 'When to use Registry Stack', href: '/start/when-to-use/' }], - }, - { - id: 'what-and-when', - question: 'What is Registry Stack, and when should I use it?', - start: { label: 'When to use Registry Stack', href: '/start/when-to-use/' }, - then: [{ label: 'Architecture overview', href: '/explanation/architecture/' }], - }, - { - id: 'why-and-who', - question: 'Why does this matter, and who benefits?', - start: { label: 'Why (registrystack.org)', href: 'https://registrystack.org/why/' }, - then: [{ label: 'Use cases (registrystack.org)', href: 'https://registrystack.org/use-cases/' }], - }, - { - id: 'try-locally', - question: 'How do I try it on my own machine?', - start: { - label: 'Publish a spreadsheet as a secured registry API', - href: '/tutorials/publish-spreadsheet-secured-registry-api/', - }, - then: [{ label: 'Verify a claim with Registry Notary', href: '/tutorials/verify-claim-registry-api/' }], - }, - { - id: 'full-demo', - question: 'How do I run the full multi-service demo locally?', - start: { label: 'First run with Registry Lab', href: '/tutorials/first-run-with-registry-lab/' }, - then: [{ label: 'Architecture overview', href: '/explanation/architecture/' }], - }, - { - id: 'fhir', - question: 'How do I try claim checks from FHIR-shaped health data?', - start: { label: 'Getting started with FHIR evidence', href: '/tutorials/getting-started-fhir-evidence/' }, - then: [{ label: 'Integration patterns', href: '/explanation/integration-patterns/' }], - }, - { - id: 'own-data', - question: 'How do I deploy Relay against my own data, not a sample?', - start: { - label: 'Deploy Relay and Notary standalone with your own data', - href: '/tutorials/deploy-standalone-with-own-data/', - }, - then: [{ label: 'Relay configuration', href: '/products/registry-relay/configuration/' }], - }, - { - id: 'existing-api', - question: 'I already have an API. How do I verify claims from it?', - start: { - label: 'Run Notary standalone for an API you operate', - href: '/tutorials/verify-claim-registry-api/#run-notary-standalone-for-an-api-you-operate', - }, - then: [{ label: 'Registry Notary', href: '/products/registry-notary/' }], - }, - { - id: 'opencrvs', - question: 'How do I verify claims from OpenCRVS?', - start: { label: 'Verify OpenCRVS claims with registryctl', href: '/tutorials/verify-opencrvs-claims/' }, - then: [ - { - label: 'OpenCRVS onboarding model', - href: 'https://docs.registrystack.org/products/registry-notary/opencrvs-onboarding/', - }, - ], - }, - { - id: 'make-usable', - question: 'How do existing files, extracts, databases, or legacy systems become usable?', - start: { label: 'Protected Registry APIs with Registry Relay', href: '/products/registry-relay/' }, - then: [{ label: 'Relay API reference', href: '/reference/apis/registry-relay/' }], - }, - { - id: 'verify-without-record', - question: 'How can a service verify a status or claim without receiving the full record?', - start: { label: 'Evidence Gateway with Registry Notary', href: '/products/registry-notary/' }, - then: [{ label: 'Evidence issuance', href: '/explanation/evidence-issuance/' }], - }, - { - id: 'how-it-fits', - question: 'How do the pieces fit together?', - start: { label: 'Architecture overview', href: '/explanation/architecture/' }, - then: [ - { label: 'Registry Relay', href: '/products/registry-relay/' }, - { label: 'Registry Notary', href: '/products/registry-notary/' }, - ], - }, -]; ---- - -
- {primary.label} - - {secondary.map((action) => ( - {action.label} - ))} - -
- -

Choose by question

- -
- {cards.map((card) => ( -
-
-

{card.question}

-
-
-
Start with
-
{card.start.label}
-
Then read
-
- {card.then.map((link, index) => ( - <> - {link.label} - {index < card.then.length - 1 ? : null} - - ))} -
-
-
- ))} -
diff --git a/docs/site/src/components/StandardsTable.astro b/docs/site/src/components/StandardsTable.astro index af15f3a2..276bfd6f 100644 --- a/docs/site/src/components/StandardsTable.astro +++ b/docs/site/src/components/StandardsTable.astro @@ -1,9 +1,12 @@ --- import standards from '../data/generated/standards.json'; + +// evidence_gap is an optional flag; absent from the JSON unless a row sets it. +const rows = standards as ((typeof standards)[number] & { evidence_gap?: boolean })[]; ---
- {standards.map((standard) => ( + {rows.map((standard) => (

@@ -21,7 +24,10 @@ import standards from '../data/generated/standards.json'; {standard.last_checked && · {standard.last_checked}}
Claim level
-
{standard.claim_level}
+
+ {standard.claim_level} + {standard.evidence_gap && · evidence gap} +
Adoption mode
{standard.adoption_mode}
Used by
diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx new file mode 100644 index 00000000..23d42b3c --- /dev/null +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -0,0 +1,93 @@ +--- +title: Data minimization and purpose limitation by design +description: How the Registry Stack architecture supports data minimization and purpose limitation, where those supports depend on operator configuration, and which data-subject-rights obligations it does not address. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc +--- + +As a data protection officer or privacy reviewer, you are asked whether a system supports the principles you work to (data minimization, purpose limitation, accountability) and where those supports stop. +This page answers one question about Registry Stack: how does its architecture support data minimization and purpose limitation by design, where are those supports conditional on how an operator configures a deployment, and which data-protection obligations does the architecture not address at all? +Two things this page does not do. +It makes no compliance claim: conformance to the underlying specifications does not imply conformance to any external standard, nor to GDPR or any data-protection regulation. +And it gives no legal advice and no jurisdiction-specific reading: that judgement remains yours. +The specifications here are all draft, pre-1.0 documents and may change, so treat the design as a posture under development rather than a finished or warranted product. +A note on terms, since you do not need Registry Stack internals to follow this. +A *claim* is one pre-modelled question (one decision or one extracted value) that a system can ask about a person against a registry the institution already holds. +A *source binding* is the configured rule that connects a claim to the specific source fields it reads. +*Minimized evidence* means a response shaped by data minimization or selective disclosure; a *purpose-bound request* is a request that carries or is evaluated against a stated purpose. +Those last two are the principles you already know, expressed in the system's own vocabulary. + +## How the claim model limits collection at the source + +The first place minimization appears is in the unit of data the system is built around. A claim definition describes one decision or one extracted value, not a whole record, so returning a full record would over-collect relative to the question actually asked. The design treats the narrow question, not the record, as the thing to be answered. + +That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads the fields it is configured to project (the declared `binding.fields`, plus the lookup and freshness fields), so "read only what the rule needs" depends on the binding being configured that way rather than being automatically derived from the rule. A request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. + +## Purpose limitation as an enforced gate, not a label + +Purpose limitation in this design is more than a field written into a log. It is enforced as a policy decision before any source is read. Where a source is configured to require a purpose, a request that omits the purpose header is rejected, and the supplied purpose is recorded in the audit record. Beyond recording it, the evaluation component combines the claim's permitted purposes with the source binding's permitted purposes and denies the request before source access if either set rejects the stated purpose. + +A matching policy carries the same idea further: it binds a source read to a declared purpose and relationship context and constrains which inputs may identify the subject, and a request whose purpose, relationship, or inputs the policy does not admit is refused before the source is touched. The effect a reviewer should take away is that purpose, when configured, acts as a precondition for access rather than an after-the-fact annotation. + +The phrase "when configured" is load-bearing, and the section on operator responsibilities returns to it. + +## Minimization at the output: disclosure modes and selective credentials + +Minimization also applies to what leaves the boundary. A claim's result can be shaped into one of three disclosure modes: *value* returns the full value, *predicate* returns only the true/false satisfaction, and *redacted* hides the value entirely. A privacy-sensitive claim is expected to default to the least-revealing mode that still answers the question. (How a mode is selected and policy-bound is covered in [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) and is out of scope here.) + +The redacted mode is worth a reviewer's attention because it shows minimization without loss of accountability: a redacted result carries neither the underlying value nor the satisfaction outcome, yet the evaluation stays referenceable and verifiable through an evaluation identifier, a verification identifier, and a claim hash. You can audit that an evaluation happened and verify it later without the result itself disclosing anything about the person. + +When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC: a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Selective disclosure here is at the claim (or configured-projection) level: by default each claim becomes one disclosure carrying its whole value, so an object-valued claim is revealed as a unit unless an explicit projection splits it into separately-disclosable fields. Holder binding (tying the credential to a holder key with `did:jwk`) is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. + +A further minimization detail guards against a subtler leak. A failed subject match collapses by default to a single public reason (evidence not available), with the granular reason kept only in the audit record, so the lookup surface cannot by default be used to confirm whether a person exists in a registry. + +## Data stays at source: distributed custody + +Underneath all of this is an architectural choice that matters for minimization at the system level: data stays at source. The design premise is distributed custody: the stack provides an API surface for lawful exchange between authorities and does not aggregate data into a central system. The read component must not mutate source registry data and exposes no write-back route, and runtime services are read-only in this version, with no source-registry data-mutation routes at all. + +For a reviewer this has a direct consequence. Because the architecture cannot alter or delete source records, it is also why no erasure flow exists at the source layer: the design has no mechanism to reach into and change the registry it reads from. That is described more fully in the next section as a limit, not a feature. + +One related boundary is positive rather than cautionary. The portable metadata the stack publishes must not carry person-level data or runtime secrets and bindings, which is precisely why those artifacts are safe to share: they hold no personal data. But publishing metadata only describes; it does not authorize access, enforce a policy, or assert that any record exists, so a published manifest carries no data-protection guarantee about live data. + +## Audit as an accountability primitive + +Accountability is supported through audit, which is treated as a security control rather than an optional log. Every request that returns person-level records or claim results must be recorded with at least the caller principal, the scopes exercised, a request identifier, and the purpose value where one was supplied, and a deployment can run audit fail-closed so that a request whose audit record cannot be written does not succeed. + +One precise reading matters here. Audit fail-closed is a capability a deployment can turn on, not a guarantee that every route in any given build has been individually audited against it. Whether a particular deployment meets it is something a reviewer verifies in that deployment, not something the design asserts on its behalf. + +## What the architecture does not provide + +Some of the most important things for a DPO to know are the absences. Stating them plainly prevents the design from being read as more than it is. The full, canonical inventory is in [Known limitations and non-guarantees](../known-limitations/). + +- No data-subject erasure or right-to-be-forgotten workflow: There is no built-in erasure or deletion flow anywhere in the design, so it does not satisfy erasure obligations on its own. As the Data stays at source: distributed custody section notes, the read-only design cannot mutate source records, so erasure, where it is required, remains an operation on the source registry outside this system. +- No rectification or data-subject-rights flow: Beyond erasure, there is no rectification or general data-subject-rights mechanism. +- No specified revocation flow; an optional status surface: The specifications define no revocation flow, but the implementation includes an optional, off-by-default credential-status surface (a public `GET /v1/credentials/{id}/status` and an admin `POST /admin/v1/credentials/{id}/status`) that an operator can enable to mark a credential `revoked`. Key rotation exists (a rotated-out key may remain published so existing results stay verifiable), but that is not a way to revoke an already-issued credential. Treat status-based revocation as an operator-enabled capability, not an always-on one. +- No broad cross-authority exchange beyond static peering: Federation between authorities is static-peer only; dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of scope, so the design supports a narrower cross-authority data-exchange surface than the word "federation" might suggest. +- No privacy-budgeted analytics: Aggregate routes produce statistical outputs, not a longitudinal privacy budget. A data-protection impact assessment should not describe an aggregate route as privacy-budgeted unless a separate, deployed control actually provides that. +- No compliance claim: Conformance to these specifications does not imply conformance to any external standard or any data-protection regulation, and the specifications themselves are draft. + +## Operator responsibilities: what the design leaves to you + +The minimization and purpose-limitation posture described above is the design default, but the operator owns the configuration. Several of the protections are conditional, and a reviewer should test the deployment, not the design, for each one. + +The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding skips the binding-level gating (no purpose gating, no relationship gating, and no input minimization) and falls back to identifier-only resolution. Claim-level purpose constraints still apply, though: the claim's own permitted purposes and the deployment's allowed-purposes list are enforced before the source is read, so a claim-purpose mismatch is refused with no source reads. Purpose limitation is therefore not entirely absent without a matching policy; what is missing is the binding-level gating. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a binding-level hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. + +The existence-oracle protection is also defeasible. The matching surface collapses failures to a single public reason by default, but a deployment may disable that collapse and surface not-found, ambiguous, or rejected outcomes, an over-disclosure risk the operator controls. + +More broadly, the architecture defines primitives and leaves a large set of data-protection-relevant controls to the operator. Secret and key provisioning, audit retention and storage, tenant isolation, transport security, edge rate limiting, deployment configuration, and incident response are not defined by the design; they are responsibilities you provision and verify in a deployment. The clear-eyed view, then, is this: Registry Stack offers minimization, purpose limitation, and accountability as enforceable design primitives, but whether a given deployment realizes them (and whether it meets any legal obligation) depends on how the operator configures and runs it. + +## Related + +- [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/): how disclosure modes are selected and policy-bound +- [Threat model](../threat-model/): the boundaries, assets, and threats behind this posture +- [Security overview](../../security/): the high-level security summary +- [Known limitations and non-guarantees](../known-limitations/): the full inventory of edges +- [Records stay home](../records-stay-home/): what stays inside the institution's boundary and what crosses it +- The security and protocol specifications: [RS-SEC-G](../../spec/rs-sec-g/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx new file mode 100644 index 00000000..4c00b700 --- /dev/null +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -0,0 +1,91 @@ +--- +title: Disclosure modes and computed answers +description: Why Registry Notary can answer a question about a subject by returning a computed result (value, predicate, or redacted) instead of handing back the source record. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: [] +--- + +You are weighing whether Registry Stack actually keeps a sensitive registry record private while still answering questions about the subjects in it. +The short answer is that a caller asks a question and receives a *computed answer*, not the record. +This page explains how that works, what each kind of answer does and does not reveal, and where the privacy claim has edges. +You will see one product term used throughout: a **claim**, which is a single pre-modelled question (one decision or one extracted value), such as "is this person registered?" or "what is this person's registration date?". +A claim is deliberately narrow: a claim that tried to return a whole record would over-collect and be hard to authorize, which is exactly the outcome this design avoids. + +## How Registry Notary controls what leaves the service + +Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. There are exactly three; there is no fourth mode. `value` is not always all-or-nothing, though: when a claim returns an object, `value` mode can have specific fields redacted (configured through `redaction_fields`), and the result carries the object minus those fields plus a list of what was withheld. + +Per-claim disclosure control is enforced at runtime by the service. It is not left to a caller's good behaviour. + +## The evaluation pipeline: computed answers, not record handoffs + +The reason an answer can stand in for a record is that the caller never supplies the value being asked about. To evaluate a claim, the caller sends a **subject identifier** and a **claim id** over a REST call authenticated the way the rest of the service is: an API key or bearer token in static-credential mode, or an OIDC token, whichever your deployment is configured for. Registry Notary then performs the evaluation against its own configured sources. The caller does not supply the evaluated value, and cannot inject one. + +The evaluation runs as a pipeline: a source connector resolves facts about the subject, a rule computes the configured condition, a disclosure mode shapes what leaves the service, and a response format carries the result. Because Notary computes the answer itself, it can return that computed answer rather than handing back the source record it read. + +The rule that does the computing is one of three kinds. An **exists** rule checks for the presence of **exactly one** matching source record. An **extract** rule reads a value from a source field. A **cel** rule derives a value from source fields or earlier claim results through a hardened expression. Each kind produces an answer about the subject without that answer being a copy of the source row. + +## The three disclosure modes + +The disclosure mode fixes how much of the computed answer the caller receives: + +| Mode | What the caller receives | +|------|--------------------------| +| `value` | the full evaluated value | +| `predicate` | only the true/false satisfaction of the rule | +| `redacted` | no value and no satisfaction outcome | + +`value` returns the full value, so this mode does hand over the computed result in full; it is the least private of the three and is appropriate only where the use case calls for the actual value. `predicate` returns only the boolean satisfaction. `redacted` returns neither the underlying value nor the yes/no outcome. + +Every evaluation result records which disclosure mode was applied, so a downstream system can tell how much was revealed. + +## Why predicate and redacted avoid sharing the underlying record + +This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` when a matching record is found, while a subject with no matching record returns `evidence.not_available` (collapsed to a single public reason by default) rather than `false`; absence is surfaced as no-evidence, not as a negative result. Either way the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. + +A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. + +One current renderer limitation to note: this no-outcome guarantee holds for the standard result body, but the CCCEV JSON-LD renderer currently emits `cccev:isConformantTo: false` for a redacted result instead of omitting it. A deployment that allows both redacted disclosure and CCCEV output should be aware that the outcome is not fully withheld in that render. + +The difference between the two matters. `predicate` is not "zero disclosure"; it still tells the caller a true-or-false fact about the subject. `redacted` goes one step further: it does not leak even a yes/no. They are not interchangeable. + +## How disclosure policy is configured per claim + +The caller does not freely pick from all three modes. Each claim configures a **disclosure policy**: a `default` mode plus an `allowed` set drawn from the three modes. A caller may request a mode: Notary honours it when it is in that claim's allowed set and refuses it (`claim.disclosure_not_allowed`) otherwise; when the caller requests no mode, Notary applies the claim's default. So the policy bounds which modes are allowed, and the caller chooses among the allowed modes. A privacy-sensitive claim can be configured to default to the least-revealing mode that still answers the question. + +## What a redacted result still reveals (and to whom) + +"Reveals nothing to the caller" is not the same as "leaves no trace." A `redacted` evaluation can still be pointed back at: it carries an `evaluation_id`, and the audit record carries a `verification_id` and a `claim_hash`. That lets you reference the evaluation that happened; it is not a cryptographic signature on the answer. Redaction shapes what the caller sees; it does not erase the fact that an evaluation happened. + +That trace is by design. Every evaluation (redacted or not) is audited, and a deployment can run audit fail-closed, so that a request whose audit record cannot be written does not succeed. So redaction does not mean an unlogged request; it means an unrevealed answer. + +There is a related protection on the matching surface. By default, a matching failure collapses to a single public reason, `evidence.not_available`, so the lookup cannot be used as an existence oracle; a "not found" answer does not tell the caller whether a record exists. This complements `redacted` disclosure by not leaking record existence. A deployment can turn this off, but only in a controlled environment; it is not always on. + +## Disclosure control is one safeguard among several + +Disclosure modes are one runtime safeguard within a broader posture that also includes authentication, scoped routes, no source mutation, and audit. They are not the whole privacy guarantee. A few protections are not the software's to make at all: secret and key custody, audit retention, tenant isolation, transport, and rate limiting are your deployment's job, not built-in guarantees. + +## Honest limits + +A few boundaries matter when you evaluate this design: + +- Minimization is conditional, not absolute: A minimized answer can be narrower than the full source record only where the configured use case supports that pattern. Notary does not always return less than the record; `value` mode returns the full value, and a claim reveals only what its policy was configured to reveal. +- Governance depends on configured policy: A source binding with no matching policy falls back to unrestricted, identifier-only resolution, with no purpose, relationship, or input-minimization gating. Minimization is not automatic; it follows the configuration. +- The caller cannot inject a value, but matching strictness is configured: The "computed answer avoids sharing the record" guarantee rests on Notary computing the answer from a subject id and claim id. Identity matching is only as strict as its configured policy. +- A sidecar token is not a per-subject boundary: A source adapter sidecar's internal static bearer token is not by itself an end-user, tenant, or subject authorization boundary; the sidecar hop does not enforce per-subject access. +- Disclosure modes are not credential selective disclosure: A holder's later choice of which credential fields to present is a separate mechanism from Notary's three evaluation modes. Do not read `value`/`predicate`/`redacted` as the same thing as a wallet holder selectively disclosing credential fields. +- Aligning with a standard is not conforming to it: This page describes Notary's own disclosure behaviour. It is not an endorsement of, or conformance to, any outside specification. + +## Related + +- [Security overview](../../security/): the full posture this fits into +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/): the privacy view +- [Known limitations and non-guarantees](../known-limitations/): where the edges are +- [Records stay home](../records-stay-home/): what stays inside the institution and what crosses out +- How a claim and its disclosure policy are defined: [RS-DM-CLAIM](../../spec/rs-dm-claim/); disclosure modes, credentials, and federation: [RS-PR-NOTARY](../../spec/rs-pr-notary/) diff --git a/docs/site/src/content/docs/explanation/evidence-issuance.mdx b/docs/site/src/content/docs/explanation/evidence-issuance.mdx index d0ecfe74..004a2c24 100644 --- a/docs/site/src/content/docs/explanation/evidence-issuance.mdx +++ b/docs/site/src/content/docs/explanation/evidence-issuance.mdx @@ -97,7 +97,7 @@ evaluation, or receipt. policy, replay state, purpose, profile, and audience before source reads, then returns a compact signed JWT response. This path returns a scoped evaluation result, not a credential. -The [hosted lab walkthrough](../../start/see-it-live/) citizen self-attestation scenario uses path 2: a +The [credential tour](../../start/credential-tour/) citizen self-attestation scenario uses path 2: a host-side Notary runs against the lab's civil registry, eSignet provides the access token, and a wallet completes the OID4VCI flow. diff --git a/docs/site/src/content/docs/explanation/integration-patterns.mdx b/docs/site/src/content/docs/explanation/integration-patterns.mdx index eaec76db..bef3cc6e 100644 --- a/docs/site/src/content/docs/explanation/integration-patterns.mdx +++ b/docs/site/src/content/docs/explanation/integration-patterns.mdx @@ -74,8 +74,8 @@ Wire Registry Stack in alongside a domain platform when: ## Workflow engines -Examples: [OpenFn](https://www.openfn.org/) (cross-system orchestration popular in humanitarian and -government data flows), [Camunda](https://camunda.com/), [Flowable](https://www.flowable.com/). +Examples: [Camunda](https://camunda.com/), [Flowable](https://www.flowable.com/), and other +casework or process orchestration engines. Workflow engines own process state, task assignment, timers, branching, retries, escalation, and history. @@ -90,8 +90,8 @@ Wire Registry Stack in alongside a workflow engine when: ## Exchange layers -Examples: [X-Road](https://x-road.global/), [OpenFn](https://www.openfn.org/) (when used as the -cross-institution data flow), GovStack-style reference exchanges. +Examples: [X-Road](https://x-road.global/), GovStack-style reference exchanges, and +country-specific interoperability layers. Exchange layers own participant onboarding, message transport, mutual trust, routing, addressing, and cross-institution policy. diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx new file mode 100644 index 00000000..eeca3373 --- /dev/null +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -0,0 +1,118 @@ +--- +title: Known limitations and non-guarantees +description: The canonical, linked inventory of what Registry Stack does not yet do or guarantee, so you know exactly where the boundaries are before committing. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc + - oid4vci + - cccev + - verifiable-credentials + - odrl +--- + +You already know what Registry Stack is for: answer a question about a record without sharing the underlying record. This page answers a different question (*what does Registry Stack not yet do or guarantee?*) by giving you, in one place, the honest list of where the boundaries are. + +## What this page is and how to use it + +This is the canonical inventory of current limitations and non-guarantees across the whole stack. Each entry states the boundary plainly and links to where it is discussed in depth, so the rest of the documentation can point here whenever it says "this is not X." + +The limitations are not invented for this page. Each protocol and data-model specification carries its own limitations section; this hub collects those and links to them so you can read any one in full context. + +Three things are out of scope here, and each is a link instead: + +- Why a given limit exists: read the relevant specification. +- Whether it will change: see the project roadmap (`ROADMAP.md` in the repository root). This page describes current behavior only. +- How to work around a limit: mitigations are not covered here. Follow the linked specification for each boundary, and see `release/VERIFY.md` in the repository for release verification. + +Use this page as a checklist before you commit. If one of these limits is a dealbreaker for your deployment, better to find that out before integrating than after. + +## Everything here is draft + +Every governing specification (architecture, security, protocol, and data-model) is currently at lifecycle status `draft`: under development or review, not `current` (in force). The contracts these limitations are stated against are themselves not yet finalized. + +Registry Stack is a pre-1.0 technical release for evaluation, integration pilots, and public review. It is not a production support commitment and carries no hosted service-level agreement. + +Two things follow, and both matter as you read the rest of the page. First, the behaviors and boundaries here may change as the specifications move toward `current`. Second, aligning with an external standard is not the same as conforming to it: speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, or the rest does not certify conformance to any of them. The [standards register](../../reference/standards/) records how each one is actually adopted. + +## Stack-wide boundaries at a glance + +These boundaries cut across components. Read them first. + +- No source mutation, no event stream: Registry Relay does not write back to or otherwise mutate source registry data, and it has no event-stream backend; sources are read as batch snapshots or table scans only. Registry Notary likewise has no file-based or database connector; a source connector reaches its target over HTTP, so the connector-to-target hop is HTTP-only. +- Aggregate routes are not a privacy budget: Relay aggregate routes return the configured statistical observations, but provide no built-in longitudinal privacy budget and do not track cumulative disclosure across repeated or overlapping queries. Do not describe an aggregate route as privacy-budgeted unless a separate deployed control provides that protection. +- A published manifest grants nothing: Publishing a dataset, policy, evidence offering, or federation relationship in a discovery artifact grants no access, enforces nothing, and asserts no fact about a live record. Federation discovery metadata is not a trust anchor: a published manifest does not bind a consumer to trust a JWKS endpoint, federation API host, or peer identity. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- Registry Lab and its hosted instance are demonstrations, not infrastructure: The local Registry Lab is a compose-based runnable demonstration that uses fixture credentials and demo-grade configuration and ships no production deployment guidance. Any hosted instance of the lab is likewise a demonstration, not production infrastructure: it carries no uptime or data-retention commitment. + +For the rationale behind each of these, follow the linked specifications in each section. + +## Credential issuance and lifecycle limits (Notary) + +Registry Notary issues credentials, but the issuance surface is deliberately narrow. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/). + +- One credential format, one binding method when binding is enabled: Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. +- A profiled issuance subset, not a full issuer: The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile (a profiled subset of Draft 13 using the `dc+sd-jwt` format), not a full OID4VCI issuer and not a claim of general external-wallet interoperability. The capability-discovery document (`/.well-known/evidence-service`) declares `openid4vci.support: not_full_issuer` (announcing it is not a full issuer); this flag lives there rather than in the OID4VCI credential-issuer metadata. +- Delegated-attestation issuance is limited to the direct path: The OID4VCI transaction-token path for delegated attestation is rejected at the credential endpoint. Direct credential issuance via `/v1/credentials` is supported when the stored evaluation's access mode is delegated-attestation and the relationship allow-lists the credential profile. +- No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow: The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, the public status endpoint reads a credential's status, and only the admin status endpoint (which requires the `registry:notary:admin` scope) can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- CCCEV output is a profiled shape, not conformant: [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. +- Two signed artifacts, not one: Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default; ES256/P-256 is also supported per credential profile; with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts: don't describe one as the other. +- Standalone admin reload is not implemented: The standalone Notary `/admin/v1/reload` route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload, and non-swappable changes require a service restart. Governed config apply and the file-watch signer do hot-apply some changes without one (signing-key rotation reports `restart_required: false`, client and OpenAPI auth-policy changes are swapped in, and a same-public-key signer replacement reloads), so not every key or configuration change needs a restart. + +## Federation limits + +Federation across institutions is static-peer only. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/) and the [security model](../../spec/rs-sec-g/). + +- Static peers only: Replay protection defaults to in-process, in-memory state, but a Redis-backed shared replay store is available for multi-instance deployments (`replay.storage = redis`, with `federation.replay.storage = redis` when the top-level store is Redis), and federation uses it for request replay and `jti` checks. There is still no dynamic trust-chain discovery and no federated credential issuance; both are out of scope for this version. +- Delegated evaluation returns a result, never a credential: Across a federation boundary, what crosses is a scoped evaluation result, never an issued credential. + +## Registry Relay limits + +Registry Relay exposes protected, read-only registry routes. Its boundaries are covered in depth in the [Registry Relay protocol](../../spec/rs-pr-relay/). + +- Read-only, batch or scan: Relay does not mutate source data in v1 and has no event-stream backend; it reads sources as batch snapshots or table scans only. +- Feature-gated surfaces: Several Relay surfaces (the OGC API Features, Records, and EDR adapters and the SP DCI sync adapter) are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. (Signed response credentials are not Cargo-gated; they are config-gated through `provenance`.) +- Aggregates are not privacy-budgeted: As noted stack-wide, Relay aggregate routes provide no longitudinal privacy-budget or cumulative-differencing protection. +- Relay does not evaluate or own the manifest: Relay does not evaluate claims or issue Notary's SD-JWT VC credentials (that is Notary's role), though with `provenance` enabled and a caller requesting `Accept: application/vc+jwt` it can attach its own VC-JWT signed response credential. It also does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. +- Governed enforcement is one PDP profile, not full interoperability: Governed runtime policy enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`, the single PDP profile this build recognizes). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of [ODRL](../../reference/standards/) terms outside the supported set. + +## Data-model limits (Claim and Manifest) + +These limits concern what the claim and manifest configuration loaders do and do not enforce. Read them in full in the [Claim data model](../../spec/rs-dm-claim/) and [Manifest data model](../../spec/rs-dm-manifest/). + +- The `plugin` rule type is unimplemented: The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. +- Load-time invariants are not all enforced: The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. A disclosure default outside the allowed set and a dangling rule source surface as request or evaluation errors at runtime, not as startup failures. Duplicate claim ids are different: they are never reported, because the runtime lookup returns the first match by id, so a later duplicate is silently shadowed. +- Identifier-only matching has no further gating: A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. +- Manifests describe; they do not enforce: The portable metadata layer only describes: publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- Rendered standards artifacts are well-formed, not certified: Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. +- Unknown manifest keys are ignored, not rejected: Manifest runtime-binding exclusion is an enumerated list, not a deny-unknown-fields schema: an unrecognized key outside the runtime-only list is ignored rather than rejected. +- Vocabulary influence is not vocabulary emission: PROV-O is a design influence only: provenance-shaped concepts appear in audit fields and the claim provenance struct, but no PROV-O vocabulary terms are emitted as JSON-LD. No standalone SKOS artifact is published yet (only embedded SKOS-shaped nodes), and CPSV-AP has no Relay runtime metadata route. + +## Security-model operator boundary + +A healthy, reachable, internally consistent deployment is not the same as a production-secure one. Read the full security boundary in the [security model](../../spec/rs-sec-g/). + +- Health checks do not certify key custody: Readiness, liveness, and protocol-conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and key-provider approval remain operator responsibilities. +- Many guarantees are the operator's to provide: Secret and key provisioning, key custody and rotation schedule, audit retention and storage, tenant isolation, transport termination, edge rate limiting, deployment configuration, and incident response are operator responsibilities, not behaviors the stack guarantees. +- Audit fail-closed is a capability, not a guarantee about every build: A deployment can run audit fail-closed, so that a request whose audit record cannot be written does not return success. That is a switch a deployment is able to turn on, not a promise that every route in a given build has been individually audited, nor that every build ships with it on. +- The source-adapter sidecar is an internal connector, not a public API: The source adapter sidecar is an internal connector surface, not a public subject-facing API. Its static bearer token is not by itself a tenant or subject authorization boundary, and it relies on deployment-network egress controls for outbound source traffic. Direct public access, broad reuse of one sidecar token across unrelated sources, or shared cache or backoff state across authorization contexts is outside the security model. +- Demo and template code is not a production profile: Demo helper code and generated workflow snippets are integration examples, not a production freshness or replay-protection profile, and must not be relied on for freshness, expiry, or replay protection. + +## Supply-chain and release limits + +These concern how releases are signed and what that signing currently covers. + +- Partial release signing: GitHub Release assets are signed with keyless cosign and, when tag-triggered, carry SLSA provenance. However, OCI image signatures are not yet published, and Git version tags are not yet cryptographically signed (GPG, SSH, or Sigstore). The `v0.8.0` prerelease was published before release-asset signing and does not currently include cosign signatures. + +For how to verify what *is* signed, see the release verification guidance in `release/VERIFY.md` in the repository. + +## Where to go next + +- Why a given limit exists: the rationale and trade-offs live in each specification's own limitations section: [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), and [RS-DM-MANIFEST](../../spec/rs-dm-manifest/). For the security and architecture boundaries, read [RS-SEC-G](../../spec/rs-sec-g/) and [RS-ARC-G](../../spec/rs-arc-g/). +- How each external standard is actually adopted: the [standards register](../../reference/standards/) records the adoption mode for every standard named across the stack. +- What might change: the project roadmap (`ROADMAP.md` in the repository root). This page does not track it. +- How to verify what is signed: release verification lives in `release/VERIFY.md` in the repository. diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx new file mode 100644 index 00000000..2e18a93a --- /dev/null +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -0,0 +1,205 @@ +--- +title: Records stay home +description: How an institution proves facts from registries it already holds, without the records leaving. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc +--- + +An institution that runs a civil registry, a social-protection database, or a health +registry already holds the records it needs. +Registry Stack lets it **answer questions about those records** (*is this person alive? is +this household eligible?*) and return a result another system can trust, while the records +themselves are **read where they already live, never written back, and never handed +over**. +This page explains what that means in practice: what stays inside the institution's +boundary, what crosses it, and (equally important) what the design does and does not +guarantee. + +## A question goes in, an answer comes out + +The mental model is one sentence: **a scoped question crosses into the institution, the +record is read in place, and only a computed answer crosses back out.** + +A caller never sends the value it is asking about and never receives the underlying +record. It sends a subject identifier and the id of a *claim* (a single, pre-modelled +question) and receives one of a few narrow shapes of answer: a yes/no, a single value, a +machine-readable evaluation result, or a credential the subject can carry in a wallet. The source row that the +answer was computed from stays behind. + +## The boundary + +```mermaid +flowchart LR + subgraph inst["Institution: data stays here"] + src[("Source registry\nCSV · XLSX · Parquet · PostgreSQL")] + relay["Registry Relay\nprotected read API"] + notary["Registry Notary\nevaluate · disclose · issue"] + key>"Signing key\n(private half never leaves)"] + audit[("Audit log")] + src -- read in place --> relay + relay -- governed read --> notary + notary -. records .-> audit + key -. signs .-> notary + end + caller["Caller / verifier"] + holder(["Subject / wallet"]) + caller == "request: subject id + claim id + scope" ==> notary + notary == "answer: yes/no · value · evaluation result" ==> caller + notary == "holder-bound credential" ==> holder + + classDef inside fill:#eef,stroke:#334,stroke-width:1px; + classDef outside fill:#f7f7f7,stroke:#777,stroke-dasharray:3 3; + class src,relay,notary,key,audit inside; + class caller,holder outside; +``` + +*A request and an answer cross the boundary. The source record does not.* Registry Relay +turns an existing file or database table into a read-only, access-controlled API without +replacing the source. Registry Notary evaluates one modelled question against that source +and returns a shaped result; it is the only component that evaluates claims, applies +disclosure policy, and issues credentials. + +## What stays home + +- Source data is read in place: Relay reads sources as batch snapshots or table scans; + there is no write-back to the source registry, and runtime services expose no + data-mutation routes. The source keeps running as it always has. "No write-back to the + source and no external handoff" is not the same as "no copy exists": in snapshot mode + (the default), Relay materializes a projected copy into its local cache (`cache_dir`), which + carries its own retention and access considerations. +- Storage internals stay private: The paths, table names, and backend credentials that + point at the source live in the service's runtime configuration, decided at startup. They + are never part of the public API surface, and never part of a portable metadata file that + gets distributed. +- The institution keeps custody: The design premise is *distributed custody*: each + authority retains control of its own registry data, and the stack does not aggregate + records into a central system. It provides the exchange surface, not a data lake. +- Private signing keys never leave the issuer: The institution publishes the *public* + half of its signing key so anyone can verify a signed credential or signed result; the + private half stays inside. + +## What crosses the boundary + +Only a computed answer crosses out: never the source row. The answer takes one of a few +shapes: + +- A yes/no: only the true/false satisfaction of the modelled rule. +- A single value: the evaluated value itself, when the claim's disclosure mode is + `value` (this returns the full value). +- A machine-readable evaluation result: a claim-result document carrying *provenance + metadata*: which evaluation produced it, under which policy, across how many sources. This + provenance lets a receiving system trace the result; it is not a cryptographic signature. +- A holder-bound credential: an SD-JWT VC the subject can store in a wallet and present + later. Unlike the plain result, the credential is cryptographically verifiable against the + issuer's published keys. + +Across a federation boundary (one institution's Notary asking another's), what crosses is +a scoped, signed evaluation result, never a credential. + +## How much an answer reveals: the three disclosure modes + +Every claim carries a **disclosure mode** that fixes how much of the answer the caller +receives. There are exactly three: + +| Mode | Discloses | Withholds | +|------|-----------|-----------| +| `value` | the full evaluated value | nothing about the value | +| `predicate` | only the true/false satisfaction | the underlying value | +| `redacted` | neither: the result carries no value **and** no yes/no | the value *and* the outcome | + +The policy bounds which modes are allowed, and the caller chooses among them: a claim +defines an `allowed` set and a `default`, the service honours a requested mode when it is in +the allowed set and refuses it otherwise, applies the default when none is requested, and +every result records which mode was applied. A privacy-sensitive claim is expected to default +to the least-revealing mode that still answers the question. + +This is the mechanism behind "prove a fact without sharing the record". To check whether a +person has a registered record, model the question as an *existence* rule and disclose it +as a `predicate`: the caller learns `true` or `false`, and the row never crosses the +boundary. To check eligibility without exposing an income figure, derive the decision with +an expression rule and disclose the eligibility boolean as a `predicate`; the income value +stays home. + +## Why the answer is not the record + +A credential is not a copy of the record. It is an **SD-JWT VC**: the signed body carries a +SHA-256 *digest* of each selectively disclosable field rather than the field value, so a +field the holder does not present stays hidden. It **can be** holder-bound (tied to the +holder's key so it is not presentable without the matching private key) when the issuing +profile enables binding (the wallet issuance path does); the default profile issues an unbound +credential. Either way, the holder chooses which fields to reveal to which verifier. Anyone can verify it against the issuer's +published public keys, served without authentication so a verifier needs no credential of +its own. The issued credential carries no full record payload. + +## How the boundary is enforced + +The "stays home" property rests on a few enforced rules, covered in depth in the Trust & +Security material: + +- Scope-before-source, deny-by-default: A service checks the caller's scope *before* it + reads any source or evaluates any claim, and does not widen a caller's reach at request + time beyond what its configuration grants. Anything that touches a record or a claim + requires authentication; the routes reachable without it are the operational, discovery, + and protocol-bootstrap surfaces: liveness and readiness probes, the public verification + keys, credential-issuance discovery metadata, the OID4VCI wallet-flow endpoints, the docs, + public credential-status reads, and credential type metadata. +- A permit, or a closed door: On a governed read, the policy decision point must return + a permit before data is returned; a denial fails closed with a stable reason rather than + falling back to an ungoverned read. +- Every person-level request is audited: An audit record captures at least the caller, + the scopes exercised, a request id, and the declared purpose where one was supplied. + A deployment can run audit fail-closed, so a request whose audit record cannot be + written does not return success. + +## What this guarantees, and what it does not + +"Records stay home" is a precise, narrow promise. Reading it as more than it is would be a +mistake, so the limits are stated plainly here. + +- It is not "data never moves" and not "air-gapped": The promise is *read-in-place, no + write-back, retained custody*. Authorized, minimized answers do leave the boundary by + design: that is the point of the system. +- Minimization is modelled, not automatic: `value` mode discloses the full value. A + claim reveals only what its author configured it to reveal; least disclosure is a design + choice the claim makes, not a property the stack imposes on every answer. +- Correctness depends on the source: Notary reports what the configured source says; it + does not independently vouch for whether the source is correct or current. +- A plain result is provenance-tagged, not signed: The everyday evaluation response + carries provenance metadata, not a cryptographic signature. Cryptographic verifiability + comes from the SD-JWT VC credential and the signed federation result: a receiving system + that must verify an answer cryptographically uses the credential, not the default response. +- Matching is only as strict as it is configured: Notary resolves a subject through its + configured matching policy and does not independently verify identity beyond that. By + default a matching failure collapses to a single public reason, so the lookup surface + cannot be used as an existence oracle. +- This is not zero-knowledge: A `predicate` answer is a policy-enforced boolean computed + inside the service; SD-JWT selective disclosure is digest omission. Neither is a + zero-knowledge proof, and the documentation should not imply one. +- No revocation flow is specified, and no erasure flow: This version specifies issuance, + disclosure, presentation, and verification, but no credential-revocation flow and no + data-subject erasure. An optional, off-by-default credential-status surface can be enabled + to mark a credential `revoked`, but that is an operator-enabled capability rather than a + specified flow. A key rotated out may remain published so existing results stay + verifiable: that is not a revocation mechanism. +- Several guarantees are the operator's to provide: Network egress limits, key custody, + tenant isolation, audit retention, and transport security are supplied by the deployment, + not guaranteed by the stack. +- The model is specified in draft: The behaviour described in this section is defined in + the `RS-*` specifications, which are still drafts and may change. Alignment with an + external standard is not a claim of conformance to it or of legal compliance. + +## Related + +- The security model and protocol contracts: [RS-SEC-G](../../spec/rs-sec-g/), + [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), + [RS-DM-CLAIM](../../spec/rs-dm-claim/) +- Evidence issuance, end to end *(explanation)* +- Disclosure and minimization in depth *(Trust & Security)* +- The threat model and security posture *(Trust & Security)* diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx new file mode 100644 index 00000000..9021a686 --- /dev/null +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -0,0 +1,258 @@ +--- +title: Threat model +description: The trust boundaries, assets, and threats the Registry Stack design considers (what it mitigates, and the residual risks it leaves to the operator or treats as out of scope). +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc + - oid4vci + - w3c-did + - odrl +--- + +This page is for a reviewer or auditor who already reasons in terms of assets, trust +boundaries, and adversaries, but does not yet know where the Registry Stack's boundaries +actually sit. +It answers one question: **why does this design produce the security +properties it claims: which threats does the architecture consider, what does it actually +mitigate, and where do the residual risks lie?** +It is deliberately a map of boundaries and limits, not a runbook. +Hardening procedures belong to the Operate section, the high-level posture summary to the +Trust & Security overview, and privacy obligations to [Data minimization and purpose +limitation](../data-minimization-and-purpose-limitation/). +Here the goal is a defensible threat model: the boundaries the design draws, the threats it +places in and out of scope, and the residual risks stated honestly. + +## The two-layer architecture and its primary trust boundary + +The stack is a two-layer design, and the split between the layers is the first trust +boundary the model relies on. A **portable metadata layer** only *describes*: it carries no +production data, no authentication, and no secrets. A **runtime services layer** *enforces*: +it is where authentication, authorization, and disclosure actually happen. + +The consequence for a reviewer is that the metadata layer authorizes nothing, enforces +nothing, and does not assert that any record exists or satisfies a claim. Publishing a +dataset, a policy, an offering, or a federation relationship does not grant access and is +not proof that a record exists; a published ODRL policy only describes intent until a +runtime service binds it into an enforced profile it supports. This also closes one threat +directly: because the metadata layer is meant to be distributed and inspected, secrets are +never embedded in it. The operator injects them at deployment, which keeps secret material +out of the portable artifact entirely. + +## Components and where they sit on the boundaries + +Five components sit on or beside the trust boundaries: + +- Registry Manifest: the offline metadata producer. No production data, no auth, no + secrets. It lives entirely on the describe side of the primary boundary. +- Registry Relay: a read-only consultation gateway over sources. +- Registry Notary: claim evaluation, credential issuance, and federation. This is the + component that evaluates a modelled question, applies disclosure policy, and issues + credentials. +- Registry Platform: the shared security primitives that the runtime services build on. +- Registry Lab: demonstration only, running on fixture credentials. Treat it as out of + scope for production trust: its demo and template integrations are integration examples, + not a production freshness or replay-protection profile, and a team copying them into + production must add request freshness, expiry, or nonce checks itself. + +Security-critical primitives are concentrated in Registry Platform so that the behavior is +identical across services and reviewable in one place. This is a deliberate attack-surface +decision: it removes the risk of divergent, per-service security code that would each need +auditing separately. + +## Trust boundaries in detail + +**The service edge (authentication).** Authentication is the trust boundary at the service +edge. Each service runs exactly one authentication mode (either static-credential +fingerprint or OIDC) and authenticates every claim- or record-bearing route before +responding. The unauthenticated surfaces are the operational, discovery, and +protocol-bootstrap routes: liveness and readiness probes, the public verification keys +(JWKS / `did:web`), credential-issuer discovery, the OID4VCI wallet-flow endpoints, the +docs, public credential-status reads, and credential type metadata. The federation +evaluations route is exempt from the API-key/OIDC middleware but still requires a verified +peer-signed JWS, so it is not anonymous. + +**Notary to its sources.** A runtime boundary sits between Registry Notary and the sources +it reads. Notary calls Relay over HTTP as a data source; it does not link Relay code. The +only connectors are HTTP (`registry_data_api`, `dci`, and the source adapter sidecar); there +is no file or database connector. + +**The source adapter sidecar.** The sidecar is an *internal* trust boundary, not a +subject-authorization boundary, and this distinction matters for an auditor. Its bearer +token authenticates only the Notary-to-sidecar hop; it is not an end-user, tenant, or +subject authorization boundary. It is an internal, private deployment surface behind Notary, +not a public subject-facing API. Keeping the token off the public network and scoping its +credentials, cache, and backoff state is outside the security model (it is an operator +responsibility), and shared cache or backoff state across authorization contexts is not +something the model reasons about. + +**Federation between two Notary services.** Federation is a deliberate trust boundary +between two Notary services, and it is **static-peer only**: only configured static peers are +admitted. It is not a dynamic trust mesh. + +**Delegated self-attestation.** Delegated self-attestation is a distinct trust context from +peer federation; don't confuse the two. It does not delegate trust to a peer +Notary; it stays inside the citizen / OIDC trust context. + +## Assets protected by the design + +The assets the design sets out to protect are: + +- Private signing keys: The private signing key never leaves the issuer; only the public + half is published (through JWKS, and through `did:web` in Relay gateway mode). A verifier + never needs a credential to verify, which keeps verification off the protected side + entirely. +- Person-level source data: The source rows behind a claim are read in place and never + cross the boundary; only a computed, disclosure-shaped answer does. +- Audit integrity: Audit is treated as a security control rather than best-effort + logging (see Audit as a control). +- Secret material: Bearer tokens, raw credentials, and key material are kept out of both + the portable layer and client-facing surfaces. + +The project itself treats a specific set of threat classes as security-relevant, which is a +useful framing for "threats considered": authentication bypass, credential disclosure, +audit redaction or integrity failure, signing-key handling bugs, source-connector data +leakage, and privacy regressions that expose raw subject identifiers. + +## Threats the design considers and mitigates + +- Secret disclosure via stored credentials or timing: Static credentials are stored only + as SHA-256 fingerprints (never the raw secret) and compared in constant time. +- Token forgery / acceptance of untrusted tokens (OIDC mode): A token is trusted only + after signature verification against the configured issuer JWKS, plus issuer, audience, and + algorithm checks. The service still owns its route scopes regardless of the token. +- Privilege escalation / over-reach: Authorization is scope-based, deny-by-default, and + scope-before-source: a caller is refused before any source read or claim evaluation if it + lacks the required scope, and reach is never widened at request time. +- An existence / matching oracle: By default a matching failure collapses to a single + public reason (`evidence.not_available`), with the granular reason kept only in audit, so + the matching surface cannot be used to probe whether a record exists. A deployment may + disable this collapse only in a controlled environment; disabling it is a residual + confidentiality risk. +- Information leakage in error and cache surfaces: `problem+json` error bodies carry + stable codes only. Bearer tokens, private keys, source values, filesystem paths, and + internal error chains stay in protected operator logs, never in responses; principal-scoped + responses are not public cache entries. +- Over-collection and value injection in claim evaluation: The caller supplies only a + subject id and a claim id and must not supply the evaluated value; bindings read only the + fields they need and reject input paths outside a declared allow-list. +- Disclosure leakage: A redacted result carries neither the value nor the predicate + outcome. Selective-disclosure credentials carry SHA-256 digests of unselected fields, so a + holder cannot present an undisclosed field. Holder binding is configurable per credential + profile and defaults to off; where a profile enables it (using `did:jwk`), and on the + self-attestation issuance path, which requires it, the holder is bound by a fresh + audience-bound proof-of-possession. Binding applies only to an issued credential; the plain + evaluation result is not a credential and is never holder-bound. +- Self-attested data crossing a trust gate: Self-attestation and delegated + self-attestation derive subject, requester, and relationship from the authenticated + principal and scoped authorization details; caller-supplied `requester`, `relationship`, and + `on_behalf_of` fields are rejected before any source read, and self-asserted target fields do + not satisfy trusted-principal gates. +- Spoofed trust context and ungoverned reads (Relay side): Relay ignores trust-context + headers unless the principal is scoped to assert that exact value, and a governed read must + receive a PDP permit or fail closed with a stable `pdp.*` code rather than falling back to an + ungoverned read. +- SSRF / uncontrolled egress: Outbound source fetches are meant to be constrained by Registry + Platform's outbound-HTTP-policy. Where it is applied, it limits the destinations a source + connector may reach. This is recommended posture rather than a hard guarantee: confirm it + is enabled rather than assuming it. One detail an auditor should note: the guard exposes an + `allow_insecure_private_network` opt-in that, per source connection, re-enables plain-HTTP + private-network and link-local destinations the strict policy would otherwise refuse. Even + with that opt-in, cloud-metadata addresses (such as `169.254.169.254` and its IPv6 + equivalent) stay denied. Treat the opt-in as a deployment decision to review, not a default. +- Untrusted federation peers, replay, and wrong-purpose requests: Only configured static + peers are admitted, and before any source read the server verifies peer identity, request + signature, freshness, single use, purpose, profile, and audience. The single-use and + freshness checks are backed by a real replay-store primitive that tracks one-time JWT `jti` + and nonce values (federation request `jti`, OID4VCI `c_nonce`, and holder-proof `jti`). + +**Audit as a control.** Every request touching person-level data is recorded (principal, +request id, and `Data-Purpose`) and a deployment can run audit fail-closed, so an +unrecordable request does not return success. Two caveats for the auditor. First, this is a +capability a deployment can turn on, not a guarantee that every route in a given build has +been individually audited; treat per-route audit coverage as something to verify in your +deployment. Second, the security model expects the audit record to capture the scopes +exercised as well, but Notary's audit record does not yet include that field (Relay's does), +so Notary audit alone cannot reconstruct which scopes authorized a request. + +## Residual risks and what is left to the operator + +The canonical inventory of every current limit is [Known limitations and non-guarantees](../known-limitations/); the residual risks in this section are the subset a threat model must weigh. + +These are the risks the design does *not* close and states honestly: + +- Key custody is not certified by health checks: Readiness, liveness, and conformance + checks do not certify production-grade private-key custody. A deployment using software + keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet + not production-secure. Custody, rotation, and provider approval remain operator + responsibilities. +- The operator boundary is where coverage ends: Secret and key provisioning, key custody + and rotation, audit retention and storage, tenant isolation, TLS termination and + certificates, edge rate limiting, deployment configuration, and incident response are + operator responsibilities, not behavior the model defines. +- The sidecar depends on deployment-network controls: The source adapter sidecar relies + on deployment-network egress controls; the model does not enforce them. +- Aggregate routes are not a privacy budget: Relay aggregate routes do not constitute a + longitudinal privacy budget; v1 does not track cumulative disclosure across repeated or + overlapping aggregate queries. Do not describe a Relay aggregate as privacy-budgeted unless + a separate deployed control provides it. This is a residual disclosure risk left to the + operator. +- Some claim-definition invariants surface only at runtime: A disclosure default outside + the allowed set, a rule source naming an undeclared binding, and the unimplemented plugin + rule are not enforced at configuration load; they surface only at evaluation or request + time. Duplicate claim ids are different and worse: they are not rejected at load and are + never reported, because the runtime lookup returns the first match by id, so a later + duplicate is silently shadowed. A source binding with no matching policy resolves on + identifiers alone, with no purpose, relationship, or input-minimization gating. These are + residual misconfiguration risks left to the operator. +- Known product gaps: No revocation flow is specified (though an optional, + off-by-default credential-status surface can be enabled to mark a credential `revoked`), + there is no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure + workflow in this version. +- Admin reload is non-functional standalone: Notary's admin reload route returns HTTP 501 + (`registry.admin.capability.not_supported`) in the standalone router and performs no + reload, and non-swappable changes require a service restart. Some changes do hot-apply + without a restart, though: signing-key rotation and auth-policy swaps among them. + +## What is explicitly out of scope + +These are non-goals for this version. None of them should be read into a Registry Stack +conformance claim: + +- Dynamic federation: Dynamic trust-chain discovery, audit checkpoint exchange, and + federated credential issuance are out of scope. Federation is static-peer only: not a + dynamic trust mesh. Replay protection defaults to in-memory, with a Redis-backed shared + store available. +- Full Evidence Gateway / external-policy interoperability: Governed Relay PDP enforcement + covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`). + It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not + dynamic external policy discovery, and not enforcement of ODRL terms outside the supported + profile. Unsupported ODRL terms or invalid policy identity fail closed. +- A full credential issuer: The OID4VCI surface is a scoped self-attestation issuance + subset of OID4VCI Draft 13, not a full issuer or general external-wallet interoperability. + Notary advertises `openid4vci.support: not_full_issuer`, and delegated-attestation + transaction tokens are rejected at the credential endpoint. +- Certified standards compliance: Aligning with a standard is not conforming to it: + speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, W3C DID, or the rest does + not certify conformance to any of them, and CCCEV-shaped output is not conformant to CCCEV + 2.00. +- Feature-gated surfaces of other builds: Several runtime surfaces (the OGC API + Features/Records/EDR adapters and SP DCI sync) are feature-gated and mount only when the + build is configured with the matching Cargo feature. Do not infer their presence from the + route catalog of a different build. Relay signed response credentials are not Cargo-gated; + they are config-gated through `provenance`. Admin routes run on + a separate optional listener and are documented in the generated OpenAPI, but standalone mode + does not implement runtime config reload; it returns HTTP 501. + +## Related + +- [RS-SEC-G](../../spec/rs-sec-g/): the security model +- [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/): protocol contracts +- [RS-ARC-G](../../spec/rs-arc-g/): the two-layer architecture +- [RS-DM-CLAIM](../../spec/rs-dm-claim/): claim definitions and disclosure +- Hardening procedures *(Operate)* · Trust posture summary *(Trust & Security overview)* · [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) diff --git a/docs/site/src/content/docs/index.mdx b/docs/site/src/content/docs/index.mdx index 0e95fc1c..39b030a1 100644 --- a/docs/site/src/content/docs/index.mdx +++ b/docs/site/src/content/docs/index.mdx @@ -12,27 +12,42 @@ locale: en standards_referenced: [] --- -import HomeLanding from '../../components/HomeLanding.astro'; +Registry Stack adds registry-facing services, protected reads, +[governed evidence](reference/glossary/), and audit +records, over data an institution already holds. +The data never leaves the source system. -Registry Stack is for building registry-facing services over data an institution already holds. It -adds controlled read access, governed evidence responses, and audit records without turning the -registry into a shared database. +Start with the [quickstart](start/quickstart/): no install required, about five minutes. +Then take the [credential tour](start/credential-tour/). -Registry Stack has two runtime patterns: +## Two products -- **Protected Registry APIs** are scoped, read-only HTTP APIs over existing files, extracts, - databases, or legacy registry systems. Registry Relay implements this surface. -- **Evidence Gateway** is the governed evidence path. A runtime service checks request context, - source context, supported policy terms, freshness, redaction, and audit provenance before it - returns a claim result, credential, redacted response, protected read, or denial. Registry Notary - implements claim evaluation and credential issuance; governed Registry Relay routes use the same - Policy Decision Point (PDP) pattern for protected reads. +Registry Relay protects what an institution already holds. Registry Notary certifies answers drawn +from that data. +Take the quickstart first; the tutorials here are the follow-up, on your own machine. -Use these docs to build and operate both patterns. The product pages are pulled from each repository -at build time, so they match the code. For audience, problem, and ecosystem context, see -[registrystack.org](https://registrystack.org/). +### Registry Relay -Start with [See it live](start/see-it-live/): a hosted lab, zero install, one protected read and one -signed credential in under two minutes. +Registry Relay exposes protected, scoped, read-only APIs over existing sources: files, extracts, +databases, and legacy registry systems. - +[Your first Relay](tutorials/publish-spreadsheet-secured-registry-api/): run a protected registry +API locally over a sample spreadsheet. + +### Registry Notary + +Registry Notary certifies evidence: claim evaluation, credential issuance, disclosure policy, and +audit provenance. + +[Your first Notary](tutorials/verify-claim-registry-api/): evaluate a claim and compare the result +with a protected row read. + +## Security + +Registry Stack authorization is scope-based and deny-by-default. +Every request writes to a hash-chained audit envelope. +These docs are evidence-anchored: every claim about product behavior points to code, a fixture, a +test, or a specification requirement ID. + +Read the [security overview](security/) for the threat model and the public evidence a reviewer can +check. diff --git a/docs/site/src/content/docs/reference/apis/registry-notary.mdx b/docs/site/src/content/docs/reference/apis/registry-notary.mdx index 661159d2..0c7149b5 100644 --- a/docs/site/src/content/docs/reference/apis/registry-notary.mdx +++ b/docs/site/src/content/docs/reference/apis/registry-notary.mdx @@ -93,10 +93,8 @@ under `source_connections`: - **SP-DCI** connector sends DCI-shaped search requests and parses the response using configured field paths. - **Source adapter sidecar** connector calls a private sidecar over HTTP, using a static - bearer token, for sources reached through the built-in `http_json`, `http_flow`, or `fhir` - engines. (Note: OpenFn as an external caller, where OpenFn workflows call Registry Notary, remains - supported; what is retired is the OpenFn Node worker pool as a source-reading engine inside the - sidecar.) + bearer token, for sources reached through the built-in `http_json`, `http_flow`, `fhir`, or + `script_rhai` engines. There is no generic file-based or database connector; source connectors must reach their targets over HTTP. @@ -113,9 +111,9 @@ path cannot affect another. - **Holder binding.** `did:jwk` is the only supported proof-of-possession binding method. - **Audit events.** Every evaluated request emits an `EvidenceAuditEvent` in a Registry Platform audit envelope to a configured sink (stdout, file, or JSONL). -- **Demo helpers.** Demo OpenFn snippets and generated workflow helpers are examples, not production - replay-protection profiles. Add freshness, expiry, or nonce checks before copying a helper into a - production workflow. +- **Demo helpers.** Generated workflow helpers are examples, not production replay-protection + profiles. Add freshness, expiry, or nonce checks before copying a helper into a production + workflow. :::caution Audit write failures surface as request errors, not as silent log entries. If the audit sink is diff --git a/docs/site/src/content/docs/reference/environment-variables.mdx b/docs/site/src/content/docs/reference/environment-variables.mdx index d621ac5a..5d932e76 100644 --- a/docs/site/src/content/docs/reference/environment-variables.mdx +++ b/docs/site/src/content/docs/reference/environment-variables.mdx @@ -47,7 +47,7 @@ The audit hash secret and other secret material are read from operator-named var ## registryctl -registryctl reads the variables below. It also reads operator-provided source and credential variables whose names are passed to `registryctl openfn` and `registryctl init notary` flags; those are not fixed names. +registryctl reads the variables below. It also reads operator-provided source and credential variables whose names are passed to `registryctl init notary` and legacy compatibility flags; those are not fixed names. | Name | Purpose | Default or required | | --- | --- | --- | @@ -56,14 +56,14 @@ registryctl reads the variables below. It also reads operator-provided source an | `REGISTRYCTL_VERSION` | Pinned release the installer downloads. Read by the install script, not the running binary. | Defaults to the installer's pinned release. | | `CI` | When set to a non-empty value other than `0` or `false`, disables the automatic update check. | Optional. | -registryctl also passes through operator-named source and sidecar variables. The defaults below are the variable names registryctl writes into generated projects and OpenFn snippets; the operator can override them with flags. +registryctl also passes through operator-named source and sidecar variables. The defaults below are the variable names registryctl writes into generated projects and compatibility snippets; the operator can override them with flags. | Name | Purpose | Default or required | | --- | --- | --- | | `EVIDENCE_SOURCE_API_TOKEN` | Default source API bearer token variable for a `registry-data-api` Notary starter project. | Default name; override with `init notary --source-token-env`. | | `FHIR_SIDECAR_TOKEN` | Default source token variable for a `fhir-sidecar` Notary starter project. | Default name; override with `init notary --source-token-env`. | -| `OPENFN_TOKEN` | OpenFn API token for `registryctl openfn import` URL imports. | Default name; override with `--openfn-token-env`. | -| `OPENFN_SIDECAR_TOKEN` | Raw notary-to-sidecar bearer token written into the generated snippet. | Default name; override with `--sidecar-token-env`. | +| `OPENFN_TOKEN` | Legacy OpenFn API token name for `registryctl openfn import` URL imports. | Default name; override with `--openfn-token-env`. | +| `OPENFN_SIDECAR_TOKEN` | Legacy notary-to-sidecar bearer token name written into compatibility snippets. | Default name; override with `--sidecar-token-env`. | | `DEV_SIDECAR_TOKEN_HASH` | Notary-to-sidecar bearer token hash. | Default name; override with `--auth-hash-env`. | | `REGISTRY_NOTARY_BASE_URL`, `REGISTRY_NOTARY_BEARER_TOKEN`, `REGISTRY_NOTARY_API_KEY`, `REGISTRY_NOTARY_PURPOSE` | Shell exports emitted by `registryctl lab env` for hosted-lab SDK quickstarts. | Emitted by `lab env`; these are public synthetic lab values. | diff --git a/docs/site/src/content/docs/reference/itb-semic-evidence.mdx b/docs/site/src/content/docs/reference/itb-semic-evidence.mdx index a1958dcf..0612ee41 100644 --- a/docs/site/src/content/docs/reference/itb-semic-evidence.mdx +++ b/docs/site/src/content/docs/reference/itb-semic-evidence.mdx @@ -36,9 +36,8 @@ It is not a certification statement, and it does not claim every generated artif [jeremi/registry-manifest#27](https://github.com/jeremi/registry-manifest/pull/27). - Registry Notary records the protected discovery policy and GITB runtime-suite target in [jeremi/registry-notary#245](https://github.com/jeremi/registry-notary/pull/245). -- The Manifest follow-up issues for BRegDCAT warnings are - [publisher CorporateBodyRestriction](https://github.com/jeremi/registry-manifest/issues/23) and - [`dcat:themeTaxonomy`](https://github.com/jeremi/registry-manifest/issues/24). +- Registry Manifest documents the BRegDCAT warning boundary in the Manifest + ITB and SEMIC validation guide. ## What is not covered diff --git a/docs/site/src/content/docs/reference/registryctl.mdx b/docs/site/src/content/docs/reference/registryctl.mdx index 6caec390..6587217b 100644 --- a/docs/site/src/content/docs/reference/registryctl.mdx +++ b/docs/site/src/content/docs/reference/registryctl.mdx @@ -89,9 +89,12 @@ Smoke commands run built-in local checks and write a JSON result file. | `notary smoke` | Run built-in local Notary smoke checks. | | `notary open` | Open or print the local Notary API docs URL. | -## OpenFn +## Legacy OpenFn conversion -`openfn` converts OpenFn workflow exports into Registry Notary OpenFn sidecar runtime files. The flags below apply to both `openfn import` and `openfn convert` unless noted. +`openfn` is a compatibility command for converting existing OpenFn workflow exports into sidecar +runtime files. Current source-adapter projects use the built-in `http_json`, `http_flow`, `fhir`, +or `script_rhai` engines instead. The flags below apply to both `openfn import` and `openfn convert` +unless noted. | Subcommand or flag | Purpose | | --- | --- | diff --git a/docs/site/src/content/docs/security/hardening-checklist.mdx b/docs/site/src/content/docs/security/hardening-checklist.mdx new file mode 100644 index 00000000..1a1ab860 --- /dev/null +++ b/docs/site/src/content/docs/security/hardening-checklist.mdx @@ -0,0 +1,171 @@ +--- +title: Harden a production deployment +description: An actionable checklist for the operator responsibilities RS-SEC-G Section 9 leaves to your deployment, grouped by key custody, audit, transport, deployment profile, and incident response. +status: draft +owner: registry-docs +source_repos: + - registry-relay + - registry-notary + - registry-platform +last_reviewed: "2026-07-02" +doc_type: how-to +locale: en +standards_referenced: [] +--- + +Registry Relay and Registry Notary enforce authentication, authorization, key publication, and +audit as software controls (see [Security overview](../)). +[RS-SEC-G](../../spec/rs-sec-g/) Section 9 draws a line at the deployment: secret and key +provisioning, key custody and rotation, audit retention and storage, tenant isolation, transport +termination and certificate management, edge rate limiting, deployment configuration, and incident +response stay with the operator. +This page turns that boundary into a checklist you work through before you call a deployment +production-ready. + +## When to use this + +Use this checklist before a `production` or `evidence_grade` deployment goes live, and again after +any change to signing keys, audit sinks, network placement, or the declared deployment profile. It +assumes you have already completed the [Registry Relay configuration reference](../../products/registry-relay/configuration/) +or the [Registry Notary operator configuration reference](../../products/registry-notary/operator-config-reference/) +for your service. + +## Before you start + +- Decide the deployment profile (`local`, `hosted_lab`, `production`, or `evidence_grade`) you are + claiming; see the deployment profile section of the + [Registry Relay configuration reference](../../products/registry-relay/configuration/). The + profile is never inferred from hostname or network position; you declare it. +- Have `registry-notary doctor` (Notary) or your Relay startup logs available to check gate + findings as you work through each item. + +## Keys and custody + +- Configure signing keys through the provider abstraction (`local_jwk_env`, `file_watch`, or + `pkcs11`) under `evidence.signing_keys`, not as inline YAML values. Startup fails closed if an + active signing provider cannot be constructed. See the + [signing key provider reference](../../products/registry-notary/signing-key-provider/). +- Provision secrets (API-key fingerprints, source tokens, private JWKs, the audit hash secret) only + through the environment variables named in config. For local development the binary accepts + `--env-file`; for shared environments use the platform secret store and do not check dotenv files + into the repository. +- Rotate a signing key by adding a new `kid`, moving credential profiles to it, then moving the old + key to `publish_only` until its configured publication window ends or verifiers no longer need + it. Do not delete a key verifiers may still need. +- Keep RS256 scoped to the eSignet RP client assertion key (eSignet, the MOSIP-project identity + provider, is the one integration that needs RS256). Credential profile, access token, and + federation response signing use Ed25519 EdDSA or ES256/P-256; RS256 is rejected there. +- If a signing key uses `provider: pkcs11`, confirm the deployed binary was built with the + `pkcs11` capability (`registry-notary build-info`) and validate the module, token, and key lookup + with `registry-notary doctor --config `. +- Relay: confirm every env-backed `fingerprint.name` referenced in config exists in the runtime + environment, and that no raw key, fingerprint, private JWK, or full environment dump reaches a + log line; see the production checklist in the + [Registry Relay configuration reference](../../products/registry-relay/configuration/). + +## Audit sink and retention + +- Configure a durable audit sink (`file` or `syslog`, not bare `stdout`) before you declare + `production` or `evidence_grade`. A missing durable sink trips `relay.audit.sink_missing` / + `notary.audit.sink_missing`, which fails startup at `evidence_grade` (`startup_fail` for both + services, and already `startup_fail` at `production` for Notary). +- Set `audit.hash_secret_env` to an environment variable holding at least 32 bytes of + deployment-specific random secret material. Relay startup fails closed if it is missing, empty, + unset, or weak. +- Keep `audit.write_policy: fail_closed` (the default) unless an explicit availability exception + accepts best-effort audit durability under `availability_first`. `fail_closed` means a request + whose audit record cannot be written returns `503 audit.write_failed` instead of a success + (REQ-SEC-G-009). +- If you rotate the audit hash secret, retain the old secret under your audit retention controls + for any period during which older records must remain comparable to new ones, or accept that new + records will not match old audit handles. +- Hash-chained envelopes (`prev_hash`, `record_hash`) detect ordering gaps and accidental + corruption, but they do not protect against an actor who can rewrite the audit sink itself. Use + an append-only external sink or independent tail-hash anchoring when you need stronger integrity + than the envelope alone provides. + +## Transport and edge + +- Terminate TLS and manage certificates at your reverse proxy or load balancer; RS-SEC-G Section 9 + leaves transport termination and certificate management to the operator. +- Keep the admin listener private. A publicly exposed admin surface trips + `relay.admin.public_exposure` or `notary.admin.shared_exposure`, both `startup_fail` at + `evidence_grade`. +- Set explicit `cors.allowed_origins`. The default CORS policy is deny by omission; add only the + origins your integration actually needs. +- Enable `server.trust_proxy` with an explicit `trusted_proxies` list only when a reverse proxy + sits in front of Relay. Both `enabled` and `trusted_proxies` default to off/empty. +- Enforce ingress rate limiting at your gateway or edge, then either declare + `deployment.evidence.ingress_rate_limit: true` or accept the `relay.ingress.rate_limit_missing` + finding it otherwise raises. The flag defaults to `false`. +- Leave `allow_insecure_localhost` and `allow_insecure_private_network` false on Notary source + connections in production unless a deployment review explicitly accepts the private-network + source; a plain `http://` source with neither allowance trips `notary.source.insecure_url`, and + enabling the escape hatch trips `notary.source.private_network_escape`. +- For Postgres sources, require `sslmode=require` on the connection string and use read-only + database credentials; for live materialization, scope that role to `SELECT` only the configured + table or view. +- Apply the Registry Platform HTTP-security response headers and outbound-HTTP-policy primitive on + source-connector egress; both are RS-SEC-G recommendations (REQ-SEC-G-012), not an enforced + default, so confirm your build turns them on. + +## Deployment profile and posture endpoint + +- Declare `deployment.profile` explicitly. An omitted profile binds no gates and only raises a + `deployment.profile_undeclared` warning, so a silent gap stays silent until you declare a + profile. +- Treat `evidence_grade` as requiring a signed, governed config bundle: running it from a plain + local YAML file trips `relay.config.unsigned` / `notary.config.unsigned` and the process refuses + to start. +- Review `GET /admin/v1/posture` regularly. It reports the declared profile, active findings, and + active waivers, so the deployment's actual state is inspectable rather than asserted. +- Give every waiver a non-empty, non-secret reason and a mandatory expiry date. `startup_fail` + gates are never waivable; a waiver naming one is rejected. Waiver reasons appear only in the + restricted posture tier. +- Use `deployment.evidence.*` flags (for example `ingress_rate_limit`, `api_key_rotation`) only to + assert controls that live outside the process and cannot be observed by it. Each flag defaults to + `false`. +- If a Notary instance runs active-active with peers, declare `multi_instance` and use Redis-backed + replay storage. In-memory replay under a multi-instance or federated declaration trips + `notary.replay.in_memory_high_risk`. + +## Incident response + +- Know your private disclosure channel before you need it: see + [Report a vulnerability](../report-a-vulnerability/). +- If credential-status is enabled, use the admin status endpoint (requires the + `registry:notary:admin` scope) to mark a compromised credential `revoked`. Registry Notary + defines no other revocation flow; a credential issued with credential-status disabled cannot be + revoked through the API. See [Known limitations and non-guarantees](../../explanation/known-limitations/). +- Rotate a compromised signing key using the documented rotation procedure (new `kid`, old key to + `publish_only`) rather than deleting it outright, so previously issued, still-valid credentials + remain verifiable through the rotation. +- Preserve the audit trail for post-incident review: hash-chained, fail-closed audit records are + the record a deployment reconstructs a request timeline from (REQ-SEC-G-008, REQ-SEC-G-009). + +## Verify + +- Start the service against your production config and confirm it starts (or fails closed as + expected for a deliberately unmet `startup_fail` gate). +- Query `GET /admin/v1/posture` and read the `deployment` object: confirm the profile matches what + you declared, and that no unwaived finding at or above your profile's threshold is outstanding. +- Send a request that should be audited and confirm a corresponding record lands in your configured + sink, not only `stdout`. +- Attempt an admin-listener request from outside the private network it is meant to be bound to, + and confirm it is refused. + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| Process refuses to start after declaring `production` or `evidence_grade` | A `startup_fail` gate tripped (for example missing audit sink or unsigned config) | Check the posture or startup log for the finding id, then fix the underlying condition; `startup_fail` gates cannot be waived | +| Posture reports `deployment.profile_undeclared` | No `deployment.profile` set | Declare a profile explicitly; an omitted profile is not the same as `local` | +| Posture reports `deployment.waiver_expired` | A waiver's `expires` date passed | Re-review the finding and either fix it or issue a new waiver with a new expiry | + +## Next + +- [Security overview](../) +- [Report a vulnerability](../report-a-vulnerability/) +- [Registry Relay configuration reference](../../products/registry-relay/configuration/) +- [Registry Notary operator configuration reference](../../products/registry-notary/operator-config-reference/) +- [Known limitations and non-guarantees](../../explanation/known-limitations/) diff --git a/docs/site/src/content/docs/security/index.mdx b/docs/site/src/content/docs/security/index.mdx new file mode 100644 index 00000000..d0636e6f --- /dev/null +++ b/docs/site/src/content/docs/security/index.mdx @@ -0,0 +1,152 @@ +--- +title: Security overview +description: "The enforced security model of Registry Stack, the responsibilities that stay with the operator, and the public evidence a security reviewer can check." +status: draft +owner: registry-docs +source_repos: + - registry-relay + - registry-notary + - registry-platform +last_reviewed: "2026-07-02" +doc_type: explanation +locale: en +standards_referenced: [] +--- + +Use this page to assess the security posture of Registry Stack before you deploy it or integrate +with it. +It summarizes the model the software enforces, names the responsibilities that stay with the +operator, and links the public evidence a reviewer can verify. +The normative source for everything on this page is the security model specification +[RS-SEC-G](../spec/rs-sec-g/); this page is its narrative summary. + +## At a glance + +| What | Where it stands | +|------|-----------------| +| Authentication on every route that touches a record or a claim | Built in | +| Deny-by-default authorization, checked before any data is read | Built in | +| Only the public half of a signing key is ever published | Built in | +| Person-level access is written to an audit log | Built in (fail-closed is yours to switch on) | +| Three disclosure modes, with `redacted` revealing neither value nor answer | Built in | +| Selective-disclosure credentials, with holder binding configurable per profile (off by default) | Built in | +| Federation limited to peers you configure | Built in | +| Hardened HTTP headers and outbound-traffic limits | Recommended, confirm your build applies them | +| Key custody, retention, isolation, TLS, rate limiting | Your deployment's job | +| Compliance with any external standard it cites | Not claimed | + +## The model in one pass + +Every request that returns person-level records or claim results moves through the same pipeline: +authenticate, authorize, serve, audit. + +- Authenticate: a runtime service runs exactly one authentication mode, fixed by + configuration: static credentials or OIDC (REQ-SEC-G-002). Static credentials are configured as + SHA-256 fingerprints, never raw secrets, and compared in constant time (REQ-SEC-G-003). OIDC + tokens are trusted only after signature, issuer, audience, and algorithm checks (REQ-SEC-G-004). +- Authorize: authorization is scope-based and deny-by-default. The required scope is enforced + before any source is read, and a caller's reach is never widened at request time + (REQ-SEC-G-005, REQ-SEC-G-006). +- Serve: the service returns the configured surface: a protected read, a claim result, a + selected value, a signed credential, or a denial. Every claim is answered in one of three + disclosure modes: `value` hands back the computed value, `predicate` returns only a yes/no, and + `redacted` returns neither the value nor the yes/no; the claim's policy fixes which modes are + allowed. [Disclosure modes and computed answers](../explanation/disclosure-modes-and-computed-answers/) + covers this in depth. +- Audit: the request is recorded in a hash-chained audit envelope capturing the caller + principal, the scopes exercised, a request identifier, and the declared purpose + (REQ-SEC-G-008). A deployment can run + audit fail-closed, so a request whose audit record cannot be written does not succeed + (REQ-SEC-G-009). + +Only liveness and readiness probes and public verification-key discovery are served without +authentication. +Issuers sign with asymmetric keys and publish only the public half, so any verifier can check an +issued credential without holding a credential of its own (REQ-SEC-G-007). + +When Notary issues a credential it is an SD-JWT VC: the signed body carries hashes of each +disclosable field rather than the values, so the holder reveals only what they choose. +Holder binding, which cryptographically ties a credential to a key the holder controls, is +configurable per credential profile and defaults to off; the self-attestation +(wallet) issuance path requires it. +See [Evidence issuance, end to end](../explanation/evidence-issuance/) for the full credential +lifecycle. + +Delegated evaluation, where one Notary asks a trusted peer to evaluate a claim against a source +only that peer can read, is admitted only from statically configured peers, and the +peer's identity, signature, freshness, and purpose are verified before any source read +(REQ-SEC-G-010). + +## What's recommended, not enforced + +Two behaviors are recommended rather than required, so confirm them rather than assume them. + +- Registry Platform sourcing: security-critical primitives (authentication, OIDC verification, + audit envelopes, HTTP security, outbound HTTP policy, cryptography, and SD-JWT VC helpers) are + meant to come from Registry Platform rather than be reimplemented per service, so they behave + the same everywhere and can be reviewed in one place. RS-SEC-G recommends this rather than + forcing it (REQ-SEC-G-001), so confirm a given service actually does it rather than + reimplementing its own. +- HTTP hardening: security response headers and limits on outbound source-fetch traffic are + recommended posture (REQ-SEC-G-012). Check that your build applies them rather than assuming + it. + +## Declared deployment assurance + +A deployment states the assurance level it claims; the software then enforces gates that match the +claim. +Both runtime services accept a `deployment` block with a profile of `local`, `hosted_lab`, +`production`, or `evidence_grade`. +Each profile binds a findings catalog at escalating severities: a `production` deployment with no +durable audit sink refuses to start, and an `evidence_grade` deployment refuses to start from an +unsigned local config file. +Findings, waivers (each with a mandatory reason and expiry), and the declared profile are reported +by the operations posture endpoint, so the deployment's actual state is inspectable rather than +asserted. +The gate catalogs are documented in the +[Registry Relay configuration reference](../products/registry-relay/configuration/) and the +[Registry Notary operator configuration reference](../products/registry-notary/operator-config-reference/). + +## What stays with the operator + +The security model ends where the deployment begins. +Secret and key provisioning, key custody and rotation schedules, audit retention and storage, +tenant isolation, transport termination and certificate management, edge rate limiting, deployment +configuration, and incident response are operator responsibilities (RS-SEC-G Section 9). +The software keeps secrets out of distributable artifacts (REQ-SEC-G-013) and provides the +primitives; the operator provisions, configures, and operates them. +[Harden a production deployment](hardening-checklist/) turns this boundary into an actionable +checklist. + +## Evidence for reviewers + +Public, checkable evidence, from most to least formal: + +- [RS-SEC-G](../spec/rs-sec-g/) is the normative security model; [RS-PR-RELAY](../spec/rs-pr-relay/) + and [RS-PR-NOTARY](../spec/rs-pr-notary/) carry its wire-level form per service. +- [Security self-assessment](self-assessment/) is a CNCF TAG Security-style maintainer + self-assessment, including known gaps. +- [OpenSSF and release trust](openssf-evidence/) records which release-integrity checks are + verifiable today (signatures, provenance, Scorecard) and which are still incomplete. +- The [standards register](../reference/standards/) records the adoption mode and evidence for each + cited standard. +- [DPI safeguards alignment](../explanation/dpi-safeguards-alignment/) maps the stack to safeguards + language for review programs. + +These documents state known gaps alongside implemented controls. +None of them is a certification, and the hosted lab is a synthetic-data demo, not a production +assurance claim. + +## Report a vulnerability + +Suspected vulnerabilities go through the private disclosure process, never public issues or pull +requests. +See [Report a vulnerability](report-a-vulnerability/). + +## Next + +- [Harden a production deployment](hardening-checklist/) +- [Threat model](../explanation/threat-model/): the boundaries, assets, and threats behind this posture +- [Known limitations and non-guarantees](../explanation/known-limitations/): the full list of edges +- [RS-SEC-G: Registry family security model](../spec/rs-sec-g/) +- [Security self-assessment](self-assessment/) diff --git a/docs/site/src/content/docs/reference/openssf-evidence.mdx b/docs/site/src/content/docs/security/openssf-evidence.mdx similarity index 100% rename from docs/site/src/content/docs/reference/openssf-evidence.mdx rename to docs/site/src/content/docs/security/openssf-evidence.mdx diff --git a/docs/site/src/content/docs/security/report-a-vulnerability.mdx b/docs/site/src/content/docs/security/report-a-vulnerability.mdx new file mode 100644 index 00000000..30c57f96 --- /dev/null +++ b/docs/site/src/content/docs/security/report-a-vulnerability.mdx @@ -0,0 +1,89 @@ +--- +title: Report a vulnerability +description: How to report a suspected Registry Stack vulnerability privately, what is in scope, and how to verify a release you downloaded. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-07-02" +doc_type: how-to +locale: en +standards_referenced: [] +--- + +Report a suspected Registry Stack vulnerability privately. Never open a public issue or pull +request for a suspected credential disclosure, auth bypass, audit redaction failure, connector +data leakage, or signing-key handling bug. + +## When to use this + +Use the private disclosure channel for anything in scope below. For a known, documented pilot +limitation that does not create an exploitable issue beyond what is already documented, file a +regular product-gap issue instead; see [Known limitations and non-guarantees](../../explanation/known-limitations/). + +## Before you start + +Gather what the report needs before you open the channel: + +- The affected commit or release tag. +- The config shape involved (redact secret values before you paste anything). +- Reproduction steps. +- The impact you observed or expect. + +Do not include live credentials, bearer tokens, API keys, private keys, or raw registry records in +the report. + +## Steps + +1. Report privately through GitHub Security Advisories at + `https://github.com/registrystack/registry-stack/security/advisories/new`. +2. If GitHub Security Advisories is unavailable, contact the maintainer through an existing + private project channel instead of opening a public issue or pull request. +3. Include the affected commit, config shape, reproduction steps, and impact from the checklist + above. + +Registry Stack aims to acknowledge private reports within 5 business days. + +## What is in scope + +Authentication bypass, credential disclosure, audit redaction failure, audit integrity failure, +signing-key handling bugs, source connector data leakage, and privacy regressions that expose raw +subject identifiers. + +Known pilot limitations, such as no revocation service, no `/.well-known/jwt-vc-issuer` endpoint, +and no built-in data-subject erasure workflow, are product gaps, not vulnerabilities, unless they +create an exploitable security or privacy issue beyond the documented limitation. The source +adapter sidecar also relies on deployment-network egress controls for outbound source traffic; see +the sidecar's README at `crates/registry-notary-source-adapter-sidecar/README.md` in the +repository. + +## Verify a release signature + +For checking what you actually downloaded: Registry Stack release assets are signed by the release +workflow with keyless Sigstore cosign, and tag-triggered releases also publish SLSA provenance. +For each signed release asset, download the asset, its `.sig` signature, and its `.pem` +certificate from the GitHub Release, then verify: + +```bash +asset=registryctl-v0.8.3-linux-amd64 # replace with the asset you downloaded + +cosign verify-blob \ + --certificate "${asset}.pem" \ + --signature "${asset}.sig" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github.com/registrystack/registry-stack/.github/workflows/release.yml@refs/tags/v[0-9]+[.][0-9]+[.][0-9]+.*$' \ + "${asset}" +``` + +A successful check prints `Verified OK`. + +If a release asset has no matching `.sig` and `.pem` files, treat it as unsigned; the `v0.8.0` +prerelease predates release-asset signing. Git version tags are not yet cryptographically signed. +The full verification steps, including SLSA provenance, are in `SECURITY.md` and +`release/VERIFY.md` in the repository. + +## Next + +- [Security overview](../) +- [Harden a production deployment](../hardening-checklist/) +- [OpenSSF and release trust](../openssf-evidence/) diff --git a/docs/site/src/content/docs/reference/security-self-assessment.mdx b/docs/site/src/content/docs/security/self-assessment.mdx similarity index 94% rename from docs/site/src/content/docs/reference/security-self-assessment.mdx rename to docs/site/src/content/docs/security/self-assessment.mdx index 7cba3737..f333827e 100644 --- a/docs/site/src/content/docs/reference/security-self-assessment.mdx +++ b/docs/site/src/content/docs/security/self-assessment.mdx @@ -76,7 +76,7 @@ Operator responsibilities include: ## Project compliance -Registry Stack documents standards alignment in [Standards](../standards/) and +Registry Stack documents standards alignment in [Standards](../../reference/standards/) and the public spec register in [Specifications](../../spec/). These pages describe implementation evidence and known gaps; they do not claim legal compliance by themselves. @@ -122,10 +122,10 @@ No public review ledger yet. Primary public evidence: - [RS-SEC-G](../../spec/rs-sec-g/) -- [Contracts](../contracts/) +- [Contracts](../../reference/contracts/) - [OpenSSF evidence](../openssf-evidence/) -- [Registry Notary API reference](../apis/registry-notary/) -- [Registry Relay API reference](../apis/registry-relay/) +- [Registry Notary API reference](../../reference/apis/registry-notary/) +- [Registry Relay API reference](../../reference/apis/registry-relay/) - GitHub Release assets, with generated release capsules after the first capsule-producing release run - [Release verification](https://github.com/registrystack/registry-stack/blob/main/release/VERIFY.md) diff --git a/docs/site/src/content/docs/start/credential-tour.mdx b/docs/site/src/content/docs/start/credential-tour.mdx new file mode 100644 index 00000000..e23a5f10 --- /dev/null +++ b/docs/site/src/content/docs/start/credential-tour.mdx @@ -0,0 +1,111 @@ +--- +title: Get a credential from the hosted lab +description: In the hosted lab, watch Registry Notary issue a signed, privacy-preserving credential to a hosted demo wallet, with zero install. +status: current +owner: registry-docs +source_repos: + - registry-lab + - registry-notary +last_reviewed: "2026-06-28" +doc_type: tutorial +locale: en +standards_referenced: + - oid4vci + - sd-jwt-vc +--- + +import QuickstartMeta from '../../../components/QuickstartMeta.astro'; + +In the hosted lab, **Registry Notary** issues a signed credential to a hosted demo wallet that +answers a question without exposing the underlying record. This is a guided browser flow, not a +curl sequence: a real [OID4VCI](../../reference/glossary/) issuance needs a holder key and +a sign-in, which is wallet territory. You do not need to install a wallet: the lab hosts a demo +wallet at [wallet.lab.registrystack.org](https://wallet.lab.registrystack.org), so you can receive +the credential end to end in the browser. Everything runs in the hosted lab at +[lab.registrystack.org](https://lab.registrystack.org), so this page has no setup on your machine. + + + +This lab uses synthetic data and public demo-only credentials by design. +Do not reuse anything you copy here outside the lab. + +## What is running + +The lab runs a citizen **Notary** you will use in this tutorial: an issuer that hands out a +privacy-preserving credential instead of the raw record, at +[citizen-notary.lab.registrystack.org](https://citizen-notary.lab.registrystack.org). The lab also +runs a health-program Notary in front of a demo DHIS2 health information system, used only by the +DHIS2 integration tutorial, at +[dhis2-notary.lab.registrystack.org](https://dhis2-notary.lab.registrystack.org). For how these +connect to the rest of the stack, see the +[architecture overview](../../explanation/architecture/). For the full set of live demo +credentials, identities, and ready-made requests, open the lab homepage at +[lab.registrystack.org](https://lab.registrystack.org). + +## Get a signed credential + +The citizen Notary issues a Selective Disclosure JWT Verifiable Credential +([SD-JWT VC](../../reference/glossary/)) of type `person_is_alive_sd_jwt`, media type +`application/dc+sd-jwt`. The credential discloses a single predicate, whether +the person is alive, as a true or false claim. It never hands over the underlying civil record. +The [quickstart](../quickstart/) Relay row-read example uses `NID-1001` / Miguel Santos, while this +wallet flow uses the lab wallet identity `NID-2001` / Maria Santos. + +Start in the **Wallet test** section of [lab.registrystack.org](https://lab.registrystack.org), +which links the credential flow end to end: start the citizen Notary flow, sign in, then paste the +generated credential offer into the hosted wallet. The Notary requires you to sign in before it +issues. The sign-in runs through eSignet, the lab's hosted demo identity provider; you do not need +an account, and the values below are synthetic demo identities: + +- National ID: `NID-2001` +- Name: Maria Santos +- Login and OTP code: `111111` +- PIN: `545411` + +After sign-in, the hosted wallet receives the signed `person_is_alive_sd_jwt` credential, with the +alive predicate set to true. If you prefer your own OID4VCI-compatible wallet, you can open the +credential offer directly instead: + +```text +https://citizen-notary.lab.registrystack.org/oid4vci/credential-offer?credential_configuration_id=person_is_alive_sd_jwt +``` + +### The negative control + +Try this in the same browser flow. Self-attestation binds the credential subject to the identity you +signed in as. If you sign in as `NID-2001` but ask for a credential bound to `NID-1001`, the Notary +refuses: it will not issue a credential for a subject you did not authenticate as. The issuer cannot +be talked into vouching for someone else. For more on this service, see +[Registry Notary](../../products/registry-notary/). + +### DHIS2 integration path + +The lab also runs a DHIS2-backed Notary for API credential and claim-evaluation work. +Because that path is about integrating an existing source system, it lives in its own tutorial: +[Configure DHIS2 claim checks](../../tutorials/configure-dhis2-claim-checks/). + +## What you built + +You watched Registry Notary issue a signed, privacy-preserving Selective Disclosure JWT Verifiable +Credential to a hosted demo wallet: the credential discloses a single true or false predicate, +whether the person is alive, without exposing the underlying civil record. Self-attestation held: signing +in as `NID-2001` and asking for a credential bound to `NID-1001` was refused, because the issuer will +not vouch for a subject you did not authenticate as. + +## Next + +- [Evaluate a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): run a Notary + against the Relay you published and evaluate a claim. +- [Run a protected registry API locally](../../tutorials/publish-spreadsheet-secured-registry-api/): + stand up your own protected Relay from a sample workbook. + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| The hosted lab or a service URL does not respond | n/a | Run the same multi-service topology locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/) | diff --git a/docs/site/src/content/docs/start/quickstart.mdx b/docs/site/src/content/docs/start/quickstart.mdx new file mode 100644 index 00000000..98137412 --- /dev/null +++ b/docs/site/src/content/docs/start/quickstart.mdx @@ -0,0 +1,150 @@ +--- +title: Quickstart +description: With zero install, read a protected registry API from your terminal against the public hosted lab and watch Registry Relay enforce scope. +status: current +owner: registry-docs +source_repos: + - registry-lab + - registry-relay +last_reviewed: "2026-06-28" +doc_type: tutorial +locale: en +standards_referenced: [] +--- + +import QuickstartMeta from '../../../components/QuickstartMeta.astro'; + +In about five minutes you can read a protected registry API from your terminal, with zero install, +against a public hosted lab. You will call **Registry Relay**, which returns records to authorized +callers, and watch it enforce scope: the same API answers some requests and refuses others, +depending on what the caller's token allows. Everything runs in the hosted lab at +[lab.registrystack.org](https://lab.registrystack.org), so this page has no setup on your machine. + + + +This lab uses synthetic data and public demo-only credentials by design. +Do not reuse anything you copy here outside the lab. + +## What is running + +The lab runs a civil-registry **Relay** you will call directly in this tutorial: a protected, +scoped, read-only HTTP API over a civil registry source, at +[civil-relay.lab.registrystack.org](https://civil-relay.lab.registrystack.org). The lab also runs +**Registry Notary** services for the next step in this journey; see +[Get a credential from the hosted lab](../credential-tour/). For how these connect to the rest of +the stack, see the [architecture overview](../../explanation/architecture/). For the full set of +live demo credentials, identities, and ready-made requests, open the lab homepage at +[lab.registrystack.org](https://lab.registrystack.org). + +:::note +Lab tokens are public demo-only values that rotate, so this page shows placeholders, never live +tokens. Get the current tokens and identities from the **For developers** section near the bottom +of the lab homepage, which shows every demo credential with a copy button and a prebuilt curl +example. The same data is available +unauthenticated as JSON at +[lab.registrystack.org/api/lab.json](https://lab.registrystack.org/api/lab.json). +::: + +## Get your bearer tokens + +The civil Relay authenticates callers with `Authorization: Bearer `. Each token carries a +scope, so the same API answers some requests and refuses others. + +Open [lab.registrystack.org](https://lab.registrystack.org), scroll to the **For developers** +section near the bottom, and copy two bearer tokens from the demo credential cards: a +metadata-scope token and a row-reader token. Set them in your shell. The placeholders below stand +in for the live tokens. + +```sh +export CIVIL_METADATA_TOKEN="" +export CIVIL_ROW_READER_TOKEN="" +``` + +## List datasets (metadata scope) + +The metadata-scope token can list the datasets the Relay publishes: + +```sh +curl -sS \ + -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ + https://civil-relay.lab.registrystack.org/v1/datasets +``` + +The Relay returns `200 OK` with the catalog. Each dataset entry carries more fields than shown; +they are omitted here for brevity: + +```json +{ "data": [ { "dataset_id": "civil_registry" } ] } +``` + +## Read rows (row scope plus a purpose) + +Reading actual records needs a token with the row scope and a `Data-Purpose` header that declares why +you are reading: + +```sh +curl -sS \ + -H "Authorization: Bearer $CIVIL_ROW_READER_TOKEN" \ + -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ + "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" +``` + +The Relay returns `200 OK` with one record and pagination metadata (synthetic demo data): + +```json +{ + "data": [ + { + "national_id": "NID-1001", + "given_name": "Miguel", + "surname": "Santos", + "birth_date": "2016-01-15", + "life_stage": "child", + "deceased": false, + "district": "north" + } + ], + "pagination": { "has_more": true, "next_cursor": "..." } +} +``` + +## See access control hold + +The point of the gateway is that it never widens reach at request time. Send the metadata-scope token +to the rows endpoint and the Relay refuses it: + +```sh +curl -sS -i \ + -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ + -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ + "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" +``` + +The Relay refuses with `403 Forbidden`, and the `problem+json` body carries the stable code +`auth.scope_denied`. For more on this service, see +[Registry Relay](../../products/registry-relay/). + +## What you built + +You read a protected registry API from your terminal and watched Registry Relay enforce scope: a +metadata-scope token listed the dataset catalog (`200`), a row-scope token with a declared purpose +read one record (`200`), and the same metadata-scope token was refused at the rows endpoint (`403`, +`auth.scope_denied`). Same API, same network call, different scope, different answer. + +## Next + +- [Get a credential from the hosted lab](../credential-tour/): continue in the same hosted lab and + see Registry Notary issue a privacy-preserving credential. +- [Run a protected registry API locally](../../tutorials/publish-spreadsheet-secured-registry-api/): + stand up your own protected Relay from a sample workbook. + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| The hosted lab or a service URL does not respond | n/a | Run the same multi-service topology locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/) | diff --git a/docs/site/src/content/docs/start/see-it-live.mdx b/docs/site/src/content/docs/start/see-it-live.mdx deleted file mode 100644 index ad2d6c13..00000000 --- a/docs/site/src/content/docs/start/see-it-live.mdx +++ /dev/null @@ -1,343 +0,0 @@ ---- -title: See it live -description: With zero install, read a protected registry API from your terminal and receive a privacy-preserving credential in a hosted demo wallet, all against the public hosted lab. -status: current -owner: registry-docs -source_repos: - - registry-lab - - registry-registryctl -last_reviewed: "2026-06-13" -doc_type: tutorial -locale: en -standards_referenced: - - oid4vci - - sd-jwt-vc ---- - -import QuickstartMeta from '../../../components/QuickstartMeta.astro'; - -In about ten minutes you can see the core of Registry Stack working against a public hosted lab. You -will read a protected registry API from your terminal (**Registry Relay**, which returns records to -authorized callers), then have **Registry Notary** issue a signed credential, delivered to a hosted -demo wallet, that answers a question without exposing the record. The Relay reads are pure `curl` -with zero install; the credential step is a guided browser flow using the lab's hosted wallet, so it -needs no install either; only the optional developer round-trip at the end also uses `registryctl`, -`jq`, and `node`. Everything runs in the hosted lab at -[lab.registrystack.org](https://lab.registrystack.org), so the main flow has no setup on your -machine. - - - -This lab uses synthetic data and public demo-only credentials by design. -Do not reuse anything you copy here outside the lab. - -## What is running - -The lab runs three services that you will touch directly: - -- A civil-registry **Relay**: a protected, scoped, read-only HTTP API over a civil registry source, - at [civil-relay.lab.registrystack.org](https://civil-relay.lab.registrystack.org). -- A citizen **Notary**: an issuer that hands out a privacy-preserving credential instead of the raw - record, at [citizen-notary.lab.registrystack.org](https://citizen-notary.lab.registrystack.org). -- A health-program **Notary** in front of a demo DHIS2 health information system, used only by the - optional developer round-trip at the end of this page, at - [dhis2-notary.lab.registrystack.org](https://dhis2-notary.lab.registrystack.org). - -For how these connect to the rest of the stack, see the -[architecture overview](../../explanation/architecture/). For the full set of live demo -credentials, identities, and ready-made requests, open the lab homepage at -[lab.registrystack.org](https://lab.registrystack.org). - -:::note -Lab tokens are public demo-only values that rotate, so this page shows placeholders, never live -tokens. Get the current tokens and identities from the **For developers** section near the bottom -of the lab homepage, which shows every demo credential with a copy button and a prebuilt curl -example. The same data is available -unauthenticated as JSON at -[lab.registrystack.org/api/lab.json](https://lab.registrystack.org/api/lab.json). -::: - -If the hosted lab or one of its service URLs is unavailable, run the same multi-service topology -locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/). - -## Read a protected registry API - -This is the part you can run right now from a terminal. The civil Relay authenticates callers with -`Authorization: Bearer `. Each token carries a scope, so the same API answers some requests -and refuses others. - -Open [lab.registrystack.org](https://lab.registrystack.org), scroll to the **For developers** -section near the bottom, and copy two bearer tokens from the demo credential cards: a -metadata-scope token and a row-reader token. Set them in your shell. The placeholders below stand -in for the live tokens. - -```sh -export CIVIL_METADATA_TOKEN="" -export CIVIL_ROW_READER_TOKEN="" -``` - -### List datasets (metadata scope) - -The metadata-scope token can list the datasets the Relay publishes: - -```sh -curl -sS \ - -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ - https://civil-relay.lab.registrystack.org/v1/datasets -``` - -The Relay returns `200 OK` with the catalog. Each dataset entry carries more fields than shown; -they are omitted here for brevity: - -```json -{ "data": [ { "dataset_id": "civil_registry" } ] } -``` - -### Read rows (row scope plus a purpose) - -Reading actual records needs a token with the row scope and a `Data-Purpose` header that declares why -you are reading: - -```sh -curl -sS \ - -H "Authorization: Bearer $CIVIL_ROW_READER_TOKEN" \ - -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ - "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" -``` - -The Relay returns `200 OK` with one record and pagination metadata (synthetic demo data): - -```json -{ - "data": [ - { - "national_id": "NID-1001", - "given_name": "Miguel", - "surname": "Santos", - "birth_date": "2016-01-15", - "life_stage": "child", - "deceased": false, - "district": "north" - } - ], - "pagination": { "has_more": true, "next_cursor": "..." } -} -``` - -### See access control hold - -The point of the gateway is that it never widens reach at request time. Send the metadata-scope token -to the rows endpoint and the Relay refuses it: - -```sh -curl -sS -o /dev/null -w "%{http_code}\n" \ - -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ - -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ - "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" -``` - -The Relay returns `403 Forbidden` with `{"code":"auth.scope_denied"}`. Same API, same network call, -different scope, different answer. For more on this service, see -[Registry Relay](../../products/registry-relay/). - -## Get a signed credential - -Now see the issuance side. The citizen Notary issues a Selective Disclosure JWT Verifiable -Credential ([SD-JWT VC](../../reference/glossary/#sd-jwt-vc)) of type `person_is_alive_sd_jwt`, -media type `application/dc+sd-jwt`. The credential discloses a single predicate, whether -the person is alive, as a true or false claim. It never hands over the underlying civil record. -The Relay row-read example uses `NID-1001` / Miguel Santos, while this wallet flow uses the lab -wallet identity `NID-2001` / Maria Santos. - -Unlike the Relay reads above, this part is a guided browser flow, not a curl sequence: a real -[OID4VCI](../../reference/glossary/#oid4vci) issuance needs a holder key and a sign-in, which is wallet -territory. You do not need to install a wallet: the lab hosts a demo wallet at -[wallet.lab.registrystack.org](https://wallet.lab.registrystack.org), so you can receive the -credential end to end in the browser. - -Start in the **Wallet test** section of [lab.registrystack.org](https://lab.registrystack.org), -which links the credential flow end to end: start the citizen Notary flow, sign in, then paste the -generated credential offer into the hosted wallet. The Notary requires you to sign in before it -issues. The sign-in runs through eSignet, the lab's hosted demo identity provider; you do not need -an account, and the values below are synthetic demo identities: - -- National ID: `NID-2001` -- Name: Maria Santos -- Login and OTP code: `111111` -- PIN: `545411` - -After sign-in, the hosted wallet receives the signed `person_is_alive_sd_jwt` credential, with the -alive predicate set to true. If you prefer your own OID4VCI-compatible wallet, you can open the -credential offer directly instead: - -```text -https://citizen-notary.lab.registrystack.org/oid4vci/credential-offer?credential_configuration_id=person_is_alive_sd_jwt -``` - -### The negative control - -Try this in the same browser flow. Self-attestation binds the credential subject to the identity you -signed in as. If you sign in as `NID-2001` but ask for a credential bound to `NID-1001`, the Notary -refuses: it will not issue a credential for a subject you did not authenticate as. The issuer cannot -be talked into vouching for someone else. For more on this service, see -[Registry Notary](../../products/registry-notary/). - -### Developer API round-trip - -For a pure API credential flow, use the hosted DHIS2 Notary: the lab runs a demo -[DHIS2](https://dhis2.org/) health information system with a Notary in front of it, so the same -issuance contract can be exercised against a second, independent source system. This path does -not use the browser wallet sign-in, and unlike the curl-only reads above it also needs -`registryctl`, `jq`, and `node` on your machine. It evaluates DHIS2 child-program claims, issues an -`application/dc+sd-jwt` credential, fetches the issuer JWKS, verifies the Ed25519 signature, and -checks that each disclosure hash is listed in the issuer-signed JWT. - -Load the hosted Notary URL, current demo bearer token, and purpose URI from the lab manifest. -The `dhis2-bearer` credential is the demo evidence-client token for this Notary. - -```sh -eval "$(registryctl lab env --credential dhis2-bearer)" -``` - -Evaluate the credential claims for the live demo tracked entity: - -```sh -cat > evaluation-request.json <<'JSON' -{ - "target": { - "type": "TrackedEntity", - "identifiers": [ - { "scheme": "dhis2_tracked_entity", "value": "PQfMcpmXeFE" } - ] - }, - "claims": [ - "dhis2-tracked-entity-first-name", - "dhis2-tracked-entity-last-name", - "dhis2-child-program-active" - ], - "disclosure": "value", - "format": "application/dc+sd-jwt" -} -JSON - -curl -fsS -X POST "$REGISTRY_NOTARY_BASE_URL/v1/evaluations" \ - -H "Authorization: Bearer $REGISTRY_NOTARY_BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - -H "Data-Purpose: $REGISTRY_NOTARY_PURPOSE" \ - --data @evaluation-request.json \ - -o evaluation.json - -jq '.results[] | {claim_id, value, evaluation_id, format}' evaluation.json -``` - -Issue the SD-JWT VC from that evaluation: - -```sh -EVALUATION_ID="$(jq -r '.results[0].evaluation_id' evaluation.json)" - -jq -n --arg evaluation_id "$EVALUATION_ID" '{ - evaluation_id: $evaluation_id, - credential_profile: "dhis2_child_program_sd_jwt", - format: "application/dc+sd-jwt", - claims: [ - "dhis2-tracked-entity-first-name", - "dhis2-tracked-entity-last-name", - "dhis2-child-program-active" - ], - disclosure: "value" -}' > credential-request.json - -curl -fsS -X POST "$REGISTRY_NOTARY_BASE_URL/v1/credentials" \ - -H "Authorization: Bearer $REGISTRY_NOTARY_BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - --data @credential-request.json \ - -o credential.json - -jq '{credential_id, credential_profile, issuer, format, disclosure_count: (.disclosures | length)}' credential.json -``` - -Fetch the issuer keys and verify the credential. -The JWKS endpoint is public: any verifier can fetch the issuer keys without a credential. - -```sh -curl -fsS "$REGISTRY_NOTARY_BASE_URL/.well-known/evidence/jwks.json" -o jwks.json - -node <<'NODE' -const { createHash, createPublicKey, verify } = require('node:crypto'); -const { readFileSync } = require('node:fs'); - -const credential = JSON.parse(readFileSync('credential.json', 'utf8')); -const jwks = JSON.parse(readFileSync('jwks.json', 'utf8')); -const [header64, payload64, signature64] = credential.issuer_signed_jwt.split('.'); -const header = JSON.parse(Buffer.from(header64, 'base64url').toString('utf8')); -const payload = JSON.parse(Buffer.from(payload64, 'base64url').toString('utf8')); -const keys = Array.isArray(jwks.keys) ? jwks.keys : []; -const key = keys.find((candidate) => candidate.kid === header.kid); - -if (!key) throw new Error(`Missing JWKS key ${header.kid}`); - -const signed = Buffer.from(`${header64}.${payload64}`); -const signature = Buffer.from(signature64, 'base64url'); -const signatureValid = verify(null, signed, createPublicKey({ key, format: 'jwk' }), signature); - -if (!signatureValid) throw new Error('Bad issuer signature'); - -const disclosureDigests = new Set(payload._sd || []); -const disclosureClaims = []; - -for (const disclosure of credential.disclosures || []) { - const digest = createHash('sha256').update(disclosure).digest('base64url'); - if (!disclosureDigests.has(digest)) { - throw new Error(`Disclosure digest not present in _sd: ${digest}`); - } - const decoded = JSON.parse(Buffer.from(disclosure, 'base64url').toString('utf8')); - disclosureClaims.push(decoded[1]); -} - -console.log(JSON.stringify({ - issuer: payload.iss, - vct: payload.vct, - signature_valid: signatureValid, - disclosure_count: disclosureClaims.length, - disclosure_claims: disclosureClaims.sort() -}, null, 2)); -NODE -``` - -Expected verification summary: - -```json -{ - "issuer": "did:web:dhis2-notary.lab.registrystack.org", - "vct": "https://dhis2-notary.lab.registrystack.org/credentials/dhis2/child-program/v1", - "signature_valid": true, - "disclosure_count": 3, - "disclosure_claims": [ - "dhis2-child-program-active", - "dhis2-tracked-entity-first-name", - "dhis2-tracked-entity-last-name" - ] -} -``` - -## Now run your own - -You have seen the two payoffs against the hosted lab. To run the same shapes on your own machine, -start with the local single-node tutorials: - -- [Publish a spreadsheet as a secured registry API](../../tutorials/publish-spreadsheet-secured-registry-api/): - stand up your own protected Relay from a sample workbook. -- [Verify a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): run a Notary - against the Relay you published and evaluate a claim. Already have your own API? The same - tutorial ends with a - [standalone Notary path](../../tutorials/verify-claim-registry-api/#run-notary-standalone-for-an-api-you-operate). - -Only assessing fit? You do not need to install anything: -[When to use Registry Stack](../when-to-use/) covers fit and non-goals, and -[DPI safeguards alignment](../../explanation/dpi-safeguards-alignment/) maps the stack to -safeguards language for review programs. diff --git a/docs/site/src/content/docs/start/when-to-use.mdx b/docs/site/src/content/docs/start/when-to-use.mdx index 521e4427..b5bd4da8 100644 --- a/docs/site/src/content/docs/start/when-to-use.mdx +++ b/docs/site/src/content/docs/start/when-to-use.mdx @@ -79,6 +79,6 @@ Registry Stack deliberately leaves these to other systems: ## Next -- [See it live](../see-it-live/) +- [Quickstart](../quickstart/) - [Choose by question (homepage router)](../../) - [Architecture overview](../../explanation/architecture/) diff --git a/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx b/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx new file mode 100644 index 00000000..b09271fc --- /dev/null +++ b/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx @@ -0,0 +1,344 @@ +--- +title: Configure DHIS2 claim checks +description: Run the DHIS2 source-adapter profile, evaluate DHIS2 Tracker claims with Registry Notary, and identify the config blocks to adapt for your own DHIS2 deployment. +status: current +owner: registry-docs +source_repos: + - registry-lab + - registry-notary +last_reviewed: "2026-06-28" +doc_type: tutorial +locale: en +standards_referenced: + - openapi + - sd-jwt-vc +--- + +import QuickstartMeta from '../../../components/QuickstartMeta.astro'; + +This tutorial runs Registry Notary against the public DHIS2 play sandbox. +A private source-adapter sidecar calls the DHIS2 Tracker API, Registry Notary evaluates +programme-status claims from the projected fields, and Notary issues a holder-bound SD-JWT VC +(Selective Disclosure JWT Verifiable Credential) from the evaluated results, with only the Notary +API exposed to the caller. + +You will configure three pieces: + +- The DHIS2 source adapter, which calls the DHIS2 Tracker API through the built-in `http_json` + engine. +- Registry Notary source bindings, which map projected DHIS2 fields into claim rules. +- Credential profiles, which issue SD-JWT VCs from selected DHIS2 claim results. + + + +The demo uses the public DHIS2 sandbox at `https://play.im.dhis2.org/stable-2-43-0-1`. +The same configuration shape applies to a private DHIS2 deployment once you replace the source URL, +credentials, Tracker query, field projection, and claim rules. + +## Before you start + +You need: + +- Docker with Compose v2.20 or later. +- The `just` command runner. +- `jq` and `node` for manual checks and holder-proof generation (a holder proof is a signed + attestation binding an issued credential to the holder's key). +- A Registry Lab checkout, which is the `lab` directory of the `registry-stack` monorepo. +- Network access to the DHIS2 source you are testing. + +Clone the monorepo if you do not already have it: + +```sh +git clone https://github.com/registrystack/registry-stack.git +cd registry-stack/lab +just setup +``` + +For an existing checkout, update submodules before running the profile: + +```sh +cd registry-stack/lab +just setup +``` + +## Set DHIS2 source access + +The local demo defaults to the public DHIS2 play credentials. The current lab profile still uses +legacy `OPENFN_*` variable names for this DHIS2 sidecar. To point the profile at another DHIS2 +environment, set these values before you run the profile: + +```sh +export OPENFN_DHIS2_HOST_URL="https://play.im.dhis2.org/stable-2-43-0-1" +export OPENFN_DHIS2_USERNAME="admin" +export OPENFN_DHIS2_PASSWORD="district" +``` + +In a private deployment, replace those values with a DHIS2 base URL and a reader account scoped to +the Tracker data you need. Do not use public sandbox credentials outside the demo. + +The Compose profile turns those values into the credential JSON consumed by the sidecar: + +```json +{ + "baseUrl": "https://play.im.dhis2.org/stable-2-43-0-1", + "username": "admin", + "password": "district" +} +``` + +## Generate local credentials + +Generate local Notary tokens, sidecar tokens, signing material, and static metadata: + +```sh +just generate +``` + +This writes `.env`, including: + +- `DHIS2_EVIDENCE_CLIENT_BEARER`, used by callers when they call Registry Notary. +- `OPENFN_SIDECAR_TOKEN_RAW`, the legacy variable name Registry Notary uses when it calls the + private DHIS2 sidecar. +- `OPENFN_SIDECAR_TOKEN_HASH`, the legacy variable name the sidecar uses to authenticate Notary. + +Do not use generated demo credentials outside the local lab. + +## Run the DHIS2 profile + +Build the lab images: + +```sh +just build +``` + +Run the DHIS2 smoke: + +```sh +just dhis2 +``` + +The profile starts: + +| Service | Local URL | Role | +| --- | --- | --- | +| `openfn-dhis2-sidecar` | private Compose network only | DHIS2 source adapter that calls DHIS2 Tracker through `http_json` | +| `dhis2-health-notary` | `http://127.0.0.1:4326` | Registry Notary claim evaluator and SD-JWT VC issuer | + +The service name keeps the legacy `openfn` prefix, but this path uses the built-in `http_json` +engine. There is no Node worker pool or OpenFn workflow runtime in the DHIS2 lookup path. + +Expected ending: + +```text +DHIS2 health evidence and VC smoke passed +``` + +## Inspect the results + +The smoke writes artifacts under: + +```text +output/dhis2-openfn/ +``` + +Inspect the predicate checks: + +```sh +jq '{claim_id: .results[0].claim_id, satisfied: .results[0].satisfied}' \ + output/dhis2-openfn/smoke-dhis2-child-program-active.json +``` + +Inspect the programme participation credential summary: + +```sh +jq '{ + credential_profile, + issuer, + format, + disclosure_count, + disclosure_claim_ids, + programme_active, + reconciliation_ref_available +}' output/dhis2-openfn/smoke-dhis2-programme-participation-credential-summary.json +``` + +The full credential response and holder proof are also written under `output/dhis2-openfn/`. +Treat those files as local debug artifacts and review them before sharing. + +## Call Notary manually + +Load the generated local credentials: + +```sh +set -a +. .env +set +a +``` + +Call Notary discovery: + +```sh +curl -fsS http://127.0.0.1:4326/.well-known/evidence-service \ + -H "Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}" | jq +``` + +Evaluate the programme participation claims for the demo tracked entity: + +```sh +curl -fsS -X POST http://127.0.0.1:4326/v1/evaluations \ + -H "Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}" \ + -H "Content-Type: application/json" \ + -H "Data-Purpose: https://demo.example.gov/purpose/dhis2-openfn-health-evidence" \ + -o output/dhis2-openfn/manual-programme-evaluation.json \ + -d '{ + "target": { + "type": "TrackedEntity", + "identifiers": [ + { "scheme": "dhis2_tracked_entity", "value": "PQfMcpmXeFE" } + ] + }, + "claims": [ + "dhis2-tracked-entity-first-name", + "dhis2-tracked-entity-last-name", + "dhis2-child-age-band", + "dhis2-programme-code", + "dhis2-child-program-active", + "dhis2-reconciliation-ref" + ], + "disclosure": "value", + "format": "application/dc+sd-jwt" + }' + +jq '.results[] | {claim_id, value, satisfied, evaluation_id}' \ + output/dhis2-openfn/manual-programme-evaluation.json +``` + +## Issue the programme credential + +Generate a holder proof for the returned evaluation: + +```sh +EVALUATION_ID="$( + jq -r '.results[0].evaluation_id' \ + output/dhis2-openfn/manual-programme-evaluation.json +)" + +CLAIMS='[ + "dhis2-tracked-entity-first-name", + "dhis2-tracked-entity-last-name", + "dhis2-child-age-band", + "dhis2-programme-code", + "dhis2-child-program-active", + "dhis2-reconciliation-ref" +]' + +scripts/generate-holder-proof.js \ + --audience dhis2-health-notary \ + --evaluation-id "$EVALUATION_ID" \ + --credential-profile dhis2_programme_participation_sd_jwt \ + --disclosure value \ + --claims-json "$CLAIMS" \ + > output/dhis2-openfn/manual-programme-holder.json +``` + +Issue the SD-JWT VC: + +```sh +curl -fsS -X POST http://127.0.0.1:4326/v1/credentials \ + -H "Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}" \ + -H "Content-Type: application/json" \ + -o output/dhis2-openfn/manual-programme-credential.json \ + -d "$(jq -nc \ + --arg evaluation_id "$EVALUATION_ID" \ + --argjson claims "$CLAIMS" \ + --slurpfile holder output/dhis2-openfn/manual-programme-holder.json \ + '{ + evaluation_id: $evaluation_id, + credential_profile: "dhis2_programme_participation_sd_jwt", + format: "application/dc+sd-jwt", + claims: $claims, + disclosure: "value", + holder: $holder[0].holder + }')" + +jq '{credential_id, credential_profile, issuer, format, disclosure_count: (.disclosures | length)}' \ + output/dhis2-openfn/manual-programme-credential.json +``` + +The issued credential is holder-bound to the generated `did:jwk` proof (a DID that encodes a +public key directly, with no separate registry lookup) and uses the +`dhis2_programme_participation_sd_jwt` profile. + +## How the adapter is configured + +The DHIS2 source adapter is configured in `config/source-adapter/dhis2-health-sidecar.yaml`. + +Key settings: + +| Setting | What it does | +| --- | --- | +| `sources.dhis2_health.engine` | Uses the built-in `http_json` engine. | +| `sources.dhis2_health.credential_env` | Reads DHIS2 `baseUrl`, `username`, and `password` from the legacy `OPENFN_DHIS2_DEMO_CREDENTIAL_JSON` variable. | +| `sources.dhis2_health.allowed_base_urls` | Pins which DHIS2 base URLs the sidecar may call. | +| `sources.dhis2_health.http_json.path` | Calls `/api/tracker/trackedEntities`. | +| `sources.dhis2_health.http_json.query` | Maps the Notary lookup value into DHIS2 Tracker query parameters. | +| `sources.dhis2_health.http_json.response.records` | Projects the DHIS2 Tracker JSON response into the fields Notary can evaluate. | + +The demo maps one DHIS2 tracked entity into fields such as `first_name`, `last_name`, +`child_program_active`, `child_age_band`, `programme_code`, and `reconciliation_ref`. + +## How Notary is configured + +Registry Notary is configured in `config/notary/dhis2-health-notary.yaml`. + +Key settings: + +| Setting | What it does | +| --- | --- | +| `evidence.source_connections.dhis2_openfn.base_url` | Points Notary at the private sidecar. | +| `evidence.source_connections.dhis2_openfn.token_env` | Sends the shared sidecar token from the legacy `OPENFN_SIDECAR_TOKEN_RAW` variable. | +| `evidence.allowed_purposes` | Restricts which `Data-Purpose` values may be used. | +| `evidence.claims[].source_bindings` | Binds each claim to the projected DHIS2 fields. | +| `evidence.claims[].rule` | Extracts a value or evaluates a predicate from the projected fields. | +| `evidence.credential_profiles` | Controls which claims can be issued as SD-JWT VCs, validity, issuer, disclosure, and holder binding. | + +## Stop the profile + +Stop the DHIS2 services: + +```sh +docker compose -f compose.yaml --profile dhis2 down +``` + +## Make it yours + +- Update `sources.dhis2_health.allowed_base_urls` in `config/source-adapter/dhis2-health-sidecar.yaml` + to your DHIS2 base URL when you point the sidecar at a private deployment. +- Change the program IDs and attribute IDs in that same file's `http_json.query` and + `http_json.response.records` blocks to match your DHIS2 Tracker configuration. +- Update `evidence.claims[].source_bindings` and `evidence.claims[].rule` in + `config/notary/dhis2-health-notary.yaml` to match the programme facts your relying services need. +- Replace `OPENFN_DHIS2_HOST_URL`, `OPENFN_DHIS2_USERNAME`, and `OPENFN_DHIS2_PASSWORD` with a + reader account scoped to your DHIS2 instance instead of the public sandbox credentials. +- Keep the DHIS2 sidecar private and expose only Registry Notary to callers, as this demo does. + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| `missing .env` | Local credentials have not been generated. | Run `just generate`. | +| Notary discovery returns `401` | The request is missing the local bearer token. | Add `Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}`. | +| The sidecar cannot reach DHIS2 | The source URL, credentials, or network path is wrong, or the public sandbox reset. | Check `OPENFN_DHIS2_HOST_URL`, `OPENFN_DHIS2_USERNAME`, `OPENFN_DHIS2_PASSWORD`, then rerun `just dhis2`. | +| A claim is unsatisfied | The Tracker record does not contain the projected field or programme state. | Inspect the JSON artifact in `output/dhis2-openfn/`, then compare the field with `config/source-adapter/dhis2-health-sidecar.yaml` and `config/notary/dhis2-health-notary.yaml`. | +| `child_age_band` looks coarse | The public child programme demo does not expose date of birth for the tracked entity. | Treat `5_to_17` as lab-derived programme context, not a clinical age calculation. | + +## Next + +- [OpenCRVS claims](../verify-opencrvs-claims/) +- [Getting started with FHIR evidence](../getting-started-fhir-evidence/) +- [Integration patterns](../../explanation/integration-patterns/) diff --git a/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx b/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx index 5533c563..7e8a9aa9 100644 --- a/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx +++ b/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx @@ -19,7 +19,7 @@ Use this guide when you operate your own registry data and want to run Registry optionally, Registry Notary) as self-hosted services against that data, rather than the `registryctl` sample project. The sample-based tutorial, -[Publish a spreadsheet as a secured registry API](../publish-spreadsheet-secured-registry-api/), +[Run a protected registry API locally](../publish-spreadsheet-secured-registry-api/), generates a project from the built-in `benefits` sample. There is no `registryctl` initializer for your own dataset today: `registryctl init relay` accepts only `--sample benefits`. @@ -601,7 +601,7 @@ docker run --rm \ fingerprints, and broken source references. `explain-config --format json` prints the resolved configuration and the env vars it requires. For exercising the claim end to end (request shapes, outcomes, and credential issuance on top), -work through [Verify a claim with Registry Notary](../verify-claim-registry-api/), which runs +work through [Evaluate a claim with Registry Notary](../verify-claim-registry-api/), which runs the same claim lifecycle against a `registryctl` sample project. ### Go deeper on Notary diff --git a/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx b/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx index a9548141..fa6df922 100644 --- a/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx +++ b/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx @@ -29,12 +29,12 @@ clients into a single topology that runs on a laptop. This tutorial brings the topology up with the `just` recipes the lab provides, verifies it with the smoke suite, runs the narrated demo client, and explains what each of the three scenarios exercises across the Relay and Notary services. If you only want the shortest hosted path before running the -stack locally, start with [See it live](../../start/see-it-live/) instead. This page is the full +stack locally, start with the [Quickstart](../../start/quickstart/) instead. This page is the full local tour. For normal local adopter projects, start with the `registryctl` tutorials instead: -[publish a secured registry API](../publish-spreadsheet-secured-registry-api/) or -[verify a claim with Registry Notary](../verify-claim-registry-api/). Clone `registry-lab` only when +[run a protected registry API](../publish-spreadsheet-secured-registry-api/) or +[evaluate a claim with Registry Notary](../verify-claim-registry-api/). Clone `registry-lab` only when you want the full multi-service demo topology rather than a generated project. + +This tutorial uses synthetic data and local demo credentials. +Do not use the generated local keys in production. + +## Start with a running source + +For a local reproducible source, complete the first tutorial and leave the Relay project running: + +```sh +registryctl init relay my-first-api --sample benefits +cd my-first-api +registryctl start +registryctl smoke +set -a +. secrets/local.env +set +a +``` + +The `set -a` block exports `ROW_READER_RAW`, which the standalone Notary project uses once when it +is generated. + +## Create the Notary project + +From the parent directory of `my-first-api`, create the standalone Notary project: + +```sh +cd .. +registryctl init notary my-standalone-notary \ + --source-url http://registry-relay:8080 \ + --source-network my-first-api_default \ + --source-token-from-env ROW_READER_RAW +cd my-standalone-notary +``` + +`--source-token-from-env ROW_READER_RAW` reads the token value from your current shell and writes it +into the new project's `secrets/local.env`. + +`my-first-api_default` is the Compose network name Docker derives from the Relay project directory. +If you named that directory differently, use that name with the `_default` suffix. + +## Start Notary + +Start the standalone project: + +```sh +registryctl start +``` + +`registryctl` starts Notary, waits for readiness, and prints the local API URL: + +```text +Notary API: http://127.0.0.1:4255 +API docs: http://127.0.0.1:4255/docs +``` + +Run the smoke check: + +```sh +registryctl notary smoke +``` + +The smoke check lists claims, evaluates the generated starter claim, and writes a detailed report: + +```text +output/notary-smoke-results.json +``` + +## Evaluate the starter claim + +Load this project's local keys: + +```sh +set -a +. secrets/local.env +set +a +``` + +Evaluate the starter claim: + +```sh +curl -sS -X POST \ + -H "x-api-key: $REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW" \ + -H "Content-Type: application/json" \ + -H "Accept: application/vnd.registry-notary.claim-result+json" \ + -d '{ + "target": { "type": "person", "id": "per-2001" }, + "claims": ["benefits-person-exists"], + "disclosure": "predicate", + "purpose": "https://example.local/purpose/tutorial" + }' \ + http://127.0.0.1:4255/v1/evaluations +``` + +Notary returns a claim result without returning the source row. + +## Use another Registry Data API source + +Change the source options at project creation time. +The three `<...>` values must match the Registry Data API lookup route your source exposes: + +```sh +registryctl init notary my-notary \ + --source-url https://api.example.com \ + --source-token-env EVIDENCE_SOURCE_API_TOKEN \ + --source-dataset \ + --source-entity \ + --source-lookup-field +``` + +Notary will read: + +```text +GET /v1/datasets/{dataset}/entities/{entity}/records?{lookup_field}={lookup_value}&fields=...&limit=2 +Authorization: Bearer +Data-Purpose: +``` + +Successful responses must use: + +```json +{ "data": [{ "field": "value" }] } +``` + +`--source-token-env` names the environment variable the Notary container reads at runtime. +After project creation, edit `secrets/local.env` and set `EVIDENCE_SOURCE_API_TOKEN` to the source +token. + +If the source API is another Compose service, pass `--source-network ` so Notary +can join that network. + +## Adapt a custom HTTP source + +Use a source-adapter sidecar when your source API has its own routes, auth, response envelopes, or +matching behavior. + +The adapter runs privately next to Notary and exposes the Registry Data API-shaped route that Notary +expects. + +Start with the smallest engine that fits: + +| Engine | Use when | +| --- | --- | +| `http_json` | One governed HTTP JSON request can return the fields Notary needs. | +| `http_flow` | You need 2 to 5 dependent GET requests, such as search first and fetch detail second. | +| `fhir` (FHIR R4 resource projection engine) | You are projecting bounded FHIR R4 resources into Notary-ready facts. | +| `script_rhai` | You need POST, fallback behavior, or small custom branching that the built-in declarative engines cannot express. | + +Keep the source-specific logic in the adapter. +Keep claim semantics, disclosure, caller policy, and credential issuance in Notary. + +## Clean up + +Stop the standalone Notary project from its directory: + +```sh +registryctl stop +``` + +If you also started the sample Relay source, stop it from the `my-first-api` directory. + +## Next + +- [Evaluate a claim with Registry Notary](../verify-claim-registry-api/): learn the core local + path where Relay and Notary run in one generated project. +- [Configure DHIS2 claim checks](../configure-dhis2-claim-checks/): see a named source-system + integration backed by a source-adapter `http_json` path. +- [Getting started with FHIR evidence](../getting-started-fhir-evidence/): see the FHIR + source-adapter path. +- [Deploy Relay and Notary standalone with your own data](../deploy-standalone-with-own-data/): + move from a local tutorial to an operator-shaped deployment. +- [Source and claim modeling](../../products/registry-notary/source-claim-modeling-guide/): + configure source connections, source adapters, and claim boundaries. + +## Troubleshooting + +| Symptom | Cause | Resolution | +| --- | --- | --- | +| `registryctl init notary` cannot read `ROW_READER_RAW` | The Relay keys were not exported in this shell. | Run the `set -a` block from the Relay project directory, then retry. | +| `registryctl start` fails with `network ... not found` | The source Compose network name is wrong or the source project is not running. | Start the source project and pass the correct `_default` network name. | +| Claim evaluation returns a source auth error | Notary cannot authenticate to the source. | Confirm the source token in `secrets/local.env` and restart Notary. | +| Claim evaluation returns `409 Evidence not available` | The target id is not available from the source, or the dataset, entity, or lookup field does not match the source contract. | Use a known target id or inspect the Registry Data API source contract. | diff --git a/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx b/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx index 2e1de825..6ac8e7e3 100644 --- a/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx +++ b/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx @@ -1,13 +1,13 @@ --- -title: Verify a claim with Registry Notary -description: Add Registry Notary to the local registry API project, evaluate one claim, compare a claim result with a Relay row read, and run Notary standalone against an API you operate. +title: Evaluate a claim with Registry Notary +description: Add Registry Notary to the local registry API project, evaluate one claim, and compare the claim result with a protected row read. status: current owner: registry-docs source_repos: - registry-registryctl - registry-relay - registry-notary -last_reviewed: "2026-06-26" +last_reviewed: "2026-06-28" doc_type: tutorial locale: en standards_referenced: [] @@ -15,27 +15,26 @@ standards_referenced: [] import QuickstartMeta from '../../../components/QuickstartMeta.astro'; -Use this tutorial after you publish the spreadsheet registry API. -You will add Registry Notary to the same local project, start Relay and Notary together, prove -anonymous claim access is denied, and evaluate one claim backed by the registry API. -A final section, [Run Notary standalone for an API you operate](#run-notary-standalone-for-an-api-you-operate), -shows the same flow with Notary in its own project pointed at a source API you choose. +Use this tutorial after you run the protected registry API locally. +You will add Registry Notary to the same project, evaluate one claim, and see the difference +between a source row read and a minimized claim result. This tutorial uses synthetic data and local demo credentials. Do not use the generated local keys in production. -Estimated time: about 10 minutes after the first tutorial passes. -## Before you start +## Start from the local API -Complete [Publish a spreadsheet as a secured registry API](../publish-spreadsheet-secured-registry-api/) -first: +Complete [Run a protected registry API locally](../publish-spreadsheet-secured-registry-api/) +first. + +If you need to recreate the project: ```sh registryctl init relay my-first-api --sample benefits @@ -44,14 +43,9 @@ registryctl start registryctl smoke ``` -The Relay smoke test must pass before you add Notary. - -If you already completed that tutorial and `my-first-api` is still on disk, do not rerun the -`init` line: `registryctl init` refuses to overwrite a directory that is not empty. -From `my-first-api`, run `registryctl start` and `registryctl smoke` to confirm the project is -healthy, then continue. +The Relay smoke check must pass before you add Notary. -## Add claim verification +## Add Notary From the Relay project directory, add Notary: @@ -59,47 +53,11 @@ From the Relay project directory, add Notary: registryctl add notary --from local-relay ``` -The command updates the local project: - -```text -my-first-api/ - registryctl.yaml - compose.yaml - README.md - relay/ - config.yaml - metadata.yaml - notary/ - config.yaml - data/ - benefits_casework.xlsx - bruno/ - registry-api/ - secrets/ - local.env - output/ - .gitignore -``` - -`registryctl` keeps Relay and Notary credentials in `secrets/local.env`. -The Relay and Notary runtime configs contain only fingerprint references and environment -variable names. -This local demo uses generated API keys only; it does not require OIDC, eSignet, or an -assisted-access service. - -The generated Compose file also starts a local Redis replay store for Notary readiness. -It is part of the local demo runtime and does not require manual configuration. - -Notary reads from Relay through the Compose network: - -```text -http://registry-relay:8080 -``` - -`registry-relay` is the service name `registryctl` writes into the generated `compose.yaml`; -it is fixed by the generator, not derived from your project directory name. +The command adds `notary/config.yaml`, updates `compose.yaml`, refreshes the Bruno collection, and +adds Notary demo keys to `secrets/local.env`. -Local browser and curl examples use the host URL: +Notary reads from Relay through the local Compose network. +You still call Notary from your workstation at: ```text http://127.0.0.1:4255 @@ -113,8 +71,7 @@ Start the project again: registryctl start ``` -The command starts both services and, after the Docker Compose progress lines, waits for both -health and readiness checks: +`registryctl` waits for both services and prints the local URLs: ```text Relay API: http://127.0.0.1:4242 @@ -123,26 +80,7 @@ Notary API: http://127.0.0.1:4255 API docs: http://127.0.0.1:4255/docs ``` -Check the project status: - -```sh -registryctl status -``` - -The Relay and Notary services report healthy and ready when startup is complete. - -Validate both generated runtime configs with the product doctors: - -```sh -registryctl doctor --format json -``` - -The JSON report uses `schema_version: registryctl.validation.report.v1`. It contains separate -Relay and Notary product reports under `products[]`; each embedded product report uses -`schema_version: registry.config.diagnostic_report.v1` and a `status` of `ok`, `warning`, -`error`, or `not_run`. - -## Run the Notary smoke test +## Run the Notary smoke check Run the Notary smoke checks: @@ -150,7 +88,7 @@ Run the Notary smoke checks: registryctl notary smoke ``` -The smoke test passes with these checks: +You should see the core checks pass: ```text PASS notary healthz is public @@ -162,15 +100,12 @@ PASS notary evaluator can list claims PASS notary evaluator can verify benefits person exists ``` -`registryctl` writes detailed results to: +`registryctl` writes the detailed report to: ```text output/notary-smoke-results.json ``` -The smoke result must not contain raw API keys, the Relay source token, local env values, Relay -source rows, or sensitive sample column values. - ## Load local demo keys Load the generated local keys into your shell: @@ -181,26 +116,11 @@ set -a set +a ``` -The Notary tutorial adds these local values: - -| Value | Environment variable | What it is for | -| --- | --- | --- | -| Notary evaluator key | `REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW` | Lets you call Notary evaluation routes | -| Notary evaluator fingerprint | `REGISTRY_NOTARY_TUTORIAL_EVALUATOR_HASH` | Lets Notary verify the local evaluator key | -| Notary audit hash secret | `REGISTRY_NOTARY_AUDIT_HASH_SECRET` | Lets Notary hash audit subjects without logging raw values | -| Relay source token for Notary | `EVIDENCE_SOURCE_REGISTRY_RELAY_TOKEN` | Lets Notary read the Relay source API | -| Notary demo issuer JWK | `REGISTRY_NOTARY_ISSUER_JWK` | Local demo signing key used only to make Notary readiness pass | -| Notary replay Redis URL | `REGISTRY_NOTARY_REPLAY_REDIS_URL` | Points Notary at the local demo Redis replay store | - The Notary evaluator key is for you calling Notary. The Relay source token is for Notary calling Relay. They are intentionally separate. -The same `secrets/local.env` still carries the keys from the Relay tutorial, including the -row-reader key `ROW_READER_RAW`, which this page uses later to compare a row read with a claim -result. - -## Prove anonymous claim access is denied +## Make one denied claim request Call the claim list without a credential: @@ -210,6 +130,8 @@ curl -i http://127.0.0.1:4255/v1/claims Notary returns `401 Unauthorized`. +## List the available claim + Call the same route with the Notary evaluator key: ```sh @@ -218,10 +140,9 @@ curl -sS \ http://127.0.0.1:4255/v1/claims ``` -The response is a JSON list of the configured claim definitions. -The starter claim appears in it with `"id": "benefits-person-exists"`. +The starter claim appears with `"id": "benefits-person-exists"`. -## Evaluate a claim from registry data +## Evaluate one claim Evaluate whether the synthetic person `per-2001` exists in the Relay-backed benefits dataset: @@ -231,10 +152,7 @@ curl -sS -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/vnd.registry-notary.claim-result+json" \ -d '{ - "target": { - "type": "person", - "id": "per-2001" - }, + "target": { "type": "person", "id": "per-2001" }, "claims": ["benefits-person-exists"], "disclosure": "predicate", "purpose": "https://example.local/purpose/tutorial" @@ -242,58 +160,28 @@ curl -sS -X POST \ http://127.0.0.1:4255/v1/evaluations ``` -Notary returns a successful claim result for `benefits-person-exists`. -The response shows the claim outcome, not the Relay source row. -It has this shape (identifiers, timestamps, and the target handle vary per run): +Notary returns a claim result. +The response contains the outcome, provenance, timestamps, and a pseudonymous target reference. +It does not return the Relay source row. + +The important fields look like this: ```json { "results": [ { "claim_id": "benefits-person-exists", - "claim_version": "2026-06", "disclosure": "predicate", - "evaluation_id": "01KTS319VPVAJWKT8SZF4R8ZVN", - "expires_at": null, "format": "application/vnd.registry-notary.claim-result+json", - "issued_at": "2026-06-10T15:39:53Z", - "provenance": { - "schema_version": "registry-notary-claim-provenance/v1", - "generated_by": { - "type": "claim_evaluation", - "service_id": "registryctl.benefits.notary", - "evaluation_id": "01KTS319VPVAJWKT8SZF4R8ZVN", - "claim_id": "benefits-person-exists", - "claim_version": "2026-06" - }, - "used": { - "source_count": 1, - "source_versions": {}, - "source_runtimes": [] - }, - "derived_from": [] - }, "satisfied": true, "subject_type": "person", - "target_ref": { - "handle": "rnref:v1:hmac-sha256:...", - "type": "person" - }, "value": true } ] } ``` -What happened: - -- You called Notary with the Notary evaluator key. -- Notary checked that the key can evaluate the configured claim. -- Notary used its internal Relay source token to query Relay. -- Relay enforced its row-read scope and purpose-header rules. -- Notary returned the configured claim result without exposing the spreadsheet row. - -## Evaluate a claim for an unknown person +## Try an unknown person Run the same evaluation for an id that is not in the sample workbook: @@ -340,7 +228,7 @@ curl -sS -G \ http://127.0.0.1:4242/v1/datasets/benefits_casework/entities/person/records ``` -Notary exposes a claim API: +Notary exposes the claim API: ```sh curl -sS -X POST \ @@ -357,137 +245,34 @@ curl -sS -X POST \ ``` Use Relay when a caller is allowed to consult configured records. -Use Notary when a caller receives a narrow claim result. - -## Explore requests in Bruno - -`registryctl add notary --from local-relay` refreshes the generated Bruno collection with Notary -requests. -Bruno is optional. -The tutorial and API work without it. - -Open the generated collection: +Use Notary when a caller should receive a narrow claim result. -```sh -registryctl bruno open -``` +## Inspect the generated claim -If Bruno is installed, the collection opens with Relay and Notary folders. -If Bruno is not installed, the command prints the collection path and an install link. - -If the Bruno CLI is installed, you can run the collection: +Now that the evaluation works, inspect the generated Notary contract: ```sh -registryctl bruno run +sed -n '1,220p' notary/config.yaml ``` -If `bru` is not installed, the command prints a fallback and exits without blocking Relay or -Notary. - -## Open the Notary API reference +The generated config defines the source connection to Relay, the evaluator key fingerprint, the +starter claim, and the disclosure format. +Raw evaluator keys and Relay source tokens stay in `secrets/local.env`. -Open the Notary API surface: +Open the local Notary API reference: ```sh registryctl notary open ``` -The command opens the docs page in your default browser. -In an environment that cannot launch one, such as a remote shell, it prints the URLs instead: +In an environment that cannot launch a browser, the command prints: ```text Notary API docs: http://127.0.0.1:4255/docs OpenAPI JSON: http://127.0.0.1:4255/openapi.json ``` -## Run Notary standalone for an API you operate - -Everything so far ran Notary inside the Relay project. -When you already operate an API and only want claim verification, create a standalone Notary -project instead and point it at your source API. -The source must expose the Registry Data API-shaped record route Notary is configured to call. - -If you arrived here directly, for example from the routing table on the docs homepage, note that the -reproducible demo below uses the local registry API from the sections above as its stand-in -source, so it needs that project on disk and running (the [Before you start](#before-you-start) -block creates it in about two minutes). -If you already have a live source API of your own, skim the demo for the shape of the flow, -then start from [For an API you operate](#for-an-api-you-operate) below and substitute your own -URL, dataset, and token. - -To keep the steps reproducible without external credentials, this section reuses the local -registry API as the stand-in source. -Leave the Relay project running, and load its demo keys in your current shell (the `set -a` -block in [Load local demo keys](#load-local-demo-keys)); the project-creation step reads -`ROW_READER_RAW` from your shell. - -From the parent directory of `my-first-api`, create the Notary project: - -```sh -cd .. -registryctl init notary my-standalone-notary \ - --source-url http://registry-relay:8080 \ - --source-network my-first-api_default \ - --source-token-from-env ROW_READER_RAW -cd my-standalone-notary -``` - -`--source-token-from-env ROW_READER_RAW` reads the token value from your current shell once, at -project-creation time, and writes it into the new project's `secrets/local.env`. -`my-first-api_default` is the Compose network name Docker derives from the Relay project -directory name; if you named that directory differently, use that name with the `_default` -suffix. -The generated Notary config contains the source API URL, source dataset, entity, lookup field, -and environment variable names. -The raw keys and tokens live only in this project's own `secrets/local.env`. - -### For an API you operate - -Change the source options at project creation time. -The three `<...>` values are placeholders for what your source API exposes; the demo values -above (`benefits_casework`, `person`, `id`) are also the CLI defaults, so leaving these flags -off wires Notary to a dataset that does not exist on your API: - -```sh -registryctl init notary my-notary \ - --source-url https://api.example.com \ - --source-token-env EVIDENCE_SOURCE_API_TOKEN \ - --source-dataset \ - --source-entity \ - --source-lookup-field -``` - -`--source-token-env` differs from `--source-token-from-env` above: it does not read a value -from your shell now, it names the environment variable the Notary container resolves at -runtime. -Supply the value afterwards by editing `secrets/local.env` and setting -`EVIDENCE_SOURCE_API_TOKEN` to the source token. -If the source API is another Compose service, pass `--source-network ` so -Notary can join that network. - -### For a FHIR source-adapter sidecar - -If you already have a FHIR source-adapter sidecar running, start from the -FHIR profile instead of the Registry Data API defaults: - -```sh -registryctl init notary my-fhir-notary --source-kind fhir-sidecar -``` - -This generates a starter `patient-record-exists` claim, points Notary at the -sidecar URL `http://host.docker.internal:4360`, and uses -`FHIR_SIDECAR_TOKEN` in `secrets/local.env`. Change `--source-url`, -`--source-token-env`, `--source-dataset`, `--source-entity`, or -`--source-lookup-field` if your sidecar uses different local names. - -From the standalone project directory, the rest of the flow is the same as the co-located -sections earlier on this page: `registryctl start`, `registryctl notary smoke`, load this -project's keys with the `set -a` block, and evaluate the claim against -`http://127.0.0.1:4255/v1/evaluations`. -The standalone project has its own `secrets/local.env`; load it from this directory, not the -Relay project's. - -## Clean up +## Stop the stack When you are done: @@ -495,22 +280,17 @@ When you are done: registryctl stop ``` -This stops both local services. +This stops the local services. It does not delete your workbook, generated configs, local keys, or smoke results. -If you created the standalone Notary project, stop it from its own directory, then stop the -source registry API from `my-first-api`. - ## Next -- [Source and claim modeling](../../products/registry-notary/source-claim-modeling-guide/): - configure source connections and claim boundaries. -- [Registry Relay client integration](../../products/registry-relay/client-integration/): call - Relay from an application. -- [See it live](../../start/see-it-live/): explore the hosted demo to see Relay, Notary, and - cross-authority flows in action. -- [First run with Registry Lab](../first-run-with-registry-lab/): run the full multi-service - topology locally, with three Relays, cross-authority Notaries, and an identity provider. +- Define policy for this claim: purpose binding, context constraints, denial reasons, and audit + inspection are the next local rung. +- [Connect Notary to a Registry Data API source](../run-notary-standalone-for-api/): run Notary in + a separate project against a Registry Data API-shaped source. +- [Credential tour](../../start/credential-tour/): explore the hosted demo to see credential issuance and + cross-authority flows. ## Troubleshooting @@ -519,11 +299,6 @@ source registry API from `my-first-api`. | `registryctl add notary --from local-relay` cannot find a Relay project | The current directory does not contain a generated `registryctl.yaml` with a Relay section. | Run the command from the Relay tutorial project directory. | | `registryctl add notary --from local-relay` cannot find a source token | `secrets/local.env` is missing or does not contain the Relay row-reader key. | Recreate the Relay project or restore the generated local env file. | | `registryctl start` starts Relay but Notary is not ready | Notary config, source token, or Compose service wiring is invalid. | Run `registryctl status`, then `registryctl logs` and check the Notary service errors. | -| The Notary container log shows `failed to parse config YAML ... unknown field` | The locally cached container image does not match the digest-pinned image in the generated `compose.yaml`. | Run `docker compose pull` in the project directory to refresh the pinned images, then `registryctl start` again. | -| Notary `/ready` is degraded while `/healthz` is healthy | The local replay store or demo issuer key is not available to Notary. | Run `registryctl stop`, then `registryctl start`; if it persists, inspect `registryctl logs`. | | `registryctl notary smoke` returns `401` for authorized calls | The Notary evaluator key was not loaded or does not match the generated Notary fingerprint. | Run `. secrets/local.env`, then retry. | | Claim evaluation returns a source auth error | Notary cannot authenticate to Relay with `EVIDENCE_SOURCE_REGISTRY_RELAY_TOKEN`. | Confirm `secrets/local.env` has the source token and Relay is running. | | Claim evaluation returns `409 Evidence not available` | The target id is not in the sample workbook or the Relay entity lookup changed. | Use `per-2001` for the tutorial target, or inspect the Relay `person` entity. | -| `registryctl init notary` fails with `failed to read source token from $ROW_READER_RAW: environment variable not found` | The Relay keys were loaded in a different shell session, or not at all. | Run the `set -a; . secrets/local.env; set +a` block from the Relay project directory in this shell, then rerun the command. | -| Standalone `registryctl start` fails and the Notary log shows `network ... not found` | The Relay project is not running, or the `--source-network` name does not match the Relay project directory. | Start the Relay project, and pass the Compose network named after that directory with the `_default` suffix. | -| Standalone `registryctl notary smoke` returns `401` for authorized calls | The shell still holds the Relay project's keys, not the standalone Notary keys. | Run `set -a; . secrets/local.env; set +a` from the standalone project directory, then retry. | diff --git a/docs/site/src/data/generated/sidebar.json b/docs/site/src/data/generated/sidebar.json index 2b6cf46c..7b513aae 100644 --- a/docs/site/src/data/generated/sidebar.json +++ b/docs/site/src/data/generated/sidebar.json @@ -8,12 +8,67 @@ "slug": "products/registry-relay" }, { - "label": "Client integration", - "slug": "products/registry-relay/client-integration" + "label": "How-to", + "items": [ + { + "label": "Client integration", + "slug": "products/registry-relay/client-integration" + }, + { + "label": "Operations runbook", + "slug": "products/registry-relay/ops" + }, + { + "label": "Signed response credentials", + "slug": "products/registry-relay/provenance" + }, + { + "label": "Call Relay from OpenFn", + "slug": "products/registry-relay/openfn-relay-adaptor-guide" + }, + { + "label": "Standards adapter operator guide", + "slug": "products/registry-relay/standards-adapter-operator-guide" + } + ] + }, + { + "label": "Reference", + "items": [ + { + "label": "API guide", + "slug": "products/registry-relay/api" + }, + { + "label": "Configuration", + "slug": "products/registry-relay/configuration" + }, + { + "label": "Portable metadata", + "slug": "products/registry-relay/metadata" + }, + { + "label": "XLSX readiness contract", + "slug": "products/registry-relay/xlsx-readiness-contract" + }, + { + "label": "Release notes", + "slug": "products/registry-relay/release-notes" + } + ] }, { - "label": "Configuration", - "slug": "products/registry-relay/configuration" + "label": "Explanation", + "items": [ + { + "label": "Standards assumptions", + "slug": "products/registry-relay/standards-assumptions" + }, + { + "label": "Relay scenario catalog", + "slug": "products/registry-relay/relay-scenario-catalog" + } + ] } ] }, @@ -25,6 +80,15 @@ "label": "Overview", "slug": "products/registry-notary" }, + { + "label": "Tutorials", + "items": [ + { + "label": "OpenCRVS DCI standalone tutorial", + "slug": "products/registry-notary/opencrvs-dci-standalone-tutorial" + } + ] + }, { "label": "How-to", "items": [ @@ -36,13 +100,41 @@ "label": "Source and claim modeling", "slug": "products/registry-notary/source-claim-modeling-guide" }, + { + "label": "FHIR source adapter", + "slug": "products/registry-notary/fhir-source-adapter-guide" + }, + { + "label": "Script and Rhai source adapters", + "slug": "products/registry-notary/script-rhai-source-adapter-guide" + }, + { + "label": "OpenCRVS onboarding", + "slug": "products/registry-notary/opencrvs-onboarding" + }, + { + "label": "Federated evaluation operator guide", + "slug": "products/registry-notary/federated-evaluation-operator-guide" + }, + { + "label": "Self-attestation operator guide", + "slug": "products/registry-notary/self-attestation-operator-guide" + }, { "label": "Credential lifecycle and status", "slug": "products/registry-notary/credential-lifecycle-status" }, + { + "label": "Sidecar trust and secrets", + "slug": "products/registry-notary/sidecar-trust-and-secrets" + }, { "label": "Signing key provider", "slug": "products/registry-notary/signing-key-provider" + }, + { + "label": "Deployment hardening runbook", + "slug": "products/registry-notary/deployment-hardening-runbook" } ] }, @@ -53,9 +145,21 @@ "label": "Operator configuration reference", "slug": "products/registry-notary/operator-config-reference" }, + { + "label": "API reference", + "slug": "products/registry-notary/api-reference" + }, + { + "label": "OID4VCI wallet interop", + "slug": "products/registry-notary/oid4vci-wallet-interop" + }, { "label": "SD-JWT VC conformance profile", "slug": "products/registry-notary/sd-jwt-vc-conformance-profile" + }, + { + "label": "Release notes", + "slug": "products/registry-notary/release-notes" } ] }, @@ -66,6 +170,14 @@ "label": "Notary architecture", "slug": "products/registry-notary/architecture-overview" }, + { + "label": "Capability matrix", + "slug": "products/registry-notary/notary-capability-matrix" + }, + { + "label": "Scenario patterns", + "slug": "products/registry-notary/notary-scenario-patterns" + }, { "label": "Identity and record matching", "slug": "products/registry-notary/identity-and-record-matching" @@ -90,6 +202,10 @@ "label": "Validate against profile fixtures", "slug": "products/registry-manifest/profile-fixtures" }, + { + "label": "ITB and SEMIC validation", + "slug": "products/registry-manifest/itb-semic-validation" + }, { "label": "Reference", "slug": "products/registry-manifest/reference" diff --git a/docs/site/src/data/repo-docs.yaml b/docs/site/src/data/repo-docs.yaml index 64e2acd1..1f2b39d2 100644 --- a/docs/site/src/data/repo-docs.yaml +++ b/docs/site/src/data/repo-docs.yaml @@ -27,6 +27,66 @@ repos: label: Configuration nav_order: 30 doc_type: reference + - src: crates/registry-relay/docs/api.md + dest: products/registry-relay/api + label: API guide + nav_order: 25 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/metadata.md + dest: products/registry-relay/metadata + label: Portable metadata + nav_order: 35 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/ops.md + dest: products/registry-relay/ops + label: Operations runbook + nav_order: 40 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/provenance.md + dest: products/registry-relay/provenance + label: Signed response credentials + nav_order: 45 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/openfn-relay-adaptor-guide.md + dest: products/registry-relay/openfn-relay-adaptor-guide + label: Call Relay from OpenFn + nav_order: 50 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/standards-adapter-operator-guide.md + dest: products/registry-relay/standards-adapter-operator-guide + label: Standards adapter operator guide + nav_order: 55 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/xlsx-readiness-contract.md + dest: products/registry-relay/xlsx-readiness-contract + label: XLSX readiness contract + nav_order: 60 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/STANDARDS_ASSUMPTIONS.md + dest: products/registry-relay/standards-assumptions + label: Standards assumptions + nav_order: 65 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/relay-scenario-catalog.md + dest: products/registry-relay/relay-scenario-catalog + label: Relay scenario catalog + nav_order: 70 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/release-notes.md + dest: products/registry-relay/release-notes + label: Release notes + nav_order: 80 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] registry-notary: remote: https://github.com/registrystack/registry-stack ref: HEAD @@ -91,6 +151,84 @@ repos: label: SD-JWT VC conformance profile nav_order: 130 doc_type: reference + - src: products/notary/docs/notary-capability-matrix.md + dest: products/registry-notary/notary-capability-matrix + label: Capability matrix + nav_order: 15 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/notary-scenario-patterns.md + dest: products/registry-notary/notary-scenario-patterns + label: Scenario patterns + nav_order: 20 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/fhir-source-adapter-guide.md + dest: products/registry-notary/fhir-source-adapter-guide + label: FHIR source adapter + nav_order: 62 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/script-rhai-source-adapter-guide.md + dest: products/registry-notary/script-rhai-source-adapter-guide + label: Script and Rhai source adapters + nav_order: 64 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/opencrvs-onboarding.md + dest: products/registry-notary/opencrvs-onboarding + label: OpenCRVS onboarding + nav_order: 66 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/opencrvs-dci-standalone-tutorial.md + dest: products/registry-notary/opencrvs-dci-standalone-tutorial + label: OpenCRVS DCI standalone tutorial + nav_order: 68 + doc_type: tutorial + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/federated-evaluation-operator-guide.md + dest: products/registry-notary/federated-evaluation-operator-guide + label: Federated evaluation operator guide + nav_order: 72 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/self-attestation-operator-guide.md + dest: products/registry-notary/self-attestation-operator-guide + label: Self-attestation operator guide + nav_order: 75 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/sidecar-trust-and-secrets.md + dest: products/registry-notary/sidecar-trust-and-secrets + label: Sidecar trust and secrets + nav_order: 85 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/deployment-hardening-runbook.md + dest: products/registry-notary/deployment-hardening-runbook + label: Deployment hardening runbook + nav_order: 95 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/api-reference.md + dest: products/registry-notary/api-reference + label: API reference + nav_order: 100 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/oid4vci-wallet-interop.md + dest: products/registry-notary/oid4vci-wallet-interop + label: OID4VCI wallet interop + nav_order: 110 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/release-notes.md + dest: products/registry-notary/release-notes + label: Release notes + nav_order: 140 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] registry-manifest: remote: https://github.com/registrystack/registry-stack ref: HEAD @@ -123,3 +261,9 @@ repos: label: Reference nav_order: 30 doc_type: reference + - src: products/manifest/docs/itb-semic-validation.md + dest: products/registry-manifest/itb-semic-validation + label: ITB and SEMIC validation + nav_order: 25 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] diff --git a/docs/site/src/styles/custom.css b/docs/site/src/styles/custom.css index 2d7ec21a..89871b65 100644 --- a/docs/site/src/styles/custom.css +++ b/docs/site/src/styles/custom.css @@ -709,7 +709,8 @@ pre, .status-chip, .claim-chip, -.adoption-chip { +.adoption-chip, +.gap-chip { display: inline; border: 0; padding: 0; @@ -724,6 +725,12 @@ pre, background: transparent; } +/* Unresolved evidence_gap flag from standards.yaml; ink weight, not color. */ +.gap-chip { + color: var(--registry-ink); + font-weight: 600; +} + .source-note { color: var(--registry-muted); font-size: 0.85rem; diff --git a/docs/site/styles/RegistryDocs/Latin.yml b/docs/site/styles/RegistryDocs/Latin.yml index c6509d48..1066f864 100644 --- a/docs/site/styles/RegistryDocs/Latin.yml +++ b/docs/site/styles/RegistryDocs/Latin.yml @@ -1,5 +1,5 @@ extends: substitution -message: "Spell out '%s' as '%s'." +message: "Use '%s' instead of '%s'." level: warning ignorecase: true nonword: true diff --git a/products/manifest/docs/itb-semic-validation.md b/products/manifest/docs/itb-semic-validation.md index 39274ace..999cda62 100644 --- a/products/manifest/docs/itb-semic-validation.md +++ b/products/manifest/docs/itb-semic-validation.md @@ -66,10 +66,9 @@ data-theme concept scheme and EuroVoc concept scheme so consumers can see both catalogue theme taxonomies. The public 2.x warning profiles may still flag one value while accepting the artifact with zero validation errors. -Tracked follow-up issues: - -- [#23 Investigate BRegDCAT publisher CorporateBodyRestriction warning](https://github.com/jeremi/registry-manifest/issues/23) -- [#24 Resolve profile-specific `dcat:themeTaxonomy` warnings from BRegDCAT validation](https://github.com/jeremi/registry-manifest/issues/24) +These warning classes are intentionally documented rather than hidden: the +smoke script retains the validator reports, and public release notes should +carry the profile and warning count when BRegDCAT-AP evidence is cited. ## Public Claim Boundary diff --git a/products/manifest/docs/overview.md b/products/manifest/docs/overview.md index c5abbda4..5d2bb11b 100644 --- a/products/manifest/docs/overview.md +++ b/products/manifest/docs/overview.md @@ -189,11 +189,11 @@ These are examples until reviewed against official OpenCRVS, OpenSPP, OpenIMIS, or maintainer-provided artifacts. Four additional subdirectories exist in `profiles/` for -[`opencrvs`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/opencrvs/), -[`openimis`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/openimis/), -[`openspp`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/openspp/), +[`opencrvs`](../profiles/opencrvs/), +[`openimis`](../profiles/openimis/), +[`openspp`](../profiles/openspp/), and -[`spdci`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/spdci/). +[`spdci`](../profiles/spdci/). Each contains only a `README.md` that marks it as a placeholder pending official review. ## v0 caveats diff --git a/products/manifest/docs/profile-fixtures.md b/products/manifest/docs/profile-fixtures.md index 01c1cc2e..b1adfa29 100644 --- a/products/manifest/docs/profile-fixtures.md +++ b/products/manifest/docs/profile-fixtures.md @@ -132,7 +132,7 @@ program area or official standard), follow these steps to create and validate it 6. Run `validate-profiles profiles` to confirm the fixture passes alongside all existing profiles. See -[`profiles/example-civil-registration/profile.yaml`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/example-civil-registration/profile.yaml) +[`profiles/example-civil-registration/profile.yaml`](../profiles/example-civil-registration/profile.yaml) for a complete example. ## Verification diff --git a/products/manifest/docs/reference.md b/products/manifest/docs/reference.md index 882b2cb6..6278db31 100644 --- a/products/manifest/docs/reference.md +++ b/products/manifest/docs/reference.md @@ -5,7 +5,7 @@ Look up CLI subcommand flags, manifest key definitions, federation metadata, pub ## CLI subcommands Source: -[`crates/registry-manifest-cli/src/main.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-cli/src/main.rs) +[`crates/registry-manifest-cli/src/main.rs`](../../../crates/registry-manifest-cli/src/main.rs) ### `validate` @@ -70,7 +70,7 @@ Use `--format cpsv-ap` for the service catalogue rather than `--format dcat --pr Schema version enforced: `registry-manifest/v1`. Source: -[`crates/registry-manifest-core/src/lib.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/src/lib.rs) +[`crates/registry-manifest-core/src/lib.rs`](../../../crates/registry-manifest-core/src/lib.rs) (`MetadataManifest` struct). | Key | Type | Required | Description | @@ -101,7 +101,7 @@ Source: | `application_profiles` | list of `ApplicationProfile` | No | Profile IDs and versions the catalog declares support for (for example, `[{id: "bregdcat-ap", version: "3.0.0"}]`). | | `standards` | `StandardsManifest` | No | Declares DCAT, SHACL, and JSON Schema versions in use. | -### DatasetManifest keys (common keys; see [source](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/src/lib.rs) for the full type definition) +### DatasetManifest keys (common keys; see [source](../../../crates/registry-manifest-core/src/lib.rs) for the full type definition) | Key | Description | | --- | --- | @@ -183,7 +183,7 @@ They belong in Registry Relay or Registry Notary runtime configuration, not in a `source`, `source_id`, `table`, `token_url`, `url`, `url_env`, `visibility` Source: -[`crates/registry-manifest-core/src/lib.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/src/lib.rs) +[`crates/registry-manifest-core/src/lib.rs`](../../../crates/registry-manifest-core/src/lib.rs) (`RUNTIME_ONLY_KEYS`). ## Schema versions @@ -244,7 +244,7 @@ for validation bugs, security fixes, or explicitly forbidden runtime-only keys. ## Publish output artifacts Source: -[`crates/registry-manifest-cli/src/main.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-cli/src/main.rs) +[`crates/registry-manifest-cli/src/main.rs`](../../../crates/registry-manifest-cli/src/main.rs) (`publish_command`). All paths are relative to the `--out` directory. @@ -272,7 +272,7 @@ The `index.json` structure contains the schema version, digest metadata, top-lev artifact URLs, and arrays for per-profile, per-schema, per-policy, and per-offering documents. Source: -[`crates/registry-manifest-cli/src/main.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-cli/src/main.rs) +[`crates/registry-manifest-cli/src/main.rs`](../../../crates/registry-manifest-cli/src/main.rs) Digest fields use `sha256:` values: @@ -356,7 +356,7 @@ Minimal example shape: ## Golden fixture coverage The test suite in -[`crates/registry-manifest-core/tests/metadata_core.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/tests/metadata_core.rs) +[`crates/registry-manifest-core/tests/metadata_core.rs`](../../../crates/registry-manifest-core/tests/metadata_core.rs) asserts exact output for the following renderer and profile combinations. These golden files live under `crates/registry-manifest-core/tests/fixtures/golden/`. diff --git a/products/manifest/docs/validate-and-render.md b/products/manifest/docs/validate-and-render.md index 3e9bc1f2..3725dcae 100644 --- a/products/manifest/docs/validate-and-render.md +++ b/products/manifest/docs/validate-and-render.md @@ -25,7 +25,7 @@ If you have installed the binary directly, replace that prefix with `registry-ma The four example profile fixtures in the repository serve as ready-made manifest inputs. The commands in this how-to use -[`profiles/example-civil-registration/fixtures/metadata.yaml`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/example-civil-registration/fixtures/metadata.yaml) +[`profiles/example-civil-registration/fixtures/metadata.yaml`](../profiles/example-civil-registration/fixtures/metadata.yaml) as the example path. Substitute your own manifest path as needed. diff --git a/products/notary/docs/README.md b/products/notary/docs/README.md index 103cd253..e9b8e21b 100644 --- a/products/notary/docs/README.md +++ b/products/notary/docs/README.md @@ -10,10 +10,12 @@ format, or issues a short-lived SD-JWT VC credential. Pick your path below. New to Registry Notary? Start with the hosted walkthrough or a runnable local tutorial. If you are configuring or operating Notary, start with the [architecture overview](architecture-overview.md). - [See it live](https://docs.registrystack.org/start/see-it-live/): watch Notary issue a privacy-preserving credential against a hosted lab, with zero install. -- [Verify a claim with Registry Notary](https://docs.registrystack.org/tutorials/verify-claim-registry-api/): add Notary to a local registry API project with `registryctl`. Its final section, [Run Notary standalone for an API you operate](https://docs.registrystack.org/tutorials/verify-claim-registry-api/#run-notary-standalone-for-an-api-you-operate), covers creating a standalone Notary project for a source API you operate. +- [Verify a claim with Registry Notary](https://docs.registrystack.org/tutorials/verify-claim-registry-api/): add Notary to a local registry API project with `registryctl`. +- [Connect Notary to a Registry Data API source](https://docs.registrystack.org/tutorials/run-notary-standalone-for-api/): run Notary in a separate project against a Registry Data API-shaped source. +- [Configure DHIS2 claim checks](https://docs.registrystack.org/tutorials/configure-dhis2-claim-checks/): use the built-in `http_json` source adapter to evaluate DHIS2 Tracker claims and issue an SD-JWT VC from the result. - [Architecture overview](architecture-overview.md): what Registry Notary is, the request lifecycle, and how the layers relate. -- [Capability matrix](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/notary-capability-matrix.md): which flows Notary supports today, by persona and system. +- [Capability matrix](notary-capability-matrix.md): which flows Notary supports today, by persona and system. - [Identity and record matching](identity-and-record-matching.md): how Notary resolves the target entity to a source record, the outcome model, and matching policy. ## Integrate @@ -21,15 +23,14 @@ Pick your path below. New to Registry Notary? Start with the hosted walkthrough For application and wallet developers calling the API or the SDKs. - [Client SDK guide](client-sdk-guide.md): evaluate claims and issue credentials from Rust, Python, and Node.js. -- [Call Notary from OpenFn](openfn-notary-caller-guide.md): use the Registry Stack OpenFn Notary adaptor to branch a workflow on a minimized claim result or certified value claim. -- [API reference](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/api-reference.md): the route-to-client-method matrix and the stable problem-code registry. +- [API reference](api-reference.md): the route-to-client-method matrix and the stable problem-code registry. - [Wallet interop with OID4VCI](oid4vci-wallet-interop.md): the OpenID4VCI wallet facade contract and compatibility checklist. - [SD-JWT VC conformance](sd-jwt-vc-conformance-profile.md): the supported credential wire contract and the explicit non-support list. - [OpenCRVS tutorial](opencrvs-dci-standalone-tutorial.md): issue local demo SD-JWT VCs from OpenCRVS birth-record evidence. - [OpenCRVS onboarding model](opencrvs-onboarding.md): understand the registryctl-generated project boundary, evidence question, demo signing posture, and lightweight PDP model. -- [Scenario patterns](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/notary-scenario-patterns.md): reusable evaluation, federation, and issuance flows with sequence diagrams. +- [Scenario patterns](notary-scenario-patterns.md): reusable evaluation, federation, and issuance flows with sequence diagrams. - [GITB conformance suite design](../specs/gitb-conformance-suite.md): target runtime scenarios and claim boundary for ITB/GITB interoperability evidence. ## Operate @@ -38,13 +39,14 @@ For operators deploying, configuring, and running a Registry Notary. - [Configuration reference](operator-config-reference.md): the config blocks for auth, evidence, sources, replay, status, self-attestation, OID4VCI, and federation. - [Model sources and claims](source-claim-modeling-guide.md): design source connectors, source adapter sidecars, claim boundaries, disclosure, and batch reads. +- [DHIS2 source adapter tutorial](https://docs.registrystack.org/tutorials/configure-dhis2-claim-checks/): configure a sidecar that calls DHIS2 Tracker through the built-in `http_json` engine. - [FHIR source adapter](fhir-source-adapter-guide.md): project bounded FHIR R4 graphs into Notary-ready source facts without exposing raw FHIR Bundles. - [Script (Rhai) source adapter](script-rhai-source-adapter-guide.md): run a sandboxed, orchestration-only Rhai script for sources that need a little branching across a few governed reads. - [Signing key providers](signing-key-provider.md): SD-JWT VC signing-key configuration, rotation, and PKCS#11 setup. - [Self-attestation](self-attestation-operator-guide.md): citizen OIDC subject binding, token policy, allow-lists, and rollout. -- [Federated evaluation](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/federated-evaluation-operator-guide.md): static-peer setup, environment variables, and the replay limitation. +- [Federated evaluation](federated-evaluation-operator-guide.md): static-peer setup, environment variables, and the replay limitation. - [Credential lifecycle and status](credential-lifecycle-status.md): short-lived credentials, optional live status, retention, and verifier caveats. -- [Sidecar trust and secret handling](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/sidecar-trust-and-secrets.md): how the source adapter sidecar verifies its configuration, how Notary pins the sidecar it trusts, how secrets are handled, and what that path does and does not protect against. +- [Sidecar trust and secret handling](sidecar-trust-and-secrets.md): how the source adapter sidecar verifies its configuration, how Notary pins the sidecar it trusts, how secrets are handled, and what that path does and does not protect against. - [Deployment hardening runbook](deployment-hardening-runbook.md): production-readiness checklist for network boundaries, secrets, Redis, audit, and rollback. ## Build and maintain @@ -54,9 +56,9 @@ For maintainers changing the code or reviewing design history. - [Workspace layout](../README.md#layout): the crates and bindings and what each owns. - [Command-line interface](../../../crates/registry-notary/README.md): the server binary and its subcommands. - [Design records](../specs/README.md): specifications and implementation traces, kept as design history. -- [Security assurance](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/security-assurance.md): CI security gates, image publication and signing policy. +- [Security assurance](security-assurance.md): CI security gates, image publication and signing policy. ## Related -- [Release notes](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/release-notes.md) +- [Release notes](release-notes.md) - [Security policy](../../../SECURITY.md) diff --git a/products/notary/docs/client-sdk-guide.md b/products/notary/docs/client-sdk-guide.md index 24ff2a32..05882027 100644 --- a/products/notary/docs/client-sdk-guide.md +++ b/products/notary/docs/client-sdk-guide.md @@ -74,16 +74,16 @@ integrations to a release tag or commit. ```toml [dependencies] -registry-notary-client = { git = "https://github.com/jeremi/registry-notary", tag = "vX.Y.Z" } -registry-notary-core = { git = "https://github.com/jeremi/registry-notary", tag = "vX.Y.Z" } +registry-notary-client = { git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z" } +registry-notary-core = { git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z" } ``` ```bash -python -m pip install "git+https://github.com/jeremi/registry-notary.git@vX.Y.Z#subdirectory=bindings/python" +python -m pip install "git+https://github.com/registrystack/registry-stack.git@vX.Y.Z#subdirectory=products/notary/bindings/python" ``` ```bash -pnpm add "github:jeremi/registry-notary#vX.Y.Z&path:bindings/node" +pnpm add "github:registrystack/registry-stack#vX.Y.Z&path:products/notary/bindings/node" ``` npm does not support installing from a subdirectory of a git repository. With @@ -857,7 +857,7 @@ In Rust, enable the `oid4vci` feature: ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["oid4vci"] } @@ -890,7 +890,7 @@ In Rust, enable the `federation` feature: ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["federation"] } @@ -1006,7 +1006,7 @@ Enable optional routes only when needed: ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["oid4vci", "federation", "json-facade"] } @@ -1059,7 +1059,7 @@ network refreshes or trust-policy decisions. ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["verifier"] } diff --git a/products/notary/docs/operator-config-reference.md b/products/notary/docs/operator-config-reference.md index 884ac8c0..7a7bf2d2 100644 --- a/products/notary/docs/operator-config-reference.md +++ b/products/notary/docs/operator-config-reference.md @@ -501,7 +501,7 @@ handling, or output normalization outside Notary. The sidecar source chooses `engine: http_json`, `engine: http_flow`, `engine: fhir`, or `engine: script_rhai` (a sandboxed, orchestration-only Rhai script for sources that need a little branching across a few governed source calls, such as a JSON -POST search followed by a GET, or a visible-404 fallback — see the +POST search followed by a GET, or a visible-404 fallback; see the [Script (Rhai) Source Adapter Guide](script-rhai-source-adapter-guide.md)) in its own signed manifest. The source connection must use static sidecar bearer auth through `token_env`. Do not configure target-service credentials in Notary; keep them diff --git a/products/notary/docs/release-notes.md b/products/notary/docs/release-notes.md index 04747716..d3b57484 100644 --- a/products/notary/docs/release-notes.md +++ b/products/notary/docs/release-notes.md @@ -81,11 +81,10 @@ `fingerprint` reference (`provider`, `name`, `commitment`) in place of `hash_env`, so signed config apply can govern caller-credential rotation. - Renamed OIDC config fields to the shared Registry service convention: - `auth.oidc.jwks_uri` -> `auth.oidc.jwks_url`, - `auth.oidc.leeway_seconds` -> `auth.oidc.leeway`, and - `auth.oidc.allowed_typ` -> `auth.oidc.allowed_token_types`. Old names fail - config load with an error naming the replacement. `auth.oidc.leeway` now uses - humantime strings such as `30s`; self-attestation + `auth.oidc.jwks_url`, `auth.oidc.leeway`, and + `auth.oidc.allowed_token_types`. Legacy aliases fail config load with an error + naming the replacement. `auth.oidc.leeway` now uses humantime strings such as + `30s`; self-attestation `token_policy.max_clock_leeway_seconds` still bounds the resolved duration. - Removed `server.cors.allow_credentials`; Registry Notary now always disables credentialed CORS on the operator-configured server CORS layer. Remove the diff --git a/products/notary/docs/script-rhai-source-adapter-guide.md b/products/notary/docs/script-rhai-source-adapter-guide.md index d9045928..fa27a396 100644 --- a/products/notary/docs/script-rhai-source-adapter-guide.md +++ b/products/notary/docs/script-rhai-source-adapter-guide.md @@ -5,7 +5,7 @@ orchestration script** when a source API needs a little imperative shaping that is awkward for the declarative `http_json` / `http_flow` engines but too small to justify a bespoke adapter. The script is **orchestration-only**: it decides which configured request to make next and shapes the returned JSON into records. It -never holds network authority, credentials, or policy — the sidecar host keeps +never holds network authority, credentials, or policy; the sidecar host keeps all of those. Notary configs use the normal `connector: source_adapter_sidecar`; the @@ -21,10 +21,10 @@ row shape every engine returns: Reach for `script_rhai` only when the declarative engines do not fit: -- `http_json` — one governed request and a JSON/CEL projection. -- `http_flow` — a fixed declarative sequence of dependent reads. -- `fhir` — a bounded FHIR R4 GET graph. -- `script_rhai` — 1–3 governed source calls where the script must **branch** on +- `http_json`: one governed request and a JSON/CEL projection. +- `http_flow`: a fixed declarative sequence of dependent reads. +- `fhir`: a bounded FHIR R4 GET graph. +- `script_rhai`: 1–3 governed source calls where the script must **branch** on a response (e.g. POST a search body, then GET a returned id; try one path, fall back on a 404), or normalize source-specific JSON that the declarative mappers cannot express. @@ -56,7 +56,7 @@ so a buggy script or a hostile *upstream response* cannot escalate: concurrency are all bounded. The script is **compiled and smoke-tested at startup**; a compile, policy, or smoke failure blocks readiness. - **Governed provenance.** The script is embedded inline in the signed runtime - target, so it is covered by the target's `config_hash` — the same TUF-verified + target, so it is covered by the target's `config_hash`, the same TUF-verified content anchor used for inline CEL today. ## The script contract @@ -222,7 +222,7 @@ for upstreams that require a JSON token request. Optional fields are `scope`, or a vendor API-version header) on every call to that target. Header values are governed config and flow through the `config_hash`. Authentication, cookie, host/length framing, hop-by-hop, and forwarding headers (and any `Proxy-*` -header) are **rejected at startup** — put credentials in `auth`, not `headers`. +header) are **rejected at startup**: put credentials in `auth`, not `headers`. ### Observable statuses @@ -230,7 +230,7 @@ By default any non-2xx terminates the run and maps to a problem code (`401`/`403` → `source.target_auth`, `429` → `source.target_rate_limit`, timeout → `source.timeout`, everything else → `source.unavailable`). List a status in a target's `visible_statuses` to let the script **observe** it as -`#{ status, body }` and branch instead — for example a `404` that means +`#{ status, body }` and branch instead, for example a `404` that means "not found here, try the fallback". The engine is compiled with the union of all targets' visible statuses as a ceiling, and the per-target list is enforced by the host. @@ -261,7 +261,7 @@ engines. Because the script is inline in the signed runtime target, it is covered by the target `config_hash` that Notary verifies before source reads. The `/ready` and `/v1/assurance` booleans (`expression_hashes_verified`, `runtime_verified`, `smoke_verified`) attest that whole-target hash together -with the startup compile and smoke check — a startup failure on any of them +with the startup compile and smoke check; a startup failure on any of them blocks readiness, so a sidecar that serves these values has satisfied all three. ## Local verification diff --git a/products/notary/docs/sidecar-trust-and-secrets.md b/products/notary/docs/sidecar-trust-and-secrets.md index aef410e6..59290d35 100644 --- a/products/notary/docs/sidecar-trust-and-secrets.md +++ b/products/notary/docs/sidecar-trust-and-secrets.md @@ -13,7 +13,7 @@ The general governed-configuration model (signed bundles, TUF verification, trus roots, signer thresholds, and anti-rollback) is a shared Registry Platform capability used by Registry Relay and Registry Notary alike. This page does not restate it. See -[Governed configuration](../../registry-platform/docs/governed-configuration.md) +[Governed configuration](../../platform/docs/governed-configuration.md) for the shared model and how verification, authorization, and rollback protection work. Configuration integrity there is built on TUF (The Update Framework) through a standard, maintained client, not homegrown cryptography. What follows @@ -39,7 +39,7 @@ is the Notary- and sidecar-specific layer on top of that model. ## What you are responsible for Key custody and trust-root distribution are part of the -[shared governed-configuration model](../../registry-platform/docs/governed-configuration.md#trust-roots-roles-and-change-classes) +[shared governed-configuration model](../../platform/docs/governed-configuration.md#trust-roots-roles-and-change-classes) and matter just as much here: the guarantees above are only as strong as your protection of the signing keys. Specific to the sidecar path: @@ -134,7 +134,7 @@ without production key custody. ## Where to go next -- [Governed configuration](../../registry-platform/docs/governed-configuration.md): +- [Governed configuration](../../platform/docs/governed-configuration.md): the shared platform model behind signed configuration, trust roots, signer thresholds, and anti-rollback. Read this first for the trust model itself. - [Model sources and claims](source-claim-modeling-guide.md): configure the diff --git a/products/notary/docs/source-claim-modeling-guide.md b/products/notary/docs/source-claim-modeling-guide.md index 32d0238e..5ed59d52 100644 --- a/products/notary/docs/source-claim-modeling-guide.md +++ b/products/notary/docs/source-claim-modeling-guide.md @@ -26,7 +26,7 @@ Keep source connectors narrow and keep claim semantics in Notary config. | --- | --- | --- | | DCI | The upstream speaks a DCI-style search envelope | `connector: dci` | | Registry Data API | The upstream exposes `/v1/datasets/{dataset}/entities/{entity}/records` lookups | `connector: registry_data_api` | -| Source adapter sidecar | A private sidecar must normalize a target system outside Notary, using built-in `http_json`, `http_flow`, or `fhir` source engines | `connector: source_adapter_sidecar` | +| Source adapter sidecar | A private sidecar must normalize a target system outside Notary, using built-in `http_json`, `http_flow`, `fhir`, or `script_rhai` source engines | `connector: source_adapter_sidecar` | Prefer the simplest direct source. Add a sidecar when the target system needs private credentials, governed request shaping, or output normalization outside @@ -118,8 +118,10 @@ The source adapter sidecar is a separate private process that normalizes a targe system into Notary's source-read contracts. The Notary connector value remains `source_adapter_sidecar`. Inside the sidecar, a source can use the built-in `http_json` engine for straightforward HTTP JSON APIs, the built-in `http_flow` -engine for short dependent GET-only HTTP JSON reads, or the built-in `fhir` -engine. Use the first-class connector for new configs: +engine for short dependent GET-only HTTP JSON reads, the built-in `fhir` +engine, or the sandboxed `script_rhai` engine for small branching cases that +do not fit the declarative engines. Use the first-class connector for new +configs: ```yaml evidence: @@ -188,7 +190,9 @@ Boundary rules: - The sidecar must be reachable only over localhost or a private pod network from Notary. Do not expose it publicly or place it behind an internet-facing ingress. -- Pin worker runtime and adaptor versions for source-adapter sources. +- Pin the sidecar image and governed runtime config for source-adapter sources. + If you use a compatibility path that carries separate runtime or adaptor + versions, pin those too. - Store sidecar target credentials in sidecar env, not in Notary config. - Return no more than two records for a lookup. - Return only normalized fields needed by Notary.