Skip to main content

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 mode used when building a Postgres connection string.
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum SslMode {
418    /// Plain TCP, no TLS (Local).
419    Disable,
420    /// Use TLS if the server offers it, otherwise plain (BYO / External default).
421    Prefer,
422    /// Require TLS (cloud).
423    Require,
424}
425
426impl SslMode {
427    fn as_str(self) -> &'static str {
428        match self {
429            SslMode::Disable => "disable",
430            SslMode::Prefer => "prefer",
431            SslMode::Require => "require",
432        }
433    }
434}
435
436/// Resolved connection details for a Postgres database.
437#[derive(Clone)]
438pub struct PostgresConnectionParams {
439    pub host: String,
440    pub port: u16,
441    pub database: String,
442    pub username: String,
443    pub password: String,
444    pub sslmode: SslMode,
445}
446
447// Hand-written Debug so the resolved password never reaches logs, error chains, or panic output
448// (`Binding`/`BindingsProviderApi` require Debug). Mirrors the KV providers' redacting Debug.
449impl std::fmt::Debug for PostgresConnectionParams {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        f.debug_struct("PostgresConnectionParams")
452            .field("host", &self.host)
453            .field("port", &self.port)
454            .field("database", &self.database)
455            .field("username", &self.username)
456            .field("password", &"<redacted>")
457            .field("sslmode", &self.sslmode)
458            .finish()
459    }
460}
461
462impl PostgresConnectionParams {
463    /// Builds a `postgres://` URL. Username and password are percent-encoded so a
464    /// generated password containing URL-special characters can never corrupt it.
465    pub fn connection_string(&self) -> String {
466        format!(
467            "postgres://{}:{}@{}:{}/{}?sslmode={}",
468            encode_userinfo(&self.username),
469            encode_userinfo(&self.password),
470            self.host,
471            self.port,
472            // Encode the database path segment so this URL stays byte-identical to the TS
473            // resolver's `encodeUserinfo` (the same RFC 3986 unreserved-set encoding).
474            encode_userinfo(&self.database),
475            self.sslmode.as_str(),
476        )
477    }
478}
479
480/// Percent-encodes a URL userinfo component; the RFC 3986 unreserved set passes through.
481fn encode_userinfo(value: &str) -> String {
482    let mut out = String::with_capacity(value.len());
483    for byte in value.bytes() {
484        match byte {
485            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
486                out.push(byte as char)
487            }
488            _ => out.push_str(&format!("%{:02X}", byte)),
489        }
490    }
491    out
492}
493
494/// Connection-only Postgres binding. Unlike every other resource, Postgres ships no
495/// gRPC service and wraps no operations (by design): every backend
496/// speaks the same wire protocol, so the binding returns connection details and the
497/// application uses its own driver.
498pub trait Postgres: Binding {
499    fn connection_params(&self) -> PostgresConnectionParams;
500
501    /// `postgres://` connection string, derived from `connection_params` and never stored.
502    fn connection_string(&self) -> String {
503        self.connection_params().connection_string()
504    }
505}
506
507/// Represents options for put operations in KV stores.
508#[derive(Debug, Clone, Default)]
509pub struct PutOptions {
510    /// Optional TTL for automatic expiration (soft hint - items MAY be deleted after expiry)
511    pub ttl: Option<Duration>,
512    /// Only put if the key does not exist
513    pub if_not_exists: bool,
514}
515
516/// Represents the result of a scan operation.
517#[derive(Debug)]
518pub struct ScanResult {
519    /// Key-value pairs found (may be ≤ limit, no guarantee to fill)
520    pub items: Vec<(String, Vec<u8>)>,
521    /// Opaque cursor for pagination. None if no more results.
522    /// **Warning**: Cursor may become invalid if data changes. No TTL guarantees.
523    pub next_cursor: Option<String>,
524}
525
526/// A trait for key-value store bindings that provide minimal, platform-agnostic KV operations.
527/// This API is designed to work consistently across DynamoDB, Firestore, Redis, and Azure Table Storage.
528#[async_trait]
529pub trait Kv: Binding {
530    /// Get a value by key. Returns None if key doesn't exist or has expired.
531    ///
532    /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. If `now >= expires_at`,
533    /// implementations SHOULD behave as if the key is absent, even if the item still exists
534    /// physically in the backend. Physical deletion is eventual and not guaranteed.
535    ///
536    /// **Validation**: Keys are validated against MAX_KEY_BYTES and portable charset.
537    /// Invalid keys return `KvError::InvalidKey` immediately.
538    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
539
540    /// Put a value with optional options. When options.if_not_exists is true, returns true if created,
541    /// false if already exists. When options.if_not_exists is false or options is None, always returns true.
542    ///
543    /// **Size Limits**:
544    /// - Keys: ≤ MAX_KEY_BYTES (512 bytes) with portable ASCII charset
545    /// - Values: ≤ MAX_VALUE_BYTES (24,576 bytes = 24 KiB)
546    ///
547    /// **Validation**: Size and charset constraints are enforced before backend calls.
548    /// Invalid inputs return `KvError::InvalidKey` or `KvError::InvalidValue` immediately.
549    ///
550    /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. If TTL is specified,
551    /// item expires at `put_time + ttl`. Expired items SHOULD appear absent on subsequent
552    /// reads, but physical deletion is eventual and not guaranteed.
553    ///
554    /// **Conditional Logic**: The if_not_exists operation maps to backend primitives:
555    /// - Redis: SETNX
556    /// - DynamoDB: PutItem with condition_expression="attribute_not_exists(pk)"
557    /// - Firestore: create() with Precondition::DoesNotExist
558    /// - Azure Table Storage: InsertEntity (409 on conflict)
559    async fn put(&self, key: &str, value: Vec<u8>, options: Option<PutOptions>) -> Result<bool>;
560
561    /// Delete a key. No error if key doesn't exist.
562    ///
563    /// **Validation**: Keys are validated against MAX_KEY_BYTES and portable charset.
564    /// Invalid keys return `KvError::InvalidKey` immediately.
565    async fn delete(&self, key: &str) -> Result<()>;
566
567    /// Check if a key exists without retrieving the value.
568    ///
569    /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. If `now >= expires_at`,
570    /// SHOULD return false even if physically present. Physical deletion is eventual and not guaranteed.
571    ///
572    /// **Validation**: Keys are validated against MAX_KEY_BYTES and portable charset.
573    /// Invalid keys return `KvError::InvalidKey` immediately.
574    async fn exists(&self, key: &str) -> Result<bool>;
575
576    /// Scan keys with a prefix, with pagination support.
577    ///
578    /// **Scan Contract**:
579    /// - Returns an **arbitrary, unordered subset** in backend-natural order
580    /// - **No ordering guarantees** across backends (Redis SCAN, Azure fan-out, etc.)
581    /// - **May return ≤ limit items** (not guaranteed to fill even if more data exists)
582    /// - **Clients MUST de-duplicate** keys across pages (backends may return duplicates)
583    /// - **No completeness guarantee** under concurrent writes (may miss or duplicate)
584    ///
585    /// **Cursor Behavior**:
586    /// - Opaque string, implementation-specific format
587    /// - **May become invalid** anytime after backend state changes
588    /// - **No TTL guarantees** - can expire without notice
589    /// - Passing invalid cursor should return error, not partial results
590    ///
591    /// **TTL Behavior**: TTL is a soft hint for automatic cleanup. Expired items SHOULD
592    /// be filtered out from results, but physical deletion is eventual and not guaranteed.
593    ///
594    /// **Validation**: Prefix follows same key validation rules.
595    /// Invalid prefix returns `KvError::InvalidKey` immediately.
596    async fn scan_prefix(
597        &self,
598        prefix: &str,
599        limit: Option<usize>,
600        cursor: Option<String>,
601    ) -> Result<ScanResult>;
602}
603
604/// JSON/Text message payload for Queue
605#[derive(Debug, Clone, Serialize, Deserialize)]
606#[serde(tag = "type", rename_all = "lowercase")]
607#[cfg_attr(feature = "openapi", derive(ToSchema))]
608pub enum MessagePayload {
609    /// JSON-serializable value
610    Json(serde_json::Value),
611    /// UTF-8 text payload
612    Text(String),
613}
614
615/// A queue message with payload and receipt handle for acknowledgment
616#[derive(Debug, Clone, Serialize, Deserialize)]
617#[serde(rename_all = "camelCase")]
618#[cfg_attr(feature = "openapi", derive(ToSchema))]
619pub struct QueueMessage {
620    /// JSON-first message payload
621    pub payload: MessagePayload,
622    /// Opaque receipt handle for acknowledgment (backend-specific, short-lived)
623    pub receipt_handle: String,
624    /// Delivery attempt for this message, 1-based (1 = first delivery).
625    ///
626    /// Providers that do not report redelivery counts always set 1; the local
627    /// provider reports the real per-message count so handlers can enforce
628    /// retry limits.
629    #[serde(default = "first_attempt")]
630    pub attempt: u32,
631}
632
633/// Serde default for [`QueueMessage::attempt`]: treat missing counts as the
634/// first delivery.
635fn first_attempt() -> u32 {
636    1
637}
638
639/// Maximum message size in bytes (64 KiB = 65,536 bytes)
640///
641/// This limit ensures compatibility across all queue backends:
642/// - **AWS SQS**: 256KB message limit (much higher, not constraining)
643/// - **Azure Service Bus**: 1MB message limit (much higher, not constraining)  
644/// - **GCP Pub/Sub**: 10MB message limit (much higher, not constraining)
645///
646/// The 64KB limit provides:
647/// - Reasonable message sizes for most use cases
648/// - Fast network transfer and low latency
649/// - Consistent behavior across all cloud providers
650/// - Efficient memory usage during batch processing
651pub const MAX_MESSAGE_BYTES: usize = 65_536; // 64 KiB
652
653/// Maximum number of messages per receive call
654///
655/// This limit balances throughput with processing simplicity:
656/// - **AWS SQS**: Supports up to 10 messages per ReceiveMessage call
657/// - **Azure Service Bus**: Can receive multiple messages via prefetch/batching
658/// - **GCP Pub/Sub**: Supports configurable max_messages per Pull request
659///
660/// The 10-message limit ensures:
661/// - Portable batch sizes across all backends
662/// - Manageable memory usage
663/// - Reasonable processing latency per batch
664pub const MAX_BATCH_SIZE: usize = 10;
665
666/// Fixed lease duration in seconds
667///
668/// Messages are leased for exactly 30 seconds after delivery:
669/// - Long enough for most processing tasks
670/// - Short enough to enable fast retry on failures
671/// - Eliminates complexity of dynamic lease management
672/// - Consistent across all platforms
673pub const LEASE_SECONDS: u64 = 30;
674
675/// A trait for queue bindings providing minimal, portable queue operations.
676#[async_trait]
677pub trait Queue: Binding {
678    /// Send a message to the specified queue
679    async fn send(&self, queue: &str, message: MessagePayload) -> Result<()>;
680
681    /// Receive up to `max_messages` (1..=10) from the specified queue
682    async fn receive(&self, queue: &str, max_messages: usize) -> Result<Vec<QueueMessage>>;
683
684    /// Acknowledge a message using its receipt handle (idempotent)
685    async fn ack(&self, queue: &str, receipt_handle: &str) -> Result<()>;
686
687    /// Negative-acknowledge a message: release its lease so it becomes
688    /// immediately available for redelivery, without waiting out the
689    /// visibility timeout. Receipt-handle rules mirror [`Queue::ack`].
690    async fn nack(&self, queue: &str, receipt_handle: &str) -> Result<()>;
691
692    /// Delete every message in the queue, whether visible or in flight.
693    async fn purge(&self, queue: &str) -> Result<()>;
694}
695
696/// Request for invoking a function directly
697#[derive(Debug, Clone, Serialize, Deserialize)]
698#[serde(rename_all = "camelCase")]
699#[cfg_attr(feature = "openapi", derive(ToSchema))]
700pub struct WorkerInvokeRequest {
701    /// Worker identifier (name, ARN, URL, etc.)
702    pub target_worker: String,
703    /// HTTP method
704    pub method: String,
705    /// Request path
706    pub path: String,
707    /// HTTP headers
708    pub headers: BTreeMap<String, String>,
709    /// Request body bytes
710    pub body: Vec<u8>,
711    /// Optional timeout for the invocation
712    pub timeout: Option<Duration>,
713}
714
715/// Response from worker invocation.
716#[derive(Debug, Clone, Serialize, Deserialize)]
717#[serde(rename_all = "camelCase")]
718#[cfg_attr(feature = "openapi", derive(ToSchema))]
719pub struct WorkerInvokeResponse {
720    /// HTTP status code
721    pub status: u16,
722    /// HTTP response headers
723    pub headers: BTreeMap<String, String>,
724    /// Response body bytes
725    pub body: Vec<u8>,
726}
727
728/// A trait for worker bindings that enable direct worker-to-worker calls.
729#[async_trait]
730pub trait Worker: Binding {
731    /// Invoke a worker with HTTP request data.
732    ///
733    /// This enables direct, low-latency worker-to-worker communication within
734    /// the same cloud environment, bypassing Commands for internal calls.
735    ///
736    /// Platform implementations:
737    /// - AWS: Uses InvokeWorker API directly
738    /// - GCP: Calls private service URL directly  
739    /// - Azure: Calls private container app URL directly
740    /// - Kubernetes: HTTP call to internal service
741    async fn invoke(&self, request: WorkerInvokeRequest) -> Result<WorkerInvokeResponse>;
742
743    /// Get the public URL of the worker, if available.
744    ///
745    /// Returns the worker's public URL if it exists and is accessible.
746    /// This is useful for exposing public endpoints or getting URLs for
747    /// external integration.
748    ///
749    /// Platform implementations:
750    /// - AWS: Uses GetWorkerUrlConfig API or returns URL from binding
751    /// - GCP: Returns Cloud Run service URL or calls get_service API
752    /// - Azure: Returns Container App URL or calls get_container_app API
753    async fn get_worker_url(&self) -> Result<Option<String>>;
754
755    /// Get a reference to this object as `Any` for dynamic casting
756    fn as_any(&self) -> &dyn std::any::Any;
757}
758
759/// A trait for container bindings that enable container-to-container communication
760#[async_trait]
761pub trait Container: Binding {
762    /// Get the internal URL for container-to-container communication.
763    ///
764    /// This returns the internal service discovery URL that other containers
765    /// in the same network can use to communicate with this container.
766    ///
767    /// Platform implementations:
768    /// - Managed cloud (AWS/GCP/Azure): Returns internal DNS URL (e.g., "http://api.svc:8080")
769    /// - Local (Docker): Returns Docker network DNS URL (e.g., "http://api.svc:3000")
770    fn get_internal_url(&self) -> &str;
771
772    /// Get the public URL of the container, if available.
773    ///
774    /// Returns the container's public URL if it exists and is accessible
775    /// from outside the cluster/network.
776    ///
777    /// Platform implementations:
778    /// - Managed cloud: Returns load balancer URL if exposed publicly
779    /// - Local: Returns localhost URL with mapped port (e.g., "http://localhost:62844")
780    fn get_public_url(&self) -> Option<&str>;
781
782    /// Get the container name/ID.
783    fn get_container_name(&self) -> &str;
784
785    /// Get a reference to this object as `Any` for dynamic casting
786    fn as_any(&self) -> &dyn std::any::Any;
787}
788
789/// A provider must implement methods to load the various types of bindings
790/// based on environment variables or other configuration sources.
791#[async_trait]
792pub trait BindingsProviderApi: Send + Sync + std::fmt::Debug {
793    /// Given a binding identifier, builds a Storage implementation.
794    async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>>;
795
796    /// Given a binding identifier, builds a Build implementation.
797    async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>>;
798
799    /// Given a binding identifier, builds an ArtifactRegistry implementation.
800    async fn load_artifact_registry(&self, binding_name: &str)
801        -> Result<Arc<dyn ArtifactRegistry>>;
802
803    /// Given a binding identifier, builds a Vault implementation.
804    async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>>;
805
806    /// Given a binding identifier, builds a KV implementation.
807    async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>>;
808
809    /// Given a binding identifier, builds a Postgres implementation.
810    ///
811    /// Only the **local** (developer) backend is supported here. Cloud backends (Aurora, CloudSQL,
812    /// Azure Flexible Server) are resolved by the TypeScript SDK only; a Rust worker that requests one
813    /// gets a runtime error from the local resolver, with no compile-time gate.
814    async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>>;
815
816    /// Given a binding identifier, builds a Queue implementation.
817    async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>>;
818
819    /// Given a binding identifier, builds a Worker implementation.
820    async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>>;
821
822    /// Given a binding identifier, builds a Container implementation.
823    async fn load_container(&self, binding_name: &str) -> Result<Arc<dyn Container>>;
824
825    /// Given a binding identifier, builds a ServiceAccount implementation.
826    async fn load_service_account(&self, binding_name: &str) -> Result<Arc<dyn ServiceAccount>>;
827
828    /// Runtime-only binding env vars (a local Postgres connection with its password) for the given
829    /// resource — re-resolved on every (re)start so the secret reaches the worker process but is
830    /// never written to persisted worker metadata. Default `None`: cloud providers carry a secret
831    /// locator (not a password) and use the normal persisted path.
832    async fn resolve_runtime_only_binding_env(
833        &self,
834        _binding_name: &str,
835    ) -> Result<Option<std::collections::HashMap<String, String>>> {
836        Ok(None)
837    }
838}