diff --git a/crates/registry-notary-server/src/relay_client.rs b/crates/registry-notary-server/src/relay_client.rs index 8b7763771..23a943e47 100644 --- a/crates/registry-notary-server/src/relay_client.rs +++ b/crates/registry-notary-server/src/relay_client.rs @@ -1010,21 +1010,6 @@ fn result_decoder(profile: &VerifiedRelayProfile) -> Result Result { if generation_id.is_empty() { @@ -1352,12 +1325,6 @@ impl FieldCursor { .ok_or(self.error) } - fn require_null(&mut self) -> Result<(), RelayClientError> { - matches!(self.scalar()?, ProjectedJsonScalar::Null) - .then_some(()) - .ok_or(self.error) - } - fn exhausted(&self) -> bool { self.fields.as_slice().is_empty() } diff --git a/crates/registry-notary-server/src/relay_client/tests.rs b/crates/registry-notary-server/src/relay_client/tests.rs index 983614a1d..c149dc5ff 100644 --- a/crates/registry-notary-server/src/relay_client/tests.rs +++ b/crates/registry-notary-server/src/relay_client/tests.rs @@ -553,14 +553,6 @@ fn result_value() -> Value { "integration": { "id": "dhis2.tracker.enrollment-status", "revision": 1 - }, - "consent": { - "outcome": "not_required", - "verifier_id": null, - "verifier_revision": null, - "checked_at": null, - "expires_at": null, - "revocation_status": "not_applicable" } } }) @@ -1380,6 +1372,36 @@ async fn result_union_requires_outputs_only_for_match() { server.shutdown().await; } +#[tokio::test] +async fn result_rejects_retired_provenance_consent_member() { + let token_file = TestTokenFile::new(&test_token()); + let mut result = result_value(); + assert!(result["provenance"].get("consent").is_none()); + result["provenance"]["consent"] = json!({ + "outcome": "not_required", + "verifier_id": null, + "verifier_revision": null, + "checked_at": null, + "expires_at": null, + "revocation_status": "not_applicable" + }); + let server = FakeRelay::start( + metadata_response(), + WireResponse::ok(serde_json::to_vec(&result).unwrap()), + ) + .await; + let client = verified(&server, &token_file).await; + + assert_eq!( + client + .execute(EVALUATION_ID, Zeroizing::new(INPUT_VALUE.to_string())) + .await + .expect_err("retired consent member fails closed"), + RelayClientError::InvalidResult + ); + server.shutdown().await; +} + #[tokio::test] async fn batch_execute_propagates_one_bounded_opaque_child_identity() { let token_file = TestTokenFile::new(&test_token()); diff --git a/crates/registry-notary-server/tests/standalone_http/support.rs b/crates/registry-notary-server/tests/standalone_http/support.rs index 0097914b7..e68d860a9 100644 --- a/crates/registry-notary-server/tests/standalone_http/support.rs +++ b/crates/registry-notary-server/tests/standalone_http/support.rs @@ -168,14 +168,6 @@ async fn test_relay_execute(State(state): State, request: Reques "integration": { "id": "dhis2.tracker.enrollment-status", "revision": 1 - }, - "consent": { - "outcome": "not_required", - "verifier_id": null, - "verifier_revision": null, - "checked_at": null, - "expires_at": null, - "revocation_status": "not_applicable" } } })) diff --git a/crates/registry-relay/CHANGELOG.md b/crates/registry-relay/CHANGELOG.md index 1f4172efd..7be525aac 100644 --- a/crates/registry-relay/CHANGELOG.md +++ b/crates/registry-relay/CHANGELOG.md @@ -2,6 +2,20 @@ ## Unreleased +- BREAKING: Remove the inert six-field `provenance.consent` member from the + pre-1.0 `registry.relay.consultation-result.v1` response and its closed + batch-terminal replay representation. The PostgreSQL terminal constraint is + replaced with the reduced exact shape, so earlier terminal payloads are not + accepted. Consent verification, policy enforcement, legal-basis handling, + evidence commitments, scopes, and trust headers are unchanged. A Relay + consultation state plane initialized with the earlier pre-release constraint + must be cleanly re-bootstrapped before running the updated binary. This is not + a rolling-compatible change: stop new consultations and let the old binary + serve retained terminal replays for their 15-minute retention window before + re-bootstrapping, or re-bootstrap immediately if discarding those pre-1.0 + terminal results is acceptable. Do not run old and updated binaries against + one state plane. + ## 0.12.2 - 2026-07-20 ### Security diff --git a/crates/registry-relay/docs/api.md b/crates/registry-relay/docs/api.md index faa95c509..a69185d55 100644 --- a/crates/registry-relay/docs/api.md +++ b/crates/registry-relay/docs/api.md @@ -153,7 +153,7 @@ A successful response has the closed `registry.relay.consultation-result.v1` env only `match`, `no_match`, or `ambiguous`. A `match` includes every declared typed output. `no_match` and `ambiguous` omit `outputs`. Every outcome includes the generated consultation id, required Notary evaluation id, exact profile id and `contract_hash`, acquisition time and class, -integration id and revision, snapshot evidence when applicable, and the closed consent result. +integration id and revision, and snapshot evidence when applicable. Raw inputs, source credentials, source URLs, static policy digests, and source diagnostics are never returned. diff --git a/crates/registry-relay/openapi/oasdiff-err-ignore.txt b/crates/registry-relay/openapi/oasdiff-err-ignore.txt index b94ab98ef..ee56082af 100644 --- a/crates/registry-relay/openapi/oasdiff-err-ignore.txt +++ b/crates/registry-relay/openapi/oasdiff-err-ignore.txt @@ -21,3 +21,7 @@ GET /v1/datasets/{dataset_id}/entities/{entity}/schema added the new path reques GET /v1/datasets/{dataset_id}/entities/{entity}/schema added the new path request parameter 'entity' GET /v1/datasets/{dataset_id}/measures added the new path request parameter 'dataset_id' GET /v1/datasets/{dataset_id}/measures/{item_id} added the new path request parameter 'dataset_id' + +# One-time accepted diff for issue #362 on 2026-07-20: remove the inert +# provenance.consent member from the pre-1.0 consultation result. +POST /v1/consultations/{profile_id}/execute added `subschema #1, subschema #2, subschema #3` to the response body `oneOf` list for the response status `200` diff --git a/crates/registry-relay/openapi/registry-relay.openapi.json b/crates/registry-relay/openapi/registry-relay.openapi.json index 570a2211e..8613a5401 100644 --- a/crates/registry-relay/openapi/registry-relay.openapi.json +++ b/crates/registry-relay/openapi/registry-relay.openapi.json @@ -1298,54 +1298,6 @@ "bounded_full_record" ] }, - "consent": { - "additionalProperties": false, - "properties": { - "checked_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "expires_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "outcome": { - "const": "not_required" - }, - "revocation_status": { - "const": "not_applicable" - }, - "verifier_id": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - }, - "verifier_revision": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "outcome", - "verifier_id", - "verifier_revision", - "checked_at", - "expires_at", - "revocation_status" - ], - "type": "object" - }, "integration": { "additionalProperties": false, "properties": { @@ -1384,8 +1336,7 @@ "source_observed_at", "source_revision", "acquisition_class", - "integration", - "consent" + "integration" ], "type": "object" }, @@ -1399,54 +1350,6 @@ "acquisition_class": { "const": "materialized_snapshot" }, - "consent": { - "additionalProperties": false, - "properties": { - "checked_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "expires_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "outcome": { - "const": "not_required" - }, - "revocation_status": { - "const": "not_applicable" - }, - "verifier_id": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - }, - "verifier_revision": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "outcome", - "verifier_id", - "verifier_revision", - "checked_at", - "expires_at", - "revocation_status" - ], - "type": "object" - }, "integration": { "additionalProperties": false, "properties": { @@ -1504,7 +1407,6 @@ "source_revision", "acquisition_class", "integration", - "consent", "snapshot" ], "type": "object" @@ -1573,54 +1475,6 @@ "bounded_full_record" ] }, - "consent": { - "additionalProperties": false, - "properties": { - "checked_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "expires_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "outcome": { - "const": "not_required" - }, - "revocation_status": { - "const": "not_applicable" - }, - "verifier_id": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - }, - "verifier_revision": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "outcome", - "verifier_id", - "verifier_revision", - "checked_at", - "expires_at", - "revocation_status" - ], - "type": "object" - }, "integration": { "additionalProperties": false, "properties": { @@ -1659,8 +1513,7 @@ "source_observed_at", "source_revision", "acquisition_class", - "integration", - "consent" + "integration" ], "type": "object" }, @@ -1674,54 +1527,6 @@ "acquisition_class": { "const": "materialized_snapshot" }, - "consent": { - "additionalProperties": false, - "properties": { - "checked_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "expires_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "outcome": { - "const": "not_required" - }, - "revocation_status": { - "const": "not_applicable" - }, - "verifier_id": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - }, - "verifier_revision": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "outcome", - "verifier_id", - "verifier_revision", - "checked_at", - "expires_at", - "revocation_status" - ], - "type": "object" - }, "integration": { "additionalProperties": false, "properties": { @@ -1779,7 +1584,6 @@ "source_revision", "acquisition_class", "integration", - "consent", "snapshot" ], "type": "object" @@ -1847,54 +1651,6 @@ "bounded_full_record" ] }, - "consent": { - "additionalProperties": false, - "properties": { - "checked_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "expires_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "outcome": { - "const": "not_required" - }, - "revocation_status": { - "const": "not_applicable" - }, - "verifier_id": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - }, - "verifier_revision": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "outcome", - "verifier_id", - "verifier_revision", - "checked_at", - "expires_at", - "revocation_status" - ], - "type": "object" - }, "integration": { "additionalProperties": false, "properties": { @@ -1933,8 +1689,7 @@ "source_observed_at", "source_revision", "acquisition_class", - "integration", - "consent" + "integration" ], "type": "object" }, @@ -1948,54 +1703,6 @@ "acquisition_class": { "const": "materialized_snapshot" }, - "consent": { - "additionalProperties": false, - "properties": { - "checked_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "expires_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "outcome": { - "const": "not_required" - }, - "revocation_status": { - "const": "not_applicable" - }, - "verifier_id": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - }, - "verifier_revision": { - "maxLength": 128, - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "outcome", - "verifier_id", - "verifier_revision", - "checked_at", - "expires_at", - "revocation_status" - ], - "type": "object" - }, "integration": { "additionalProperties": false, "properties": { @@ -2053,7 +1760,6 @@ "source_revision", "acquisition_class", "integration", - "consent", "snapshot" ], "type": "object" diff --git a/crates/registry-relay/src/api/openapi.rs b/crates/registry-relay/src/api/openapi.rs index 2ff311489..04332cf0b 100644 --- a/crates/registry-relay/src/api/openapi.rs +++ b/crates/registry-relay/src/api/openapi.rs @@ -3127,7 +3127,6 @@ fn consultation_provenance_schema() -> Value { "source_revision", "acquisition_class", "integration", - "consent", ]; let common_properties = json!({ "acquired_at": { "type": "string", "format": "date-time" }, @@ -3141,19 +3140,6 @@ fn consultation_provenance_schema() -> Value { "revision": { "type": "integer", "minimum": 1 } }, "additionalProperties": false - }, - "consent": { - "type": "object", - "required": ["outcome", "verifier_id", "verifier_revision", "checked_at", "expires_at", "revocation_status"], - "properties": { - "outcome": { "const": "not_required" }, - "verifier_id": { "type": ["string", "null"], "maxLength": 128 }, - "verifier_revision": { "type": ["string", "null"], "maxLength": 128 }, - "checked_at": { "type": ["string", "null"], "format": "date-time" }, - "expires_at": { "type": ["string", "null"], "format": "date-time" }, - "revocation_status": { "const": "not_applicable" } - }, - "additionalProperties": false } }); let mut live_properties = common_properties.as_object().cloned().expect("object"); @@ -6091,6 +6077,16 @@ mod tests { .len(), 2 ); + for provenance in variant["properties"]["provenance"]["oneOf"] + .as_array() + .unwrap() + { + assert!(provenance["properties"]["consent"].is_null()); + assert!(!provenance["required"] + .as_array() + .expect("required provenance fields") + .contains(&json!("consent"))); + } } assert_eq!( variants[0]["properties"]["outputs"]["additionalProperties"]["type"], diff --git a/crates/registry-relay/src/consultation/response.rs b/crates/registry-relay/src/consultation/response.rs index 9f24866e2..a9e825508 100644 --- a/crates/registry-relay/src/consultation/response.rs +++ b/crates/registry-relay/src/consultation/response.rs @@ -96,7 +96,6 @@ struct BatchTerminalProvenance { integration: BatchTerminalIntegration, #[serde(skip_serializing_if = "Option::is_none")] snapshot: Option, - consent: BatchTerminalConsent, } #[derive(Serialize, Deserialize)] @@ -113,17 +112,6 @@ struct BatchTerminalSnapshot { published_at: String, } -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct BatchTerminalConsent { - outcome: String, - verifier_id: Option, - verifier_revision: Option, - checked_at: Option, - expires_at: Option, - revocation_status: String, -} - enum BatchTerminalScalar { Null, String(Zeroizing), @@ -365,14 +353,6 @@ impl PublishableConsultationResponse { revision: integration_pack.version().get(), }, snapshot, - consent: BatchTerminalConsent { - outcome: "not_required".to_owned(), - verifier_id: None, - verifier_revision: None, - checked_at: None, - expires_at: None, - revocation_status: "not_applicable".to_owned(), - }, }, }; let configured_limit = @@ -514,12 +494,6 @@ impl BatchTerminalPayload { != acquisition_class_str(profile.footprint().acquisition_class()) || self.provenance.integration.id != profile.integration_pack().id().as_str() || self.provenance.integration.revision != profile.integration_pack().version().get() - || self.provenance.consent.outcome != "not_required" - || self.provenance.consent.revocation_status != "not_applicable" - || self.provenance.consent.verifier_id.is_some() - || self.provenance.consent.verifier_revision.is_some() - || self.provenance.consent.checked_at.is_some() - || self.provenance.consent.expires_at.is_some() || !valid_rfc3339_milliseconds(&self.provenance.acquired_at) { return Err(ConsultationResponseError::Serialization); @@ -656,14 +630,6 @@ impl BatchTerminalPayload { generation_id: &snapshot.generation_id, published_at: &snapshot.published_at, }), - consent: ResponseConsent { - outcome: &self.provenance.consent.outcome, - verifier_id: self.provenance.consent.verifier_id.as_deref(), - verifier_revision: self.provenance.consent.verifier_revision.as_deref(), - checked_at: self.provenance.consent.checked_at.as_deref(), - expires_at: self.provenance.consent.expires_at.as_deref(), - revocation_status: &self.provenance.consent.revocation_status, - }, }, }; let mut bytes = Zeroizing::new(Vec::with_capacity(limit)); @@ -715,7 +681,6 @@ struct ResponseProvenance<'a> { integration: ResponseIntegration<'a>, #[serde(skip_serializing_if = "Option::is_none")] snapshot: Option>, - consent: ResponseConsent<'a>, } #[derive(Serialize)] @@ -730,16 +695,6 @@ struct ResponseSnapshot<'a> { published_at: &'a str, } -#[derive(Serialize)] -struct ResponseConsent<'a> { - outcome: &'a str, - verifier_id: Option<&'a str>, - verifier_revision: Option<&'a str>, - checked_at: Option<&'a str>, - expires_at: Option<&'a str>, - revocation_status: &'a str, -} - enum ProjectedRecord<'a> { Outputs(&'a ValidatedOutputMap), Snapshot { @@ -1272,17 +1227,7 @@ mod tests { assert!(value["provenance"].get("snapshot").is_none()); assert!(value["provenance"].get("policy_id").is_none()); assert!(value["provenance"].get("policy_hash").is_none()); - assert_eq!( - value["provenance"]["consent"], - json!({ - "outcome": "not_required", - "verifier_id": null, - "verifier_revision": null, - "checked_at": null, - "expires_at": null, - "revocation_status": "not_applicable" - }) - ); + assert!(value["provenance"].get("consent").is_none()); assert_eq!( value.get("notary_evaluation_id").and_then(Value::as_str), Some(evaluation_id.to_canonical_string()).as_deref() @@ -1290,6 +1235,41 @@ mod tests { } } + #[test] + fn batch_terminal_replay_rejects_retired_consent_member() { + let plan = dhis2_runtime_vector_plan_fixture(); + let profile = plan.runtime_profile(); + let candidate = PublishableConsultationResponse::serialize_validated_live_result( + ConsultationId::generate(), + Some(NotaryEvaluationId::try_parse("01JYZZZZZZZZZZZZZZZZZZZZZZ").unwrap()), + profile, + ConsultationOutcome::NoMatch, + None, + 1_752_148_800_123, + ) + .expect("frozen response serializes"); + let mut terminal: Value = serde_json::from_str( + candidate + .batch_terminal_json() + .expect("batch terminal serializes") + .as_str(), + ) + .expect("batch terminal is JSON"); + terminal["provenance"]["consent"] = json!({ + "outcome": "not_required", + "verifier_id": null, + "verifier_revision": null, + "checked_at": null, + "expires_at": null, + "revocation_status": "not_applicable" + }); + + assert!(matches!( + BatchTerminalPayload::from_persisted(&terminal.to_string()), + Err(ConsultationResponseError::Serialization) + )); + } + #[test] fn public_result_requires_the_notary_evaluation_id() { let plan = dhis2_runtime_vector_plan_fixture(); diff --git a/crates/registry-relay/src/state_plane/migration.rs b/crates/registry-relay/src/state_plane/migration.rs index d3cbf9db0..e2e790e35 100644 --- a/crates/registry-relay/src/state_plane/migration.rs +++ b/crates/registry-relay/src/state_plane/migration.rs @@ -31,14 +31,14 @@ const STATE_PLANE_SCHEMA_IDENTITY_PREIMAGE_V1: &str = concat!( "materialization-publication=registry.relay.postgres-materialization-publication/v1\0", "materialization-publication-order=attempt-before-access-atomic-completion-pointer-monotonic-generation-v1\0", "consultation-completion=atomic-intent-sealed-seed-closed-three-outcomes-script-verification-permit-dynamic-ordinal-call-ack-known-unfinished-recovery-date-schema-v5\0", - "consultation-batch-child-replay=authenticated-hmac-binding-reserve-before-quota-atomic-terminal-publication-fixed-retention-v2\0", + "consultation-batch-child-replay=authenticated-hmac-binding-reserve-before-quota-atomic-terminal-publication-provenance-without-consent-fixed-retention-v3\0", "consultation-authorization=database-expiry-credential-verification-data-order-keyed-request-effect-call-ack-v4\0", "consultation-credentials=direct-data-auth-reference-distinct-authored-verification-no-expiry-v3\0", "serving-fence-order=fence-row-keyring-intent-permit-audit-head-v1\0", "key-order=utf8-bytewise-key-order-v1\0", ); pub(crate) const STATE_PLANE_SCHEMA_FINGERPRINT_V1: &str = - "sha256:a70a81bb46e49a97d24d8fec0716938639e760ced8834eba7965aa423dc5431b"; + "sha256:de5f85061217001d7e37fe5681aec6cdaa36b01072fca97dda609485e8938713"; pub(super) const MIGRATION_ADVISORY_LOCK_KEY_V1: i64 = 7_221_091_440; const SUPPORTED_POSTGRES_MIN_MAJOR: i32 = 16; @@ -47,16 +47,16 @@ const SUPPORTED_POSTGRES_MAX_MAJOR: i32 = 18; // Filled from the semantic catalog descriptor below on disposable supported // PostgreSQL majors. Constraint rendering is explicitly versioned because // pg_get_constraintdef is not a cross-major wire contract. -const CONSTRAINT_FINGERPRINT_PG16: &str = "9496b43b708e2f6358200899c00eaca1"; -const CONSTRAINT_FINGERPRINT_PG17: &str = "9496b43b708e2f6358200899c00eaca1"; -const CONSTRAINT_FINGERPRINT_PG18: &str = "50b7c98eb7274c63d10c7b84a41f33f8"; +const CONSTRAINT_FINGERPRINT_PG16: &str = "25dbf98e1e26411436c24a391b534e07"; +const CONSTRAINT_FINGERPRINT_PG17: &str = "25dbf98e1e26411436c24a391b534e07"; +const CONSTRAINT_FINGERPRINT_PG18: &str = "a85469cded76f1f5f72f674c09fc08b3"; const COLUMN_FINGERPRINT_PG16: &str = "f1cce8b8398fd1b177d3d6b61a112cec"; const COLUMN_FINGERPRINT_PG17: &str = "f1cce8b8398fd1b177d3d6b61a112cec"; const COLUMN_FINGERPRINT_PG18: &str = "f1cce8b8398fd1b177d3d6b61a112cec"; -const FUNCTION_FINGERPRINT_PG16: &str = "5c72567f21536f434bfd5f21ab7fddf7"; -const FUNCTION_FINGERPRINT_PG17: &str = "5c72567f21536f434bfd5f21ab7fddf7"; -const FUNCTION_FINGERPRINT_PG18: &str = "5c72567f21536f434bfd5f21ab7fddf7"; -const CAPABILITY_HELPER_BODY_FINGERPRINT_V1: &str = "31835c50f5c67b30e888fa8d6b8270a5"; +const FUNCTION_FINGERPRINT_PG16: &str = "974de91ab404047b05d2fbe76ab3bc45"; +const FUNCTION_FINGERPRINT_PG17: &str = "974de91ab404047b05d2fbe76ab3bc45"; +const FUNCTION_FINGERPRINT_PG18: &str = "974de91ab404047b05d2fbe76ab3bc45"; +const CAPABILITY_HELPER_BODY_FINGERPRINT_V1: &str = "a16f5fe68a6a6751ecb0f935bf987059"; /// Runtime-forceable session semantics. Server/SUSET state that the runtime /// cannot safely repair is rejected by the attested SQL capability instead. @@ -112,7 +112,7 @@ CREATE TABLE IF NOT EXISTS relay_state_private.state_plane_metadata ( ), CONSTRAINT state_plane_metadata_fingerprint_check CHECK ( capability_fingerprint = - 'sha256:a70a81bb46e49a97d24d8fec0716938639e760ced8834eba7965aa423dc5431b' + 'sha256:de5f85061217001d7e37fe5681aec6cdaa36b01072fca97dda609485e8938713' ), CONSTRAINT state_plane_metadata_roles_distinct_check CHECK ( owner_role_oid <> runtime_role_oid @@ -1547,7 +1547,7 @@ WITH metadata AS ( AND schema_version = 1 AND capability_id = 'registry.relay.postgres-durable-audit/v1' AND capability_fingerprint = - 'sha256:a70a81bb46e49a97d24d8fec0716938639e760ced8834eba7965aa423dc5431b' + 'sha256:de5f85061217001d7e37fe5681aec6cdaa36b01072fca97dda609485e8938713' AND serving_fence_capability_id = 'registry.relay.postgres-serving-fence/v1' AND serving_fence_lock_key <> 0 AND serving_fence_lock_key <> 7221091440 @@ -2199,9 +2199,9 @@ SELECT ) ) AND (SELECT value = CASE server.major - WHEN 16 THEN '9496b43b708e2f6358200899c00eaca1' - WHEN 17 THEN '9496b43b708e2f6358200899c00eaca1' - WHEN 18 THEN '50b7c98eb7274c63d10c7b84a41f33f8' + WHEN 16 THEN '25dbf98e1e26411436c24a391b534e07' + WHEN 17 THEN '25dbf98e1e26411436c24a391b534e07' + WHEN 18 THEN 'a85469cded76f1f5f72f674c09fc08b3' ELSE '' END FROM constraint_fingerprint, server) AND (SELECT value = CASE server.major WHEN 16 THEN 'f1cce8b8398fd1b177d3d6b61a112cec' @@ -2209,9 +2209,9 @@ SELECT WHEN 18 THEN 'f1cce8b8398fd1b177d3d6b61a112cec' ELSE '' END FROM column_fingerprint, server) AND (SELECT value = CASE server.major - WHEN 16 THEN '5c72567f21536f434bfd5f21ab7fddf7' - WHEN 17 THEN '5c72567f21536f434bfd5f21ab7fddf7' - WHEN 18 THEN '5c72567f21536f434bfd5f21ab7fddf7' + WHEN 16 THEN '974de91ab404047b05d2fbe76ab3bc45' + WHEN 17 THEN '974de91ab404047b05d2fbe76ab3bc45' + WHEN 18 THEN '974de91ab404047b05d2fbe76ab3bc45' ELSE '' END FROM function_fingerprint, server); $function$; @@ -7327,11 +7327,11 @@ CREATE TABLE IF NOT EXISTS relay_state_private.consultation_batch_child_replay ( AND jsonb_typeof(terminal_payload -> 'provenance') = 'object' AND (terminal_payload -> 'provenance') ?& ARRAY[ 'acquired_at', 'source_observed_at', 'source_revision', - 'acquisition_class', 'integration', 'consent' + 'acquisition_class', 'integration' ]::text[] AND ((terminal_payload -> 'provenance') - ARRAY[ 'acquired_at', 'source_observed_at', 'source_revision', - 'acquisition_class', 'integration', 'snapshot', 'consent' + 'acquisition_class', 'integration', 'snapshot' ]::text[]) = '{}'::jsonb AND jsonb_typeof(terminal_payload #> '{provenance,acquired_at}') = 'string' AND jsonb_typeof(terminal_payload #> '{provenance,source_observed_at}') @@ -7353,25 +7353,6 @@ CREATE TABLE IF NOT EXISTS relay_state_private.consultation_batch_child_replay ( trunc((terminal_payload #>> '{provenance,integration,revision}')::numeric) AND (terminal_payload #>> '{provenance,integration,revision}')::numeric BETWEEN 1 AND 9999999999 - AND jsonb_typeof(terminal_payload #> '{provenance,consent}') = 'object' - AND (terminal_payload #> '{provenance,consent}') ?& ARRAY[ - 'outcome', 'verifier_id', 'verifier_revision', 'checked_at', 'expires_at', - 'revocation_status' - ]::text[] - AND ((terminal_payload #> '{provenance,consent}') - ARRAY[ - 'outcome', 'verifier_id', 'verifier_revision', 'checked_at', 'expires_at', - 'revocation_status' - ]::text[]) = '{}'::jsonb - AND jsonb_typeof(terminal_payload #> '{provenance,consent,outcome}') = 'string' - AND jsonb_typeof(terminal_payload #> '{provenance,consent,verifier_id}') - IN ('string', 'null') - AND jsonb_typeof(terminal_payload #> '{provenance,consent,verifier_revision}') - IN ('string', 'null') - AND jsonb_typeof(terminal_payload #> '{provenance,consent,checked_at}') - IN ('string', 'null') - AND jsonb_typeof(terminal_payload #> '{provenance,consent,expires_at}') - IN ('string', 'null') - AND jsonb_typeof(terminal_payload #> '{provenance,consent,revocation_status}') = 'string' AND ( (terminal_payload #>> '{provenance,acquisition_class}' IN ( 'source_projected_exact', 'bounded_full_record' @@ -7559,11 +7540,11 @@ BEGIN OR jsonb_typeof(v_terminal -> 'provenance') <> 'object' OR NOT (v_terminal -> 'provenance') ?& ARRAY[ 'acquired_at', 'source_observed_at', 'source_revision', - 'acquisition_class', 'integration', 'consent' + 'acquisition_class', 'integration' ]::text[] OR ((v_terminal -> 'provenance') - ARRAY[ 'acquired_at', 'source_observed_at', 'source_revision', - 'acquisition_class', 'integration', 'snapshot', 'consent' + 'acquisition_class', 'integration', 'snapshot' ]::text[]) <> '{}'::jsonb OR jsonb_typeof(v_terminal #> '{provenance,acquired_at}') <> 'string' OR jsonb_typeof(v_terminal #> '{provenance,source_observed_at}') @@ -7585,25 +7566,6 @@ BEGIN trunc((v_terminal #>> '{provenance,integration,revision}')::numeric) OR (v_terminal #>> '{provenance,integration,revision}')::numeric NOT BETWEEN 1 AND 9999999999 - OR jsonb_typeof(v_terminal #> '{provenance,consent}') <> 'object' - OR NOT (v_terminal #> '{provenance,consent}') ?& ARRAY[ - 'outcome', 'verifier_id', 'verifier_revision', 'checked_at', 'expires_at', - 'revocation_status' - ]::text[] - OR ((v_terminal #> '{provenance,consent}') - ARRAY[ - 'outcome', 'verifier_id', 'verifier_revision', 'checked_at', 'expires_at', - 'revocation_status' - ]::text[]) <> '{}'::jsonb - OR jsonb_typeof(v_terminal #> '{provenance,consent,outcome}') <> 'string' - OR jsonb_typeof(v_terminal #> '{provenance,consent,verifier_id}') - NOT IN ('string', 'null') - OR jsonb_typeof(v_terminal #> '{provenance,consent,verifier_revision}') - NOT IN ('string', 'null') - OR jsonb_typeof(v_terminal #> '{provenance,consent,checked_at}') - NOT IN ('string', 'null') - OR jsonb_typeof(v_terminal #> '{provenance,consent,expires_at}') - NOT IN ('string', 'null') - OR jsonb_typeof(v_terminal #> '{provenance,consent,revocation_status}') <> 'string' OR ( (v_terminal #>> '{provenance,acquisition_class}' IN ( 'source_projected_exact', 'bounded_full_record' @@ -8658,7 +8620,7 @@ mod tests { "'schema', 'consultation_id', 'outcome', 'outputs', 'profile', 'provenance'", "'id', 'contract_hash'", "'acquired_at', 'source_observed_at', 'source_revision'", - "'acquisition_class', 'integration', 'snapshot', 'consent'", + "'acquisition_class', 'integration', 'snapshot'", "ARRAY['id', 'revision']::text[]", "ARRAY['generation_id', 'published_at']::text[]", "'outputs'", @@ -8675,6 +8637,7 @@ mod tests { "integration_pack", "policy_hash", "snapshot_generation_id", + "'consent'", ] { assert!( !section.contains(obsolete), @@ -8934,7 +8897,7 @@ mod tests { AUDIT_PSEUDONYM_KEYRING_CAPABILITY_V1, MATERIALIZATION_PUBLICATION_CAPABILITY_V1, "atomic-intent-sealed-seed-closed-three-outcomes-script-verification-permit-dynamic-ordinal-call-ack-known-unfinished-recovery-date-schema-v5", - "authenticated-hmac-binding-reserve-before-quota-atomic-terminal-publication-fixed-retention-v2", + "authenticated-hmac-binding-reserve-before-quota-atomic-terminal-publication-provenance-without-consent-fixed-retention-v3", "database-expiry-credential-verification-data-order-keyed-request-effect-call-ack-v4", "direct-data-auth-reference-distinct-authored-verification-no-expiry-v3", "utf8-bytewise-key-order-v1", diff --git a/crates/registry-relay/src/state_plane/postgres_tests.rs b/crates/registry-relay/src/state_plane/postgres_tests.rs index b427d3e16..0172197e9 100644 --- a/crates/registry-relay/src/state_plane/postgres_tests.rs +++ b/crates/registry-relay/src/state_plane/postgres_tests.rs @@ -1859,8 +1859,7 @@ async fn exercise_batch_child_replay_reservation_contract( "source_observed_at": null, "source_revision": null, "acquisition_class": "source_projected_exact", - "integration": {"id": "synthetic.pack", "revision": 1}, - "consent": {"outcome": "not_required", "verifier_id": null, "verifier_revision": null, "checked_at": null, "expires_at": null, "revocation_status": "not_applicable"} + "integration": {"id": "synthetic.pack", "revision": 1} } }))?; let mismatch = admin @@ -2074,6 +2073,7 @@ async fn exercise_batch_terminal_publication_contract( replay_json["notary_evaluation_id"], first_evaluation.to_canonical_string() ); + assert!(replay_json["provenance"].get("consent").is_none()); set_role(admin, owner_role).await?; let after_replay = admin .query_one( diff --git a/products/notary/CHANGELOG.md b/products/notary/CHANGELOG.md index dcafbe3b4..6fa70de79 100644 --- a/products/notary/CHANGELOG.md +++ b/products/notary/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- BREAKING: The closed Relay-result decoder now matches the reduced pre-1.0 + `registry.relay.consultation-result.v1` contract and rejects the retired + `provenance.consent` member as unknown. Registry-backed issuance still + requires compiler-pinned Relay evidence; source-free declarations remain + evaluation-only and cannot be issued as credentials. Consent verification, + policy enforcement, legal-basis handling, and authorization are unchanged. + ## [0.12.2] - 2026-07-20 - No new Notary product features or public API contract changes. This release