Skip to main content

alien_bindings/
error.rs

1use alien_error::{AlienError, AlienErrorData, ContextError};
2use serde::{Deserialize, Serialize};
3
4/// Derives the exact `ALIEN_<NAME>_BINDING` environment variable name that would have
5/// configured a binding with the given name — the same derivation `alien-core` uses to
6/// generate binding env vars, so this is guaranteed to match `parse_bindings_from_env`'s
7/// reverse parsing (see `provider.rs`).
8pub fn binding_env_var(binding_name: &str) -> String {
9    alien_core::bindings::binding_env_var_name(binding_name)
10}
11
12/// Errors related to alien-bindings operations.
13#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum ErrorData {
16    /// Plaintext or context is outside the portable Key binding limits.
17    #[error(
18        code = "KEY_INPUT_INVALID",
19        message = "Key input is invalid: {reason}",
20        retryable = "false",
21        internal = "false",
22        http_status_code = 400
23    )]
24    KeyInputInvalid { reason: String },
25
26    /// Decrypted provider data is not a valid Alien Key frame.
27    #[error(
28        code = "KEY_CIPHERTEXT_INVALID",
29        message = "Key ciphertext is invalid: {reason}",
30        retryable = "false",
31        internal = "false",
32        http_status_code = 400
33    )]
34    KeyCiphertextInvalid { reason: String },
35
36    /// No binding configuration was found for the requested binding name (the
37    /// `ALIEN_<NAME>_BINDING` environment variable was not set).
38    #[error(
39        code = "BINDING_NOT_CONFIGURED",
40        message = "No binding configured for '{binding_name}': environment variable '{env_var}' is not set",
41        retryable = "false",
42        internal = "false",
43        http_status_code = 400
44    )]
45    BindingNotConfigured {
46        /// Name of the binding that was requested
47        binding_name: String,
48        /// The exact environment variable name that would configure this binding
49        env_var: String,
50    },
51
52    /// Binding provider configuration is invalid or missing.
53    #[error(
54        code = "BINDING_CONFIG_INVALID",
55        message = "Binding configuration invalid for binding '{binding_name}' (env var '{env_var}'): {reason}",
56        retryable = "false",
57        internal = "false",
58        http_status_code = 400
59    )]
60    BindingConfigInvalid {
61        /// Name of the binding
62        binding_name: String,
63        /// The exact environment variable name that configures this binding
64        env_var: String,
65        /// Specific reason why the configuration is invalid
66        reason: String,
67    },
68
69    /// Binding configuration named a provider that this build does not support.
70    #[error(
71        code = "UNSUPPORTED_BINDING_PROVIDER",
72        message = "Binding '{binding_name}' (env var '{env_var}') uses unsupported provider '{provider}'",
73        retryable = "false",
74        internal = "false",
75        http_status_code = 501
76    )]
77    UnsupportedBindingProvider {
78        /// Name of the binding
79        binding_name: String,
80        /// The exact environment variable name that configures this binding
81        env_var: String,
82        /// The provider string that is not supported
83        provider: String,
84    },
85
86    /// Storage operation failed due to provider issues.
87    #[error(
88        code = "STORAGE_OPERATION_FAILED",
89        message = "Storage operation failed for binding '{binding_name}': {operation}",
90        retryable = "true",
91        internal = "false",
92        http_status_code = 502
93    )]
94    StorageOperationFailed {
95        /// Name of the storage binding
96        binding_name: String,
97        /// Description of the operation that failed
98        operation: String,
99    },
100
101    /// Storage credentials are missing permission for an operation.
102    #[error(
103        code = "STORAGE_ACCESS_DENIED",
104        message = "Storage access denied for binding '{binding_name}' while attempting to {operation}",
105        retryable = "false",
106        internal = "false",
107        http_status_code = 403,
108        hint = "Check that the customer's Storage connection is active and its Access identity still has the required permissions."
109    )]
110    StorageAccessDenied {
111        /// Name of the storage binding.
112        binding_name: String,
113        /// Provider-independent operation name.
114        operation: String,
115    },
116
117    /// The requested object does not exist.
118    #[error(
119        code = "STORAGE_OBJECT_NOT_FOUND",
120        message = "Storage object not found for binding '{binding_name}' while attempting to {operation}",
121        retryable = "false",
122        internal = "false",
123        http_status_code = 404
124    )]
125    StorageObjectNotFound {
126        /// Name of the storage binding.
127        binding_name: String,
128        /// Provider-independent operation name.
129        operation: String,
130    },
131
132    /// Build operation failed due to provider issues.
133    #[error(
134        code = "BUILD_OPERATION_FAILED",
135        message = "Build operation failed for binding '{binding_name}': {operation}",
136        retryable = "true",
137        internal = "false",
138        http_status_code = 502
139    )]
140    BuildOperationFailed {
141        /// Name of the build binding
142        binding_name: String,
143        /// Description of the operation that failed
144        operation: String,
145    },
146
147    /// Required environment variable is missing or invalid.
148    #[error(
149        code = "ENVIRONMENT_VARIABLE_MISSING",
150        message = "Required environment variable '{variable_name}' is missing",
151        retryable = "false",
152        internal = "false",
153        http_status_code = 500
154    )]
155    EnvironmentVariableMissing {
156        /// Name of the missing environment variable
157        variable_name: String,
158    },
159
160    /// Environment variable has an invalid value.
161    #[error(
162        code = "INVALID_ENVIRONMENT_VARIABLE",
163        message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
164        retryable = "false",
165        internal = "false",
166        http_status_code = 500
167    )]
168    InvalidEnvironmentVariable {
169        /// Name of the environment variable
170        variable_name: String,
171        /// The invalid value
172        value: String,
173        /// Reason why the value is invalid
174        reason: String,
175    },
176
177    /// Configuration URL is malformed or invalid.
178    #[error(
179        code = "INVALID_CONFIGURATION_URL",
180        message = "Invalid configuration URL '{url}': {reason}",
181        retryable = "false",
182        internal = "false",
183        http_status_code = 400
184    )]
185    InvalidConfigurationUrl {
186        /// The invalid URL
187        url: String,
188        /// Specific reason why the URL is invalid
189        reason: String,
190    },
191
192    /// Event processing failed due to malformed or unsupported data.
193    #[error(
194        code = "EVENT_PROCESSING_FAILED",
195        message = "Event processing failed for type '{event_type}': {reason}",
196        retryable = "true",
197        internal = "false",
198        http_status_code = 400
199    )]
200    EventProcessingFailed {
201        /// Type of event that failed to process
202        event_type: String,
203        /// Specific reason for the processing failure
204        reason: String,
205    },
206
207    /// gRPC connection failed or became unavailable.
208    #[error(
209        code = "GRPC_CONNECTION_FAILED",
210        message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
211        retryable = "true",
212        internal = "false",
213        http_status_code = 502
214    )]
215    GrpcConnectionFailed {
216        /// The gRPC endpoint that failed to connect
217        endpoint: String,
218        /// Reason for the connection failure
219        reason: String,
220    },
221
222    /// gRPC service unavailable or returned error.
223    #[error(
224        code = "GRPC_SERVICE_UNAVAILABLE",
225        message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
226        retryable = "true",
227        internal = "false",
228        http_status_code = 503
229    )]
230    GrpcServiceUnavailable {
231        /// Name of the gRPC service
232        service: String,
233        /// The gRPC endpoint
234        endpoint: String,
235        /// Reason for service unavailability
236        reason: String,
237    },
238
239    /// gRPC request failed with an error status.
240    #[error(
241        code = "GRPC_REQUEST_FAILED",
242        message = "gRPC request to service '{service}' method '{method}' failed: {details}",
243        retryable = "true",
244        internal = "false",
245        http_status_code = 502
246    )]
247    GrpcRequestFailed {
248        /// Name of the gRPC service
249        service: String,
250        /// Name of the gRPC method
251        method: String,
252        /// Error details from the gRPC status
253        details: String,
254    },
255
256    /// Server failed to bind to the specified address.
257    #[error(
258        code = "SERVER_BIND_FAILED",
259        message = "Failed to bind server to address '{address}': {reason}",
260        retryable = "true",
261        internal = "true",
262        http_status_code = 500
263    )]
264    ServerBindFailed {
265        /// The address that failed to bind
266        address: String,
267        /// Reason for the bind failure
268        reason: String,
269    },
270
271    /// Authentication failed for the configured provider.
272    #[error(
273        code = "AUTHENTICATION_FAILED",
274        message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
275        retryable = "true",
276        internal = "false",
277        http_status_code = 401
278    )]
279    AuthenticationFailed {
280        /// Name of the provider (aws, gcp, azure, etc.)
281        provider: String,
282        /// Name of the binding
283        binding_name: String,
284        /// Reason for authentication failure
285        reason: String,
286    },
287
288    /// Operation not supported by the configured provider.
289    #[error(
290        code = "OPERATION_NOT_SUPPORTED",
291        message = "Operation '{operation}' not supported: {reason}",
292        retryable = "false",
293        internal = "false",
294        http_status_code = 501
295    )]
296    OperationNotSupported {
297        /// Name of the unsupported operation
298        operation: String,
299        /// Reason why the operation is not supported
300        reason: String,
301    },
302
303    /// A command run inside a sandbox did not complete.
304    ///
305    /// Visibility is inherited for the reason `SandboxUnreachable` gives below: what this wraps is
306    /// often a cloud client's error carrying the response text of the call that failed, and
307    /// `into_external` reads only the outermost flag — so a fixed `false` here would publish it.
308    #[error(
309        code = "SANDBOX_COMMAND_FAILED",
310        message = "Sandbox command failed ({failure}): {reason}",
311        retryable = "false",
312        internal = "inherit",
313        http_status_code = 400
314    )]
315    SandboxCommandFailed {
316        /// Diagnostic label from the provider or agent, for logs and support; callers branch on
317        /// the error code
318        failure: String,
319        /// Human-readable detail from the agent
320        reason: String,
321    },
322
323    /// A sandbox operation was dispatched and never reported its outcome.
324    ///
325    /// The counterpart to `SandboxUnreachable`: there the call did not take effect and repeating it
326    /// is safe, here it may already have taken effect, so a repeat can run the same command twice.
327    /// Visibility is inherited for the reason `SandboxUnreachable` gives below.
328    ///
329    /// 502 rather than the 503 its neighbour carries: the exchange with the sandbox broke down,
330    /// which is not the same invitation to try again later.
331    #[error(
332        code = "SANDBOX_OUTCOME_UNKNOWN",
333        message = "Sandbox operation '{operation}' did not report its outcome and may have taken effect: {reason}",
334        retryable = "false",
335        internal = "inherit",
336        http_status_code = 502
337    )]
338    SandboxOutcomeUnknown {
339        /// Operation that was in flight
340        operation: String,
341        /// What went wrong on the wire
342        reason: String,
343    },
344
345    /// A sandbox came up without a restriction its declaration asked for.
346    ///
347    /// Distinct from a refused call: the data plane accepted the request and answered, and what
348    /// it built is not what was asked for. The sandbox id is carried because the caller never
349    /// receives one — this is the failure where an operator has to be able to find what was left
350    /// behind if deleting it also failed.
351    #[error(
352        code = "SANDBOX_NOT_AS_DECLARED",
353        message = "Sandbox '{sandbox_id}' does not carry its declared {restriction}, so it cannot be used; create a new one. {reason}",
354        retryable = "false",
355        internal = "false",
356        http_status_code = 502
357    )]
358    SandboxNotAsDeclared {
359        /// Provider-scoped id of the sandbox that was built
360        sandbox_id: String,
361        /// What the declaration asked for, such as `egress policy`
362        restriction: String,
363        /// What the sandbox came up with instead
364        reason: String,
365    },
366
367    /// The sandbox agent could not be reached, or the connection dropped mid-response.
368    ///
369    /// Visibility is inherited rather than declared public: what this wraps is often the cloud
370    /// client's catch-all, which carries the raw request and response text of the call that
371    /// failed. Marking the wrapper external does not redact a source, so a fixed `false` here
372    /// would carry that text out to a caller.
373    #[error(
374        code = "SANDBOX_UNREACHABLE",
375        message = "Sandbox operation '{operation}' could not reach the agent: {reason}",
376        retryable = "true",
377        internal = "inherit",
378        http_status_code = 503
379    )]
380    SandboxUnreachable {
381        /// Operation that was in flight
382        operation: String,
383        /// What went wrong on the wire
384        reason: String,
385    },
386
387    /// Feature is not enabled in the compiled binary.
388    #[error(
389        code = "FEATURE_NOT_ENABLED",
390        message = "Feature '{feature}' is not enabled in this build",
391        retryable = "false",
392        internal = "false",
393        http_status_code = 501
394    )]
395    FeatureNotEnabled {
396        /// Name of the feature that is not enabled
397        feature: String,
398    },
399
400    /// gRPC call failed.
401    #[error(
402        code = "GRPC_CALL_FAILED",
403        message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
404        retryable = "true",
405        internal = "false",
406        http_status_code = 502
407    )]
408    GrpcCallFailed {
409        /// Name of the gRPC service
410        service: String,
411        /// Name of the gRPC method
412        method: String,
413        /// Reason for the call failure
414        reason: String,
415    },
416
417    /// Deserialization of data failed.
418    #[error(
419        code = "DESERIALIZATION_FAILED",
420        message = "Failed to deserialize {type_name}: {message}",
421        retryable = "false",
422        internal = "false",
423        http_status_code = 400
424    )]
425    DeserializationFailed {
426        /// Human-readable error message
427        message: String,
428        /// Name of the type being deserialized
429        type_name: String,
430    },
431
432    /// Serialization of data failed.
433    #[error(
434        code = "SERIALIZATION_FAILED",
435        message = "Failed to serialize data: {message}",
436        retryable = "false",
437        internal = "false",
438        http_status_code = 500
439    )]
440    SerializationFailed {
441        /// Human-readable error message
442        message: String,
443    },
444
445    /// Response format from provider API is unexpected or missing required fields.
446    #[error(
447        code = "UNEXPECTED_RESPONSE_FORMAT",
448        message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
449        retryable = "false",
450        internal = "false",
451        http_status_code = 502
452    )]
453    UnexpectedResponseFormat {
454        /// Name of the provider (aws, gcp, azure, etc.)
455        provider: String,
456        /// Name of the binding
457        binding_name: String,
458        /// Name of the missing or malformed field
459        field: String,
460        /// The full response JSON for debugging
461        response_json: String,
462    },
463
464    /// Cloud platform API error.
465    #[error(
466        code = "CLOUD_PLATFORM_ERROR",
467        message = "Cloud platform error: {message}",
468        retryable = "true",
469        internal = "false",
470        http_status_code = 502
471    )]
472    CloudPlatformError {
473        /// Human-readable description of the error
474        message: String,
475        /// Optional resource ID that was involved in the error
476        resource_id: Option<String>,
477    },
478
479    /// Reading a cloud Postgres binding's password from its secret store failed.
480    ///
481    /// The retry and visibility metadata comes from the provider error being wrapped:
482    /// throttling remains retryable, while permanent provider failures do not become
483    /// retryable merely because they happened during secret resolution.
484    ///
485    /// `secret` is the locator, never the secret value.
486    #[error(
487        code = "POSTGRES_SECRET_RESOLUTION_FAILED",
488        message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
489        retryable = "inherit",
490        internal = "inherit",
491        http_status_code = 502
492    )]
493    PostgresSecretResolutionFailed {
494        /// Name of the Postgres binding whose password could not be resolved
495        binding_name: String,
496        /// The secret locator that was read (an ARN / name / URI — never the value)
497        secret: String,
498        /// What went wrong while reading the secret
499        reason: String,
500    },
501
502    /// A cloud Postgres secret was read, but did not contain a usable password.
503    ///
504    /// Provider responses with missing, empty, or malformed values cannot become valid
505    /// by retrying the same version. `secret` is the locator, never the secret value.
506    #[error(
507        code = "POSTGRES_SECRET_VALUE_INVALID",
508        message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
509        retryable = "false",
510        internal = "false",
511        http_status_code = 502
512    )]
513    PostgresSecretValueInvalid {
514        /// Name of the Postgres binding whose password value was invalid
515        binding_name: String,
516        /// The secret locator that was read (an ARN / name / URI — never the value)
517        secret: String,
518        /// Why the returned value cannot be used as a password
519        reason: String,
520    },
521
522    /// Resource not found in the cloud platform.
523    #[error(
524        code = "RESOURCE_NOT_FOUND",
525        message = "Resource '{resource_id}' not found",
526        retryable = "false",
527        internal = "false",
528        http_status_code = 404
529    )]
530    ResourceNotFound {
531        /// ID of the resource that was not found
532        resource_id: String,
533    },
534
535    /// The requested remote resource does not exist.
536    #[error(
537        code = "REMOTE_RESOURCE_NOT_FOUND",
538        message = "{operation_context}: {resource_type} '{resource_name}' not found",
539        retryable = "false",
540        internal = "false",
541        http_status_code = 404
542    )]
543    RemoteResourceNotFound {
544        /// Context of the operation that failed (e.g., "Failed to get ECR repository details")
545        operation_context: String,
546        /// Type of the resource that was not found
547        resource_type: String,
548        /// Name of the resource that was not found
549        resource_name: String,
550    },
551
552    /// Operation conflicts with current remote resource state.
553    #[error(
554        code = "REMOTE_RESOURCE_CONFLICT",
555        message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
556        retryable = "true",
557        internal = "false",
558        http_status_code = 409
559    )]
560    RemoteResourceConflict {
561        /// Context of the operation that failed
562        operation_context: String,
563        /// Type of the resource that has a conflict
564        resource_type: String,
565        /// Name of the resource that has a conflict
566        resource_name: String,
567        /// Specific reason for the conflict
568        conflict_reason: String,
569    },
570
571    /// Access denied due to insufficient permissions.
572    #[error(
573        code = "REMOTE_ACCESS_DENIED",
574        message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
575        retryable = "true",
576        internal = "false",
577        http_status_code = 403
578    )]
579    RemoteAccessDenied {
580        /// Context of the operation that failed
581        operation_context: String,
582        /// Type of the resource access was denied to
583        resource_type: String,
584        /// Name of the resource access was denied to
585        resource_name: String,
586    },
587
588    /// Request rate limit exceeded.
589    #[error(
590        code = "RATE_LIMIT_EXCEEDED",
591        message = "{operation_context}: Rate limit exceeded - {details}",
592        retryable = "true",
593        internal = "false",
594        http_status_code = 429
595    )]
596    RateLimitExceeded {
597        /// Context of the operation that failed
598        operation_context: String,
599        /// Additional details about the rate limit
600        details: String,
601    },
602
603    /// Operation exceeded the allowed timeout.
604    #[error(
605        code = "TIMEOUT",
606        message = "{operation_context}: Operation timed out - {details}",
607        retryable = "true",
608        internal = "false",
609        http_status_code = 408
610    )]
611    Timeout {
612        /// Context of the operation that failed
613        operation_context: String,
614        /// Additional details about the timeout
615        details: String,
616    },
617
618    /// Remote service is temporarily unavailable.
619    #[error(
620        code = "REMOTE_SERVICE_UNAVAILABLE",
621        message = "{operation_context}: Service unavailable - {details}",
622        retryable = "true",
623        internal = "false",
624        http_status_code = 503
625    )]
626    RemoteServiceUnavailable {
627        /// Context of the operation that failed
628        operation_context: String,
629        /// Additional details about the service unavailability
630        details: String,
631    },
632
633    /// Quota or resource limits have been exceeded.
634    #[error(
635        code = "QUOTA_EXCEEDED",
636        message = "{operation_context}: Quota exceeded - {details}",
637        retryable = "true",
638        internal = "false",
639        http_status_code = 429
640    )]
641    QuotaExceeded {
642        /// Context of the operation that failed
643        operation_context: String,
644        /// Additional details about the quota violation
645        details: String,
646    },
647
648    /// Invalid or malformed input parameters provided to the operation.
649    #[error(
650        code = "INVALID_INPUT",
651        message = "{operation_context}: Invalid input - {details}",
652        retryable = "false",
653        internal = "false",
654        http_status_code = 400
655    )]
656    InvalidInput {
657        /// Context of the operation that failed
658        operation_context: String,
659        /// Details about what input was invalid
660        details: String,
661        /// Optional field name that was invalid
662        field_name: Option<String>,
663    },
664
665    /// Authentication with cloud provider failed.
666    #[error(
667        code = "AUTHENTICATION_ERROR",
668        message = "{operation_context}: Authentication failed - {details}",
669        retryable = "true",
670        internal = "false",
671        http_status_code = 401
672    )]
673    AuthenticationError {
674        /// Context of the operation that failed
675        operation_context: String,
676        /// Details about the authentication failure
677        details: String,
678    },
679
680    /// Generic bindings error for uncommon cases.
681    #[error(
682        code = "BINDINGS_ERROR",
683        message = "Bindings error: {message}",
684        retryable = "true",
685        internal = "true",
686        http_status_code = 500
687    )]
688    Other {
689        /// Human-readable description of the error
690        message: String,
691    },
692
693    /// Presigned request has expired and can no longer be used.
694    #[error(
695        code = "PRESIGNED_REQUEST_EXPIRED",
696        message = "Presigned request for path '{path}' expired at {expired_at}",
697        retryable = "false",
698        internal = "false",
699        http_status_code = 403
700    )]
701    PresignedRequestExpired {
702        /// Path that the presigned request was for
703        path: String,
704        /// When the request expired
705        expired_at: chrono::DateTime<chrono::Utc>,
706    },
707
708    /// HTTP request to external service failed.
709    #[error(
710        code = "HTTP_REQUEST_FAILED",
711        message = "HTTP {method} request to '{url}' failed",
712        retryable = "true",
713        internal = "false",
714        http_status_code = 502
715    )]
716    HttpRequestFailed {
717        /// URL that was requested
718        url: String,
719        /// HTTP method that was used
720        method: String,
721    },
722
723    /// Local filesystem operation failed.
724    #[error(
725        code = "LOCAL_FILESYSTEM_ERROR",
726        message = "Local filesystem operation '{operation}' failed for path '{path}'",
727        retryable = "true",
728        internal = "false",
729        http_status_code = 500
730    )]
731    LocalFilesystemError {
732        /// Path that the operation was performed on
733        path: String,
734        /// Operation that failed
735        operation: String,
736    },
737
738    /// Failed to load platform configuration for the provider.
739    #[error(
740        code = "client_config_LOAD_FAILED",
741        message = "Failed to load platform configuration for provider '{provider}'",
742        retryable = "false",
743        internal = "false",
744        http_status_code = 400
745    )]
746    ClientConfigLoadFailed {
747        /// Name of the provider (aws, gcp, azure, etc.)
748        provider: String,
749    },
750
751    /// Binding setup failed during initialization.
752    #[error(
753        code = "BINDING_SETUP_FAILED",
754        message = "Binding setup failed for type '{binding_type}': {reason}",
755        retryable = "false",
756        internal = "false",
757        http_status_code = 500
758    )]
759    BindingSetupFailed {
760        /// Type of binding being set up
761        binding_type: String,
762        /// Reason for the setup failure
763        reason: String,
764    },
765
766    /// KV operation failed.
767    #[error(
768        code = "KV_OPERATION_FAILED",
769        message = "KV operation '{operation}' failed for key '{key}': {reason}",
770        retryable = "true",
771        internal = "false",
772        http_status_code = 502
773    )]
774    KvOperationFailed {
775        /// The KV operation that failed
776        operation: String,
777        /// The key involved in the operation
778        key: String,
779        /// Reason for the operation failure
780        reason: String,
781    },
782
783    /// Queue operation failed.
784    #[error(
785        code = "QUEUE_OPERATION_FAILED",
786        message = "Queue operation '{operation}' failed: {reason}",
787        retryable = "true",
788        internal = "false",
789        http_status_code = 502
790    )]
791    QueueOperationFailed {
792        /// The queue operation that failed
793        operation: String,
794        /// Reason for the operation failure
795        reason: String,
796    },
797
798    /// Remote access to deployment resources failed.
799    #[error(
800        code = "REMOTE_ACCESS_FAILED",
801        message = "Remote access failed during operation: {operation}",
802        retryable = "true",
803        internal = "false",
804        http_status_code = 502
805    )]
806    RemoteAccessFailed {
807        /// Description of the operation that failed
808        operation: String,
809    },
810
811    /// Client configuration is invalid or missing for the platform.
812    #[error(
813        code = "CLIENT_CONFIG_INVALID",
814        message = "Client configuration invalid for platform '{platform}': {message}",
815        retryable = "false",
816        internal = "false",
817        http_status_code = 400
818    )]
819    ClientConfigInvalid {
820        /// The platform that was expected
821        platform: alien_core::Platform,
822        /// Description of the configuration issue
823        message: String,
824    },
825}
826
827impl ErrorData {
828    /// Construct a [`ErrorData::BindingConfigInvalid`] for `binding_name`,
829    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally so call
830    /// sites cannot forget it (the omission that repeatedly broke bindings).
831    pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
832        ErrorData::BindingConfigInvalid {
833            env_var: binding_env_var(binding_name),
834            binding_name: binding_name.to_string(),
835            reason: reason.into(),
836        }
837    }
838
839    /// Construct a [`ErrorData::BindingNotConfigured`] for `binding_name`,
840    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally.
841    pub fn not_configured(binding_name: &str) -> Self {
842        ErrorData::BindingNotConfigured {
843            binding_name: binding_name.to_string(),
844            env_var: binding_env_var(binding_name),
845        }
846    }
847}
848
849/// Convenient alias with default error type `ErrorData`.
850pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
851
852/// Convenience alias representing a constructed AlienError with our `ErrorData` payload.
853pub type Error = AlienError<ErrorData>;
854
855/// Maps an `alien_client_core::Error` to an appropriate `alien_bindings::Error`.
856///
857/// Important error types (like resource not found, access denied, etc.) are mapped
858/// to their corresponding variants in alien-bindings while preserving the operation context.
859/// Less important errors are wrapped in `CloudPlatformError`.
860///
861/// # Arguments
862/// * `cloud_error` - The error from cloud client crates
863/// * `operation_context` - Description of the operation that failed (e.g., "Failed to get ECR repository details")
864/// * `resource_id` - Optional resource ID for fallback error context
865///
866/// # Example
867/// ```rust
868/// use alien_bindings::error::map_cloud_client_error;
869///
870/// async fn example() {
871///     // This would be an actual cloud client operation
872///     let result = some_cloud_operation().await
873///         .map_err(|e| map_cloud_client_error(e, "Failed to get ECR repository details".to_string(), Some("my-repo".to_string())));
874/// }
875///
876/// async fn some_cloud_operation() -> Result<(), alien_client_core::Error> {
877///     // Mock implementation
878///     Ok(())
879/// }
880/// ```
881pub fn map_cloud_client_error(
882    cloud_error: alien_client_core::Error,
883    operation_context: String,
884    resource_id: Option<String>,
885) -> Error {
886    use alien_client_core::ErrorData as CloudErrorData;
887
888    // Check the error type first to determine the right context to add
889    let error_data = match cloud_error.error.as_ref() {
890        Some(CloudErrorData::RemoteResourceNotFound {
891            resource_type,
892            resource_name,
893        }) => ErrorData::RemoteResourceNotFound {
894            operation_context,
895            resource_type: resource_type.clone(),
896            resource_name: resource_name.clone(),
897        },
898        Some(CloudErrorData::RemoteResourceConflict {
899            resource_type,
900            resource_name,
901            message,
902        }) => ErrorData::RemoteResourceConflict {
903            operation_context,
904            resource_type: resource_type.clone(),
905            resource_name: resource_name.clone(),
906            conflict_reason: message.clone(),
907        },
908        Some(CloudErrorData::RemoteAccessDenied {
909            resource_type,
910            resource_name,
911        }) => ErrorData::RemoteAccessDenied {
912            operation_context,
913            resource_type: resource_type.clone(),
914            resource_name: resource_name.clone(),
915        },
916        Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
917            operation_context,
918            details: message.clone(),
919        },
920        Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
921            operation_context,
922            details: message.clone(),
923        },
924        Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
925            ErrorData::RemoteServiceUnavailable {
926                operation_context,
927                details: message.clone(),
928            }
929        }
930        Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
931            operation_context,
932            details: message.clone(),
933        },
934        Some(CloudErrorData::InvalidInput {
935            message,
936            field_name,
937        }) => ErrorData::InvalidInput {
938            operation_context,
939            details: message.clone(),
940            field_name: field_name.clone(),
941        },
942        Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
943            operation_context,
944            details: message.clone(),
945        },
946        // For other error types or None, wrap in CloudPlatformError
947        _ => ErrorData::CloudPlatformError {
948            message: operation_context,
949            resource_id,
950        },
951    };
952
953    // Now add the context to the cloud error
954    cloud_error.context(error_data)
955}