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