alien_bindings/traits.rs
1use crate::error::Result;
2use crate::presigned::PresignedRequest;
3use alien_core::{BuildConfig, BuildExecution};
4use async_trait::async_trait;
5use object_store::path::Path;
6use object_store::ObjectStore;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::sync::Arc;
10use std::time::Duration;
11use url::Url;
12
13#[cfg(feature = "openapi")]
14use utoipa::ToSchema;
15
16/// Marker trait for all binding types.
17pub trait Binding: Send + Sync + std::fmt::Debug {}
18
19/// A storage binding that provides object store capabilities.
20#[async_trait]
21pub trait Storage: Binding + ObjectStore {
22 /// Gets the base directory path configured for this storage binding.
23 fn get_base_dir(&self) -> Path;
24 /// Gets the underlying URL configured for this storage binding.
25 fn get_url(&self) -> Url;
26
27 /// Creates a presigned request for uploading data to the specified path.
28 /// The request can be serialized, stored, and executed later.
29 async fn presigned_put(&self, path: &Path, expires_in: Duration) -> Result<PresignedRequest>;
30
31 /// Creates a presigned request for downloading data from the specified path.
32 /// The request can be serialized, stored, and executed later.
33 async fn presigned_get(&self, path: &Path, expires_in: Duration) -> Result<PresignedRequest>;
34
35 /// Creates a presigned request for deleting the object at the specified path.
36 /// The request can be serialized, stored, and executed later.
37 async fn presigned_delete(&self, path: &Path, expires_in: Duration)
38 -> Result<PresignedRequest>;
39}
40
41/// A build binding that provides build execution capabilities.
42#[async_trait]
43pub trait Build: Binding {
44 /// Starts a new build with the given configuration.
45 /// Returns the build execution information.
46 async fn start_build(&self, config: BuildConfig) -> Result<BuildExecution>;
47
48 /// Gets the status of a specific build execution.
49 async fn get_build_status(&self, build_id: &str) -> Result<BuildExecution>;
50
51 /// Stops or cancels a running build.
52 async fn stop_build(&self, build_id: &str) -> Result<()>;
53}
54
55/// AWS IAM Role service account information
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58#[cfg_attr(feature = "openapi", derive(ToSchema))]
59pub struct AwsServiceAccountInfo {
60 /// The IAM role name
61 pub role_name: String,
62 /// The IAM role ARN (for AssumeRole)
63 pub role_arn: String,
64}
65
66/// GCP Service Account information
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69#[cfg_attr(feature = "openapi", derive(ToSchema))]
70pub struct GcpServiceAccountInfo {
71 /// The service account email (for impersonation)
72 pub email: String,
73 /// The service account unique ID
74 pub unique_id: String,
75}
76
77/// Azure User-Assigned Managed Identity information
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80#[cfg_attr(feature = "openapi", derive(ToSchema))]
81pub struct AzureServiceAccountInfo {
82 /// The managed identity client ID (for authentication)
83 pub client_id: String,
84 /// The managed identity resource ID (ARM ID)
85 pub resource_id: String,
86 /// The managed identity principal ID
87 pub principal_id: String,
88}
89
90/// Platform-specific service account information
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "platform", rename_all = "camelCase")]
93#[cfg_attr(feature = "openapi", derive(ToSchema))]
94pub enum ServiceAccountInfo {
95 /// AWS IAM Role
96 Aws(AwsServiceAccountInfo),
97 /// GCP Service Account
98 Gcp(GcpServiceAccountInfo),
99 /// Azure User-Assigned Managed Identity
100 Azure(AzureServiceAccountInfo),
101}
102
103/// Configuration for impersonation
104#[derive(Debug, Clone)]
105pub struct ImpersonationRequest {
106 /// Optional session name (AWS only)
107 pub session_name: Option<String>,
108 /// Optional session duration in seconds
109 pub duration_seconds: Option<i32>,
110 /// Optional scopes (GCP only)
111 pub scopes: Option<Vec<String>>,
112}
113
114impl Default for ImpersonationRequest {
115 fn default() -> Self {
116 Self {
117 session_name: None,
118 duration_seconds: Some(3600), // 1 hour default
119 scopes: None,
120 }
121 }
122}
123
124/// A service account binding that provides identity and impersonation capabilities.
125#[async_trait]
126pub trait ServiceAccount: Binding {
127 /// Gets information about the service account
128 async fn get_info(&self) -> Result<ServiceAccountInfo>;
129
130 /// Impersonates the service account and returns credentials as a ClientConfig.
131 ///
132 /// This performs the cloud-specific impersonation:
133 /// - AWS: STS AssumeRole to get temporary credentials
134 /// - GCP: IAM Credentials API generateAccessToken
135 /// - Azure: Uses the attached managed identity (no API call needed)
136 async fn impersonate(&self, request: ImpersonationRequest) -> Result<alien_core::ClientConfig>;
137
138 /// Helper for downcasting trait object
139 fn as_any(&self) -> &dyn std::any::Any;
140}
141
142/// Response from repository operations.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145#[cfg_attr(feature = "openapi", derive(ToSchema))]
146pub struct RepositoryResponse {
147 /// The **routable name** of the repository — the full, platform-specific
148 /// path used for subsequent calls (`get_repository`, `delete_repository`,
149 /// `generate_credentials`, `*_cross_account_access`).
150 ///
151 /// Per-platform format (matches
152 /// `alien.dev/content/docs/infrastructure/artifact-registry/behavior.mdx`):
153 ///
154 /// | Platform | Format |
155 /// |---|---|
156 /// | AWS (ECR) | `{registry_prefix}-{logical}` (e.g. `alien-artifacts-my-app`) |
157 /// | GCP (GAR) | `{project_id}/{gar_repo}/{logical}` |
158 /// | Azure (ACR) | `{logical}` (used directly) |
159 /// | Local | `{binding_name}/{logical}` |
160 ///
161 /// **Round-trip invariant:** callers MUST be able to pass this value back
162 /// to any other method on the trait without further transformation.
163 /// Implementations MUST NOT re-apply prefixing in receivers — assume
164 /// `repo_id` arguments are already routable.
165 pub name: String,
166 /// Repository URI for pushing/pulling images. None if repository is not ready yet.
167 pub uri: Option<String>,
168 /// Optional creation timestamp in ISO8601 format.
169 pub created_at: Option<String>,
170}
171
172/// Permissions level for artifact registry access.
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "kebab-case")]
175#[cfg_attr(feature = "openapi", derive(ToSchema))]
176pub enum ArtifactRegistryPermissions {
177 /// Pull-only access (download artifacts).
178 Pull,
179 /// Push and pull access (upload and download artifacts).
180 PushPull,
181}
182
183/// How the registry expects credentials to be presented.
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185#[serde(rename_all = "lowercase")]
186#[cfg_attr(feature = "openapi", derive(ToSchema))]
187pub enum RegistryAuthMethod {
188 /// HTTP Basic auth (username:password). Used by ECR, GAR, Local.
189 Basic,
190 /// HTTP Bearer token. Used by ACR (Azure).
191 Bearer,
192}
193
194/// Credentials for accessing a repository.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197#[cfg_attr(feature = "openapi", derive(ToSchema))]
198pub struct ArtifactRegistryCredentials {
199 /// How to present these credentials to the registry.
200 pub auth_method: RegistryAuthMethod,
201 /// Username for authentication (empty for Bearer auth).
202 pub username: String,
203 /// Password or token for authentication.
204 pub password: String,
205 /// Optional expiration time in ISO8601 format.
206 pub expires_at: Option<String>,
207}
208
209/// Types of compute services that can access artifact registries.
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "kebab-case")]
212#[cfg_attr(feature = "openapi", derive(ToSchema))]
213pub enum ComputeServiceType {
214 /// Serverless functions
215 Worker,
216 // In the future, we could add Container, VirtualMachine, Kubernetes, etc.
217}
218
219/// Cross-account access configuration for AWS artifact registries.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222#[cfg_attr(feature = "openapi", derive(ToSchema))]
223pub struct AwsCrossAccountAccess {
224 /// AWS account IDs that should have cross-account access.
225 pub account_ids: Vec<String>,
226 /// AWS regions where the target Lambda functions run.
227 /// Used to construct `aws:sourceArn` patterns for the Lambda service principal condition.
228 pub regions: Vec<String>,
229 /// Types of compute services that should have access.
230 pub allowed_service_types: Vec<ComputeServiceType>,
231 /// Specific IAM role ARNs to grant access to.
232 /// These are typically deployment/management roles or service-specific roles.
233 pub role_arns: Vec<String>,
234}
235
236/// Cross-account access configuration for GCP artifact registries.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239#[cfg_attr(feature = "openapi", derive(ToSchema))]
240pub struct GcpCrossAccountAccess {
241 /// GCP project numbers that should have access.
242 pub project_numbers: Vec<String>,
243 /// Types of compute services that should have access.
244 pub allowed_service_types: Vec<ComputeServiceType>,
245 /// Additional service account emails to grant access to.
246 /// These are typically deployment/management service accounts.
247 pub service_account_emails: Vec<String>,
248}
249
250/// Platform-specific cross-account access configuration.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252#[serde(tag = "platform", rename_all = "lowercase")]
253#[cfg_attr(feature = "openapi", derive(ToSchema))]
254pub enum CrossAccountAccess {
255 /// AWS-specific cross-account access configuration.
256 Aws(AwsCrossAccountAccess),
257 /// GCP-specific cross-account access configuration.
258 Gcp(GcpCrossAccountAccess),
259}
260
261/// Current cross-account access permissions for a repository.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264#[cfg_attr(feature = "openapi", derive(ToSchema))]
265pub struct CrossAccountPermissions {
266 /// Platform-specific access configuration currently applied.
267 pub access: CrossAccountAccess,
268 /// Timestamp when permissions were last updated.
269 pub last_updated: Option<String>,
270}
271
272/// A trait for artifact registry bindings that provide container image repository management.
273#[async_trait]
274pub trait ArtifactRegistry: Binding {
275 /// Returns the raw registry endpoint URL (e.g., "https://123456.dkr.ecr.us-east-1.amazonaws.com"
276 /// or "http://localhost:5000"). Used by the push proxy to forward requests transparently.
277 ///
278 /// Default returns empty string — cloud provider implementations should override.
279 fn registry_endpoint(&self) -> String {
280 String::new()
281 }
282
283 /// Returns the OCI repository path prefix used for upstream operations.
284 ///
285 /// This identifier serves two related roles, both pointing at the same
286 /// upstream location:
287 ///
288 /// 1. **Proxy routing.** When the push proxy forwards push/pull requests
289 /// to the upstream registry, this prefix is prepended to the image
290 /// name portion of the OCI path.
291 /// 2. **Shared deployment-image repository name.** `alien release` pushes
292 /// every function image as `{prefix}:{logical}-{hash}` into one shared
293 /// repository whose routable name is exactly this prefix. Pass it as
294 /// `repo_id` when calling `add_cross_account_access` /
295 /// `remove_cross_account_access` for the deployment cross-account
296 /// flow.
297 ///
298 /// Examples:
299 /// - ECR: `"alien-e2e"` — flat repo prefix; also the routable repo name
300 /// for the shared deployment-image repository
301 /// - GAR: `"my-project/alien-e2e"` — project/repo structure
302 /// - ACR: `"alien-e2e"` when configured, or `""` for registry-root
303 /// repositories; principal pull access is granted on the parent registry
304 /// - Local: `"artifacts"` or similar — cross-account not supported
305 ///
306 /// An empty return value indicates the platform has no shared
307 /// deployment-image repo at the binding level.
308 ///
309 /// Default returns empty string.
310 fn upstream_repository_prefix(&self) -> String {
311 String::new()
312 }
313
314 /// Creates a repository within the artifact registry.
315 ///
316 /// `repo_name` is the **logical** identifier the caller chose (e.g.
317 /// `"my-app"`). The implementation transforms it to the routable
318 /// platform-specific form before calling any backend API; what's
319 /// returned in [`RepositoryResponse::name`] is the routable form.
320 ///
321 /// On platforms where image paths are implicit (GAR, ACR, Local),
322 /// this may not call any backend API — but it still returns a valid
323 /// routable name.
324 async fn create_repository(&self, repo_name: &str) -> Result<RepositoryResponse>;
325
326 /// Gets repository details. `repo_id` is the routable name returned by
327 /// [`Self::create_repository`]; implementations MUST NOT re-apply
328 /// prefixing.
329 async fn get_repository(&self, repo_id: &str) -> Result<RepositoryResponse>;
330
331 /// Adds cross-account access permissions for a repository.
332 /// This adds the specified permissions to any existing cross-account permissions.
333 ///
334 /// `repo_id` is the routable name from [`Self::create_repository`].
335 ///
336 /// For AWS: grants access to specified account IDs with configurable principals and compute service types (ECR repository policy).
337 /// For GCP: grants access to serverless robots and service accounts on the parent GAR registry (image-path-level IAM is not supported).
338 /// For Azure: not supported — returns `OperationNotSupported`.
339 async fn add_cross_account_access(
340 &self,
341 repo_id: &str,
342 access: CrossAccountAccess,
343 ) -> Result<()>;
344
345 /// Removes cross-account access permissions for a repository.
346 ///
347 /// `repo_id` is the routable name from [`Self::create_repository`].
348 ///
349 /// For AWS: removes access from the ECR repository policy.
350 /// For GCP: removes IAM bindings on the parent GAR registry.
351 /// For Azure: not supported — returns `OperationNotSupported`.
352 async fn remove_cross_account_access(
353 &self,
354 repo_id: &str,
355 access: CrossAccountAccess,
356 ) -> Result<()>;
357
358 /// Gets the current cross-account access permissions for a repository.
359 ///
360 /// `repo_id` is the routable name from [`Self::create_repository`].
361 /// For Azure: not supported — returns `OperationNotSupported`.
362 async fn get_cross_account_access(&self, repo_id: &str) -> Result<CrossAccountPermissions>;
363
364 /// Generates credentials for accessing a repository with the specified
365 /// permissions.
366 ///
367 /// `repo_id` is the routable name from [`Self::create_repository`].
368 ///
369 /// Most platforms produce registry-scoped (not repo-scoped) credentials,
370 /// so `repo_id` typically only affects logging — not the credentials
371 /// themselves.
372 async fn generate_credentials(
373 &self,
374 repo_id: &str,
375 permissions: ArtifactRegistryPermissions,
376 ttl_seconds: Option<u32>,
377 ) -> Result<ArtifactRegistryCredentials>;
378
379 /// Deletes a repository and all contained images.
380 ///
381 /// `repo_id` is the routable name from [`Self::create_repository`].
382 /// Implementations MUST NOT delete the *parent* registry (which is owned
383 /// by `alien-infra`); on platforms with implicit image paths (GAR, ACR,
384 /// Local) this is a no-op.
385 async fn delete_repository(&self, repo_id: &str) -> Result<()>;
386}
387
388/// A trait for vault bindings that provide secure secret management.
389#[async_trait]
390pub trait Vault: Binding {
391 /// Gets a secret value by name.
392 async fn get_secret(&self, secret_name: &str) -> Result<String>;
393
394 /// Sets a secret value, creating it if it doesn't exist or updating it if it does.
395 async fn set_secret(&self, secret_name: &str, value: &str) -> Result<()>;
396
397 /// Deletes a secret by name.
398 async fn delete_secret(&self, secret_name: &str) -> Result<()>;
399
400 /// Lists the names of all secrets stored in this vault.
401 ///
402 /// Returned names are in the vault's own namespace (any provider-specific
403 /// prefix is stripped), so each name can be passed straight back to
404 /// [`Vault::get_secret`].
405 ///
406 /// Implementations backed by a flat namespace (a single string prefix
407 /// with no reserved separator, e.g. `"{vault_prefix}-{secret_name}"`)
408 /// cannot implement this safely with a prefix/`BeginsWith` scan: a vault
409 /// named `"app"` would also match a sibling vault named `"app-prod"`,
410 /// aliasing across vaults. Such providers should return
411 /// `OperationNotSupported` rather than list under this hazard.
412 async fn list_secrets(&self) -> Result<Vec<String>>;
413}
414
415/// TLS policy used when building a Postgres connection string.
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum SslMode {
418 /// Plain TCP, no TLS (Local or an explicit BYO opt-out).
419 Disable,
420 /// Require TLS and verify the server certificate against a trusted CA.
421 VerifyCa,
422 /// Require TLS and verify both the trusted CA chain and the dialed hostname
423 /// (BYO / External default, Aurora, and Flexible Server).
424 VerifyFull,
425}
426
427impl SslMode {
428 /// The `sslmode` query-parameter value, and the wire form embedders (the napi addon)
429 /// hand to their own callers.
430 pub fn as_str(self) -> &'static str {
431 match self {
432 SslMode::Disable => "disable",
433 SslMode::VerifyCa => "verify-ca",
434 SslMode::VerifyFull => "verify-full",
435 }
436 }
437}
438
439/// Invalid PEM roots supplied to a verified Postgres TLS policy.
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub struct InvalidPostgresCaCertificates;
442
443impl std::fmt::Display for InvalidPostgresCaCertificates {
444 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445 f.write_str("expected one or more non-empty PEM certificates")
446 }
447}
448
449impl std::error::Error for InvalidPostgresCaCertificates {}
450
451/// A complete Postgres TLS policy.
452///
453/// The fields are private so callers cannot pair plaintext with CA roots or construct
454/// `verify-ca` without a root. Cloning the policy is cheap: certificate bundles are
455/// reference-counted and copied only when crossing an FFI boundary.
456#[derive(Clone, PartialEq, Eq)]
457pub struct PostgresTlsPolicy {
458 sslmode: SslMode,
459 ca_certificates: Arc<[String]>,
460}
461
462impl std::fmt::Debug for PostgresTlsPolicy {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 f.debug_struct("PostgresTlsPolicy")
465 .field("sslmode", &self.sslmode)
466 .field("ca_certificate_count", &self.ca_certificates.len())
467 .finish()
468 }
469}
470
471impl PostgresTlsPolicy {
472 /// Plain TCP with no certificate roots.
473 pub fn disabled() -> Self {
474 Self {
475 sslmode: SslMode::Disable,
476 ca_certificates: Arc::default(),
477 }
478 }
479
480 /// TLS with CA verification but no hostname verification.
481 ///
482 /// This mode requires at least one valid PEM root.
483 pub fn verify_ca(
484 ca_certificates: Vec<String>,
485 ) -> std::result::Result<Self, InvalidPostgresCaCertificates> {
486 Self::verified(SslMode::VerifyCa, ca_certificates, true)
487 }
488
489 /// TLS with CA and hostname verification.
490 ///
491 /// An empty root set intentionally selects the runtime's system trust store.
492 pub fn verify_full(
493 ca_certificates: Vec<String>,
494 ) -> std::result::Result<Self, InvalidPostgresCaCertificates> {
495 Self::verified(SslMode::VerifyFull, ca_certificates, false)
496 }
497
498 /// TLS with CA and hostname verification using the runtime's system trust store.
499 pub fn verify_full_with_system_roots() -> Self {
500 Self {
501 sslmode: SslMode::VerifyFull,
502 ca_certificates: Arc::default(),
503 }
504 }
505
506 fn verified(
507 sslmode: SslMode,
508 ca_certificates: Vec<String>,
509 require_roots: bool,
510 ) -> std::result::Result<Self, InvalidPostgresCaCertificates> {
511 if (require_roots && ca_certificates.is_empty())
512 || ca_certificates.iter().any(|certificate| {
513 let certificate = certificate.trim();
514 !certificate.starts_with("-----BEGIN CERTIFICATE-----")
515 || !certificate.ends_with("-----END CERTIFICATE-----")
516 })
517 {
518 return Err(InvalidPostgresCaCertificates);
519 }
520
521 Ok(Self {
522 sslmode,
523 ca_certificates: ca_certificates.into(),
524 })
525 }
526
527 /// The libpq-compatible `sslmode` represented by this complete policy.
528 pub fn sslmode(&self) -> SslMode {
529 self.sslmode
530 }
531
532 /// PEM-encoded roots, or an empty slice when this policy uses no roots or the
533 /// runtime's system trust store.
534 pub fn ca_certificates(&self) -> &[String] {
535 &self.ca_certificates
536 }
537}
538
539/// Resolved connection details for a Postgres database.
540#[derive(Clone)]
541pub struct PostgresConnectionParams {
542 pub host: String,
543 pub port: u16,
544 pub database: String,
545 pub username: String,
546 pub password: String,
547 pub tls: PostgresTlsPolicy,
548}
549
550// Hand-written Debug so the resolved password never reaches logs, error chains, or panic output
551// (`Binding`/`BindingsProviderApi` require Debug). Mirrors the KV providers' redacting Debug.
552impl std::fmt::Debug for PostgresConnectionParams {
553 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554 f.debug_struct("PostgresConnectionParams")
555 .field("host", &self.host)
556 .field("port", &self.port)
557 .field("database", &self.database)
558 .field("username", &self.username)
559 .field("password", &"<redacted>")
560 .field("tls", &self.tls)
561 .finish()
562 }
563}
564
565impl PostgresConnectionParams {
566 /// Creates resolved connection details from a complete, internally consistent TLS
567 /// policy.
568 pub fn new(
569 host: String,
570 port: u16,
571 database: String,
572 username: String,
573 password: String,
574 tls: PostgresTlsPolicy,
575 ) -> Self {
576 Self {
577 host,
578 port,
579 database,
580 username,
581 password,
582 tls,
583 }
584 }
585
586 /// The libpq-compatible TLS mode used by this connection.
587 pub fn sslmode(&self) -> SslMode {
588 self.tls.sslmode()
589 }
590
591 /// PEM-encoded root CA certificates.
592 pub fn ca_certificates(&self) -> &[String] {
593 self.tls.ca_certificates()
594 }
595
596 /// Builds a `postgres://` URL. Username and password are percent-encoded so a
597 /// generated password containing URL-special characters can never corrupt it.
598 pub fn connection_string(&self) -> String {
599 format!(
600 "postgres://{}:{}@{}:{}/{}?sslmode={}",
601 encode_userinfo(&self.username),
602 encode_userinfo(&self.password),
603 self.host,
604 self.port,
605 // Encode the database path segment so this URL stays byte-identical to the TS
606 // resolver's `encodeUserinfo` (the same RFC 3986 unreserved-set encoding).
607 encode_userinfo(&self.database),
608 self.sslmode().as_str(),
609 )
610 }
611}
612
613/// Percent-encodes a URL userinfo component; the RFC 3986 unreserved set passes through.
614fn encode_userinfo(value: &str) -> String {
615 let mut out = String::with_capacity(value.len());
616 for byte in value.bytes() {
617 match byte {
618 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
619 out.push(byte as char)
620 }
621 _ => out.push_str(&format!("%{:02X}", byte)),
622 }
623 }
624 out
625}
626
627/// Connection-only Postgres binding. Unlike every other resource, Postgres ships no
628/// gRPC service and wraps no operations (by design): every backend
629/// speaks the same wire protocol, so the binding returns connection details and the
630/// application uses its own driver.
631pub trait Postgres: Binding {
632 fn connection_params(&self) -> &PostgresConnectionParams;
633
634 /// `postgres://` connection string, derived from `connection_params` and never stored.
635 fn connection_string(&self) -> String {
636 self.connection_params().connection_string()
637 }
638}
639
640/// Represents options for put operations in KV stores.
641#[derive(Debug, Clone, Default)]
642pub struct PutOptions {
643 /// Optional TTL for automatic expiration (soft hint - items MAY be deleted after expiry)
644 pub ttl: Option<Duration>,
645 /// Only put if the key does not exist
646 pub if_not_exists: bool,
647}
648
649/// Represents the result of a scan operation.
650#[derive(Debug)]
651pub struct ScanResult {
652 /// Key-value pairs found (may be ≤ limit, no guarantee to fill)
653 pub items: Vec<(String, Vec<u8>)>,
654 /// Opaque cursor for pagination. None if no more results.
655 /// **Warning**: Cursor may become invalid if data changes. No TTL guarantees.
656 pub next_cursor: Option<String>,
657}
658
659/// A trait for key-value store bindings that provide minimal, platform-agnostic KV operations.
660/// This API is designed to work consistently across DynamoDB, Firestore, Redis, and Azure Table Storage.
661#[async_trait]
662pub trait Kv: Binding {
663 /// Get a value by key. Returns None if key doesn't exist or has expired.
664 ///
665 /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. If `now >= expires_at`,
666 /// implementations SHOULD behave as if the key is absent, even if the item still exists
667 /// physically in the backend. Physical deletion is eventual and not guaranteed.
668 ///
669 /// **Validation**: Keys are validated against MAX_KEY_BYTES and portable charset.
670 /// Invalid keys return `KvError::InvalidKey` immediately.
671 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
672
673 /// Put a value with optional options. When options.if_not_exists is true, returns true if created,
674 /// false if already exists. When options.if_not_exists is false or options is None, always returns true.
675 ///
676 /// **Size Limits**:
677 /// - Keys: ≤ MAX_KEY_BYTES (512 bytes) with portable ASCII charset
678 /// - Values: ≤ MAX_VALUE_BYTES (24,576 bytes = 24 KiB)
679 ///
680 /// **Validation**: Size and charset constraints are enforced before backend calls.
681 /// Invalid inputs return `KvError::InvalidKey` or `KvError::InvalidValue` immediately.
682 ///
683 /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. If TTL is specified,
684 /// item expires at `put_time + ttl`. Expired items SHOULD appear absent on subsequent
685 /// reads, but physical deletion is eventual and not guaranteed.
686 ///
687 /// **Conditional Logic**: The if_not_exists operation maps to backend primitives:
688 /// - Redis: SETNX
689 /// - DynamoDB: PutItem with condition_expression="attribute_not_exists(pk)"
690 /// - Firestore: create() with Precondition::DoesNotExist
691 /// - Azure Table Storage: InsertEntity (409 on conflict)
692 async fn put(&self, key: &str, value: Vec<u8>, options: Option<PutOptions>) -> Result<bool>;
693
694 /// Delete a key. No error if key doesn't exist.
695 ///
696 /// **Validation**: Keys are validated against MAX_KEY_BYTES and portable charset.
697 /// Invalid keys return `KvError::InvalidKey` immediately.
698 async fn delete(&self, key: &str) -> Result<()>;
699
700 /// Check if a key exists without retrieving the value.
701 ///
702 /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. If `now >= expires_at`,
703 /// SHOULD return false even if physically present. Physical deletion is eventual and not guaranteed.
704 ///
705 /// **Validation**: Keys are validated against MAX_KEY_BYTES and portable charset.
706 /// Invalid keys return `KvError::InvalidKey` immediately.
707 async fn exists(&self, key: &str) -> Result<bool>;
708
709 /// Scan keys with a prefix, with pagination support.
710 ///
711 /// **Scan Contract**:
712 /// - Returns an **arbitrary, unordered subset** in backend-natural order
713 /// - **No ordering guarantees** across backends (Redis SCAN, Azure fan-out, etc.)
714 /// - **May return ≤ limit items** (not guaranteed to fill even if more data exists)
715 /// - **Clients MUST de-duplicate** keys across pages (backends may return duplicates)
716 /// - **No completeness guarantee** under concurrent writes (may miss or duplicate)
717 ///
718 /// **Cursor Behavior**:
719 /// - Opaque string, implementation-specific format
720 /// - **May become invalid** anytime after backend state changes
721 /// - **No TTL guarantees** - can expire without notice
722 /// - Passing invalid cursor should return error, not partial results
723 ///
724 /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. Expired items SHOULD
725 /// be filtered out from results, but physical deletion is eventual and not guaranteed.
726 ///
727 /// **Validation**: Prefix follows same key validation rules.
728 /// Invalid prefix returns `KvError::InvalidKey` immediately.
729 async fn scan_prefix(
730 &self,
731 prefix: &str,
732 limit: Option<usize>,
733 cursor: Option<String>,
734 ) -> Result<ScanResult>;
735}
736
737/// JSON/Text message payload for Queue
738#[derive(Debug, Clone, Serialize, Deserialize)]
739#[serde(tag = "type", rename_all = "lowercase")]
740#[cfg_attr(feature = "openapi", derive(ToSchema))]
741pub enum MessagePayload {
742 /// JSON-serializable value
743 Json(serde_json::Value),
744 /// UTF-8 text payload
745 Text(String),
746}
747
748/// A queue message with payload and receipt handle for acknowledgment
749#[derive(Debug, Clone, Serialize, Deserialize)]
750#[serde(rename_all = "camelCase")]
751#[cfg_attr(feature = "openapi", derive(ToSchema))]
752pub struct QueueMessage {
753 /// JSON-first message payload
754 pub payload: MessagePayload,
755 /// Opaque receipt handle for acknowledgment (backend-specific, short-lived)
756 pub receipt_handle: String,
757 /// Delivery attempt for this message, 1-based (1 = first delivery).
758 ///
759 /// Providers that do not report redelivery counts always set 1; the local
760 /// provider reports the real per-message count so handlers can enforce
761 /// retry limits.
762 #[serde(default = "first_attempt")]
763 pub attempt: u32,
764}
765
766/// Serde default for [`QueueMessage::attempt`]: treat missing counts as the
767/// first delivery.
768fn first_attempt() -> u32 {
769 1
770}
771
772/// Maximum message size in bytes (64 KiB = 65,536 bytes)
773///
774/// This limit ensures compatibility across all queue backends:
775/// - **AWS SQS**: 256KB message limit (much higher, not constraining)
776/// - **Azure Service Bus**: 1MB message limit (much higher, not constraining)
777/// - **GCP Pub/Sub**: 10MB message limit (much higher, not constraining)
778///
779/// The 64KB limit provides:
780/// - Reasonable message sizes for most use cases
781/// - Fast network transfer and low latency
782/// - Consistent behavior across all cloud providers
783/// - Efficient memory usage during batch processing
784pub const MAX_MESSAGE_BYTES: usize = 65_536; // 64 KiB
785
786/// Maximum number of messages per receive call
787///
788/// This limit balances throughput with processing simplicity:
789/// - **AWS SQS**: Supports up to 10 messages per ReceiveMessage call
790/// - **Azure Service Bus**: Can receive multiple messages via prefetch/batching
791/// - **GCP Pub/Sub**: Supports configurable max_messages per Pull request
792///
793/// The 10-message limit ensures:
794/// - Portable batch sizes across all backends
795/// - Manageable memory usage
796/// - Reasonable processing latency per batch
797pub const MAX_BATCH_SIZE: usize = 10;
798
799/// Fixed lease duration in seconds
800///
801/// Messages are leased for exactly 30 seconds after delivery:
802/// - Long enough for most processing tasks
803/// - Short enough to enable fast retry on failures
804/// - Eliminates complexity of dynamic lease management
805/// - Consistent across all platforms
806pub const LEASE_SECONDS: u64 = 30;
807
808/// A trait for queue bindings providing minimal, portable queue operations.
809#[async_trait]
810pub trait Queue: Binding {
811 /// Send a message to the specified queue
812 async fn send(&self, queue: &str, message: MessagePayload) -> Result<()>;
813
814 /// Receive up to `max_messages` (1..=10) from the specified queue
815 async fn receive(&self, queue: &str, max_messages: usize) -> Result<Vec<QueueMessage>>;
816
817 /// Acknowledge a message using its receipt handle (idempotent)
818 async fn ack(&self, queue: &str, receipt_handle: &str) -> Result<()>;
819
820 /// Negative-acknowledge a message: release its lease so it becomes
821 /// immediately available for redelivery, without waiting out the
822 /// visibility timeout. Receipt-handle rules mirror [`Queue::ack`].
823 async fn nack(&self, queue: &str, receipt_handle: &str) -> Result<()>;
824
825 /// Delete every message in the queue, whether visible or in flight.
826 async fn purge(&self, queue: &str) -> Result<()>;
827}
828
829/// Request for invoking a function directly
830#[derive(Debug, Clone, Serialize, Deserialize)]
831#[serde(rename_all = "camelCase")]
832#[cfg_attr(feature = "openapi", derive(ToSchema))]
833pub struct WorkerInvokeRequest {
834 /// Worker identifier (name, ARN, URL, etc.)
835 pub target_worker: String,
836 /// HTTP method
837 pub method: String,
838 /// Request path
839 pub path: String,
840 /// HTTP headers
841 pub headers: BTreeMap<String, String>,
842 /// Request body bytes
843 pub body: Vec<u8>,
844 /// Optional timeout for the invocation
845 pub timeout: Option<Duration>,
846}
847
848/// Response from worker invocation.
849#[derive(Debug, Clone, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851#[cfg_attr(feature = "openapi", derive(ToSchema))]
852pub struct WorkerInvokeResponse {
853 /// HTTP status code
854 pub status: u16,
855 /// HTTP response headers
856 pub headers: BTreeMap<String, String>,
857 /// Response body bytes
858 pub body: Vec<u8>,
859}
860
861/// A trait for worker bindings that enable direct worker-to-worker calls.
862#[async_trait]
863pub trait Worker: Binding {
864 /// Invoke a worker with HTTP request data.
865 ///
866 /// This enables direct, low-latency worker-to-worker communication within
867 /// the same cloud environment, bypassing Commands for internal calls.
868 ///
869 /// Platform implementations:
870 /// - AWS: Uses InvokeWorker API directly
871 /// - GCP: Calls private service URL directly
872 /// - Azure: Calls private container app URL directly
873 /// - Kubernetes: HTTP call to internal service
874 async fn invoke(&self, request: WorkerInvokeRequest) -> Result<WorkerInvokeResponse>;
875
876 /// Get the public URL of the worker, if available.
877 ///
878 /// Returns the worker's public URL if it exists and is accessible.
879 /// This is useful for exposing public endpoints or getting URLs for
880 /// external integration.
881 ///
882 /// Platform implementations:
883 /// - AWS: Uses GetWorkerUrlConfig API or returns URL from binding
884 /// - GCP: Returns Cloud Run service URL or calls get_service API
885 /// - Azure: Returns Container App URL or calls get_container_app API
886 async fn get_worker_url(&self) -> Result<Option<String>>;
887
888 /// Get a reference to this object as `Any` for dynamic casting
889 fn as_any(&self) -> &dyn std::any::Any;
890}
891
892/// A trait for container bindings that enable container-to-container communication
893#[async_trait]
894pub trait Container: Binding {
895 /// Get the internal URL for container-to-container communication.
896 ///
897 /// This returns the internal service discovery URL that other containers
898 /// in the same network can use to communicate with this container.
899 ///
900 /// Platform implementations:
901 /// - Managed cloud (AWS/GCP/Azure): Returns internal DNS URL (e.g., "http://api.svc:8080")
902 /// - Local (Docker): Returns Docker network DNS URL (e.g., "http://api.svc:3000")
903 fn get_internal_url(&self) -> &str;
904
905 /// Get the public URL of the container, if available.
906 ///
907 /// Returns the container's public URL if it exists and is accessible
908 /// from outside the cluster/network.
909 ///
910 /// Platform implementations:
911 /// - Managed cloud: Returns load balancer URL if exposed publicly
912 /// - Local: Returns localhost URL with mapped port (e.g., "http://localhost:62844")
913 fn get_public_url(&self) -> Option<&str>;
914
915 /// Get the container name/ID.
916 fn get_container_name(&self) -> &str;
917
918 /// Get a reference to this object as `Any` for dynamic casting
919 fn as_any(&self) -> &dyn std::any::Any;
920}
921
922/// A provider must implement methods to load the various types of bindings
923/// based on environment variables or other configuration sources.
924#[async_trait]
925pub trait BindingsProviderApi: Send + Sync + std::fmt::Debug {
926 /// Given a binding identifier, builds a Storage implementation.
927 async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>>;
928
929 /// Given a binding identifier, builds a Build implementation.
930 async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>>;
931
932 /// Given a binding identifier, builds an ArtifactRegistry implementation.
933 async fn load_artifact_registry(&self, binding_name: &str)
934 -> Result<Arc<dyn ArtifactRegistry>>;
935
936 /// Given a binding identifier, builds a Vault implementation.
937 async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>>;
938
939 /// Given a binding identifier, builds a KV implementation.
940 async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>>;
941
942 /// Given a binding identifier, builds a Postgres implementation.
943 ///
944 /// Every backend is resolved here. Local and External carry their password inline;
945 /// Aurora, Cloud SQL, and Azure Flexible Server carry only a locator for it and read
946 /// the value from that cloud's secret store during this call, using the workload's own
947 /// identity. Resolution happens once per load, so the returned handle is synchronous.
948 async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>>;
949
950 /// Given a binding identifier, builds a Queue implementation.
951 async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>>;
952
953 /// Given a binding identifier, builds a Worker implementation.
954 async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>>;
955
956 /// Given a binding identifier, builds a Container implementation.
957 async fn load_container(&self, binding_name: &str) -> Result<Arc<dyn Container>>;
958
959 /// Given a binding identifier, builds a ServiceAccount implementation.
960 async fn load_service_account(&self, binding_name: &str) -> Result<Arc<dyn ServiceAccount>>;
961
962 /// Runtime-only binding env vars (a local Postgres connection with its password, a local
963 /// BYO-key AI binding) for the given resource — re-resolved on every (re)start so the secret
964 /// reaches the worker process but is never written to persisted worker metadata. The resource
965 /// type routes resolution to the right local source. Default `None`: cloud providers carry a
966 /// secret locator (not a raw secret) and use the normal persisted path.
967 async fn resolve_runtime_only_binding_env(
968 &self,
969 _binding_name: &str,
970 _resource_type: &str,
971 ) -> Result<Option<std::collections::HashMap<String, String>>> {
972 Ok(None)
973 }
974}