#![deny(missing_docs)]
use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::{Component, Path, PathBuf};
use sha2::{Digest as _, Sha256};
use type_bridge_contract::capability::{CapabilityId, CapabilitySet};
use type_bridge_contract::fingerprint::SemanticProfileId;
use type_bridge_contract::managed_scope::{
ManagedScopeBinding, ManagedScopeId, ManagedScopeProfileId,
};
use type_bridge_contract::migration::MigrationAppLabel;
use type_bridge_contract::projection::{BindingTarget, CSymbolPrefix};
use type_bridge_contract::reserved::TYPEBRIDGE_JOURNAL_DATABASE_SUFFIX;
use type_bridge_contract::schema::SourceSpan;
use type_bridge_contract::semantic_profile::SemanticProfile;
use type_bridge_schema::{SchemaSourceKind, SchemaSourceService};
use type_bridge_schema_migration::{MigrationSafetyPolicy, validate_portable_direct_child};
use unicode_casefold::UnicodeCaseFold as _;
use unicode_normalization::UnicodeNormalization as _;
mod authority;
mod backfill_intent;
mod bundle;
mod lock;
mod migration;
mod workspace;
mod workspace_yaml;
pub use authority::{WorkspaceDirectoryAuthority, WorkspaceOutputDirectory};
pub use backfill_intent::{
MAX_BACKFILL_INTENT_BYTES, TYPEBRIDGE_BACKFILL_INTENT_V1, parse_backfill_intent,
};
pub use bundle::{
BundleProjectionContext, BundleVerificationContext, MAX_SCHEMA_BUNDLE_BYTES,
SCHEMA_BUNDLE_FINGERPRINT_CANONICALIZATION, SCHEMA_BUNDLE_FINGERPRINT_DOMAIN,
SchemaBundleError, SchemaBundleErrorCode, TYPEBRIDGE_SCHEMA_BUNDLE_V1, TypeBridgeRuntime,
VerifiedSchemaBundle, build_verified_schema_bundle, decode_verified_schema_bundle,
encode_verified_schema_bundle,
};
pub use lock::{
MAX_WORKSPACE_LOCK_BYTES, TYPEBRIDGE_WORKSPACE_LOCK_V1, VerifiedWorkspaceLock, WorkspaceLock,
WorkspaceLockError, WorkspaceLockErrorCode, generate_workspace_lock, verify_workspace_lock,
};
pub use migration::{MigrationDirectoryAuthority, MigrationPlanEntry};
pub use workspace::{TypeBridgeWorkspace, TypeBridgeWorkspaceError, TypeBridgeWorkspaceServices};
pub use workspace_yaml::{
ConfigOrigin, LocatedConfigSpec, TYPEBRIDGE_WORKSPACE_V1_FORMAT, TypeBridgeConfigSpec,
};
pub const TYPEBRIDGE_WORKSPACE_SEMANTIC_PROFILE_ID: &str = "typedb-3.12.1/v1";
pub const TYPEBRIDGE_WORKSPACE_SEMANTIC_PROFILE_IDS: &[&str] =
&["typedb-3.11.5/v1", TYPEBRIDGE_WORKSPACE_SEMANTIC_PROFILE_ID];
const MAX_SYMBOLIC_ID_BYTES: usize = 255;
const MAX_EXTENSION_VERSION_BYTES: usize = 64;
const C_SYMBOL_PREFIX_MAX_BYTES: usize = 63;
const C_SYMBOL_PREFIX_SHORT_MARKER: &str = "tb_";
const C_SYMBOL_PREFIX_DIGEST_MARKER: &str = "tbh_sha256_";
const C_SYMBOL_PREFIX_DIGEST_DOMAIN: &[u8] = b"typebridge.c-symbol-prefix/v1\0";
const BASE32_LOWER_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorkspaceConfigErrorCode {
WorkspaceRootNotAbsolute,
WorkspaceRootNotCanonical,
WorkspaceRootCanonicalizationFailed,
PathNotConfined,
InvalidSchemaSetPath,
InvalidSchemaAuthorityOutputPath,
InvalidMigrationV2Directory,
MissingRequiredField,
DuplicateRequiredField,
UnsupportedSemanticProfile,
InvalidManagedScope,
OverlappingWorkspacePath,
DuplicateOutputTarget,
UnsupportedBindingTarget,
DuplicateSecretSlot,
DuplicateExtensionHandler,
InvalidSymbolicIdentifier,
EnvironmentDatabaseCollision,
InvalidConfigOrigin,
InvalidWorkspaceEncoding,
InvalidWorkspaceYaml,
UnsupportedWorkspaceFormat,
UnknownWorkspaceKey,
MissingWorkspaceField,
InvalidWorkspaceValue,
DuplicateCapabilityRequirement,
SecretLiteralRejected,
InvalidSecretReference,
SecretReferenceRejected,
ExtensionRequirementRejected,
InvalidTlsBoolean,
TlsRootCaRequiresTls,
TlsRootCaWithDisabledTls,
InvalidTlsRootCa,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceConfigError {
code: WorkspaceConfigErrorCode,
detail: Option<String>,
message: &'static str,
origin: Option<String>,
source_span: Option<Box<SourceSpan>>,
}
impl WorkspaceConfigError {
fn new(code: WorkspaceConfigErrorCode, message: &'static str) -> Self {
Self {
code,
detail: None,
message,
origin: None,
source_span: None,
}
}
fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
pub(crate) fn with_source(
mut self,
origin: impl Into<String>,
source_span: SourceSpan,
) -> Self {
self.origin = Some(origin.into());
self.source_span = Some(Box::new(source_span));
self
}
#[must_use]
pub const fn code(&self) -> WorkspaceConfigErrorCode {
self.code
}
#[must_use]
pub fn detail(&self) -> Option<&str> {
self.detail.as_deref()
}
#[must_use]
pub fn origin(&self) -> Option<&str> {
self.origin.as_deref()
}
#[must_use]
pub fn source_span(&self) -> Option<&SourceSpan> {
self.source_span.as_deref()
}
}
impl fmt::Display for WorkspaceConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.message)?;
if let Some(detail) = &self.detail {
write!(formatter, ": {detail}")?;
}
Ok(())
}
}
impl Error for WorkspaceConfigError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorkspaceServiceError {
code: &'static str,
}
impl WorkspaceServiceError {
#[must_use]
pub const fn new(code: &'static str) -> Self {
Self { code }
}
#[must_use]
pub const fn code(self) -> &'static str {
self.code
}
}
impl fmt::Display for WorkspaceServiceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.code)
}
}
impl Error for WorkspaceServiceError {}
pub trait WorkspaceSourceService {
fn canonicalize_workspace_root(&self, root: &Path) -> Result<PathBuf, WorkspaceServiceError>;
fn canonicalize_workspace_path(&self, _path: &Path) -> Result<PathBuf, WorkspaceServiceError> {
Err(WorkspaceServiceError::new(
"workspace_path_canonicalization_unavailable",
))
}
fn readable_workspace_file_len(&self, _path: &Path) -> Result<u64, WorkspaceServiceError> {
Err(WorkspaceServiceError::new(
"workspace_file_observation_unavailable",
))
}
}
impl<T> WorkspaceSourceService for T
where
T: SchemaSourceService + ?Sized,
{
fn canonicalize_workspace_root(&self, root: &Path) -> Result<PathBuf, WorkspaceServiceError> {
self.canonicalize(root)
.map_err(|_| WorkspaceServiceError::new("schema_source_canonicalize_failed"))
}
fn canonicalize_workspace_path(&self, path: &Path) -> Result<PathBuf, WorkspaceServiceError> {
self.canonicalize(path)
.map_err(|_| WorkspaceServiceError::new("workspace_path_canonicalize_failed"))
}
fn readable_workspace_file_len(&self, path: &Path) -> Result<u64, WorkspaceServiceError> {
let observation = self
.metadata(path)
.map_err(|_| WorkspaceServiceError::new("workspace_file_metadata_failed"))?;
if observation.kind() != SchemaSourceKind::File {
return Err(WorkspaceServiceError::new("workspace_path_is_not_a_file"));
}
let capture = self
.capture_file(path, 0)
.map_err(|_| WorkspaceServiceError::new("workspace_file_read_failed"))?;
if capture.before() != &observation || capture.after() != &observation {
return Err(WorkspaceServiceError::new(
"workspace_file_changed_during_validation",
));
}
Ok(observation.len())
}
}
pub trait SecretReferenceService {
fn validate_reference(&self, reference: &SecretReference) -> Result<(), WorkspaceServiceError>;
}
pub trait ExtensionRegistryService {
fn validate_requirement(
&self,
requirement: &ExtensionRequirement,
) -> Result<(), WorkspaceServiceError>;
}
pub struct TypeBridgeConfigServices<'a> {
extensions: &'a dyn ExtensionRegistryService,
secrets: &'a dyn SecretReferenceService,
sources: &'a dyn WorkspaceSourceService,
}
impl<'a> TypeBridgeConfigServices<'a> {
#[must_use]
pub const fn new(
sources: &'a dyn WorkspaceSourceService,
secrets: &'a dyn SecretReferenceService,
extensions: &'a dyn ExtensionRegistryService,
) -> Self {
Self {
extensions,
secrets,
sources,
}
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct WorkspaceRoot(PathBuf);
impl WorkspaceRoot {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, WorkspaceConfigError> {
let path = path.into();
if !path.is_absolute() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::WorkspaceRootNotAbsolute,
"workspace root must be explicit and absolute",
));
}
if path.to_str().is_none()
|| path
.components()
.any(|component| matches!(component, Component::CurDir | Component::ParentDir))
{
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::WorkspaceRootNotCanonical,
"workspace root must have a portable canonical spelling",
));
}
Ok(Self(path))
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
pub(crate) fn confined_relative_path(
path: impl Into<PathBuf>,
subject: &'static str,
) -> Result<PathBuf, WorkspaceConfigError> {
let path = path.into();
let Some(portable) = path.to_str() else {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::PathNotConfined,
"workspace-relative path must be valid UTF-8",
)
.with_detail(subject));
};
let invalid_spelling = portable.is_empty()
|| portable.contains(['\\', ':', '\0'])
|| portable.bytes().any(|byte| byte.is_ascii_control())
|| portable
.split('/')
.any(|segment| segment.is_empty() || matches!(segment, "." | ".."));
let invalid_components = path.is_absolute()
|| path
.components()
.any(|component| !matches!(component, Component::Normal(_)));
if invalid_spelling || invalid_components {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::PathNotConfined,
"workspace-relative path escapes or is not portable",
)
.with_detail(subject));
}
Ok(path)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceRootCa(PathBuf);
impl WorkspaceRootCa {
pub fn new(
workspace_root: &WorkspaceRoot,
relative_path: impl Into<PathBuf>,
sources: &dyn WorkspaceSourceService,
) -> Result<Self, WorkspaceConfigError> {
let relative_path = confined_relative_path(relative_path, "environment.tls-root-ca")?;
let canonical_root = sources
.canonicalize_workspace_root(workspace_root.as_path())
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::WorkspaceRootCanonicalizationFailed,
"custom root CA validation could not canonicalize the workspace root",
)
.with_detail(error.code())
})?;
if canonical_root != workspace_root.as_path() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::WorkspaceRootNotCanonical,
"custom root CA validation requires a canonical workspace root",
));
}
let mut candidate = canonical_root.clone();
candidate.extend(relative_path.components());
let canonical_path = sources
.canonicalize_workspace_path(&candidate)
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidTlsRootCa,
"custom root CA path cannot be canonicalized",
)
.with_detail(error.code())
})?;
if canonical_path == canonical_root
|| !canonical_path.starts_with(&canonical_root)
|| canonical_path.to_str().is_none()
{
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidTlsRootCa,
"custom root CA path escapes the canonical workspace root",
));
}
let length = sources
.readable_workspace_file_len(&canonical_path)
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidTlsRootCa,
"custom root CA path must be a readable regular file",
)
.with_detail(error.code())
})?;
if length == 0 {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidTlsRootCa,
"custom root CA file must not be empty",
));
}
Ok(Self(canonical_path))
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum WorkspaceTransportPolicy {
#[default]
Disabled,
NativeRoots,
CustomRootCa(WorkspaceRootCa),
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct SchemaSetPath(PathBuf);
impl SchemaSetPath {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, WorkspaceConfigError> {
let path = confined_relative_path(path, "schema_set")?;
if path.extension().and_then(|extension| extension.to_str()) != Some("yaml") {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidSchemaSetPath,
"schema-set path must end in lowercase .yaml",
));
}
Ok(Self(path))
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct MigrationV2Directory(PathBuf);
impl MigrationV2Directory {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, WorkspaceConfigError> {
let path = confined_relative_path(path, "migration_v2_directory")?;
if path.file_name().and_then(|name| name.to_str()) != Some("v2") {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidMigrationV2Directory,
"canonical migration directory must identify the V2 history directly",
));
}
Ok(Self(path))
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct OutputDirectory(PathBuf);
impl OutputDirectory {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, WorkspaceConfigError> {
Ok(Self(confined_relative_path(path, "binding_output")?))
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct SchemaAuthorityOutputPath(PathBuf);
impl SchemaAuthorityOutputPath {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, WorkspaceConfigError> {
let path = confined_relative_path(path, "schema_authority_output")?;
if path.components().any(|component| {
let Component::Normal(name) = component else {
return true;
};
validate_portable_direct_child(name).is_err()
}) {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::PathNotConfined,
"schema-authority output path is not portable",
)
.with_detail("schema_authority_output"));
}
if path.extension().and_then(|extension| extension.to_str()) != Some("json") {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidSchemaAuthorityOutputPath,
"schema-authority output path must end in lowercase .json",
)
.with_detail("schema_authority_output"));
}
Ok(Self(path))
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
fn output_field_name(target: BindingTarget) -> Result<&'static str, WorkspaceConfigError> {
match target {
BindingTarget::Python => Ok("output.python"),
BindingTarget::TypeScript => Ok("output.typescript"),
BindingTarget::Rust => Ok("output.rust"),
BindingTarget::C => Ok("output.c"),
_ => Err(unsupported_binding_target(target)),
}
}
fn unsupported_binding_target(target: BindingTarget) -> WorkspaceConfigError {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::UnsupportedBindingTarget,
"workspace implementation does not support this binding target",
)
.with_detail(target.as_str())
}
#[must_use]
pub fn c_symbol_prefix_for_app_label(app_label: &MigrationAppLabel) -> CSymbolPrefix {
let mut short =
String::with_capacity(C_SYMBOL_PREFIX_SHORT_MARKER.len() + app_label.as_str().len());
short.push_str(C_SYMBOL_PREFIX_SHORT_MARKER);
for byte in app_label.as_str().bytes() {
match byte {
b'_' => short.push_str("_u"),
b'-' => short.push_str("_h"),
_ => short.push(char::from(byte)),
}
}
if short.len() <= C_SYMBOL_PREFIX_MAX_BYTES {
return CSymbolPrefix::new(short)
.expect("a validated migration app label always yields a valid short C prefix");
}
let mut hasher = Sha256::new();
hasher.update(C_SYMBOL_PREFIX_DIGEST_DOMAIN);
hasher.update(
u64::try_from(app_label.as_str().len())
.expect("migration app-label length fits u64")
.to_be_bytes(),
);
hasher.update(app_label.as_str().as_bytes());
let digest = hasher.finalize();
let prefix = format!(
"{C_SYMBOL_PREFIX_DIGEST_MARKER}{}",
encode_base32_lower(&digest)
);
debug_assert_eq!(prefix.len(), C_SYMBOL_PREFIX_MAX_BYTES);
CSymbolPrefix::new(prefix)
.expect("the frozen digest spelling always yields a valid bounded C prefix")
}
fn encode_base32_lower(bytes: &[u8]) -> String {
let mut encoded = String::with_capacity((bytes.len() * 8).div_ceil(5));
let mut accumulator = 0_u16;
let mut retained_bits = 0_u8;
for &byte in bytes {
accumulator = (accumulator << 8) | u16::from(byte);
retained_bits += 8;
while retained_bits >= 5 {
retained_bits -= 5;
let index = usize::from((accumulator >> retained_bits) & 0x1f);
encoded.push(char::from(BASE32_LOWER_ALPHABET[index]));
accumulator &= (1_u16 << retained_bits) - 1;
}
}
if retained_bits != 0 {
let index = usize::from((accumulator << (5 - retained_bits)) & 0x1f);
encoded.push(char::from(BASE32_LOWER_ALPHABET[index]));
}
encoded
}
pub(crate) fn portable_path_collision_key(path: &Path) -> Option<Vec<String>> {
path.components()
.map(|component| match component {
Component::Prefix(prefix) => prefix
.as_os_str()
.to_str()
.map(|value| format!("prefix:{}", value.case_fold().nfc().collect::<String>())),
Component::RootDir => Some("root:".to_owned()),
Component::CurDir => Some("cur:".to_owned()),
Component::ParentDir => Some("parent:".to_owned()),
Component::Normal(value) => value
.to_str()
.map(|value| value.case_fold().nfc().collect::<String>()),
})
.collect()
}
pub(crate) fn workspace_paths_overlap(left: &Path, right: &Path) -> bool {
let (Some(left_key), Some(right_key)) = (
portable_path_collision_key(left),
portable_path_collision_key(right),
) else {
return left == right || left.starts_with(right) || right.starts_with(left);
};
left_key.starts_with(&right_key) || right_key.starts_with(&left_key)
}
fn valid_namespaced_id(value: &str) -> bool {
let mut count = 0_usize;
let valid = value.split('.').all(|segment| {
count += 1;
let mut bytes = segment.bytes();
bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
&& bytes.all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_')
})
});
valid && count >= 2 && value.len() <= MAX_SYMBOLIC_ID_BYTES
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct SecretSlot(String);
impl SecretSlot {
pub fn new(value: impl Into<String>) -> Result<Self, WorkspaceConfigError> {
let value = value.into();
if !valid_namespaced_id(&value) {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidSymbolicIdentifier,
"secret slot must be a bounded lowercase namespaced identifier",
));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct SecretReference {
environment_variable: String,
}
impl SecretReference {
pub fn environment(variable: impl Into<String>) -> Result<Self, WorkspaceConfigError> {
let variable = variable.into();
let mut bytes = variable.bytes();
let valid = variable.len() <= MAX_SYMBOLIC_ID_BYTES
&& bytes
.next()
.is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
if !valid {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidSecretReference,
"environment reference contains an invalid variable name",
));
}
Ok(Self {
environment_variable: variable,
})
}
pub fn parse_symbolic(value: impl AsRef<str>) -> Result<Self, WorkspaceConfigError> {
let value = value.as_ref();
let Some(variable) = value.strip_prefix("env:") else {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::SecretLiteralRejected,
"secret literals are forbidden; use a symbolic reference",
));
};
Self::environment(variable)
}
#[must_use]
pub fn environment_variable(&self) -> &str {
&self.environment_variable
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ExtensionRequirement {
handler_id: String,
version: String,
}
impl ExtensionRequirement {
pub fn new(
handler_id: impl Into<String>,
version: impl Into<String>,
) -> Result<Self, WorkspaceConfigError> {
let handler_id = handler_id.into();
let version = version.into();
let valid_version = !version.is_empty()
&& version.len() <= MAX_EXTENSION_VERSION_BYTES
&& version.bytes().all(|byte| {
byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| matches!(byte, b'.' | b'-' | b'_')
});
if !valid_namespaced_id(&handler_id) || !valid_version {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidSymbolicIdentifier,
"extension requirement has an invalid handler ID or version",
));
}
Ok(Self {
handler_id,
version,
})
}
#[must_use]
pub fn handler_id(&self) -> &str {
&self.handler_id
}
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceEnvironment {
database: String,
http_port: Option<u16>,
migrate: bool,
password: SecretReference,
requirements: CapabilitySet,
transport_policy: WorkspaceTransportPolicy,
uri: String,
username: SecretReference,
}
fn invalid_environment_uri() -> WorkspaceConfigError {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidWorkspaceValue,
"environment uri must be a comma-separated list of host:port or [IPv6]:port endpoints without credentials, schemes, or control characters",
)
}
fn valid_endpoint_port(port: &str) -> bool {
!port.is_empty()
&& port.bytes().all(|byte| byte.is_ascii_digit())
&& port.parse::<u16>().is_ok_and(|port| port != 0)
}
fn valid_endpoint_host(host: &str) -> bool {
let host = host.strip_suffix('.').unwrap_or(host);
!host.is_empty()
&& host.len() <= 253
&& host.split('.').all(|label| {
!label.is_empty()
&& label.len() <= 63
&& label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
&& label
.as_bytes()
.first()
.is_some_and(u8::is_ascii_alphanumeric)
&& label
.as_bytes()
.last()
.is_some_and(u8::is_ascii_alphanumeric)
})
}
fn valid_environment_endpoint(endpoint: &str) -> bool {
if let Some(bracketed) = endpoint.strip_prefix('[') {
let Some((address, port)) = bracketed.split_once("]:") else {
return false;
};
!address.is_empty()
&& !port.contains(['[', ']', ':'])
&& address.parse::<Ipv6Addr>().is_ok()
&& valid_endpoint_port(port)
} else {
let Some((host, port)) = endpoint.rsplit_once(':') else {
return false;
};
!host.contains(['[', ']', ':']) && valid_endpoint_host(host) && valid_endpoint_port(port)
}
}
pub(crate) fn validate_environment_uri(uri: &str) -> Result<(), WorkspaceConfigError> {
if uri.is_empty() || !uri.split(',').all(valid_environment_endpoint) {
return Err(invalid_environment_uri());
}
Ok(())
}
fn normalized_environment_endpoints(uri: &str) -> Result<BTreeSet<String>, WorkspaceConfigError> {
uri.split(',')
.map(|endpoint| {
if let Some(bracketed) = endpoint.strip_prefix('[') {
let (address, port) = bracketed.split_once("]:")?;
let address = address.parse::<Ipv6Addr>().ok()?;
let port = port.parse::<u16>().ok()?;
Some(format!("[{address}]:{port}"))
} else {
let (host, port) = endpoint.rsplit_once(':')?;
let host = host.strip_suffix('.').unwrap_or(host);
let host = host
.parse::<Ipv4Addr>()
.map_or_else(|_| host.to_ascii_lowercase(), |address| address.to_string());
let port = port.parse::<u16>().ok()?;
Some(format!("{host}:{port}"))
}
})
.collect::<Option<BTreeSet<_>>>()
.ok_or_else(invalid_environment_uri)
}
pub(crate) fn validate_environment_database(database: &str) -> Result<(), WorkspaceConfigError> {
if database.is_empty() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidWorkspaceValue,
"environment database must be non-empty",
));
}
Ok(())
}
impl WorkspaceEnvironment {
fn from_validated(
uri: String,
database: String,
username: SecretReference,
password: SecretReference,
) -> Self {
Self {
database,
http_port: None,
migrate: false,
password,
requirements: CapabilitySet::new(),
transport_policy: WorkspaceTransportPolicy::Disabled,
uri,
username,
}
}
pub fn new(
uri: impl Into<String>,
database: impl Into<String>,
username: SecretReference,
password: SecretReference,
) -> Result<Self, WorkspaceConfigError> {
let uri = uri.into();
let database = database.into();
validate_environment_uri(&uri)?;
validate_environment_database(&database)?;
Ok(Self::from_validated(uri, database, username, password))
}
#[must_use]
pub fn with_http_port(mut self, port: u16) -> Self {
self.http_port = Some(port);
self
}
#[must_use]
pub const fn with_migrate(mut self, migrate: bool) -> Self {
self.migrate = migrate;
self
}
#[must_use]
pub fn with_transport_policy(mut self, policy: WorkspaceTransportPolicy) -> Self {
self.transport_policy = policy;
self
}
#[must_use]
pub fn require_capabilities(
mut self,
capabilities: impl IntoIterator<Item = CapabilityId>,
) -> Self {
for capability in capabilities {
self.requirements.insert(capability);
}
self
}
#[must_use]
pub fn uri(&self) -> &str {
&self.uri
}
#[must_use]
pub fn database(&self) -> &str {
&self.database
}
#[must_use]
pub const fn http_port(&self) -> Option<u16> {
self.http_port
}
#[must_use]
pub const fn username(&self) -> &SecretReference {
&self.username
}
#[must_use]
pub const fn password(&self) -> &SecretReference {
&self.password
}
#[must_use]
pub const fn migrate(&self) -> bool {
self.migrate
}
#[must_use]
pub const fn transport_policy(&self) -> &WorkspaceTransportPolicy {
&self.transport_policy
}
#[must_use]
pub const fn requirements(&self) -> &CapabilitySet {
&self.requirements
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TypeBridgeConfig {
app_label: MigrationAppLabel,
environments: BTreeMap<String, WorkspaceEnvironment>,
extensions: BTreeSet<ExtensionRequirement>,
managed_scope: ManagedScopeBinding,
migration_policy: MigrationSafetyPolicy,
migration_v2_directory: MigrationV2Directory,
outputs: BTreeMap<BindingTarget, OutputDirectory>,
type_names: BTreeMap<BindingTarget, Vec<type_bridge_contract::projection::TypeNameOverride>>,
required_capabilities: CapabilitySet,
schema_authority_output: Option<SchemaAuthorityOutputPath>,
schema_set: SchemaSetPath,
secret_references: BTreeMap<SecretSlot, SecretReference>,
semantic_profile: SemanticProfileId,
workspace_root: WorkspaceRoot,
}
impl TypeBridgeConfig {
#[must_use]
pub fn builder(workspace_root: WorkspaceRoot) -> TypeBridgeConfigBuilder {
TypeBridgeConfigBuilder::new(workspace_root)
}
#[must_use]
pub const fn workspace_root(&self) -> &WorkspaceRoot {
&self.workspace_root
}
#[must_use]
pub const fn schema_set(&self) -> &SchemaSetPath {
&self.schema_set
}
#[must_use]
pub fn schema_set_absolute_path(&self) -> PathBuf {
let mut path = self.workspace_root.as_path().to_path_buf();
path.extend(self.schema_set.as_path().components());
path
}
#[must_use]
pub const fn app_label(&self) -> &MigrationAppLabel {
&self.app_label
}
#[must_use]
pub const fn managed_scope(&self) -> &ManagedScopeBinding {
&self.managed_scope
}
#[must_use]
pub const fn semantic_profile(&self) -> &SemanticProfileId {
&self.semantic_profile
}
#[must_use]
pub const fn migration_v2_directory(&self) -> &MigrationV2Directory {
&self.migration_v2_directory
}
#[must_use]
pub const fn environments(&self) -> &BTreeMap<String, WorkspaceEnvironment> {
&self.environments
}
#[must_use]
pub fn environment(&self, name: &str) -> Option<&WorkspaceEnvironment> {
self.environments.get(name)
}
#[must_use]
pub const fn migration_policy(&self) -> &MigrationSafetyPolicy {
&self.migration_policy
}
#[must_use]
pub fn migration_v2_absolute_path(&self) -> PathBuf {
self.workspace_root
.as_path()
.join(self.migration_v2_directory.as_path())
}
#[must_use]
pub const fn required_capabilities(&self) -> &CapabilitySet {
&self.required_capabilities
}
#[must_use]
pub const fn outputs(&self) -> &BTreeMap<BindingTarget, OutputDirectory> {
&self.outputs
}
#[must_use]
pub fn type_name_overrides(
&self,
target: BindingTarget,
) -> &[type_bridge_contract::projection::TypeNameOverride] {
self.type_names.get(&target).map_or(&[], Vec::as_slice)
}
#[must_use]
pub const fn schema_authority_output(&self) -> Option<&SchemaAuthorityOutputPath> {
self.schema_authority_output.as_ref()
}
#[must_use]
pub const fn secret_references(&self) -> &BTreeMap<SecretSlot, SecretReference> {
&self.secret_references
}
#[must_use]
pub const fn extensions(&self) -> &BTreeSet<ExtensionRequirement> {
&self.extensions
}
}
pub struct TypeBridgeConfigBuilder {
app_label: Option<MigrationAppLabel>,
environments: Vec<(String, WorkspaceEnvironment)>,
duplicate_required_fields: BTreeSet<&'static str>,
extensions: Vec<ExtensionRequirement>,
managed_scope_id: Option<ManagedScopeId>,
migration_policy: Option<MigrationSafetyPolicy>,
migration_v2_directory: Option<MigrationV2Directory>,
outputs: Vec<(BindingTarget, OutputDirectory)>,
type_names: Vec<(BindingTarget, type_bridge_contract::id::TypeId, String)>,
required_capabilities: CapabilitySet,
schema_authority_output: Option<SchemaAuthorityOutputPath>,
schema_set: Option<SchemaSetPath>,
secrets: Vec<(SecretSlot, SecretReference)>,
semantic_profile: Option<SemanticProfileId>,
workspace_root: WorkspaceRoot,
}
impl TypeBridgeConfigBuilder {
fn new(workspace_root: WorkspaceRoot) -> Self {
Self {
app_label: None,
environments: Vec::new(),
duplicate_required_fields: BTreeSet::new(),
extensions: Vec::new(),
managed_scope_id: None,
migration_policy: None,
migration_v2_directory: None,
outputs: Vec::new(),
type_names: Vec::new(),
required_capabilities: CapabilitySet::new(),
schema_authority_output: None,
schema_set: None,
secrets: Vec::new(),
semantic_profile: None,
workspace_root,
}
}
fn mark_duplicate<T>(
slot: &mut Option<T>,
value: T,
field: &'static str,
duplicates: &mut BTreeSet<&'static str>,
) {
if slot.replace(value).is_some() {
duplicates.insert(field);
}
}
#[must_use]
pub fn schema_set(mut self, path: SchemaSetPath) -> Self {
Self::mark_duplicate(
&mut self.schema_set,
path,
"schema_set",
&mut self.duplicate_required_fields,
);
self
}
#[must_use]
pub fn app_label(mut self, app_label: MigrationAppLabel) -> Self {
Self::mark_duplicate(
&mut self.app_label,
app_label,
"app_label",
&mut self.duplicate_required_fields,
);
self
}
#[must_use]
pub fn exclusive_managed_scope(mut self, scope_id: ManagedScopeId) -> Self {
Self::mark_duplicate(
&mut self.managed_scope_id,
scope_id,
"managed_scope",
&mut self.duplicate_required_fields,
);
self
}
#[must_use]
pub fn semantic_profile(mut self, profile: SemanticProfileId) -> Self {
Self::mark_duplicate(
&mut self.semantic_profile,
profile,
"semantic_profile",
&mut self.duplicate_required_fields,
);
self
}
#[must_use]
pub fn migration_v2_directory(mut self, directory: MigrationV2Directory) -> Self {
Self::mark_duplicate(
&mut self.migration_v2_directory,
directory,
"migration_v2_directory",
&mut self.duplicate_required_fields,
);
self
}
#[must_use]
pub fn migration_policy(mut self, policy: MigrationSafetyPolicy) -> Self {
Self::mark_duplicate(
&mut self.migration_policy,
policy,
"migration_policy",
&mut self.duplicate_required_fields,
);
self
}
#[must_use]
pub fn require_capability(mut self, capability: CapabilityId) -> Self {
self.required_capabilities.insert(capability);
self
}
#[must_use]
pub fn require_capabilities(
mut self,
capabilities: impl IntoIterator<Item = CapabilityId>,
) -> Self {
for capability in capabilities {
self.required_capabilities.insert(capability);
}
self
}
#[must_use]
pub fn output(mut self, target: BindingTarget, directory: OutputDirectory) -> Self {
self.outputs.push((target, directory));
self
}
#[must_use]
pub fn type_name_override(
mut self,
target: BindingTarget,
type_id: type_bridge_contract::id::TypeId,
name: impl Into<String>,
) -> Self {
self.type_names.push((target, type_id, name.into()));
self
}
#[must_use]
pub fn schema_authority_output(mut self, output: SchemaAuthorityOutputPath) -> Self {
Self::mark_duplicate(
&mut self.schema_authority_output,
output,
"schema_authority_output",
&mut self.duplicate_required_fields,
);
self
}
#[must_use]
pub fn environment(
mut self,
name: impl Into<String>,
environment: WorkspaceEnvironment,
) -> Self {
self.environments.push((name.into(), environment));
self
}
#[must_use]
pub fn secret(mut self, slot: SecretSlot, reference: SecretReference) -> Self {
self.secrets.push((slot, reference));
self
}
#[must_use]
pub fn require_extension(mut self, requirement: ExtensionRequirement) -> Self {
self.extensions.push(requirement);
self
}
pub fn build(
self,
services: &TypeBridgeConfigServices<'_>,
) -> Result<TypeBridgeConfig, WorkspaceConfigError> {
if let Some(field) = self.duplicate_required_fields.iter().next() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::DuplicateRequiredField,
"a singleton workspace field was assigned more than once",
)
.with_detail(*field));
}
fn required<T>(value: Option<T>, field: &'static str) -> Result<T, WorkspaceConfigError> {
value.ok_or_else(|| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::MissingRequiredField,
"required workspace field is missing",
)
.with_detail(field)
})
}
let schema_set = required(self.schema_set, "schema_set")?;
let app_label = required(self.app_label, "app_label")?;
let managed_scope_id = required(self.managed_scope_id, "managed_scope")?;
let semantic_profile = required(self.semantic_profile, "semantic_profile")?;
let migration_v2_directory =
required(self.migration_v2_directory, "migration_v2_directory")?;
let schema_authority_output = self.schema_authority_output;
if !TYPEBRIDGE_WORKSPACE_SEMANTIC_PROFILE_IDS.contains(&semantic_profile.as_str())
|| SemanticProfile::resolve(&semantic_profile).is_err()
{
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::UnsupportedSemanticProfile,
"workspace requires a frozen TypeDB 3.11.5 or 3.12.1 semantic profile",
)
.with_detail(semantic_profile.as_str()));
}
let canonical_root = services
.sources
.canonicalize_workspace_root(self.workspace_root.as_path())
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::WorkspaceRootCanonicalizationFailed,
"injected source service could not canonicalize workspace root",
)
.with_detail(error.code())
})?;
if canonical_root != self.workspace_root.as_path() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::WorkspaceRootNotCanonical,
"explicit workspace root differs from its canonical spelling",
));
}
let managed_scope = ManagedScopeBinding::exclusive(managed_scope_id).map_err(|_| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidManagedScope,
"managed scope could not bind to the exclusive profile",
)
})?;
debug_assert_eq!(
managed_scope.profile().id(),
&ManagedScopeProfileId::exclusive()
);
let mut outputs = BTreeMap::new();
for (target, directory) in self.outputs {
output_field_name(target)?;
if outputs.insert(target, directory).is_some() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::DuplicateOutputTarget,
"binding output target is configured more than once",
));
}
}
let mut name_configs = BTreeMap::new();
for (target, type_id, name) in self.type_names {
use type_bridge_contract::projection::{CSymbolPrefix, ProjectionConfig};
if !outputs.contains_key(&target) {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidWorkspaceValue,
"type-name overrides require a configured binding output",
));
}
let config = name_configs
.remove(&target)
.unwrap_or_else(|| match target {
BindingTarget::Python => ProjectionConfig::python(),
BindingTarget::TypeScript => ProjectionConfig::typescript(),
BindingTarget::Rust => ProjectionConfig::rust(),
_ => ProjectionConfig::c(CSymbolPrefix::new("tb").expect("static C prefix")),
});
let config = config
.with_type_name_override(type_id, name)
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidWorkspaceValue,
"invalid binding type-name override",
)
.with_detail(error.to_string())
})?;
name_configs.insert(target, config);
}
let type_names = name_configs
.into_iter()
.map(|(target, config)| (target, config.type_name_overrides().to_vec()))
.collect();
let mut workspace_paths: Vec<(&'static str, &Path)> = vec![
("schema_set", schema_set.as_path()),
("migration_v2_directory", migration_v2_directory.as_path()),
];
for (target, directory) in &outputs {
workspace_paths.push((output_field_name(*target)?, directory.as_path()));
}
if let Some(output) = &schema_authority_output {
workspace_paths.push(("artifact.schema_authority", output.as_path()));
}
for left_index in 0..workspace_paths.len() {
for right_index in (left_index + 1)..workspace_paths.len() {
let (left_name, left_path) = workspace_paths[left_index];
let (right_name, right_path) = workspace_paths[right_index];
if workspace_paths_overlap(left_path, right_path) {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::OverlappingWorkspacePath,
"workspace-owned paths must be pairwise disjoint",
)
.with_detail(format!("{left_name},{right_name}")));
}
}
}
let mut environments = BTreeMap::new();
for (name, environment) in self.environments {
if name.is_empty()
|| name.len() > MAX_SYMBOLIC_ID_BYTES
|| !name.bytes().all(|byte| {
byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| matches!(byte, b'-' | b'_')
})
{
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::InvalidSymbolicIdentifier,
"environment names must be bounded lowercase identifiers",
)
.with_detail(name));
}
for reference in [environment.username(), environment.password()] {
services
.secrets
.validate_reference(reference)
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::SecretReferenceRejected,
"local secret-reference service rejected an environment credential",
)
.with_detail(error.code())
})?;
}
if environments.insert(name, environment).is_some() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::DuplicateRequiredField,
"environment name is configured more than once",
));
}
}
let environment_namespaces = environments
.iter()
.map(|(name, environment)| {
normalized_environment_endpoints(environment.uri())
.map(|endpoints| (name, environment, endpoints))
})
.collect::<Result<Vec<_>, _>>()?;
for left_index in 0..environment_namespaces.len() {
for right_index in (left_index + 1)..environment_namespaces.len() {
let (left_name, left, left_endpoints) = &environment_namespaces[left_index];
let (right_name, right, right_endpoints) = &environment_namespaces[right_index];
if left_endpoints.is_disjoint(right_endpoints) {
continue;
}
let left_journal =
format!("{}{TYPEBRIDGE_JOURNAL_DATABASE_SUFFIX}", left.database());
let right_journal =
format!("{}{TYPEBRIDGE_JOURNAL_DATABASE_SUFFIX}", right.database());
if left_journal == right.database() || right_journal == left.database() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::EnvironmentDatabaseCollision,
"one environment's managed database aliases another environment's reserved migration journal on overlapping TypeDB endpoint sets",
)
.with_detail(format!("{left_name},{right_name}")));
}
}
}
if let Some(output) = &schema_authority_output {
let mut output_absolute = self.workspace_root.as_path().to_path_buf();
output_absolute.extend(output.as_path().components());
for (name, environment) in &environments {
if let WorkspaceTransportPolicy::CustomRootCa(root_ca) =
environment.transport_policy()
&& workspace_paths_overlap(&output_absolute, root_ca.as_path())
{
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::OverlappingWorkspacePath,
"schema-authority output cannot overlap custom trust material",
)
.with_detail(format!(
"artifact.schema_authority,environment.{name}.tls_root_ca"
)));
}
}
}
let mut secret_references = BTreeMap::new();
for (slot, reference) in self.secrets {
if secret_references.insert(slot, reference).is_some() {
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::DuplicateSecretSlot,
"symbolic secret slot is configured more than once",
));
}
}
let mut extensions_by_handler = BTreeMap::new();
for requirement in self.extensions {
if extensions_by_handler
.insert(requirement.handler_id.clone(), requirement)
.is_some()
{
return Err(WorkspaceConfigError::new(
WorkspaceConfigErrorCode::DuplicateExtensionHandler,
"extension handler is required more than once",
));
}
}
let extensions = extensions_by_handler.into_values().collect::<BTreeSet<_>>();
for reference in secret_references.values() {
services
.secrets
.validate_reference(reference)
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::SecretReferenceRejected,
"local secret-reference service rejected a symbolic reference",
)
.with_detail(error.code())
})?;
}
for requirement in &extensions {
services
.extensions
.validate_requirement(requirement)
.map_err(|error| {
WorkspaceConfigError::new(
WorkspaceConfigErrorCode::ExtensionRequirementRejected,
"local extension registry rejected a handler requirement",
)
.with_detail(error.code())
})?;
}
Ok(TypeBridgeConfig {
app_label,
environments,
extensions,
managed_scope,
migration_policy: self
.migration_policy
.unwrap_or_else(MigrationSafetyPolicy::default_policy),
migration_v2_directory,
outputs,
type_names,
required_capabilities: self.required_capabilities,
schema_authority_output,
schema_set,
secret_references,
semantic_profile,
workspace_root: self.workspace_root,
})
}
}