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