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