Skip to main content

type_bridge/
schema.rs

1//! Schema package markers and type-branded installation handshake.
2
3use core::marker::PhantomData;
4use std::sync::Arc;
5
6use type_bridge_contract::schema::encode_declared_schema;
7use type_bridge_schema::{
8    MAX_SCHEMA_AUTHORITY_BYTES, VerifiedSchemaAuthority, decode_schema_authority,
9    schema_authority_capability_vocabulary,
10};
11
12use crate::error::{Error, Result};
13
14#[doc(hidden)]
15pub mod sealed {
16    pub trait Sealed {}
17}
18
19/// A type-level marker representing a generated schema package.
20pub trait Schema: sealed::Sealed + Send + Sync + 'static {}
21
22/// Default marker representing an unbound database handle.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct Unbound;
25
26impl sealed::Sealed for Unbound {}
27impl Schema for Unbound {}
28
29/// A generated schema package marker carrying fingerprint evidence branded by `S: Schema`.
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct SchemaPackage<S: Schema> {
32    semantic_fingerprint_json: &'static str,
33    projection_fingerprint_json: &'static str,
34    runtime_projection_json: &'static str,
35    declared_schema_json: Option<&'static str>,
36    schema_authority_json: Option<&'static str>,
37    managed_scope_id: Option<&'static str>,
38    semantic_profile_id: Option<&'static str>,
39    marker: PhantomData<fn() -> S>,
40}
41
42impl<S: Schema> SchemaPackage<S> {
43    /// Construct a type-branded schema package marker from verified JSON evidence (generated-code SPI).
44    #[doc(hidden)]
45    #[must_use]
46    pub const fn new(
47        semantic_fingerprint_json: &'static str,
48        projection_fingerprint_json: &'static str,
49        runtime_projection_json: &'static str,
50    ) -> Self {
51        Self {
52            semantic_fingerprint_json,
53            projection_fingerprint_json,
54            runtime_projection_json,
55            declared_schema_json: None,
56            schema_authority_json: None,
57            managed_scope_id: None,
58            semantic_profile_id: None,
59            marker: PhantomData,
60        }
61    }
62
63    /// Construct a generated package carrying canonical remote query
64    /// authority in addition to verified runtime projection evidence.
65    #[doc(hidden)]
66    #[must_use]
67    pub const fn new_with_declared(
68        semantic_fingerprint_json: &'static str,
69        projection_fingerprint_json: &'static str,
70        runtime_projection_json: &'static str,
71        declared_schema_json: &'static str,
72    ) -> Self {
73        Self {
74            semantic_fingerprint_json,
75            projection_fingerprint_json,
76            runtime_projection_json,
77            declared_schema_json: Some(declared_schema_json),
78            schema_authority_json: None,
79            managed_scope_id: None,
80            semantic_profile_id: None,
81            marker: PhantomData,
82        }
83    }
84
85    /// Construct a generated package carrying one exact compiled schema
86    /// authority and its constructor-extracted query inputs.
87    #[doc(hidden)]
88    #[must_use]
89    pub const fn new_with_authority(
90        semantic_fingerprint_json: &'static str,
91        projection_fingerprint_json: &'static str,
92        runtime_projection_json: &'static str,
93        schema_authority_json: &'static str,
94        declared_schema_json: &'static str,
95        managed_scope_id: &'static str,
96        semantic_profile_id: &'static str,
97    ) -> Self {
98        Self {
99            semantic_fingerprint_json,
100            projection_fingerprint_json,
101            runtime_projection_json,
102            declared_schema_json: Some(declared_schema_json),
103            schema_authority_json: Some(schema_authority_json),
104            managed_scope_id: Some(managed_scope_id),
105            semantic_profile_id: Some(semantic_profile_id),
106            marker: PhantomData,
107        }
108    }
109
110    /// Perform offline fingerprint verification without connecting to a live server.
111    pub fn verify(&self) -> Result<()> {
112        let _ = self.verify_and_install_with_authority()?;
113        Ok(())
114    }
115
116    /// Return the semantic schema fingerprint JSON string (generated-code SPI).
117    #[doc(hidden)]
118    #[must_use]
119    pub const fn semantic_fingerprint_json(&self) -> &'static str {
120        self.semantic_fingerprint_json
121    }
122
123    /// Return the binding target projection fingerprint JSON string (generated-code SPI).
124    #[doc(hidden)]
125    #[must_use]
126    pub const fn projection_fingerprint_json(&self) -> &'static str {
127        self.projection_fingerprint_json
128    }
129
130    /// Return the canonical runtime projection JSON string (generated-code SPI).
131    #[doc(hidden)]
132    #[must_use]
133    pub const fn runtime_projection_json(&self) -> &'static str {
134        self.runtime_projection_json
135    }
136
137    pub(crate) const fn declared_schema_json(&self) -> Option<&'static str> {
138        self.declared_schema_json
139    }
140
141    /// Perform runtime fingerprint verification and derive provider descriptors (crate-internal).
142    pub(crate) fn verify_and_install(
143        &self,
144    ) -> Result<Arc<type_bridge_orm::InstalledRuntimeProjection>> {
145        self.verify_and_install_with_authority()
146            .map(|(projection, _authority)| projection)
147    }
148
149    pub(crate) fn verify_and_install_with_authority(
150        &self,
151    ) -> Result<(
152        Arc<type_bridge_orm::InstalledRuntimeProjection>,
153        Option<VerifiedSchemaAuthority>,
154    )> {
155        let authority = self.verify_embedded_authority()?;
156        let projection = type_bridge_orm::InstalledRuntimeProjection::from_verified_rust_json(
157            self.runtime_projection_json.as_bytes(),
158            self.semantic_fingerprint_json.as_bytes(),
159            self.projection_fingerprint_json.as_bytes(),
160        )
161        .map_err(|err| Error::SchemaVerification {
162            message: err.to_string(),
163            source: Some(Box::new(err)),
164        })?;
165        if authority.as_ref().is_some_and(|authority| {
166            authority.resolved_schema().semantic_fingerprint()
167                != projection.projection().semantic_fingerprint()
168        }) {
169            return Err(authority_error(
170                "generated schema authority does not match the installed runtime projection",
171            ));
172        }
173        Ok((Arc::new(projection), authority))
174    }
175
176    fn verify_embedded_authority(&self) -> Result<Option<VerifiedSchemaAuthority>> {
177        let parts = (
178            self.schema_authority_json,
179            self.declared_schema_json,
180            self.managed_scope_id,
181            self.semantic_profile_id,
182        );
183        let (Some(envelope), Some(declared), Some(scope), Some(profile)) = parts else {
184            if parts.0.is_none() && parts.2.is_none() && parts.3.is_none() {
185                return Ok(None);
186            }
187            return Err(authority_error(
188                "generated schema package contains incomplete compiled authority evidence",
189            ));
190        };
191        if envelope.len() > MAX_SCHEMA_AUTHORITY_BYTES {
192            return Err(authority_error(
193                "generated schema authority exceeds the canonical byte ceiling",
194            ));
195        }
196        let authority = decode_schema_authority(
197            envelope.as_bytes(),
198            &schema_authority_capability_vocabulary(),
199        )
200        .map_err(|error| Error::SchemaVerification {
201            message: format!(
202                "generated schema package contains invalid compiled authority ({:?})",
203                error.code()
204            ),
205            source: Some(Box::new(error)),
206        })?;
207        let reconstructed_declared =
208            encode_declared_schema(authority.declared_schema()).map_err(|error| {
209                Error::SchemaVerification {
210                    message: "generated schema authority declaration cannot be reconstructed"
211                        .into(),
212                    source: Some(Box::new(error)),
213                }
214            })?;
215        if reconstructed_declared != declared.as_bytes()
216            || authority.managed_scope().id().as_str() != scope
217            || authority.semantic_profile().id().as_str() != profile
218        {
219            return Err(authority_error(
220                "generated schema authority disagrees with its extracted query evidence",
221            ));
222        }
223        Ok(Some(authority))
224    }
225}
226
227fn authority_error(message: &'static str) -> Error {
228    Error::SchemaVerification {
229        message: message.into(),
230        source: None,
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use serde_json::Value;
238    use type_bridge_contract::codec::to_canonical_json;
239    use type_bridge_contract::fingerprint::{
240        CanonicalizationVersion, Fingerprint, FingerprintDomain, SemanticProfileId,
241    };
242    use type_bridge_contract::managed_scope::ManagedScopeId;
243    use type_bridge_contract::projection::{BindingTarget, ProjectionConfig};
244    use type_bridge_contract::schema::{DocumentId, encode_declared_schema};
245    use type_bridge_schema::{
246        ManagedDeltaContext, SCHEMA_AUTHORITY_FINGERPRINT_CANONICALIZATION,
247        SCHEMA_AUTHORITY_FINGERPRINT_DOMAIN, SchemaDocumentSet, build_schema_authority,
248        encode_schema_authority, normalize_documents, project, resolve,
249    };
250    use type_bridge_schema_codegen::{PythonEmitter, RustEmitter};
251
252    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
253    struct TestSchema;
254    impl sealed::Sealed for TestSchema {}
255    impl Schema for TestSchema {}
256
257    fn leak(bytes: Vec<u8>) -> &'static str {
258        Box::leak(String::from_utf8(bytes).unwrap().into_boxed_str())
259    }
260
261    fn generated_package(source: &str, scope: &str) -> SchemaPackage<TestSchema> {
262        let documents =
263            SchemaDocumentSet::parse([(DocumentId::new("authority-test.yaml").unwrap(), source)])
264                .unwrap();
265        let declared = normalize_documents(&documents).unwrap();
266        let profile = SemanticProfileId::new("typedb-3.12.1/v1").unwrap();
267        let resolved = resolve(&declared, &profile).unwrap();
268        let authority = build_schema_authority(
269            &declared,
270            declared.required_capabilities(),
271            &ManagedDeltaContext::new(
272                ManagedScopeId::new(scope).unwrap(),
273                profile,
274                schema_authority_capability_vocabulary(),
275            ),
276        )
277        .unwrap();
278        let emitter = RustEmitter::new();
279        let projection = project(
280            &resolved,
281            BindingTarget::Rust,
282            &ProjectionConfig::rust(),
283            &emitter.generator_handlers(),
284            &emitter.code_resources().unwrap(),
285        )
286        .unwrap();
287        SchemaPackage::new_with_authority(
288            leak(to_canonical_json(projection.semantic_fingerprint()).unwrap()),
289            leak(to_canonical_json(projection.projection_fingerprint()).unwrap()),
290            leak(to_canonical_json(&projection).unwrap()),
291            leak(encode_schema_authority(&authority)),
292            leak(encode_declared_schema(&declared).unwrap()),
293            Box::leak(scope.to_owned().into_boxed_str()),
294            "typedb-3.12.1/v1",
295        )
296    }
297
298    fn with_envelope(
299        package: SchemaPackage<TestSchema>,
300        envelope: &'static str,
301    ) -> SchemaPackage<TestSchema> {
302        SchemaPackage::new_with_authority(
303            package.semantic_fingerprint_json,
304            package.projection_fingerprint_json,
305            package.runtime_projection_json,
306            envelope,
307            package.declared_schema_json.unwrap(),
308            package.managed_scope_id.unwrap(),
309            package.semantic_profile_id.unwrap(),
310        )
311    }
312
313    fn canonical(value: &Value) -> &'static str {
314        leak(to_canonical_json(value).unwrap())
315    }
316
317    fn resign(value: &mut Value) {
318        let content = to_canonical_json(&value["content"]).unwrap();
319        let fingerprint = Fingerprint::compute(
320            FingerprintDomain::new(SCHEMA_AUTHORITY_FINGERPRINT_DOMAIN).unwrap(),
321            CanonicalizationVersion::new(SCHEMA_AUTHORITY_FINGERPRINT_CANONICALIZATION).unwrap(),
322            None,
323            &content,
324        );
325        value["authority_fingerprint"] = serde_json::to_value(fingerprint).unwrap();
326    }
327
328    #[test]
329    fn compiled_authority_is_fully_verified_and_bound_to_projection() {
330        let package = generated_package(
331            "format: typebridge.schema/v2\nentities:\n  person: {}\n",
332            "rust-authority-test",
333        );
334        let (projection, authority) = package.verify_and_install_with_authority().unwrap();
335        let authority = authority.expect("generated package has compiled authority");
336        assert_eq!(
337            projection.projection().semantic_fingerprint(),
338            authority.resolved_schema().semantic_fingerprint(),
339        );
340
341        let foreign = generated_package(
342            "format: typebridge.schema/v2\nentities:\n  organization: {}\n",
343            "rust-authority-test",
344        );
345        let mismatched: SchemaPackage<TestSchema> = SchemaPackage::new_with_authority(
346            package.semantic_fingerprint_json,
347            package.projection_fingerprint_json,
348            package.runtime_projection_json,
349            foreign.schema_authority_json.unwrap(),
350            foreign.declared_schema_json.unwrap(),
351            foreign.managed_scope_id.unwrap(),
352            foreign.semantic_profile_id.unwrap(),
353        );
354        let error = mismatched
355            .verify()
356            .expect_err("foreign semantic authority must not bind to the projection");
357        assert!(error.to_string().contains("installed runtime projection"));
358    }
359
360    #[test]
361    fn compiled_authority_rejects_missing_and_stale_outer_fingerprints() {
362        let package = generated_package(
363            "format: typebridge.schema/v2\nentities:\n  person: {}\n",
364            "rust-authority-test",
365        );
366        let original: Value = serde_json::from_str(package.schema_authority_json.unwrap()).unwrap();
367
368        let mut missing = original.clone();
369        missing
370            .as_object_mut()
371            .unwrap()
372            .remove("authority_fingerprint");
373        assert!(
374            with_envelope(package, canonical(&missing))
375                .verify()
376                .is_err()
377        );
378
379        let mut stale = original;
380        stale["authority_fingerprint"]["digest"] = "0".repeat(64).into();
381        assert!(with_envelope(package, canonical(&stale)).verify().is_err());
382    }
383
384    #[test]
385    fn compiled_authority_reconstructs_managed_state_and_rejects_unsupported_claims() {
386        let package = generated_package(
387            "format: typebridge.schema/v2\nentities:\n  person: {}\n",
388            "rust-authority-test",
389        );
390        let original: Value = serde_json::from_str(package.schema_authority_json.unwrap()).unwrap();
391
392        let mut managed = original.clone();
393        managed["content"]["managed_state"]["declared_identity"]["digest"] = "0".repeat(64).into();
394        resign(&mut managed);
395        assert!(
396            with_envelope(package, canonical(&managed))
397                .verify()
398                .is_err()
399        );
400
401        let mut capabilities = original.clone();
402        capabilities["content"]["required_capabilities"] =
403            Value::Array(vec![Value::String("unsupported.runtime".into())]);
404        resign(&mut capabilities);
405        let error = with_envelope(package, canonical(&capabilities))
406            .verify()
407            .expect_err("unsupported artifact capability must fail closed");
408        assert!(error.to_string().contains("UnsupportedCapability"));
409
410        let mut version = original;
411        version["content"]["authority_version"] =
412            Value::String("typebridge.schema-authority/v2".into());
413        resign(&mut version);
414        let error = with_envelope(package, canonical(&version))
415            .verify()
416            .expect_err("unsupported artifact version must fail closed");
417        assert!(error.to_string().contains("UnsupportedVersion"));
418    }
419
420    #[test]
421    fn compiled_authority_rejects_oversize_and_detached_evidence() {
422        let package = generated_package(
423            "format: typebridge.schema/v2\nentities:\n  person: {}\n",
424            "rust-authority-test",
425        );
426        let oversized = Box::leak(" ".repeat(MAX_SCHEMA_AUTHORITY_BYTES + 1).into_boxed_str());
427        let error = with_envelope(package, oversized)
428            .verify()
429            .expect_err("oversize authority must fail before parsing");
430        assert!(error.to_string().contains("byte ceiling"));
431
432        let detached: SchemaPackage<TestSchema> = SchemaPackage::new_with_authority(
433            package.semantic_fingerprint_json,
434            package.projection_fingerprint_json,
435            package.runtime_projection_json,
436            package.schema_authority_json.unwrap(),
437            package.declared_schema_json.unwrap(),
438            "other-scope",
439            package.semantic_profile_id.unwrap(),
440        );
441        let error = detached
442            .verify()
443            .expect_err("detached scope must not override compiled authority");
444        assert!(error.to_string().contains("extracted query evidence"));
445    }
446
447    #[test]
448    fn schema_package_fingerprint_verification() {
449        let documents = SchemaDocumentSet::parse([(
450            DocumentId::new("test.yaml").unwrap(),
451            "format: typebridge.schema/v2\nentities:\n  person: {}\n",
452        )])
453        .unwrap();
454        let declared = normalize_documents(&documents).unwrap();
455        let profile = SemanticProfileId::new("typedb-3.12.1/v1").unwrap();
456        let resolved = resolve(&declared, &profile).unwrap();
457        let emitter = RustEmitter::new();
458        let resources = emitter.code_resources().unwrap();
459        let projection = project(
460            &resolved,
461            BindingTarget::Rust,
462            &ProjectionConfig::rust(),
463            &emitter.generator_handlers(),
464            &resources,
465        )
466        .unwrap();
467
468        let semantic_json =
469            String::from_utf8(to_canonical_json(projection.semantic_fingerprint()).unwrap())
470                .unwrap();
471        let projection_json =
472            String::from_utf8(to_canonical_json(projection.projection_fingerprint()).unwrap())
473                .unwrap();
474        let runtime_json = String::from_utf8(to_canonical_json(&projection).unwrap()).unwrap();
475
476        let semantic_ref: &'static str = Box::leak(semantic_json.into_boxed_str());
477        let projection_ref: &'static str = Box::leak(projection_json.into_boxed_str());
478        let runtime_ref: &'static str = Box::leak(runtime_json.into_boxed_str());
479
480        let valid_package: SchemaPackage<TestSchema> =
481            SchemaPackage::new(semantic_ref, projection_ref, runtime_ref);
482        assert!(valid_package.verify().is_ok());
483
484        let tampered_package: SchemaPackage<TestSchema> =
485            SchemaPackage::new(semantic_ref, r#""rust/v1-tampered""#, runtime_ref);
486        match tampered_package.verify() {
487            Err(err) => {
488                use std::error::Error as _;
489                assert!(err.source().is_some());
490            }
491            Ok(_) => panic!("tampered schema package must fail verification"),
492        }
493    }
494
495    #[test]
496    fn rejects_non_rust_target_projection() {
497        let documents = SchemaDocumentSet::parse([(
498            DocumentId::new("test.yaml").unwrap(),
499            "format: typebridge.schema/v2\nentities:\n  person: {}\n",
500        )])
501        .unwrap();
502        let declared = normalize_documents(&documents).unwrap();
503        let profile = SemanticProfileId::new("typedb-3.12.1/v1").unwrap();
504        let resolved = resolve(&declared, &profile).unwrap();
505        let py_emitter = PythonEmitter::new();
506        let py_resources = py_emitter.code_resources().unwrap();
507        let py_projection = project(
508            &resolved,
509            BindingTarget::Python,
510            &ProjectionConfig::python(),
511            &py_emitter.generator_handlers(),
512            &py_resources,
513        )
514        .unwrap();
515
516        let semantic_json =
517            String::from_utf8(to_canonical_json(py_projection.semantic_fingerprint()).unwrap())
518                .unwrap();
519        let projection_json =
520            String::from_utf8(to_canonical_json(py_projection.projection_fingerprint()).unwrap())
521                .unwrap();
522        let runtime_json = String::from_utf8(to_canonical_json(&py_projection).unwrap()).unwrap();
523
524        let semantic_ref: &'static str = Box::leak(semantic_json.into_boxed_str());
525        let projection_ref: &'static str = Box::leak(projection_json.into_boxed_str());
526        let runtime_ref: &'static str = Box::leak(runtime_json.into_boxed_str());
527
528        let py_package: SchemaPackage<TestSchema> =
529            SchemaPackage::new(semantic_ref, projection_ref, runtime_ref);
530        let err = py_package.verify().unwrap_err();
531        assert!(err.to_string().contains("target mismatch") || err.to_string().contains("Rust"));
532    }
533}