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