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