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/astro.config.mjs b/docs/site/astro.config.mjs index fa60de1d..79f44582 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -67,8 +67,8 @@ export default defineConfig({ // 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/`, @@ -204,8 +204,9 @@ export default defineConfig({ // // "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. + // evaluation page. The core Tutorials path stays on one generated local + // project; operator paths live under Apply to your stack, and named + // source-system paths live under Integrate existing systems. sidebar: [ { label: 'Get started', @@ -220,11 +221,23 @@ export default defineConfig({ { label: 'Tutorials', 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' }, + { label: 'Run a protected API', slug: 'tutorials/publish-spreadsheet-secured-registry-api' }, + { label: 'Evaluate a claim', slug: 'tutorials/verify-claim-registry-api' }, + ], + }, + { + label: 'Apply to your stack', + items: [ + { label: 'Notary for a Registry Data API', slug: 'tutorials/run-notary-standalone-for-api' }, { label: 'Deploy with own data', slug: 'tutorials/deploy-standalone-with-own-data' }, { label: 'Run the lab', slug: 'tutorials/first-run-with-registry-lab' }, + ], + }, + { + label: 'Integrate existing systems', + 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' }, ], }, 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/src/components/HomeLanding.astro b/docs/site/src/components/HomeLanding.astro index 5d19dbac..3719c488 100644 --- a/docs/site/src/components/HomeLanding.astro +++ b/docs/site/src/components/HomeLanding.astro @@ -51,10 +51,10 @@ const cards: RouteCard[] = [ id: 'try-locally', question: 'How do I try it on my own machine?', start: { - label: 'Publish a spreadsheet as a secured registry API', + label: 'Run a protected registry API locally', href: '/tutorials/publish-spreadsheet-secured-registry-api/', }, - then: [{ label: 'Verify a claim with Registry Notary', href: '/tutorials/verify-claim-registry-api/' }], + then: [{ label: 'Evaluate a claim with Registry Notary', href: '/tutorials/verify-claim-registry-api/' }], }, { id: 'full-demo', @@ -64,10 +64,16 @@ const cards: RouteCard[] = [ }, { id: 'fhir', - question: 'How do I try claim checks from FHIR-shaped health data?', + question: 'How do I integrate 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: 'dhis2', + question: 'How do I configure claim checks from DHIS2?', + start: { label: 'Configure DHIS2 claim checks', href: '/tutorials/configure-dhis2-claim-checks/' }, + then: [{ label: 'Integration patterns', href: '/explanation/integration-patterns/' }], + }, { id: 'own-data', question: 'How do I deploy Relay against my own data, not a sample?', @@ -79,16 +85,16 @@ const cards: RouteCard[] = [ }, { id: 'existing-api', - question: 'I already have an API. How do I verify claims from it?', + question: 'I already have a Registry Data API source. How do I evaluate 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', + label: 'Connect Notary to a Registry Data API source', + href: '/tutorials/run-notary-standalone-for-api/', }, then: [{ label: 'Registry Notary', href: '/products/registry-notary/' }], }, { id: 'opencrvs', - question: 'How do I verify claims from OpenCRVS?', + question: 'How do I integrate OpenCRVS claim checks?', start: { label: 'Verify OpenCRVS claims with registryctl', href: '/tutorials/verify-opencrvs-claims/' }, then: [ { 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/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/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/start/see-it-live.mdx b/docs/site/src/content/docs/start/see-it-live.mdx index ad2d6c13..0a7819c3 100644 --- a/docs/site/src/content/docs/start/see-it-live.mdx +++ b/docs/site/src/content/docs/start/see-it-live.mdx @@ -6,7 +6,7 @@ owner: registry-docs source_repos: - registry-lab - registry-registryctl -last_reviewed: "2026-06-13" +last_reviewed: "2026-06-28" doc_type: tutorial locale: en standards_referenced: @@ -21,8 +21,7 @@ will read a protected registry API from your terminal (**Registry Relay**, which 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 +needs no install either. Everything runs in the hosted lab at [lab.registrystack.org](https://lab.registrystack.org), so the main flow has no setup on your machine. @@ -30,7 +29,7 @@ machine. outcome="One protected registry read and one signed, privacy-preserving credential, against the public hosted lab." time="About 10 minutes" level="Hosted lab, zero install" - prerequisites={['A web browser', 'Optional: curl, registryctl, jq, node']} + prerequisites={['A web browser', 'Optional: curl']} /> This lab uses synthetic data and public demo-only credentials by design. @@ -45,7 +44,7 @@ The lab runs three services that you will touch directly: - 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 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 @@ -187,155 +186,25 @@ refuses: it will not issue a credential for a subject you did not authenticate a be talked into vouching for someone else. For more on this service, see [Registry Notary](../../products/registry-notary/). -### Developer API round-trip +### DHIS2 integration path -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" - ] -} -``` +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/). ## 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/): +- [Run a protected registry API locally](../../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). +- [Evaluate a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): run a Notary + against the Relay you published and evaluate a claim. +- [Connect Notary to a Registry Data API source](../../tutorials/run-notary-standalone-for-api/): + run Notary in a separate project against a Registry Data API-shaped source. +- [Configure DHIS2 claim checks](../../tutorials/configure-dhis2-claim-checks/): connect Registry + Notary to a DHIS2 source adapter and inspect the fields to change for your own DHIS2 deployment. Only assessing fit? You do not need to install anything: [When to use Registry Stack](../when-to-use/) covers fit and non-goals, and 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..7b6e7dfe --- /dev/null +++ b/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx @@ -0,0 +1,334 @@ +--- +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'; + +Use this tutorial when DHIS2 is already your source system and you want Registry Notary to answer +narrow programme-status claims without sharing full DHIS2 records. +The tutorial uses the public DHIS2 play instance as the source, runs a private source-adapter +sidecar, and exposes only the Registry Notary API 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 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 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`. +For your deployment, change the DHIS2 base URL allow-list, program IDs, attribute IDs, source +projection, and smoke lookup subject to match your DHIS2 configuration. + +## 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. | + +For your deployment, update the claims to match the programme facts your relying services need. +Keep the sidecar private; expose Notary, not DHIS2, to callers. + +## Stop the profile + +Stop the DHIS2 services: + +```sh +docker compose -f compose.yaml --profile dhis2 down +``` + +## 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..643cea40 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 @@ -33,8 +33,8 @@ stack locally, start with [See it live](../../start/see-it-live/) instead. This 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` | 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 4c0eaec5..15527029 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 -``` +The command adds `notary/config.yaml`, updates `compose.yaml`, refreshes the Bruno collection, and +adds Notary demo keys to `secrets/local.env`. -`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. - -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: @@ -311,24 +199,10 @@ curl -sS -X POST \ http://127.0.0.1:4255/v1/evaluations ``` -Notary does not return `satisfied: false` for a missing person. -It returns a `409` problem document, because there is no evidence to evaluate: - -```json -{ - "code": "evidence.not_available", - "detail": "the evidence is not available", - "request_id": "01KTS31QC7184F5487VW0P7XM3", - "status": 409, - "title": "Evidence not available", - "type": "https://docs.registry-notary.dev/problems/evidence/not_available" -} -``` - -This distinction matters to integrators: a claim result answers the configured question about a -known subject, while a missing subject is an evidence error, not a negative answer. +Notary returns `409 Evidence not available`. +A missing subject is an evidence error, not a negative claim result. -## Compare a row read and a claim result +## Compare row read and claim result Relay still exposes the source-facing consultation API: @@ -340,7 +214,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 +231,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 +Use Notary when a caller should receive a narrow claim result. -`registryctl add notary --from local-relay` refreshes the generated Bruno collection with Notary -requests. -Bruno is optional. -The tutorial and API work without it. +## Inspect the generated claim -Open the generated collection: +Now that the evaluation works, inspect the generated Notary contract: ```sh -registryctl bruno open +sed -n '1,220p' notary/config.yaml ``` -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. +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`. -If the Bruno CLI is installed, you can run the collection: - -```sh -registryctl bruno run -``` - -If `bru` is not installed, the command prints a fallback and exits without blocking Relay or -Notary. - -## Open the Notary API reference - -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 +266,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. +- [See it live](../../start/see-it-live/): explore the hosted demo to see credential issuance and + cross-authority flows. ## Troubleshooting @@ -519,11 +285,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/products/notary/docs/README.md b/products/notary/docs/README.md index 103cd253..afb65d3d 100644 --- a/products/notary/docs/README.md +++ b/products/notary/docs/README.md @@ -10,7 +10,9 @@ 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. @@ -21,7 +23,6 @@ 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. - [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. @@ -38,6 +39,7 @@ 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. 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.