Skip to main content

lenso_service/production_delivery/
edge.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
4use ed25519_dalek::{Signature, Signer as _, SigningKey, Verifier as _, VerifyingKey};
5use http::Uri;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use utoipa::ToSchema;
9
10use crate::extraction_input_digest;
11
12use super::{
13    DeliveryEffects, DeliveryIssue, DeliveryIssueCode, ReleaseSignerStatus, ReleaseTrustProvider,
14    ServiceRelease, issue, service_release_integrity_is_valid,
15};
16
17pub const EDGE_CONTRACT_PROTOCOL: &str = "lenso.edge-contract.v1";
18pub const GATEWAY_PLAN_PROTOCOL: &str = "lenso.gateway-plan.v1";
19pub const GATEWAY_OBSERVATION_PROTOCOL: &str = "lenso.gateway-observation.v1";
20
21pub trait GatewayObservationProvider: std::fmt::Debug + Send + Sync {
22    fn provider_id(&self) -> &str;
23
24    fn sign(&self, observation_id: &str) -> Option<String>;
25
26    fn verify(&self, observation_id: &str, proof: &str) -> bool;
27}
28
29#[derive(Debug, Clone)]
30pub struct DeterministicGatewayObservationProvider {
31    provider_id: String,
32    key: String,
33}
34
35impl DeterministicGatewayObservationProvider {
36    #[must_use]
37    pub fn new(provider_id: impl Into<String>, key: impl Into<String>) -> Self {
38        Self {
39            provider_id: provider_id.into(),
40            key: key.into(),
41        }
42    }
43
44    fn expected_proof(&self, observation_id: &str) -> String {
45        digest_json(&(
46            "lenso.gateway-observation-authority-proof.v1",
47            self.provider_id.as_str(),
48            observation_id,
49            self.key.as_str(),
50        ))
51    }
52}
53
54impl GatewayObservationProvider for DeterministicGatewayObservationProvider {
55    fn provider_id(&self) -> &str {
56        &self.provider_id
57    }
58
59    fn sign(&self, observation_id: &str) -> Option<String> {
60        Some(self.expected_proof(observation_id))
61    }
62
63    fn verify(&self, observation_id: &str, proof: &str) -> bool {
64        self.expected_proof(observation_id) == proof
65    }
66}
67
68/// Verify-only Gateway observation authority backed by an Ed25519 public key.
69#[derive(Debug, Clone)]
70pub struct Ed25519GatewayObservationProvider {
71    provider_id: String,
72    public_key: VerifyingKey,
73    signing_key: Option<SigningKey>,
74}
75
76impl Ed25519GatewayObservationProvider {
77    pub fn from_base64_public_key(
78        provider_id: impl Into<String>,
79        encoded: &str,
80    ) -> Result<Self, String> {
81        let bytes = BASE64
82            .decode(encoded)
83            .map_err(|error| format!("invalid Ed25519 public key encoding: {error}"))?;
84        let bytes: [u8; 32] = bytes
85            .try_into()
86            .map_err(|_| "Ed25519 public keys must contain exactly 32 bytes".to_owned())?;
87        let public_key = VerifyingKey::from_bytes(&bytes)
88            .map_err(|error| format!("invalid Ed25519 public key: {error}"))?;
89        Ok(Self {
90            provider_id: provider_id.into(),
91            public_key,
92            signing_key: None,
93        })
94    }
95
96    pub fn from_base64_private_key(
97        provider_id: impl Into<String>,
98        encoded: &str,
99    ) -> Result<Self, String> {
100        let bytes = BASE64
101            .decode(encoded)
102            .map_err(|error| format!("invalid Ed25519 private key encoding: {error}"))?;
103        let bytes: [u8; 32] = bytes
104            .try_into()
105            .map_err(|_| "Ed25519 private keys must contain exactly 32 bytes".to_owned())?;
106        let signing_key = SigningKey::from_bytes(&bytes);
107        Ok(Self {
108            provider_id: provider_id.into(),
109            public_key: signing_key.verifying_key(),
110            signing_key: Some(signing_key),
111        })
112    }
113}
114
115impl GatewayObservationProvider for Ed25519GatewayObservationProvider {
116    fn provider_id(&self) -> &str {
117        &self.provider_id
118    }
119
120    fn sign(&self, observation_id: &str) -> Option<String> {
121        self.signing_key
122            .as_ref()
123            .map(|key| BASE64.encode(key.sign(observation_id.as_bytes()).to_bytes()))
124    }
125
126    fn verify(&self, observation_id: &str, proof: &str) -> bool {
127        let Ok(bytes) = BASE64.decode(proof) else {
128            return false;
129        };
130        let Ok(signature) = Signature::from_slice(&bytes) else {
131            return false;
132        };
133        self.public_key
134            .verify(observation_id.as_bytes(), &signature)
135            .is_ok()
136    }
137}
138
139#[derive(
140    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
141)]
142#[serde(rename_all = "snake_case")]
143pub enum EdgeOperationVisibility {
144    PublicEligible,
145    Internal,
146}
147
148#[derive(
149    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
150)]
151#[serde(rename_all = "camelCase")]
152pub struct EdgeServiceOperation {
153    pub contract_id: String,
154    pub contract_version: String,
155    pub contract_digest: String,
156    pub operation_id: String,
157    pub visibility: EdgeOperationVisibility,
158    pub request_schema_reference: String,
159    pub response_schema_reference: String,
160}
161
162#[derive(
163    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
164)]
165#[serde(rename_all = "snake_case")]
166pub enum EdgeAuthentication {
167    Public,
168    Workload,
169    User,
170    WorkloadOrUser,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
174#[serde(rename_all = "camelCase")]
175pub struct CorsIntent {
176    #[serde(default)]
177    pub allowed_origins: Vec<String>,
178    #[serde(default)]
179    pub allowed_methods: Vec<String>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
183#[serde(rename_all = "camelCase")]
184pub struct RateIntent {
185    pub requests: u32,
186    pub window_seconds: u32,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
190#[serde(rename_all = "camelCase")]
191pub struct EdgeRoute {
192    pub contract_id: String,
193    pub contract_version: String,
194    pub operation_id: String,
195    pub public_path: String,
196    pub authentication: EdgeAuthentication,
197    pub cors: CorsIntent,
198    pub rate: RateIntent,
199    pub deprecated: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
203#[serde(rename_all = "camelCase")]
204pub struct ResolvedEdgeRoute {
205    pub contract_id: String,
206    pub contract_version: String,
207    pub operation_id: String,
208    pub public_path: String,
209    pub authentication: EdgeAuthentication,
210    pub cors: CorsIntent,
211    pub rate: RateIntent,
212    pub deprecated: bool,
213    pub request_schema_reference: String,
214    pub response_schema_reference: String,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
218#[serde(rename_all = "camelCase")]
219pub struct EdgeContract {
220    pub protocol: String,
221    pub edge_contract_id: String,
222    pub edge_contract_digest: String,
223    pub service_id: String,
224    pub release_id: String,
225    pub release_digest: String,
226    pub operation_catalog_digest: String,
227    pub provider_id: String,
228    pub provider_proof: String,
229    pub routes: Vec<ResolvedEdgeRoute>,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
233#[serde(rename_all = "camelCase")]
234pub struct GatewayEnvironmentBinding {
235    pub environment: String,
236    pub gateway_adapter: String,
237    pub public_origin: String,
238    pub expected_gateway_revision: u64,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
242#[serde(rename_all = "camelCase")]
243pub struct GatewayObservation {
244    pub protocol: String,
245    pub observation_id: String,
246    pub plan_id: String,
247    pub plan_digest: String,
248    pub environment: String,
249    pub release_id: String,
250    pub release_digest: String,
251    pub resource_uid: String,
252    pub resource_version: String,
253    pub authority_context: String,
254    pub configuration_identity: String,
255    pub revision: u64,
256    pub observed_after: String,
257    pub fresh: bool,
258    pub provider_id: String,
259    pub provider_proof: String,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
263#[serde(rename_all = "camelCase")]
264pub struct GatewayPlanDiffEntry {
265    pub subject: String,
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub before: Option<String>,
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub after: Option<String>,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
273#[serde(rename_all = "camelCase")]
274pub struct GatewayConfigurationPlan {
275    pub protocol: String,
276    pub plan_id: String,
277    pub plan_digest: String,
278    pub edge_contract_id: String,
279    pub edge_contract_digest: String,
280    pub edge_release_id: String,
281    pub edge_release_digest: String,
282    pub operation_catalog_digest: String,
283    pub edge_provider_id: String,
284    pub edge_provider_proof: String,
285    pub environment: String,
286    pub gateway_adapter: String,
287    pub public_origin: String,
288    pub expected_gateway_revision: u64,
289    pub configuration_identity: String,
290    pub routes: Vec<ResolvedEdgeRoute>,
291    pub diff: Vec<GatewayPlanDiffEntry>,
292    pub drifted: bool,
293    pub issues: Vec<DeliveryIssue>,
294    pub next_actions: Vec<String>,
295    pub effects: DeliveryEffects,
296}
297
298pub fn build_edge_contract(
299    release: &ServiceRelease,
300    available_operations: &[EdgeServiceOperation],
301    provider_id: &str,
302    provider: &dyn ReleaseTrustProvider,
303    mut routes: Vec<EdgeRoute>,
304) -> Result<EdgeContract, Vec<DeliveryIssue>> {
305    let service_id = release.service_id.clone();
306    let mut canonical_operations = available_operations.to_vec();
307    canonical_operations.sort();
308    let operation_catalog_digest = digest_json(&(
309        "lenso.service-operation-catalog.v1",
310        release.release_id.as_str(),
311        release.release_digest.as_str(),
312        canonical_operations.as_slice(),
313    ));
314    routes.sort_by(|left, right| {
315        (
316            &left.public_path,
317            &left.operation_id,
318            &left.contract_version,
319        )
320            .cmp(&(
321                &right.public_path,
322                &right.operation_id,
323                &right.contract_version,
324            ))
325    });
326    let operations = available_operations
327        .iter()
328        .map(|operation| {
329            (
330                (
331                    operation.contract_id.as_str(),
332                    operation.contract_version.as_str(),
333                    operation.operation_id.as_str(),
334                ),
335                operation,
336            )
337        })
338        .collect::<BTreeMap<_, _>>();
339    let mut issues = Vec::new();
340    let operation_keys = available_operations
341        .iter()
342        .map(|operation| {
343            (
344                operation.contract_id.as_str(),
345                operation.contract_version.as_str(),
346                operation.operation_id.as_str(),
347            )
348        })
349        .collect::<BTreeSet<_>>();
350    if !service_release_integrity_is_valid(release)
351        || provider_id.trim().is_empty()
352        || operation_keys.len() != available_operations.len()
353        || available_operations.is_empty()
354        || available_operations.iter().any(|operation| {
355            !release.contract_versions.iter().any(|contract| {
356                contract.contract_id == operation.contract_id
357                    && contract.version == operation.contract_version
358                    && contract.artifact.digest == operation.contract_digest
359            })
360        })
361    {
362        issues.push(issue(
363            DeliveryIssueCode::EdgeOperationUnknown,
364            "The Service operation catalog is not a trusted, release-bound projection of its exact Contract artifacts.",
365            "Project all operations from the immutable Service Contract set through a trusted provider.",
366            "Refresh the operation catalog for the exact Service Release.",
367        ));
368    }
369    let mut paths = BTreeSet::new();
370    let mut resolved = Vec::new();
371    for route in routes {
372        if !public_path_template_is_safe(&route.public_path)
373            || route.rate.requests == 0
374            || route.rate.window_seconds == 0
375            || !cors_intent_is_safe(&route.cors)
376        {
377            issues.push(issue(
378                DeliveryIssueCode::EdgeExposureUnsafe,
379                format!(
380                    "Edge route `{}` has an invalid public path, CORS intent, or rate intent.",
381                    route.operation_id
382                ),
383                "Declare an absolute path, HTTP(S) origins, standard uppercase methods, and a positive bounded rate intent.",
384                "Correct the Edge route and generate it again.",
385            ));
386            continue;
387        }
388        if !paths.insert(route.public_path.clone()) {
389            issues.push(issue(
390                DeliveryIssueCode::EdgePathConflict,
391                format!(
392                    "Public path `{}` is declared more than once.",
393                    route.public_path
394                ),
395                "Assign one explicit Service Contract operation to each public path.",
396                "Resolve the path conflict and generate the Edge Contract again.",
397            ));
398            continue;
399        }
400        let key = (
401            route.contract_id.as_str(),
402            route.contract_version.as_str(),
403            route.operation_id.as_str(),
404        );
405        let Some(operation) = operations.get(&key) else {
406            issues.push(issue(
407                DeliveryIssueCode::EdgeOperationUnknown,
408                format!(
409                    "Edge route `{}` does not reference an exact Service Contract operation.",
410                    route.operation_id
411                ),
412                "Reference an operation and version from the authoritative Service Contract.",
413                "Correct the operation reference and generate the Edge Contract again.",
414            ));
415            continue;
416        };
417        if operation.visibility != EdgeOperationVisibility::PublicEligible {
418            issues.push(issue(
419                DeliveryIssueCode::EdgeExposureUnsafe,
420                format!("Operation `{}` is internal and cannot be exposed.", route.operation_id),
421                "Keep administration, Story feeds, health internals, and Workload management private.",
422                "Select an explicitly public-eligible Service Contract operation.",
423            ));
424            continue;
425        }
426        let request_schema_reference = operation.request_schema_reference.clone();
427        let response_schema_reference = operation.response_schema_reference.clone();
428        resolved.push(ResolvedEdgeRoute {
429            contract_id: route.contract_id,
430            contract_version: route.contract_version,
431            operation_id: route.operation_id,
432            public_path: route.public_path,
433            authentication: route.authentication,
434            cors: route.cors,
435            rate: route.rate,
436            deprecated: route.deprecated,
437            request_schema_reference,
438            response_schema_reference,
439        });
440    }
441    if service_id.trim().is_empty() || resolved.is_empty() {
442        issues.push(issue(
443            DeliveryIssueCode::EdgeExposureUnsafe,
444            "An Edge Contract requires a Service identity and at least one valid public operation.",
445            "Declare only intended public operations from an authoritative Service Contract.",
446            "Correct the Edge Contract input and generate it again.",
447        ));
448    }
449    if !issues.is_empty() {
450        return Err(issues);
451    }
452    let authority_subject = edge_authority_subject(
453        release.release_id.as_str(),
454        release.release_digest.as_str(),
455        operation_catalog_digest.as_str(),
456        resolved.as_slice(),
457    );
458    let Some(provider_proof) = provider.sign(provider_id, &authority_subject) else {
459        return Err(vec![issue(
460            DeliveryIssueCode::EdgeOperationUnknown,
461            "The selected Edge authority provider is not trusted for the resolved public routes.",
462            "Use a configured provider that attests the exact release, operation catalog, and resolved routes.",
463            "Configure the provider and generate the Edge Contract again.",
464        )]);
465    };
466    let edge_contract_digest = digest_json(&(
467        EDGE_CONTRACT_PROTOCOL,
468        service_id.as_str(),
469        release.release_id.as_str(),
470        release.release_digest.as_str(),
471        operation_catalog_digest.as_str(),
472        provider_id,
473        provider_proof.as_str(),
474        resolved.as_slice(),
475    ));
476    Ok(EdgeContract {
477        protocol: EDGE_CONTRACT_PROTOCOL.to_owned(),
478        edge_contract_id: format!("edge-contract:{edge_contract_digest}"),
479        edge_contract_digest,
480        service_id,
481        release_id: release.release_id.clone(),
482        release_digest: release.release_digest.clone(),
483        operation_catalog_digest,
484        provider_id: provider_id.to_owned(),
485        provider_proof,
486        routes: resolved,
487    })
488}
489
490pub fn plan_gateway_configuration(
491    edge: &EdgeContract,
492    provider: &dyn ReleaseTrustProvider,
493    binding: &GatewayEnvironmentBinding,
494    observed: Option<&GatewayObservation>,
495    observation_provider: &dyn GatewayObservationProvider,
496) -> Result<GatewayConfigurationPlan, Vec<DeliveryIssue>> {
497    if !edge_contract_authority_is_valid(edge, provider)
498        || binding.environment.trim().is_empty()
499        || binding.gateway_adapter.trim().is_empty()
500        || !cors_origin_is_safe(&binding.public_origin)
501        || observed.is_some_and(|observation| {
502            !gateway_observation_integrity_is_valid(observation, observation_provider)
503        })
504    {
505        return Err(vec![issue(
506            DeliveryIssueCode::EdgeExposureUnsafe,
507            "Gateway planning requires an integrity-valid Edge Contract and explicit environment binding.",
508            "Correct the Edge Contract, adapter identity, and public origin.",
509            "Regenerate the Gateway plan before mutation.",
510        )]);
511    }
512    let configuration_identity = digest_json(&(
513        edge.edge_contract_digest.as_str(),
514        edge.release_id.as_str(),
515        edge.release_digest.as_str(),
516        edge.operation_catalog_digest.as_str(),
517        edge.provider_id.as_str(),
518        edge.provider_proof.as_str(),
519        binding.environment.as_str(),
520        binding.gateway_adapter.as_str(),
521        binding.public_origin.as_str(),
522        edge.routes.as_slice(),
523    ));
524    let drifted = observed.is_some_and(|observation| {
525        observation.configuration_identity != configuration_identity
526            || observation.revision != binding.expected_gateway_revision
527            || !observation.fresh
528    });
529    let diff = observed
530        .filter(|observation| observation.configuration_identity != configuration_identity)
531        .map(|observation| {
532            vec![GatewayPlanDiffEntry {
533                subject: "gateway.configurationIdentity".to_owned(),
534                before: Some(observation.configuration_identity.clone()),
535                after: Some(configuration_identity.clone()),
536            }]
537        })
538        .unwrap_or_default();
539    let issues = Vec::new();
540    let next_actions =
541        vec!["Review and apply this plan through the selected Gateway Adapter.".to_owned()];
542    let effects = DeliveryEffects::default();
543    let plan_digest = digest_json(&(
544        GATEWAY_PLAN_PROTOCOL,
545        edge.edge_contract_id.as_str(),
546        edge.edge_contract_digest.as_str(),
547        edge.release_id.as_str(),
548        edge.release_digest.as_str(),
549        edge.operation_catalog_digest.as_str(),
550        edge.provider_id.as_str(),
551        edge.provider_proof.as_str(),
552        binding,
553        configuration_identity.as_str(),
554        edge.routes.as_slice(),
555        diff.as_slice(),
556        drifted,
557        issues.as_slice(),
558        next_actions.as_slice(),
559        &effects,
560    ));
561    Ok(GatewayConfigurationPlan {
562        protocol: GATEWAY_PLAN_PROTOCOL.to_owned(),
563        plan_id: format!("gateway-plan:{plan_digest}"),
564        plan_digest,
565        edge_contract_id: edge.edge_contract_id.clone(),
566        edge_contract_digest: edge.edge_contract_digest.clone(),
567        edge_release_id: edge.release_id.clone(),
568        edge_release_digest: edge.release_digest.clone(),
569        operation_catalog_digest: edge.operation_catalog_digest.clone(),
570        edge_provider_id: edge.provider_id.clone(),
571        edge_provider_proof: edge.provider_proof.clone(),
572        environment: binding.environment.clone(),
573        gateway_adapter: binding.gateway_adapter.clone(),
574        public_origin: binding.public_origin.clone(),
575        expected_gateway_revision: binding.expected_gateway_revision,
576        configuration_identity,
577        routes: edge.routes.clone(),
578        diff,
579        drifted,
580        issues,
581        next_actions,
582        effects,
583    })
584}
585
586#[must_use]
587pub fn edge_contract_integrity_is_valid(edge: &EdgeContract) -> bool {
588    edge.protocol == EDGE_CONTRACT_PROTOCOL
589        && edge.routes.iter().all(resolved_edge_route_is_safe)
590        && edge.edge_contract_id == format!("edge-contract:{}", edge.edge_contract_digest)
591        && digest_json(&(
592            edge.protocol.as_str(),
593            edge.service_id.as_str(),
594            edge.release_id.as_str(),
595            edge.release_digest.as_str(),
596            edge.operation_catalog_digest.as_str(),
597            edge.provider_id.as_str(),
598            edge.provider_proof.as_str(),
599            edge.routes.as_slice(),
600        )) == edge.edge_contract_digest
601}
602
603#[must_use]
604pub fn edge_contract_authority_is_valid(
605    edge: &EdgeContract,
606    provider: &dyn ReleaseTrustProvider,
607) -> bool {
608    edge_contract_integrity_is_valid(edge)
609        && provider.verify(
610            edge.provider_id.as_str(),
611            edge_authority_subject(
612                edge.release_id.as_str(),
613                edge.release_digest.as_str(),
614                edge.operation_catalog_digest.as_str(),
615                edge.routes.as_slice(),
616            )
617            .as_str(),
618            edge.provider_proof.as_str(),
619        ) == ReleaseSignerStatus::Trusted
620}
621
622#[must_use]
623pub fn gateway_plan_integrity_is_valid(plan: &GatewayConfigurationPlan) -> bool {
624    let binding = GatewayEnvironmentBinding {
625        environment: plan.environment.clone(),
626        gateway_adapter: plan.gateway_adapter.clone(),
627        public_origin: plan.public_origin.clone(),
628        expected_gateway_revision: plan.expected_gateway_revision,
629    };
630    plan.protocol == GATEWAY_PLAN_PROTOCOL
631        && cors_origin_is_safe(&plan.public_origin)
632        && plan.routes.iter().all(resolved_edge_route_is_safe)
633        && plan.plan_id == format!("gateway-plan:{}", plan.plan_digest)
634        && digest_json(&(
635            plan.protocol.as_str(),
636            plan.edge_contract_id.as_str(),
637            plan.edge_contract_digest.as_str(),
638            plan.edge_release_id.as_str(),
639            plan.edge_release_digest.as_str(),
640            plan.operation_catalog_digest.as_str(),
641            plan.edge_provider_id.as_str(),
642            plan.edge_provider_proof.as_str(),
643            &binding,
644            plan.configuration_identity.as_str(),
645            plan.routes.as_slice(),
646            plan.diff.as_slice(),
647            plan.drifted,
648            plan.issues.as_slice(),
649            plan.next_actions.as_slice(),
650            &plan.effects,
651        )) == plan.plan_digest
652}
653
654fn resolved_edge_route_is_safe(route: &ResolvedEdgeRoute) -> bool {
655    public_path_template_is_safe(&route.public_path)
656        && route.rate.requests > 0
657        && route.rate.window_seconds > 0
658        && cors_intent_is_safe(&route.cors)
659}
660
661fn public_path_template_is_safe(path: &str) -> bool {
662    if path.len() > 2_048 || !path.starts_with('/') || path == "/" || path.ends_with('/') {
663        return false;
664    }
665    let mut parameters = BTreeSet::new();
666    path[1..].split('/').all(|segment| {
667        if segment.is_empty() {
668            return false;
669        }
670        if let Some(parameter) = segment
671            .strip_prefix('{')
672            .and_then(|value| value.strip_suffix('}'))
673        {
674            !parameter.is_empty()
675                && parameter.len() <= 64
676                && parameter
677                    .chars()
678                    .all(|character| character.is_ascii_alphanumeric() || character == '_')
679                && parameter
680                    .chars()
681                    .next()
682                    .is_some_and(|character| character.is_ascii_alphabetic())
683                && parameters.insert(parameter)
684        } else {
685            segment.len() <= 128
686                && segment.chars().all(|character| {
687                    character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')
688                })
689        }
690    })
691}
692
693fn cors_intent_is_safe(cors: &CorsIntent) -> bool {
694    let origins = cors.allowed_origins.iter().collect::<BTreeSet<_>>();
695    let methods = cors.allowed_methods.iter().collect::<BTreeSet<_>>();
696    origins.len() == cors.allowed_origins.len()
697        && methods.len() == cors.allowed_methods.len()
698        && cors.allowed_origins.len() <= 64
699        && cors.allowed_methods.len() <= 7
700        && (cors.allowed_origins.is_empty() == cors.allowed_methods.is_empty())
701        && cors
702            .allowed_origins
703            .iter()
704            .all(|origin| cors_origin_is_safe(origin))
705        && cors
706            .allowed_methods
707            .iter()
708            .all(|method| cors_method_is_safe(method))
709}
710
711fn cors_origin_is_safe(origin: &str) -> bool {
712    let Ok(uri) = origin.parse::<Uri>() else {
713        return false;
714    };
715    origin.len() <= 2_048
716        && !origin.contains(['@', '"', '\\', '?', '#'])
717        && !origin.chars().any(char::is_whitespace)
718        && matches!(uri.scheme_str(), Some("http" | "https"))
719        && uri.authority().is_some()
720        && matches!(uri.path(), "" | "/")
721        && uri.query().is_none()
722}
723
724fn cors_method_is_safe(method: &str) -> bool {
725    matches!(
726        method,
727        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS"
728    )
729}
730
731#[must_use]
732pub fn gateway_plan_authority_is_valid(
733    plan: &GatewayConfigurationPlan,
734    provider: &dyn ReleaseTrustProvider,
735) -> bool {
736    gateway_plan_integrity_is_valid(plan)
737        && provider.verify(
738            plan.edge_provider_id.as_str(),
739            edge_authority_subject(
740                plan.edge_release_id.as_str(),
741                plan.edge_release_digest.as_str(),
742                plan.operation_catalog_digest.as_str(),
743                plan.routes.as_slice(),
744            )
745            .as_str(),
746            plan.edge_provider_proof.as_str(),
747        ) == ReleaseSignerStatus::Trusted
748}
749
750#[must_use]
751pub fn edge_authority_subject(
752    release_id: &str,
753    release_digest: &str,
754    operation_catalog_digest: &str,
755    routes: &[ResolvedEdgeRoute],
756) -> String {
757    digest_json(&(
758        "lenso.edge-authority-subject.v1",
759        release_id,
760        release_digest,
761        operation_catalog_digest,
762        routes,
763    ))
764}
765
766#[must_use]
767pub fn observe_gateway(
768    plan: &GatewayConfigurationPlan,
769    revision: u64,
770    observed_after: impl Into<String>,
771    fresh: bool,
772    provider: &dyn GatewayObservationProvider,
773) -> Result<GatewayObservation, DeliveryIssue> {
774    let observed_after = observed_after.into();
775    let fresh = fresh && revision == plan.expected_gateway_revision;
776    let resource_uid = format!("synthetic:{}", plan.plan_id);
777    let resource_version = "1";
778    let authority_context = plan.plan_id.as_str();
779    let digest = digest_json(&(
780        GATEWAY_OBSERVATION_PROTOCOL,
781        plan.plan_id.as_str(),
782        plan.plan_digest.as_str(),
783        plan.environment.as_str(),
784        plan.edge_release_id.as_str(),
785        plan.edge_release_digest.as_str(),
786        resource_uid.as_str(),
787        resource_version,
788        authority_context,
789        plan.configuration_identity.as_str(),
790        revision,
791        observed_after.as_str(),
792        fresh,
793        provider.provider_id(),
794    ));
795    let observation_id = format!("gateway-observation:{digest}");
796    let provider_proof = provider.sign(&observation_id).ok_or_else(|| {
797        issue(
798            DeliveryIssueCode::ObservationStale,
799            "The Gateway adapter authority refused to attest the observation.",
800            "Use the configured Gateway observation provider at the adapter read boundary.",
801            "Collect a new Gateway observation before continuing.",
802        )
803    })?;
804    Ok(GatewayObservation {
805        protocol: GATEWAY_OBSERVATION_PROTOCOL.to_owned(),
806        observation_id,
807        plan_id: plan.plan_id.clone(),
808        plan_digest: plan.plan_digest.clone(),
809        environment: plan.environment.clone(),
810        release_id: plan.edge_release_id.clone(),
811        release_digest: plan.edge_release_digest.clone(),
812        resource_uid,
813        resource_version: resource_version.to_owned(),
814        authority_context: authority_context.to_owned(),
815        configuration_identity: plan.configuration_identity.clone(),
816        revision,
817        observed_after,
818        fresh,
819        provider_id: provider.provider_id().to_owned(),
820        provider_proof,
821    })
822}
823
824#[must_use]
825pub fn gateway_observation_content_integrity_is_valid(observation: &GatewayObservation) -> bool {
826    observation.protocol == GATEWAY_OBSERVATION_PROTOCOL
827        && !observation.provider_id.trim().is_empty()
828        && !observation.provider_proof.trim().is_empty()
829        && observation.observation_id
830            == format!(
831                "gateway-observation:{}",
832                digest_json(&(
833                    observation.protocol.as_str(),
834                    observation.plan_id.as_str(),
835                    observation.plan_digest.as_str(),
836                    observation.environment.as_str(),
837                    observation.release_id.as_str(),
838                    observation.release_digest.as_str(),
839                    observation.resource_uid.as_str(),
840                    observation.resource_version.as_str(),
841                    observation.authority_context.as_str(),
842                    observation.configuration_identity.as_str(),
843                    observation.revision,
844                    observation.observed_after.as_str(),
845                    observation.fresh,
846                    observation.provider_id.as_str(),
847                ))
848            )
849}
850
851#[must_use]
852pub fn gateway_observation_integrity_is_valid(
853    observation: &GatewayObservation,
854    provider: &dyn GatewayObservationProvider,
855) -> bool {
856    gateway_observation_content_integrity_is_valid(observation)
857        && observation.provider_id == provider.provider_id()
858        && provider.verify(&observation.observation_id, &observation.provider_proof)
859}
860
861fn digest_json(value: &impl Serialize) -> String {
862    extraction_input_digest(serde_json::to_vec(value).expect("edge values must serialize"))
863}