1#![cfg_attr(not(feature = "std"), no_std)]
2#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4#![warn(clippy::unwrap_used, clippy::expect_used)]
5
6extern crate alloc;
55
56pub const VERSION: &str = env!("CARGO_PKG_VERSION");
58pub const SCHEMA_BASE_URL: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1";
60
61pub mod bindings;
62pub mod capabilities;
63#[cfg(feature = "std")]
64pub mod cbor;
65pub mod component;
66pub mod deployment;
67pub mod distributor;
68pub mod events;
69pub mod events_provider;
70pub mod flow;
71pub mod messaging;
72pub mod pack_manifest;
73pub mod provider;
74pub mod store;
75pub mod supply_chain;
76pub mod worker;
77
78pub mod context;
79pub mod error;
80pub mod outcome;
81pub mod pack;
82pub mod policy;
83pub mod run;
84#[cfg(all(feature = "schemars", feature = "std"))]
85pub mod schema;
86pub mod secrets;
87pub mod session;
88pub mod state;
89pub mod telemetry;
90pub mod tenant;
91pub mod tenant_config;
92
93pub use bindings::hints::{
94 BindingsHints, EnvHints, McpHints, McpServer, NetworkHints, SecretsHints,
95};
96pub use capabilities::{
97 Capabilities, FsCaps, HttpCaps, KvCaps, Limits, NetCaps, SecretsCaps, TelemetrySpec, ToolsCaps,
98};
99#[cfg(feature = "std")]
100pub use cbor::{CborError, decode_pack_manifest, encode_pack_manifest};
101pub use component::{
102 ComponentCapabilities, ComponentConfigurators, ComponentDevFlow, ComponentManifest,
103 ComponentOperation, ComponentProfileError, ComponentProfiles, EnvCapabilities,
104 EventsCapabilities, FilesystemCapabilities, FilesystemMode, FilesystemMount, HostCapabilities,
105 HttpCapabilities, IaCCapabilities, MessagingCapabilities, ResourceHints, SecretsCapabilities,
106 StateCapabilities, TelemetryCapabilities, TelemetryScope, WasiCapabilities,
107};
108pub use context::{Cloud, DeploymentCtx, Platform};
109pub use deployment::{
110 ChannelPlan, DeploymentPlan, MessagingPlan, MessagingSubjectPlan, OAuthPlan, RunnerPlan,
111 TelemetryPlan,
112};
113pub use distributor::{
114 ArtifactLocation, CacheInfo, ComponentDigest, ComponentStatus, DistributorEnvironmentId,
115 PackStatusResponseV2, ResolveComponentRequest, ResolveComponentResponse, SignatureSummary,
116};
117pub use error::{ErrorCode, GResult, GreenticError};
118pub use events::{EventEnvelope, EventId, EventMetadata};
119pub use events_provider::{
120 EventProviderDescriptor, EventProviderKind, OrderingKind, ReliabilityKind, TransportKind,
121};
122pub use flow::{
123 ComponentRef as FlowComponentRef, Flow, FlowKind, FlowMetadata, InputMapping, Node,
124 OutputMapping, Routing, TelemetryHints,
125};
126pub use messaging::{Attachment, ChannelMessageEnvelope, MessageMetadata};
127pub use outcome::Outcome;
128pub use pack::{PackRef, Signature, SignatureAlgorithm};
129pub use pack_manifest::{
130 BootstrapSpec, ComponentCapability, ExtensionInline, ExtensionRef, PackDependency,
131 PackFlowEntry, PackKind, PackManifest, PackSignatures,
132};
133pub use policy::{AllowList, NetworkPolicy, PolicyDecision, PolicyDecisionStatus, Protocol};
134pub use provider::{
135 PROVIDER_EXTENSION_ID, ProviderDecl, ProviderExtensionInline, ProviderManifest,
136 ProviderRuntimeRef,
137};
138#[cfg(feature = "time")]
139pub use run::RunResult;
140pub use run::{NodeFailure, NodeStatus, NodeSummary, RunStatus, TranscriptOffset};
141pub use secrets::{SecretFormat, SecretKey, SecretRequirement, SecretScope};
142pub use session::canonical_session_key;
143pub use session::{SessionCursor, SessionData, SessionKey};
144pub use state::{StateKey, StatePath};
145pub use store::{
146 ArtifactSelector, BundleSpec, CapabilityMap, Collection, ConnectionKind, DesiredState,
147 DesiredStateExportSpec, DesiredSubscriptionEntry, Environment, LayoutSection,
148 LayoutSectionKind, PackOrComponentRef, PlanLimits, PriceModel, ProductOverride, RolloutState,
149 RolloutStatus, StoreFront, StorePlan, StoreProduct, StoreProductKind, Subscription,
150 SubscriptionStatus, Theme, VersionStrategy,
151};
152pub use supply_chain::{
153 AttestationStatement, BuildPlan, BuildStatus, BuildStatusKind, MetadataRecord, PredicateType,
154 RepoContext, ScanKind, ScanRequest, ScanResult, ScanStatusKind, SignRequest, StoreContext,
155 VerifyRequest, VerifyResult,
156};
157#[cfg(feature = "otel-keys")]
158pub use telemetry::OtlpKeys;
159pub use telemetry::SpanContext;
160#[cfg(feature = "telemetry-autoinit")]
161pub use telemetry::TelemetryCtx;
162pub use tenant::{Impersonation, TenantIdentity};
163pub use tenant_config::{
164 DefaultPipeline, DidContext, DidService, DistributorTarget, EnabledPacks,
165 IdentityProviderOption, RepoAuth, RepoConfigFeatures, RepoSkin, RepoSkinLayout, RepoSkinLinks,
166 RepoSkinTheme, RepoTenantConfig, RepoWorkerPanel, StoreTarget, TenantDidDocument,
167 VerificationMethod,
168};
169pub use worker::{WorkerMessage, WorkerRequest, WorkerResponse};
170
171#[cfg(feature = "schemars")]
172use alloc::borrow::Cow;
173use alloc::{borrow::ToOwned, collections::BTreeMap, format, string::String, vec::Vec};
174use core::fmt;
175use core::str::FromStr;
176#[cfg(feature = "schemars")]
177use schemars::JsonSchema;
178use semver::VersionReq;
179#[cfg(feature = "time")]
180use time::OffsetDateTime;
181
182#[cfg(feature = "serde")]
183use serde::{Deserialize, Serialize};
184
185#[cfg(feature = "std")]
186use alloc::boxed::Box;
187
188#[cfg(feature = "std")]
189use std::error::Error as StdError;
190
191pub(crate) fn validate_identifier(value: &str, label: &str) -> GResult<()> {
193 if value.is_empty() {
194 return Err(GreenticError::new(
195 ErrorCode::InvalidInput,
196 format!("{label} must not be empty"),
197 ));
198 }
199 if value
200 .chars()
201 .any(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
202 {
203 return Err(GreenticError::new(
204 ErrorCode::InvalidInput,
205 format!("{label} must contain only ASCII letters, digits, '.', '-', or '_'"),
206 ));
207 }
208 Ok(())
209}
210
211pub(crate) fn validate_api_key_ref(value: &str) -> GResult<()> {
213 if value.trim().is_empty() {
214 return Err(GreenticError::new(
215 ErrorCode::InvalidInput,
216 "ApiKeyRef must not be empty",
217 ));
218 }
219 if value.chars().any(char::is_whitespace) {
220 return Err(GreenticError::new(
221 ErrorCode::InvalidInput,
222 "ApiKeyRef must not contain whitespace",
223 ));
224 }
225 if !value.is_ascii() {
226 return Err(GreenticError::new(
227 ErrorCode::InvalidInput,
228 "ApiKeyRef must contain only ASCII characters",
229 ));
230 }
231 Ok(())
232}
233
234pub mod ids {
236 pub const PACK_ID: &str =
238 "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-id.schema.json";
239 pub const COMPONENT_ID: &str =
241 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-id.schema.json";
242 pub const FLOW_ID: &str =
244 "https://greentic-ai.github.io/greentic-types/schemas/v1/flow-id.schema.json";
245 pub const NODE_ID: &str =
247 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-id.schema.json";
248 pub const TENANT_CONTEXT: &str =
250 "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-context.schema.json";
251 pub const HASH_DIGEST: &str =
253 "https://greentic-ai.github.io/greentic-types/schemas/v1/hash-digest.schema.json";
254 pub const SEMVER_REQ: &str =
256 "https://greentic-ai.github.io/greentic-types/schemas/v1/semver-req.schema.json";
257 pub const REDACTION_PATH: &str =
259 "https://greentic-ai.github.io/greentic-types/schemas/v1/redaction-path.schema.json";
260 pub const CAPABILITIES: &str =
262 "https://greentic-ai.github.io/greentic-types/schemas/v1/capabilities.schema.json";
263 pub const REPO_SKIN: &str =
265 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-skin.schema.json";
266 pub const REPO_AUTH: &str =
268 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-auth.schema.json";
269 pub const REPO_TENANT_CONFIG: &str =
271 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-tenant-config.schema.json";
272 pub const TENANT_DID_DOCUMENT: &str =
274 "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-did-document.schema.json";
275 pub const FLOW: &str = "greentic.flow.v1";
277 pub const NODE: &str =
279 "https://greentic-ai.github.io/greentic-types/schemas/v1/node.schema.json";
280 pub const COMPONENT_MANIFEST: &str =
282 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-manifest.schema.json";
283 pub const PACK_MANIFEST: &str = "greentic.pack-manifest.v1";
285 pub const PROVIDER_MANIFEST: &str =
287 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-manifest.schema.json";
288 pub const PROVIDER_RUNTIME_REF: &str =
290 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-runtime-ref.schema.json";
291 pub const PROVIDER_DECL: &str =
293 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-decl.schema.json";
294 pub const PROVIDER_EXTENSION_INLINE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-extension-inline.schema.json";
296 pub const LIMITS: &str =
298 "https://greentic-ai.github.io/greentic-types/schemas/v1/limits.schema.json";
299 pub const TELEMETRY_SPEC: &str =
301 "https://greentic-ai.github.io/greentic-types/schemas/v1/telemetry-spec.schema.json";
302 pub const NODE_SUMMARY: &str =
304 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-summary.schema.json";
305 pub const NODE_FAILURE: &str =
307 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-failure.schema.json";
308 pub const NODE_STATUS: &str =
310 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-status.schema.json";
311 pub const RUN_STATUS: &str =
313 "https://greentic-ai.github.io/greentic-types/schemas/v1/run-status.schema.json";
314 pub const TRANSCRIPT_OFFSET: &str =
316 "https://greentic-ai.github.io/greentic-types/schemas/v1/transcript-offset.schema.json";
317 pub const TOOLS_CAPS: &str =
319 "https://greentic-ai.github.io/greentic-types/schemas/v1/tools-caps.schema.json";
320 pub const SECRETS_CAPS: &str =
322 "https://greentic-ai.github.io/greentic-types/schemas/v1/secrets-caps.schema.json";
323 pub const BRANCH_REF: &str =
325 "https://greentic-ai.github.io/greentic-types/schemas/v1/branch-ref.schema.json";
326 pub const COMMIT_REF: &str =
328 "https://greentic-ai.github.io/greentic-types/schemas/v1/commit-ref.schema.json";
329 pub const GIT_PROVIDER_REF: &str =
331 "https://greentic-ai.github.io/greentic-types/schemas/v1/git-provider-ref.schema.json";
332 pub const SCANNER_REF: &str =
334 "https://greentic-ai.github.io/greentic-types/schemas/v1/scanner-ref.schema.json";
335 pub const WEBHOOK_ID: &str =
337 "https://greentic-ai.github.io/greentic-types/schemas/v1/webhook-id.schema.json";
338 pub const REPO_REF: &str =
340 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-ref.schema.json";
341 pub const COMPONENT_REF: &str =
343 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-ref.schema.json";
344 pub const VERSION_REF: &str =
346 "https://greentic-ai.github.io/greentic-types/schemas/v1/version-ref.schema.json";
347 pub const BUILD_REF: &str =
349 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-ref.schema.json";
350 pub const SCAN_REF: &str =
352 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-ref.schema.json";
353 pub const ATTESTATION_REF: &str =
355 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-ref.schema.json";
356 pub const ATTESTATION_ID: &str =
358 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-id.schema.json";
359 pub const POLICY_REF: &str =
361 "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-ref.schema.json";
362 pub const POLICY_INPUT_REF: &str =
364 "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-input-ref.schema.json";
365 pub const STORE_REF: &str =
367 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-ref.schema.json";
368 pub const REGISTRY_REF: &str =
370 "https://greentic-ai.github.io/greentic-types/schemas/v1/registry-ref.schema.json";
371 pub const OCI_IMAGE_REF: &str =
373 "https://greentic-ai.github.io/greentic-types/schemas/v1/oci-image-ref.schema.json";
374 pub const ARTIFACT_REF: &str =
376 "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-ref.schema.json";
377 pub const SBOM_REF: &str =
379 "https://greentic-ai.github.io/greentic-types/schemas/v1/sbom-ref.schema.json";
380 pub const SIGNING_KEY_REF: &str =
382 "https://greentic-ai.github.io/greentic-types/schemas/v1/signing-key-ref.schema.json";
383 pub const SIGNATURE_REF: &str =
385 "https://greentic-ai.github.io/greentic-types/schemas/v1/signature-ref.schema.json";
386 pub const STATEMENT_REF: &str =
388 "https://greentic-ai.github.io/greentic-types/schemas/v1/statement-ref.schema.json";
389 pub const BUILD_LOG_REF: &str =
391 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-log-ref.schema.json";
392 pub const METADATA_RECORD_REF: &str =
394 "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record-ref.schema.json";
395 pub const API_KEY_REF: &str =
397 "https://greentic-ai.github.io/greentic-types/schemas/v1/api-key-ref.schema.json";
398 pub const ENVIRONMENT_REF: &str =
400 "https://greentic-ai.github.io/greentic-types/schemas/v1/environment-ref.schema.json";
401 pub const DISTRIBUTOR_REF: &str =
403 "https://greentic-ai.github.io/greentic-types/schemas/v1/distributor-ref.schema.json";
404 pub const STOREFRONT_ID: &str =
406 "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront-id.schema.json";
407 pub const STORE_PRODUCT_ID: &str =
409 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-id.schema.json";
410 pub const STORE_PLAN_ID: &str =
412 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan-id.schema.json";
413 pub const SUBSCRIPTION_ID: &str =
415 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-id.schema.json";
416 pub const BUNDLE_ID: &str =
418 "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle-id.schema.json";
419 pub const COLLECTION_ID: &str =
421 "https://greentic-ai.github.io/greentic-types/schemas/v1/collection-id.schema.json";
422 pub const ARTIFACT_SELECTOR: &str =
424 "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-selector.schema.json";
425 pub const CAPABILITY_MAP: &str =
427 "https://greentic-ai.github.io/greentic-types/schemas/v1/capability-map.schema.json";
428 pub const STORE_PRODUCT_KIND: &str =
430 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-kind.schema.json";
431 pub const VERSION_STRATEGY: &str =
433 "https://greentic-ai.github.io/greentic-types/schemas/v1/version-strategy.schema.json";
434 pub const ROLLOUT_STATUS: &str =
436 "https://greentic-ai.github.io/greentic-types/schemas/v1/rollout-status.schema.json";
437 pub const CONNECTION_KIND: &str =
439 "https://greentic-ai.github.io/greentic-types/schemas/v1/connection-kind.schema.json";
440 pub const PACK_OR_COMPONENT_REF: &str =
442 "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-or-component-ref.schema.json";
443 pub const PLAN_LIMITS: &str =
445 "https://greentic-ai.github.io/greentic-types/schemas/v1/plan-limits.schema.json";
446 pub const PRICE_MODEL: &str =
448 "https://greentic-ai.github.io/greentic-types/schemas/v1/price-model.schema.json";
449 pub const SUBSCRIPTION_STATUS: &str =
451 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-status.schema.json";
452 pub const BUILD_PLAN: &str =
454 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-plan.schema.json";
455 pub const BUILD_STATUS: &str =
457 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-status.schema.json";
458 pub const SCAN_REQUEST: &str =
460 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-request.schema.json";
461 pub const SCAN_RESULT: &str =
463 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-result.schema.json";
464 pub const SIGN_REQUEST: &str =
466 "https://greentic-ai.github.io/greentic-types/schemas/v1/sign-request.schema.json";
467 pub const VERIFY_REQUEST: &str =
469 "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-request.schema.json";
470 pub const VERIFY_RESULT: &str =
472 "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-result.schema.json";
473 pub const ATTESTATION_STATEMENT: &str =
475 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-statement.schema.json";
476 pub const METADATA_RECORD: &str =
478 "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record.schema.json";
479 pub const REPO_CONTEXT: &str =
481 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-context.schema.json";
482 pub const STORE_CONTEXT: &str =
484 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-context.schema.json";
485 pub const BUNDLE: &str =
487 "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle.schema.json";
488 pub const DESIRED_STATE_EXPORT: &str =
490 "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state-export.schema.json";
491 pub const DESIRED_STATE: &str =
493 "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state.schema.json";
494 pub const DESIRED_SUBSCRIPTION_ENTRY: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-subscription-entry.schema.json";
496 pub const STOREFRONT: &str =
498 "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront.schema.json";
499 pub const STORE_PRODUCT: &str =
501 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product.schema.json";
502 pub const STORE_PLAN: &str =
504 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan.schema.json";
505 pub const SUBSCRIPTION: &str =
507 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription.schema.json";
508 pub const ENVIRONMENT: &str =
510 "https://greentic-ai.github.io/greentic-types/schemas/v1/environment.schema.json";
511 pub const THEME: &str =
513 "https://greentic-ai.github.io/greentic-types/schemas/v1/theme.schema.json";
514 pub const LAYOUT_SECTION: &str =
516 "https://greentic-ai.github.io/greentic-types/schemas/v1/layout-section.schema.json";
517 pub const COLLECTION: &str =
519 "https://greentic-ai.github.io/greentic-types/schemas/v1/collection.schema.json";
520 pub const PRODUCT_OVERRIDE: &str =
522 "https://greentic-ai.github.io/greentic-types/schemas/v1/product-override.schema.json";
523 pub const EVENT_ENVELOPE: &str =
525 "https://greentic-ai.github.io/greentic-types/schemas/v1/event-envelope.schema.json";
526 pub const EVENT_PROVIDER_DESCRIPTOR: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/event-provider-descriptor.schema.json";
528 pub const CHANNEL_MESSAGE_ENVELOPE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/channel-message-envelope.schema.json";
530 pub const ATTACHMENT: &str =
532 "https://greentic-ai.github.io/greentic-types/schemas/v1/attachment.schema.json";
533 pub const WORKER_REQUEST: &str =
535 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-request.schema.json";
536 pub const WORKER_MESSAGE: &str =
538 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-message.schema.json";
539 pub const WORKER_RESPONSE: &str =
541 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-response.schema.json";
542 pub const OTLP_KEYS: &str =
544 "https://greentic-ai.github.io/greentic-types/schemas/v1/otlp-keys.schema.json";
545 pub const RUN_RESULT: &str =
547 "https://greentic-ai.github.io/greentic-types/schemas/v1/run-result.schema.json";
548}
549
550#[cfg(all(feature = "schema", feature = "std"))]
551pub fn write_all_schemas(out_dir: &std::path::Path) -> anyhow::Result<()> {
553 use anyhow::Context;
554 use std::fs;
555
556 fs::create_dir_all(out_dir)
557 .with_context(|| format!("failed to create {}", out_dir.display()))?;
558
559 for entry in crate::schema::entries() {
560 let schema = (entry.generator)();
561 let path = out_dir.join(entry.file_name);
562 if let Some(parent) = path.parent() {
563 fs::create_dir_all(parent)
564 .with_context(|| format!("failed to create {}", parent.display()))?;
565 }
566
567 let json =
568 serde_json::to_vec_pretty(&schema).context("failed to serialize schema to JSON")?;
569 fs::write(&path, json).with_context(|| format!("failed to write {}", path.display()))?;
570 }
571
572 Ok(())
573}
574
575macro_rules! id_newtype {
576 ($name:ident, $doc:literal) => {
577 #[doc = $doc]
578 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
579 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
580 #[cfg_attr(feature = "schemars", derive(JsonSchema))]
581 #[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
582 pub struct $name(pub String);
583
584 impl $name {
585 pub fn as_str(&self) -> &str {
587 &self.0
588 }
589
590 pub fn new(value: impl AsRef<str>) -> GResult<Self> {
592 value.as_ref().parse()
593 }
594 }
595
596 impl From<$name> for String {
597 fn from(value: $name) -> Self {
598 value.0
599 }
600 }
601
602 impl AsRef<str> for $name {
603 fn as_ref(&self) -> &str {
604 self.as_str()
605 }
606 }
607
608 impl fmt::Display for $name {
609 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610 f.write_str(self.as_str())
611 }
612 }
613
614 impl FromStr for $name {
615 type Err = GreenticError;
616
617 fn from_str(value: &str) -> Result<Self, Self::Err> {
618 validate_identifier(value, stringify!($name))?;
619 Ok(Self(value.to_owned()))
620 }
621 }
622
623 impl TryFrom<String> for $name {
624 type Error = GreenticError;
625
626 fn try_from(value: String) -> Result<Self, Self::Error> {
627 $name::from_str(&value)
628 }
629 }
630
631 impl TryFrom<&str> for $name {
632 type Error = GreenticError;
633
634 fn try_from(value: &str) -> Result<Self, Self::Error> {
635 $name::from_str(value)
636 }
637 }
638 };
639}
640
641id_newtype!(EnvId, "Environment identifier for a tenant context.");
642id_newtype!(TenantId, "Tenant identifier within an environment.");
643id_newtype!(TeamId, "Team identifier belonging to a tenant.");
644id_newtype!(UserId, "User identifier within a tenant.");
645id_newtype!(BranchRef, "Reference to a source control branch.");
646id_newtype!(CommitRef, "Reference to a source control commit.");
647id_newtype!(
648 GitProviderRef,
649 "Identifier referencing a source control provider."
650);
651id_newtype!(ScannerRef, "Identifier referencing a scanner provider.");
652id_newtype!(WebhookId, "Identifier referencing a registered webhook.");
653id_newtype!(PackId, "Globally unique pack identifier.");
654id_newtype!(
655 ComponentId,
656 "Identifier referencing a component binding in a pack."
657);
658id_newtype!(FlowId, "Identifier referencing a flow inside a pack.");
659id_newtype!(NodeId, "Identifier referencing a node inside a flow graph.");
660id_newtype!(
661 EnvironmentRef,
662 "Identifier referencing a deployment environment."
663);
664id_newtype!(
665 DistributorRef,
666 "Identifier referencing a distributor instance."
667);
668id_newtype!(StoreFrontId, "Identifier referencing a storefront.");
669id_newtype!(
670 StoreProductId,
671 "Identifier referencing a product in the store catalog."
672);
673id_newtype!(
674 StorePlanId,
675 "Identifier referencing a plan for a store product."
676);
677id_newtype!(
678 SubscriptionId,
679 "Identifier referencing a subscription entry."
680);
681id_newtype!(BundleId, "Identifier referencing a distributor bundle.");
682id_newtype!(CollectionId, "Identifier referencing a product collection.");
683id_newtype!(RepoRef, "Repository reference within a supply chain.");
684id_newtype!(
685 ComponentRef,
686 "Supply-chain component reference (distinct from pack ComponentId)."
687);
688id_newtype!(
689 VersionRef,
690 "Version reference for a component or metadata record."
691);
692id_newtype!(BuildRef, "Build reference within a supply chain.");
693id_newtype!(ScanRef, "Scan reference within a supply chain.");
694id_newtype!(
695 AttestationRef,
696 "Attestation reference within a supply chain."
697);
698id_newtype!(AttestationId, "Identifier referencing an attestation.");
699id_newtype!(PolicyRef, "Policy reference within a supply chain.");
700id_newtype!(
701 PolicyInputRef,
702 "Reference to a policy input payload for evaluation."
703);
704id_newtype!(StoreRef, "Content store reference within a supply chain.");
705id_newtype!(
706 RegistryRef,
707 "Registry reference for OCI or artifact storage."
708);
709id_newtype!(OciImageRef, "Reference to an OCI image for distribution.");
710id_newtype!(
711 ArtifactRef,
712 "Artifact reference within a build or scan result."
713);
714id_newtype!(
715 SbomRef,
716 "Reference to a Software Bill of Materials artifact."
717);
718id_newtype!(SigningKeyRef, "Reference to a signing key handle.");
719id_newtype!(SignatureRef, "Reference to a generated signature.");
720id_newtype!(StatementRef, "Reference to an attestation statement.");
721id_newtype!(
722 BuildLogRef,
723 "Reference to a build log output produced during execution."
724);
725id_newtype!(
726 MetadataRecordRef,
727 "Reference to a metadata record attached to artifacts or bundles."
728);
729
730#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
732#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
733#[cfg_attr(feature = "schemars", derive(JsonSchema))]
734#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
735pub struct ApiKeyRef(pub String);
736
737impl ApiKeyRef {
738 pub fn as_str(&self) -> &str {
740 &self.0
741 }
742
743 pub fn new(value: impl AsRef<str>) -> GResult<Self> {
745 value.as_ref().parse()
746 }
747}
748
749impl fmt::Display for ApiKeyRef {
750 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
751 f.write_str(self.as_str())
752 }
753}
754
755impl FromStr for ApiKeyRef {
756 type Err = GreenticError;
757
758 fn from_str(value: &str) -> Result<Self, Self::Err> {
759 validate_api_key_ref(value)?;
760 Ok(Self(value.to_owned()))
761 }
762}
763
764impl TryFrom<String> for ApiKeyRef {
765 type Error = GreenticError;
766
767 fn try_from(value: String) -> Result<Self, Self::Error> {
768 ApiKeyRef::from_str(&value)
769 }
770}
771
772impl TryFrom<&str> for ApiKeyRef {
773 type Error = GreenticError;
774
775 fn try_from(value: &str) -> Result<Self, Self::Error> {
776 ApiKeyRef::from_str(value)
777 }
778}
779
780impl From<ApiKeyRef> for String {
781 fn from(value: ApiKeyRef) -> Self {
782 value.0
783 }
784}
785
786impl AsRef<str> for ApiKeyRef {
787 fn as_ref(&self) -> &str {
788 self.as_str()
789 }
790}
791
792#[derive(Clone, Debug, PartialEq, Eq, Hash)]
794#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
795#[cfg_attr(feature = "schemars", derive(JsonSchema))]
796pub struct TenantContext {
797 pub tenant_id: TenantId,
799 #[cfg_attr(
801 feature = "serde",
802 serde(default, skip_serializing_if = "Option::is_none")
803 )]
804 pub team_id: Option<TeamId>,
805 #[cfg_attr(
807 feature = "serde",
808 serde(default, skip_serializing_if = "Option::is_none")
809 )]
810 pub user_id: Option<UserId>,
811 #[cfg_attr(
813 feature = "serde",
814 serde(default, skip_serializing_if = "Option::is_none")
815 )]
816 pub session_id: Option<String>,
817 #[cfg_attr(
819 feature = "serde",
820 serde(default, skip_serializing_if = "BTreeMap::is_empty")
821 )]
822 pub attributes: BTreeMap<String, String>,
823}
824
825impl TenantContext {
826 pub fn new(tenant_id: TenantId) -> Self {
828 Self {
829 tenant_id,
830 team_id: None,
831 user_id: None,
832 session_id: None,
833 attributes: BTreeMap::new(),
834 }
835 }
836}
837
838impl From<&TenantCtx> for TenantContext {
839 fn from(ctx: &TenantCtx) -> Self {
840 Self {
841 tenant_id: ctx.tenant_id.clone(),
842 team_id: ctx.team_id.clone().or_else(|| ctx.team.clone()),
843 user_id: ctx.user_id.clone().or_else(|| ctx.user.clone()),
844 session_id: ctx.session_id.clone(),
845 attributes: ctx.attributes.clone(),
846 }
847 }
848}
849
850#[derive(Clone, Debug, PartialEq, Eq, Hash)]
852#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
853#[cfg_attr(feature = "schemars", derive(JsonSchema))]
854#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
855pub enum HashAlgorithm {
856 Blake3,
858 Other(String),
860}
861
862#[derive(Clone, Debug, PartialEq, Eq, Hash)]
864#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
865#[cfg_attr(
866 feature = "serde",
867 serde(into = "HashDigestRepr", try_from = "HashDigestRepr")
868)]
869#[cfg_attr(feature = "schemars", derive(JsonSchema))]
870pub struct HashDigest {
871 pub algo: HashAlgorithm,
873 pub hex: String,
875}
876
877impl HashDigest {
878 pub fn new(algo: HashAlgorithm, hex: impl Into<String>) -> GResult<Self> {
880 let hex = hex.into();
881 validate_hex(&hex)?;
882 Ok(Self { algo, hex })
883 }
884
885 pub fn blake3(hex: impl Into<String>) -> GResult<Self> {
887 Self::new(HashAlgorithm::Blake3, hex)
888 }
889}
890
891#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
892#[cfg_attr(feature = "schemars", derive(JsonSchema))]
893struct HashDigestRepr {
894 algo: HashAlgorithm,
895 hex: String,
896}
897
898impl From<HashDigest> for HashDigestRepr {
899 fn from(value: HashDigest) -> Self {
900 Self {
901 algo: value.algo,
902 hex: value.hex,
903 }
904 }
905}
906
907impl TryFrom<HashDigestRepr> for HashDigest {
908 type Error = GreenticError;
909
910 fn try_from(value: HashDigestRepr) -> Result<Self, Self::Error> {
911 HashDigest::new(value.algo, value.hex)
912 }
913}
914
915fn validate_hex(hex: &str) -> GResult<()> {
916 if hex.is_empty() {
917 return Err(GreenticError::new(
918 ErrorCode::InvalidInput,
919 "digest hex payload must not be empty",
920 ));
921 }
922 if hex.len() % 2 != 0 {
923 return Err(GreenticError::new(
924 ErrorCode::InvalidInput,
925 "digest hex payload must have an even number of digits",
926 ));
927 }
928 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
929 return Err(GreenticError::new(
930 ErrorCode::InvalidInput,
931 "digest hex payload must be hexadecimal",
932 ));
933 }
934 Ok(())
935}
936
937#[derive(Clone, Debug, PartialEq, Eq, Hash)]
939#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
940#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
941pub struct SemverReq(String);
942
943impl SemverReq {
944 pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
946 let value = value.as_ref();
947 VersionReq::parse(value).map_err(|err| {
948 GreenticError::new(
949 ErrorCode::InvalidInput,
950 format!("invalid semver requirement '{value}': {err}"),
951 )
952 })?;
953 Ok(Self(value.to_owned()))
954 }
955
956 pub fn as_str(&self) -> &str {
958 &self.0
959 }
960
961 pub fn to_version_req(&self) -> VersionReq {
963 VersionReq::parse(&self.0)
964 .unwrap_or_else(|err| unreachable!("SemverReq::parse validated inputs: {err}"))
965 }
966}
967
968impl fmt::Display for SemverReq {
969 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
970 f.write_str(self.as_str())
971 }
972}
973
974impl From<SemverReq> for String {
975 fn from(value: SemverReq) -> Self {
976 value.0
977 }
978}
979
980impl TryFrom<String> for SemverReq {
981 type Error = GreenticError;
982
983 fn try_from(value: String) -> Result<Self, Self::Error> {
984 SemverReq::parse(&value)
985 }
986}
987
988impl TryFrom<&str> for SemverReq {
989 type Error = GreenticError;
990
991 fn try_from(value: &str) -> Result<Self, Self::Error> {
992 SemverReq::parse(value)
993 }
994}
995
996impl FromStr for SemverReq {
997 type Err = GreenticError;
998
999 fn from_str(s: &str) -> Result<Self, Self::Err> {
1000 SemverReq::parse(s)
1001 }
1002}
1003
1004#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1006#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1007#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1008pub struct RedactionPath(String);
1009
1010impl RedactionPath {
1011 pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1013 let value = value.as_ref();
1014 validate_jsonpath(value)?;
1015 Ok(Self(value.to_owned()))
1016 }
1017
1018 pub fn as_str(&self) -> &str {
1020 &self.0
1021 }
1022}
1023
1024impl fmt::Display for RedactionPath {
1025 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1026 f.write_str(self.as_str())
1027 }
1028}
1029
1030impl From<RedactionPath> for String {
1031 fn from(value: RedactionPath) -> Self {
1032 value.0
1033 }
1034}
1035
1036impl TryFrom<String> for RedactionPath {
1037 type Error = GreenticError;
1038
1039 fn try_from(value: String) -> Result<Self, Self::Error> {
1040 RedactionPath::parse(&value)
1041 }
1042}
1043
1044impl TryFrom<&str> for RedactionPath {
1045 type Error = GreenticError;
1046
1047 fn try_from(value: &str) -> Result<Self, Self::Error> {
1048 RedactionPath::parse(value)
1049 }
1050}
1051
1052fn validate_jsonpath(path: &str) -> GResult<()> {
1053 if path.is_empty() {
1054 return Err(GreenticError::new(
1055 ErrorCode::InvalidInput,
1056 "redaction path cannot be empty",
1057 ));
1058 }
1059 if !path.starts_with('$') {
1060 return Err(GreenticError::new(
1061 ErrorCode::InvalidInput,
1062 "redaction path must start with '$'",
1063 ));
1064 }
1065 if path.chars().any(|c| c.is_control()) {
1066 return Err(GreenticError::new(
1067 ErrorCode::InvalidInput,
1068 "redaction path cannot contain control characters",
1069 ));
1070 }
1071 Ok(())
1072}
1073
1074#[cfg(feature = "schemars")]
1075impl JsonSchema for SemverReq {
1076 fn schema_name() -> Cow<'static, str> {
1077 Cow::Borrowed("SemverReq")
1078 }
1079
1080 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1081 let mut schema = <String>::json_schema(generator);
1082 if schema.get("description").is_none() {
1083 schema.insert(
1084 "description".into(),
1085 "Validated semantic version requirement string".into(),
1086 );
1087 }
1088 schema
1089 }
1090}
1091
1092#[cfg(feature = "schemars")]
1093impl JsonSchema for RedactionPath {
1094 fn schema_name() -> Cow<'static, str> {
1095 Cow::Borrowed("RedactionPath")
1096 }
1097
1098 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1099 let mut schema = <String>::json_schema(generator);
1100 if schema.get("description").is_none() {
1101 schema.insert(
1102 "description".into(),
1103 "JSONPath expression used for runtime redaction".into(),
1104 );
1105 }
1106 schema
1107 }
1108}
1109
1110#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1113#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1114pub struct InvocationDeadline {
1115 unix_millis: i128,
1116}
1117
1118impl InvocationDeadline {
1119 pub const fn from_unix_millis(unix_millis: i128) -> Self {
1121 Self { unix_millis }
1122 }
1123
1124 pub const fn unix_millis(&self) -> i128 {
1126 self.unix_millis
1127 }
1128
1129 #[cfg(feature = "time")]
1131 pub fn to_offset_date_time(&self) -> Result<OffsetDateTime, time::error::ComponentRange> {
1132 OffsetDateTime::from_unix_timestamp_nanos(self.unix_millis * 1_000_000)
1133 }
1134
1135 #[cfg(feature = "time")]
1137 pub fn from_offset_date_time(value: OffsetDateTime) -> Self {
1138 let nanos = value.unix_timestamp_nanos();
1139 Self {
1140 unix_millis: nanos / 1_000_000,
1141 }
1142 }
1143}
1144
1145#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1147#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1148#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1149pub struct TenantCtx {
1150 pub env: EnvId,
1152 pub tenant: TenantId,
1154 pub tenant_id: TenantId,
1156 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1158 pub team: Option<TeamId>,
1159 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1161 pub team_id: Option<TeamId>,
1162 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1164 pub user: Option<UserId>,
1165 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1167 pub user_id: Option<UserId>,
1168 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1170 pub session_id: Option<String>,
1171 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1173 pub flow_id: Option<String>,
1174 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1176 pub node_id: Option<String>,
1177 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1179 pub provider_id: Option<String>,
1180 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1182 pub trace_id: Option<String>,
1183 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1185 pub correlation_id: Option<String>,
1186 #[cfg_attr(
1188 feature = "serde",
1189 serde(default, skip_serializing_if = "BTreeMap::is_empty")
1190 )]
1191 pub attributes: BTreeMap<String, String>,
1192 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1194 pub deadline: Option<InvocationDeadline>,
1195 pub attempt: u32,
1197 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1199 pub idempotency_key: Option<String>,
1200 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1202 pub impersonation: Option<Impersonation>,
1203}
1204
1205impl TenantCtx {
1206 pub fn new(env: EnvId, tenant: TenantId) -> Self {
1208 let tenant_id = tenant.clone();
1209 Self {
1210 env,
1211 tenant: tenant.clone(),
1212 tenant_id,
1213 team: None,
1214 team_id: None,
1215 user: None,
1216 user_id: None,
1217 session_id: None,
1218 flow_id: None,
1219 node_id: None,
1220 provider_id: None,
1221 trace_id: None,
1222 correlation_id: None,
1223 attributes: BTreeMap::new(),
1224 deadline: None,
1225 attempt: 0,
1226 idempotency_key: None,
1227 impersonation: None,
1228 }
1229 }
1230
1231 pub fn with_team(mut self, team: Option<TeamId>) -> Self {
1233 self.team = team.clone();
1234 self.team_id = team;
1235 self
1236 }
1237
1238 pub fn with_user(mut self, user: Option<UserId>) -> Self {
1240 self.user = user.clone();
1241 self.user_id = user;
1242 self
1243 }
1244
1245 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1247 self.session_id = Some(session.into());
1248 self
1249 }
1250
1251 pub fn with_flow(mut self, flow: impl Into<String>) -> Self {
1253 self.flow_id = Some(flow.into());
1254 self
1255 }
1256
1257 pub fn with_node(mut self, node: impl Into<String>) -> Self {
1259 self.node_id = Some(node.into());
1260 self
1261 }
1262
1263 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1265 self.provider_id = Some(provider.into());
1266 self
1267 }
1268
1269 pub fn with_attributes(mut self, attributes: BTreeMap<String, String>) -> Self {
1271 self.attributes = attributes;
1272 self
1273 }
1274
1275 pub fn with_impersonation(mut self, impersonation: Option<Impersonation>) -> Self {
1277 self.impersonation = impersonation;
1278 self
1279 }
1280
1281 pub fn with_attempt(mut self, attempt: u32) -> Self {
1283 self.attempt = attempt;
1284 self
1285 }
1286
1287 pub fn with_deadline(mut self, deadline: Option<InvocationDeadline>) -> Self {
1289 self.deadline = deadline;
1290 self
1291 }
1292
1293 pub fn session_id(&self) -> Option<&str> {
1295 self.session_id.as_deref()
1296 }
1297
1298 pub fn flow_id(&self) -> Option<&str> {
1300 self.flow_id.as_deref()
1301 }
1302
1303 pub fn node_id(&self) -> Option<&str> {
1305 self.node_id.as_deref()
1306 }
1307
1308 pub fn provider_id(&self) -> Option<&str> {
1310 self.provider_id.as_deref()
1311 }
1312}
1313
1314pub type BinaryPayload = Vec<u8>;
1316
1317#[derive(Clone, Debug, PartialEq, Eq)]
1319#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1320#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1321pub struct InvocationEnvelope {
1322 pub ctx: TenantCtx,
1324 pub flow_id: String,
1326 pub node_id: Option<String>,
1328 pub op: String,
1330 pub payload: BinaryPayload,
1332 pub metadata: BinaryPayload,
1334}
1335
1336#[derive(Clone, Debug, PartialEq, Eq)]
1338#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1339#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1340pub enum ErrorDetail {
1341 Text(String),
1343 Binary(BinaryPayload),
1345}
1346
1347#[derive(Debug)]
1349#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1350#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1351pub struct NodeError {
1352 pub code: String,
1354 pub message: String,
1356 pub retryable: bool,
1358 pub backoff_ms: Option<u64>,
1360 pub details: Option<ErrorDetail>,
1362 #[cfg(feature = "std")]
1363 #[cfg_attr(feature = "serde", serde(skip, default = "default_source"))]
1364 #[cfg_attr(feature = "schemars", schemars(skip))]
1365 source: Option<Box<dyn StdError + Send + Sync>>,
1366}
1367
1368#[cfg(all(feature = "std", feature = "serde"))]
1369fn default_source() -> Option<Box<dyn StdError + Send + Sync>> {
1370 None
1371}
1372
1373impl NodeError {
1374 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
1376 Self {
1377 code: code.into(),
1378 message: message.into(),
1379 retryable: false,
1380 backoff_ms: None,
1381 details: None,
1382 #[cfg(feature = "std")]
1383 source: None,
1384 }
1385 }
1386
1387 pub fn with_retry(mut self, backoff_ms: Option<u64>) -> Self {
1389 self.retryable = true;
1390 self.backoff_ms = backoff_ms;
1391 self
1392 }
1393
1394 pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
1396 self.details = Some(detail);
1397 self
1398 }
1399
1400 pub fn with_detail_text(mut self, detail: impl Into<String>) -> Self {
1402 self.details = Some(ErrorDetail::Text(detail.into()));
1403 self
1404 }
1405
1406 pub fn with_detail_binary(mut self, detail: BinaryPayload) -> Self {
1408 self.details = Some(ErrorDetail::Binary(detail));
1409 self
1410 }
1411
1412 #[cfg(feature = "std")]
1413 pub fn with_source<E>(mut self, source: E) -> Self
1415 where
1416 E: StdError + Send + Sync + 'static,
1417 {
1418 self.source = Some(Box::new(source));
1419 self
1420 }
1421
1422 pub fn detail(&self) -> Option<&ErrorDetail> {
1424 self.details.as_ref()
1425 }
1426
1427 #[cfg(feature = "std")]
1428 pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
1430 self.source.as_deref()
1431 }
1432}
1433
1434impl fmt::Display for NodeError {
1435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1436 write!(f, "{}: {}", self.code, self.message)
1437 }
1438}
1439
1440#[cfg(feature = "std")]
1441impl StdError for NodeError {
1442 fn source(&self) -> Option<&(dyn StdError + 'static)> {
1443 self.source
1444 .as_ref()
1445 .map(|err| err.as_ref() as &(dyn StdError + 'static))
1446 }
1447}
1448
1449pub type NodeResult<T> = Result<T, NodeError>;
1451
1452pub fn make_idempotency_key(
1457 ctx: &TenantCtx,
1458 flow_id: &str,
1459 node_id: Option<&str>,
1460 correlation: Option<&str>,
1461) -> String {
1462 let node_segment = node_id.unwrap_or_default();
1463 let correlation_segment = correlation
1464 .or(ctx.correlation_id.as_deref())
1465 .unwrap_or_default();
1466 let input = format!(
1467 "{}|{}|{}|{}",
1468 ctx.tenant_id.as_str(),
1469 flow_id,
1470 node_segment,
1471 correlation_segment
1472 );
1473 fnv1a_128_hex(input.as_bytes())
1474}
1475
1476const FNV_OFFSET_BASIS: u128 = 0x6c62272e07bb014262b821756295c58d;
1477const FNV_PRIME: u128 = 0x0000000001000000000000000000013b;
1478
1479fn fnv1a_128_hex(bytes: &[u8]) -> String {
1480 let mut hash = FNV_OFFSET_BASIS;
1481 for &byte in bytes {
1482 hash ^= byte as u128;
1483 hash = hash.wrapping_mul(FNV_PRIME);
1484 }
1485
1486 let mut output = String::with_capacity(32);
1487 for shift in (0..32).rev() {
1488 let nibble = ((hash >> (shift * 4)) & 0x0f) as u8;
1489 output.push(match nibble {
1490 0..=9 => (b'0' + nibble) as char,
1491 _ => (b'a' + (nibble - 10)) as char,
1492 });
1493 }
1494 output
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499 use super::*;
1500 use core::convert::TryFrom;
1501 use time::OffsetDateTime;
1502
1503 fn sample_ctx() -> TenantCtx {
1504 let env = EnvId::try_from("prod").unwrap_or_else(|err| panic!("{err}"));
1505 let tenant = TenantId::try_from("tenant-123").unwrap_or_else(|err| panic!("{err}"));
1506 let team = TeamId::try_from("team-456").unwrap_or_else(|err| panic!("{err}"));
1507 let user = UserId::try_from("user-789").unwrap_or_else(|err| panic!("{err}"));
1508
1509 let mut ctx = TenantCtx::new(env, tenant)
1510 .with_team(Some(team))
1511 .with_user(Some(user))
1512 .with_attempt(2)
1513 .with_deadline(Some(InvocationDeadline::from_unix_millis(
1514 1_700_000_000_000,
1515 )));
1516 ctx.trace_id = Some("trace-abc".to_owned());
1517 ctx.correlation_id = Some("corr-xyz".to_owned());
1518 ctx.idempotency_key = Some("key-123".to_owned());
1519 ctx
1520 }
1521
1522 #[test]
1523 fn idempotent_key_stable() {
1524 let ctx = sample_ctx();
1525 let key_a = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1526 let key_b = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1527 assert_eq!(key_a, key_b);
1528 assert_eq!(key_a.len(), 32);
1529 }
1530
1531 #[test]
1532 fn idempotent_key_uses_context_correlation() {
1533 let ctx = sample_ctx();
1534 let key = make_idempotency_key(&ctx, "flow-1", None, None);
1535 let expected = make_idempotency_key(&ctx, "flow-1", None, ctx.correlation_id.as_deref());
1536 assert_eq!(key, expected);
1537 }
1538
1539 #[test]
1540 #[cfg(feature = "time")]
1541 fn deadline_roundtrips_through_offset_datetime() {
1542 let dt = OffsetDateTime::from_unix_timestamp(1_700_000_000)
1543 .unwrap_or_else(|err| panic!("valid timestamp: {err}"));
1544 let deadline = InvocationDeadline::from_offset_date_time(dt);
1545 let roundtrip = deadline
1546 .to_offset_date_time()
1547 .unwrap_or_else(|err| panic!("round-trip conversion failed: {err}"));
1548 let millis = dt.unix_timestamp_nanos() / 1_000_000;
1549 assert_eq!(deadline.unix_millis(), millis);
1550 assert_eq!(roundtrip.unix_timestamp_nanos() / 1_000_000, millis);
1551 }
1552
1553 #[test]
1554 fn node_error_builder_sets_fields() {
1555 let err = NodeError::new("TEST", "example")
1556 .with_retry(Some(500))
1557 .with_detail_text("context");
1558
1559 assert!(err.retryable);
1560 assert_eq!(err.backoff_ms, Some(500));
1561 match err.detail() {
1562 Some(ErrorDetail::Text(detail)) => assert_eq!(detail, "context"),
1563 other => panic!("unexpected detail {other:?}"),
1564 }
1565 }
1566
1567 #[cfg(feature = "std")]
1568 #[test]
1569 fn node_error_source_roundtrips() {
1570 use std::io::Error;
1571
1572 let source = Error::other("boom");
1573 let err = NodeError::new("TEST", "example").with_source(source);
1574 assert!(err.source().is_some());
1575 }
1576}