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        /// The agent's own cause, kept as a field so a caller can branch on it
317        failure: String,
318        /// Human-readable detail from the agent
319        reason: String,
320    },
321
322    /// A session came up without a restriction its declaration asked for.
323    ///
324    /// Distinct from a refused call: the data plane accepted the request and answered, and what
325    /// it built is not what was asked for. The session id is carried because the caller never
326    /// receives one — this is the failure where an operator has to be able to find what was left
327    /// behind if deleting it also failed.
328    #[error(
329        code = "SANDBOX_NOT_AS_DECLARED",
330        message = "Sandbox session '{session_id}' does not carry its declared {restriction}, so it cannot be used; create a new session. {reason}",
331        retryable = "false",
332        internal = "false",
333        http_status_code = 502
334    )]
335    SandboxNotAsDeclared {
336        /// Provider-scoped id of the session that was built
337        session_id: String,
338        /// What the declaration asked for, such as `egress policy`
339        restriction: String,
340        /// What the session came up with instead
341        reason: String,
342    },
343
344    /// The sandbox agent could not be reached, or the connection dropped mid-response.
345    ///
346    /// Visibility is inherited rather than declared public: what this wraps is often the cloud
347    /// client's catch-all, which carries the raw request and response text of the call that
348    /// failed. Marking the wrapper external does not redact a source, so a fixed `false` here
349    /// would carry that text out to a caller.
350    #[error(
351        code = "SANDBOX_UNREACHABLE",
352        message = "Sandbox operation '{operation}' could not reach the agent: {reason}",
353        retryable = "true",
354        internal = "inherit",
355        http_status_code = 503
356    )]
357    SandboxUnreachable {
358        /// Operation that was in flight
359        operation: String,
360        /// What went wrong on the wire
361        reason: String,
362    },
363
364    /// Feature is not enabled in the compiled binary.
365    #[error(
366        code = "FEATURE_NOT_ENABLED",
367        message = "Feature '{feature}' is not enabled in this build",
368        retryable = "false",
369        internal = "false",
370        http_status_code = 501
371    )]
372    FeatureNotEnabled {
373        /// Name of the feature that is not enabled
374        feature: String,
375    },
376
377    /// gRPC call failed.
378    #[error(
379        code = "GRPC_CALL_FAILED",
380        message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
381        retryable = "true",
382        internal = "false",
383        http_status_code = 502
384    )]
385    GrpcCallFailed {
386        /// Name of the gRPC service
387        service: String,
388        /// Name of the gRPC method
389        method: String,
390        /// Reason for the call failure
391        reason: String,
392    },
393
394    /// Deserialization of data failed.
395    #[error(
396        code = "DESERIALIZATION_FAILED",
397        message = "Failed to deserialize {type_name}: {message}",
398        retryable = "false",
399        internal = "false",
400        http_status_code = 400
401    )]
402    DeserializationFailed {
403        /// Human-readable error message
404        message: String,
405        /// Name of the type being deserialized
406        type_name: String,
407    },
408
409    /// Serialization of data failed.
410    #[error(
411        code = "SERIALIZATION_FAILED",
412        message = "Failed to serialize data: {message}",
413        retryable = "false",
414        internal = "false",
415        http_status_code = 500
416    )]
417    SerializationFailed {
418        /// Human-readable error message
419        message: String,
420    },
421
422    /// Response format from provider API is unexpected or missing required fields.
423    #[error(
424        code = "UNEXPECTED_RESPONSE_FORMAT",
425        message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
426        retryable = "false",
427        internal = "false",
428        http_status_code = 502
429    )]
430    UnexpectedResponseFormat {
431        /// Name of the provider (aws, gcp, azure, etc.)
432        provider: String,
433        /// Name of the binding
434        binding_name: String,
435        /// Name of the missing or malformed field
436        field: String,
437        /// The full response JSON for debugging
438        response_json: String,
439    },
440
441    /// Cloud platform API error.
442    #[error(
443        code = "CLOUD_PLATFORM_ERROR",
444        message = "Cloud platform error: {message}",
445        retryable = "true",
446        internal = "false",
447        http_status_code = 502
448    )]
449    CloudPlatformError {
450        /// Human-readable description of the error
451        message: String,
452        /// Optional resource ID that was involved in the error
453        resource_id: Option<String>,
454    },
455
456    /// Reading a cloud Postgres binding's password from its secret store failed.
457    ///
458    /// The retry and visibility metadata comes from the provider error being wrapped:
459    /// throttling remains retryable, while permanent provider failures do not become
460    /// retryable merely because they happened during secret resolution.
461    ///
462    /// `secret` is the locator, never the secret value.
463    #[error(
464        code = "POSTGRES_SECRET_RESOLUTION_FAILED",
465        message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
466        retryable = "inherit",
467        internal = "inherit",
468        http_status_code = 502
469    )]
470    PostgresSecretResolutionFailed {
471        /// Name of the Postgres binding whose password could not be resolved
472        binding_name: String,
473        /// The secret locator that was read (an ARN / name / URI — never the value)
474        secret: String,
475        /// What went wrong while reading the secret
476        reason: String,
477    },
478
479    /// A cloud Postgres secret was read, but did not contain a usable password.
480    ///
481    /// Provider responses with missing, empty, or malformed values cannot become valid
482    /// by retrying the same version. `secret` is the locator, never the secret value.
483    #[error(
484        code = "POSTGRES_SECRET_VALUE_INVALID",
485        message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
486        retryable = "false",
487        internal = "false",
488        http_status_code = 502
489    )]
490    PostgresSecretValueInvalid {
491        /// Name of the Postgres binding whose password value was invalid
492        binding_name: String,
493        /// The secret locator that was read (an ARN / name / URI — never the value)
494        secret: String,
495        /// Why the returned value cannot be used as a password
496        reason: String,
497    },
498
499    /// Resource not found in the cloud platform.
500    #[error(
501        code = "RESOURCE_NOT_FOUND",
502        message = "Resource '{resource_id}' not found",
503        retryable = "false",
504        internal = "false",
505        http_status_code = 404
506    )]
507    ResourceNotFound {
508        /// ID of the resource that was not found
509        resource_id: String,
510    },
511
512    /// The requested remote resource does not exist.
513    #[error(
514        code = "REMOTE_RESOURCE_NOT_FOUND",
515        message = "{operation_context}: {resource_type} '{resource_name}' not found",
516        retryable = "false",
517        internal = "false",
518        http_status_code = 404
519    )]
520    RemoteResourceNotFound {
521        /// Context of the operation that failed (e.g., "Failed to get ECR repository details")
522        operation_context: String,
523        /// Type of the resource that was not found
524        resource_type: String,
525        /// Name of the resource that was not found
526        resource_name: String,
527    },
528
529    /// Operation conflicts with current remote resource state.
530    #[error(
531        code = "REMOTE_RESOURCE_CONFLICT",
532        message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
533        retryable = "true",
534        internal = "false",
535        http_status_code = 409
536    )]
537    RemoteResourceConflict {
538        /// Context of the operation that failed
539        operation_context: String,
540        /// Type of the resource that has a conflict
541        resource_type: String,
542        /// Name of the resource that has a conflict
543        resource_name: String,
544        /// Specific reason for the conflict
545        conflict_reason: String,
546    },
547
548    /// Access denied due to insufficient permissions.
549    #[error(
550        code = "REMOTE_ACCESS_DENIED",
551        message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
552        retryable = "true",
553        internal = "false",
554        http_status_code = 403
555    )]
556    RemoteAccessDenied {
557        /// Context of the operation that failed
558        operation_context: String,
559        /// Type of the resource access was denied to
560        resource_type: String,
561        /// Name of the resource access was denied to
562        resource_name: String,
563    },
564
565    /// Request rate limit exceeded.
566    #[error(
567        code = "RATE_LIMIT_EXCEEDED",
568        message = "{operation_context}: Rate limit exceeded - {details}",
569        retryable = "true",
570        internal = "false",
571        http_status_code = 429
572    )]
573    RateLimitExceeded {
574        /// Context of the operation that failed
575        operation_context: String,
576        /// Additional details about the rate limit
577        details: String,
578    },
579
580    /// Operation exceeded the allowed timeout.
581    #[error(
582        code = "TIMEOUT",
583        message = "{operation_context}: Operation timed out - {details}",
584        retryable = "true",
585        internal = "false",
586        http_status_code = 408
587    )]
588    Timeout {
589        /// Context of the operation that failed
590        operation_context: String,
591        /// Additional details about the timeout
592        details: String,
593    },
594
595    /// Remote service is temporarily unavailable.
596    #[error(
597        code = "REMOTE_SERVICE_UNAVAILABLE",
598        message = "{operation_context}: Service unavailable - {details}",
599        retryable = "true",
600        internal = "false",
601        http_status_code = 503
602    )]
603    RemoteServiceUnavailable {
604        /// Context of the operation that failed
605        operation_context: String,
606        /// Additional details about the service unavailability
607        details: String,
608    },
609
610    /// Quota or resource limits have been exceeded.
611    #[error(
612        code = "QUOTA_EXCEEDED",
613        message = "{operation_context}: Quota exceeded - {details}",
614        retryable = "true",
615        internal = "false",
616        http_status_code = 429
617    )]
618    QuotaExceeded {
619        /// Context of the operation that failed
620        operation_context: String,
621        /// Additional details about the quota violation
622        details: String,
623    },
624
625    /// Invalid or malformed input parameters provided to the operation.
626    #[error(
627        code = "INVALID_INPUT",
628        message = "{operation_context}: Invalid input - {details}",
629        retryable = "false",
630        internal = "false",
631        http_status_code = 400
632    )]
633    InvalidInput {
634        /// Context of the operation that failed
635        operation_context: String,
636        /// Details about what input was invalid
637        details: String,
638        /// Optional field name that was invalid
639        field_name: Option<String>,
640    },
641
642    /// Authentication with cloud provider failed.
643    #[error(
644        code = "AUTHENTICATION_ERROR",
645        message = "{operation_context}: Authentication failed - {details}",
646        retryable = "true",
647        internal = "false",
648        http_status_code = 401
649    )]
650    AuthenticationError {
651        /// Context of the operation that failed
652        operation_context: String,
653        /// Details about the authentication failure
654        details: String,
655    },
656
657    /// Generic bindings error for uncommon cases.
658    #[error(
659        code = "BINDINGS_ERROR",
660        message = "Bindings error: {message}",
661        retryable = "true",
662        internal = "true",
663        http_status_code = 500
664    )]
665    Other {
666        /// Human-readable description of the error
667        message: String,
668    },
669
670    /// Presigned request has expired and can no longer be used.
671    #[error(
672        code = "PRESIGNED_REQUEST_EXPIRED",
673        message = "Presigned request for path '{path}' expired at {expired_at}",
674        retryable = "false",
675        internal = "false",
676        http_status_code = 403
677    )]
678    PresignedRequestExpired {
679        /// Path that the presigned request was for
680        path: String,
681        /// When the request expired
682        expired_at: chrono::DateTime<chrono::Utc>,
683    },
684
685    /// HTTP request to external service failed.
686    #[error(
687        code = "HTTP_REQUEST_FAILED",
688        message = "HTTP {method} request to '{url}' failed",
689        retryable = "true",
690        internal = "false",
691        http_status_code = 502
692    )]
693    HttpRequestFailed {
694        /// URL that was requested
695        url: String,
696        /// HTTP method that was used
697        method: String,
698    },
699
700    /// Local filesystem operation failed.
701    #[error(
702        code = "LOCAL_FILESYSTEM_ERROR",
703        message = "Local filesystem operation '{operation}' failed for path '{path}'",
704        retryable = "true",
705        internal = "false",
706        http_status_code = 500
707    )]
708    LocalFilesystemError {
709        /// Path that the operation was performed on
710        path: String,
711        /// Operation that failed
712        operation: String,
713    },
714
715    /// Failed to load platform configuration for the provider.
716    #[error(
717        code = "client_config_LOAD_FAILED",
718        message = "Failed to load platform configuration for provider '{provider}'",
719        retryable = "false",
720        internal = "false",
721        http_status_code = 400
722    )]
723    ClientConfigLoadFailed {
724        /// Name of the provider (aws, gcp, azure, etc.)
725        provider: String,
726    },
727
728    /// Binding setup failed during initialization.
729    #[error(
730        code = "BINDING_SETUP_FAILED",
731        message = "Binding setup failed for type '{binding_type}': {reason}",
732        retryable = "false",
733        internal = "false",
734        http_status_code = 500
735    )]
736    BindingSetupFailed {
737        /// Type of binding being set up
738        binding_type: String,
739        /// Reason for the setup failure
740        reason: String,
741    },
742
743    /// KV operation failed.
744    #[error(
745        code = "KV_OPERATION_FAILED",
746        message = "KV operation '{operation}' failed for key '{key}': {reason}",
747        retryable = "true",
748        internal = "false",
749        http_status_code = 502
750    )]
751    KvOperationFailed {
752        /// The KV operation that failed
753        operation: String,
754        /// The key involved in the operation
755        key: String,
756        /// Reason for the operation failure
757        reason: String,
758    },
759
760    /// Queue operation failed.
761    #[error(
762        code = "QUEUE_OPERATION_FAILED",
763        message = "Queue operation '{operation}' failed: {reason}",
764        retryable = "true",
765        internal = "false",
766        http_status_code = 502
767    )]
768    QueueOperationFailed {
769        /// The queue operation that failed
770        operation: String,
771        /// Reason for the operation failure
772        reason: String,
773    },
774
775    /// Remote access to deployment resources failed.
776    #[error(
777        code = "REMOTE_ACCESS_FAILED",
778        message = "Remote access failed during operation: {operation}",
779        retryable = "true",
780        internal = "false",
781        http_status_code = 502
782    )]
783    RemoteAccessFailed {
784        /// Description of the operation that failed
785        operation: String,
786    },
787
788    /// Client configuration is invalid or missing for the platform.
789    #[error(
790        code = "CLIENT_CONFIG_INVALID",
791        message = "Client configuration invalid for platform '{platform}': {message}",
792        retryable = "false",
793        internal = "false",
794        http_status_code = 400
795    )]
796    ClientConfigInvalid {
797        /// The platform that was expected
798        platform: alien_core::Platform,
799        /// Description of the configuration issue
800        message: String,
801    },
802}
803
804impl ErrorData {
805    /// Construct a [`ErrorData::BindingConfigInvalid`] for `binding_name`,
806    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally so call
807    /// sites cannot forget it (the omission that repeatedly broke bindings).
808    pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
809        ErrorData::BindingConfigInvalid {
810            env_var: binding_env_var(binding_name),
811            binding_name: binding_name.to_string(),
812            reason: reason.into(),
813        }
814    }
815
816    /// Construct a [`ErrorData::BindingNotConfigured`] for `binding_name`,
817    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally.
818    pub fn not_configured(binding_name: &str) -> Self {
819        ErrorData::BindingNotConfigured {
820            binding_name: binding_name.to_string(),
821            env_var: binding_env_var(binding_name),
822        }
823    }
824}
825
826/// Convenient alias with default error type `ErrorData`.
827pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
828
829/// Convenience alias representing a constructed AlienError with our `ErrorData` payload.
830pub type Error = AlienError<ErrorData>;
831
832/// Maps an `alien_client_core::Error` to an appropriate `alien_bindings::Error`.
833///
834/// Important error types (like resource not found, access denied, etc.) are mapped
835/// to their corresponding variants in alien-bindings while preserving the operation context.
836/// Less important errors are wrapped in `CloudPlatformError`.
837///
838/// # Arguments
839/// * `cloud_error` - The error from cloud client crates
840/// * `operation_context` - Description of the operation that failed (e.g., "Failed to get ECR repository details")
841/// * `resource_id` - Optional resource ID for fallback error context
842///
843/// # Example
844/// ```rust
845/// use alien_bindings::error::map_cloud_client_error;
846///
847/// async fn example() {
848///     // This would be an actual cloud client operation
849///     let result = some_cloud_operation().await
850///         .map_err(|e| map_cloud_client_error(e, "Failed to get ECR repository details".to_string(), Some("my-repo".to_string())));
851/// }
852///
853/// async fn some_cloud_operation() -> Result<(), alien_client_core::Error> {
854///     // Mock implementation
855///     Ok(())
856/// }
857/// ```
858pub fn map_cloud_client_error(
859    cloud_error: alien_client_core::Error,
860    operation_context: String,
861    resource_id: Option<String>,
862) -> Error {
863    use alien_client_core::ErrorData as CloudErrorData;
864
865    // Check the error type first to determine the right context to add
866    let error_data = match cloud_error.error.as_ref() {
867        Some(CloudErrorData::RemoteResourceNotFound {
868            resource_type,
869            resource_name,
870        }) => ErrorData::RemoteResourceNotFound {
871            operation_context,
872            resource_type: resource_type.clone(),
873            resource_name: resource_name.clone(),
874        },
875        Some(CloudErrorData::RemoteResourceConflict {
876            resource_type,
877            resource_name,
878            message,
879        }) => ErrorData::RemoteResourceConflict {
880            operation_context,
881            resource_type: resource_type.clone(),
882            resource_name: resource_name.clone(),
883            conflict_reason: message.clone(),
884        },
885        Some(CloudErrorData::RemoteAccessDenied {
886            resource_type,
887            resource_name,
888        }) => ErrorData::RemoteAccessDenied {
889            operation_context,
890            resource_type: resource_type.clone(),
891            resource_name: resource_name.clone(),
892        },
893        Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
894            operation_context,
895            details: message.clone(),
896        },
897        Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
898            operation_context,
899            details: message.clone(),
900        },
901        Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
902            ErrorData::RemoteServiceUnavailable {
903                operation_context,
904                details: message.clone(),
905            }
906        }
907        Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
908            operation_context,
909            details: message.clone(),
910        },
911        Some(CloudErrorData::InvalidInput {
912            message,
913            field_name,
914        }) => ErrorData::InvalidInput {
915            operation_context,
916            details: message.clone(),
917            field_name: field_name.clone(),
918        },
919        Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
920            operation_context,
921            details: message.clone(),
922        },
923        // For other error types or None, wrap in CloudPlatformError
924        _ => ErrorData::CloudPlatformError {
925            message: operation_context,
926            resource_id,
927        },
928    };
929
930    // Now add the context to the cloud error
931    cloud_error.context(error_data)
932}