@distilled.cloud/aws 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/auth.ts CHANGED
@@ -5,9 +5,14 @@ import * as FileSystem from "effect/FileSystem";
5
5
  import * as Option from "effect/Option";
6
6
  import * as Path from "effect/Path";
7
7
  import * as Redacted from "effect/Redacted";
8
- import * as Context from "effect/Context";
9
8
  import * as HttpClient from "effect/unstable/http/HttpClient";
10
9
  import { createHash } from "node:crypto";
10
+ import {
11
+ Auth,
12
+ type AwsProfileConfig,
13
+ type SsoProfileConfig,
14
+ type SSOToken,
15
+ } from "./auth.browser.ts";
11
16
  import {
12
17
  ConflictingSSORegion,
13
18
  ConflictingSSOStartUrl,
@@ -18,11 +23,12 @@ import {
18
23
  SsoPortalError,
19
24
  SsoRegion,
20
25
  SsoStartUrl,
21
- type CredentialsError,
22
26
  type ResolvedCredentials,
23
27
  } from "./credentials.ts";
24
28
  import { parseIni, parseSSOSessionData } from "./util/parse-ini.ts";
25
29
 
30
+ export * from "./auth.browser.ts";
31
+
26
32
  /**
27
33
  * The time window (5 mins) that SDK will treat the SSO token expires in before the defined expiration date in token.
28
34
  * This is needed because server side may have invalidated the token before the defined expiration date.
@@ -31,18 +37,6 @@ const EXPIRE_WINDOW_MS = 5 * 60 * 1000;
31
37
 
32
38
  const REFRESH_MESSAGE = `To refresh this SSO session run 'aws sso login' with the corresponding profile.`;
33
39
 
34
- export class Auth extends Context.Service<
35
- Auth,
36
- {
37
- loadProfile: (
38
- profileName: string,
39
- ) => Effect.Effect<AwsProfileConfig, CredentialsError>;
40
- loadProfileCredentials: (
41
- profileName: string,
42
- ) => Effect.Effect<ResolvedCredentials, CredentialsError>;
43
- }
44
- >()("distilled-aws/AWS/Auth") {}
45
-
46
40
  export const Default = Effect.serviceOption(Auth).pipe(
47
41
  Effect.map(Option.getOrUndefined),
48
42
  Effect.flatMap((c) => (c ? Effect.succeed(c) : makeAuthService())),
@@ -286,67 +280,3 @@ export const makeAuthService = () =>
286
280
  loadProfileCredentials,
287
281
  });
288
282
  });
289
-
290
- export interface AwsProfileConfig {
291
- sso_session?: string;
292
- sso_account_id?: string;
293
- sso_role_name?: string;
294
- region?: string;
295
- output?: string;
296
- sso_start_url?: string;
297
- sso_region?: string;
298
- }
299
- export interface SsoProfileConfig extends AwsProfileConfig {
300
- sso_start_url: string;
301
- sso_region: string;
302
- sso_account_id: string;
303
- sso_role_name: string;
304
- }
305
-
306
- /**
307
- * Cached SSO token retrieved from SSO login flow.
308
- * @public
309
- */
310
- export interface SSOToken {
311
- /**
312
- * A base64 encoded string returned by the sso-oidc service.
313
- */
314
- accessToken: string;
315
-
316
- /**
317
- * The expiration time of the accessToken as an RFC 3339 formatted timestamp.
318
- */
319
- expiresAt: string;
320
-
321
- /**
322
- * The token used to obtain an access token in the event that the accessToken is invalid or expired.
323
- */
324
- refreshToken?: string;
325
-
326
- /**
327
- * The unique identifier string for each client. The client ID generated when performing the registration
328
- * portion of the OIDC authorization flow. This is used to refresh the accessToken.
329
- */
330
- clientId?: string;
331
-
332
- /**
333
- * A secret string generated when performing the registration portion of the OIDC authorization flow.
334
- * This is used to refresh the accessToken.
335
- */
336
- clientSecret?: string;
337
-
338
- /**
339
- * The expiration time of the client registration (clientId and clientSecret) as an RFC 3339 formatted timestamp.
340
- */
341
- registrationExpiresAt?: string;
342
-
343
- /**
344
- * The configured sso_region for the profile that credentials are being resolved for.
345
- */
346
- region?: string;
347
-
348
- /**
349
- * The configured sso_start_url for the profile that credentials are being resolved for.
350
- */
351
- startUrl?: string;
352
- }
package/src/client/api.ts CHANGED
@@ -22,7 +22,9 @@ import {
22
22
  type ResponseParserOptions,
23
23
  } from "./response-parser.ts";
24
24
 
25
- import { Credentials, Endpoint, Region } from "../index.ts";
25
+ import * as Credentials from "../credentials.browser.ts";
26
+ import * as Endpoint from "../endpoint.ts";
27
+ import * as Region from "../region.ts";
26
28
 
27
29
  export interface MakeOptions extends ResponseParserOptions {}
28
30
 
@@ -0,0 +1,267 @@
1
+ import { fromHttp as _fromHttp } from "@aws-sdk/credential-providers";
2
+
3
+ import {
4
+ type AwsCredentialIdentity,
5
+ type AwsCredentialIdentityProvider,
6
+ } from "@smithy/types";
7
+ import * as Context from "effect/Context";
8
+ import * as Data from "effect/Data";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Layer from "effect/Layer";
11
+ import type { PlatformError } from "effect/PlatformError";
12
+ import * as Redacted from "effect/Redacted";
13
+ import type { HttpClientError } from "effect/unstable/http/HttpClientError";
14
+ export * as AWSTypes from "@aws-sdk/types";
15
+
16
+ export interface AwsCredentials {
17
+ readonly accessKeyId: string;
18
+ readonly secretAccessKey: string;
19
+ readonly sessionToken?: string;
20
+ }
21
+
22
+ /**
23
+ * Resolved credential values ready for request signing.
24
+ */
25
+ export interface ResolvedCredentials {
26
+ readonly accessKeyId: Redacted.Redacted<string>;
27
+ readonly secretAccessKey: Redacted.Redacted<string>;
28
+ readonly sessionToken: Redacted.Redacted<string> | undefined;
29
+ readonly expiration?: number;
30
+ }
31
+
32
+ /**
33
+ * The requirements for resolving credentials (HttpClient for SSO, FileSystem for cache).
34
+ */
35
+
36
+ /**
37
+ * Error types that can occur during credential resolution.
38
+ */
39
+ export type CredentialsError =
40
+ | AwsCredentialProviderError
41
+ | ProfileNotFound
42
+ | InvalidSSOProfile
43
+ | InvalidSSOToken
44
+ | ExpiredSSOToken
45
+ | ConflictingSSORegion
46
+ | ConflictingSSOStartUrl
47
+ | SsoPortalError
48
+ | HttpClientError
49
+ | PlatformError;
50
+
51
+ export class Credentials extends Context.Service<
52
+ Credentials,
53
+ Effect.Effect<ResolvedCredentials, CredentialsError>
54
+ >()("AWS::Credentials") {}
55
+
56
+ export const mock = Layer.succeed(
57
+ Credentials,
58
+ Effect.succeed({
59
+ accessKeyId: Redacted.make("test"),
60
+ secretAccessKey: Redacted.make("test"),
61
+ sessionToken: Redacted.make("test"),
62
+ }),
63
+ );
64
+
65
+ /**
66
+ * Create resolved credentials from an AWS credential identity.
67
+ */
68
+ export const fromAwsCredentialIdentity = (
69
+ identity: AwsCredentialIdentity,
70
+ ): ResolvedCredentials => ({
71
+ accessKeyId: Redacted.make(identity.accessKeyId),
72
+ secretAccessKey: Redacted.make(identity.secretAccessKey),
73
+ sessionToken: identity.sessionToken
74
+ ? Redacted.make(identity.sessionToken)
75
+ : undefined,
76
+ expiration: identity.expiration?.getTime(),
77
+ });
78
+
79
+ type ProviderName =
80
+ | "env"
81
+ | "ini"
82
+ | "chain"
83
+ | "container"
84
+ | "http"
85
+ | "process"
86
+ | "token-file";
87
+
88
+ export const providerHints = (
89
+ provider: ProviderName,
90
+ ): ReadonlyArray<string> | undefined => {
91
+ switch (provider) {
92
+ case "env":
93
+ return [
94
+ "Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (and AWS_SESSION_TOKEN if needed).",
95
+ ];
96
+ case "ini":
97
+ return ["Check ~/.aws/credentials and ~/.aws/config for the profile."];
98
+ case "chain":
99
+ return [
100
+ "Configure at least one credential source for the default chain.",
101
+ "If using SSO, run `aws sso login` for the profile.",
102
+ ];
103
+ case "container":
104
+ return ["Ensure a container credential endpoint is available."];
105
+ case "http":
106
+ return ["Ensure the configured credential endpoint is reachable."];
107
+ case "process":
108
+ return [
109
+ "Set AWS_CREDENTIAL_PROCESS to a valid command and ensure it exits successfully.",
110
+ ];
111
+ case "token-file":
112
+ return [
113
+ "Set AWS_WEB_IDENTITY_TOKEN_FILE and ensure the file is readable.",
114
+ ];
115
+ default:
116
+ return;
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Time window (5 mins) to refresh credentials before they actually expire.
122
+ * This prevents using credentials that are about to expire.
123
+ */
124
+ const CREDENTIAL_REFRESH_WINDOW_MS = 5 * 60 * 1000;
125
+
126
+ /**
127
+ * Create a credentials effect with lazy resolution and expiration-aware caching.
128
+ * Uses Effect.cachedWithTTL where the TTL is computed from the credentials' expiration.
129
+ */
130
+ export const createCachedCredentialsEffect = <E, R>(
131
+ resolve: Effect.Effect<ResolvedCredentials, E, R>,
132
+ ): Effect.Effect<ResolvedCredentials, E, R> => {
133
+ let cachedCreds: ResolvedCredentials | undefined;
134
+ let expiresAt: number | undefined;
135
+
136
+ return Effect.suspend(() => {
137
+ const now = Date.now();
138
+ if (cachedCreds && expiresAt && now < expiresAt) {
139
+ return Effect.succeed(cachedCreds);
140
+ }
141
+ return Effect.map(resolve, (creds) => {
142
+ cachedCreds = creds;
143
+ expiresAt = creds.expiration
144
+ ? creds.expiration - CREDENTIAL_REFRESH_WINDOW_MS
145
+ : undefined;
146
+ return creds;
147
+ });
148
+ });
149
+ };
150
+
151
+ /**
152
+ * Create a lazy, cached credentials provider from an AWS SDK credential provider.
153
+ * Credentials are resolved on first access and cached based on their expiration time.
154
+ */
155
+ export const createLazyProvider = (
156
+ provider: (config: {}) => AwsCredentialIdentityProvider,
157
+ providerName: ProviderName,
158
+ ): Layer.Layer<Credentials> => {
159
+ const resolve = Effect.gen(function* () {
160
+ const hints = providerHints(providerName);
161
+ const identity = yield* Effect.tryPromise({
162
+ try: () => provider({})(),
163
+ catch: (cause) =>
164
+ new AwsCredentialProviderError({
165
+ message: `Failed to resolve credentials from ${providerName}.`,
166
+ provider: providerName,
167
+ cause,
168
+ hints,
169
+ }),
170
+ });
171
+ return fromAwsCredentialIdentity(identity);
172
+ });
173
+
174
+ return Layer.succeed(Credentials, createCachedCredentialsEffect(resolve));
175
+ };
176
+
177
+ /**
178
+ * Create a credentials provider from static credentials.
179
+ * No lazy loading or caching needed since credentials are already available.
180
+ */
181
+ export const fromCredentials = (
182
+ credentials: AwsCredentialIdentity,
183
+ ): Layer.Layer<Credentials> =>
184
+ Layer.succeed(
185
+ Credentials,
186
+ Effect.succeed(fromAwsCredentialIdentity(credentials)),
187
+ );
188
+
189
+ export const fromHttp = () => createLazyProvider(_fromHttp, "http");
190
+
191
+ export const ssoRegion = (region: string) => Layer.succeed(SsoRegion, region);
192
+
193
+ export class SsoRegion extends Context.Service<SsoRegion, string>()(
194
+ "AWS::SsoRegion",
195
+ ) {}
196
+ export class SsoStartUrl extends Context.Service<SsoStartUrl, string>()(
197
+ "AWS::SsoStartUrl",
198
+ ) {}
199
+
200
+ export class ProfileNotFound extends Data.TaggedError(
201
+ "Alchemy::AWS::ProfileNotFound",
202
+ )<{
203
+ message: string;
204
+ profile: string;
205
+ }> {}
206
+
207
+ export class ConflictingSSORegion extends Data.TaggedError(
208
+ "Alchemy::AWS::ConflictingSSORegion",
209
+ )<{
210
+ message: string;
211
+ ssoRegion: string;
212
+ profile: string;
213
+ }> {}
214
+
215
+ export class ConflictingSSOStartUrl extends Data.TaggedError(
216
+ "Alchemy::AWS::ConflictingSSOStartUrl",
217
+ )<{
218
+ message: string;
219
+ ssoStartUrl: string;
220
+ profile: string;
221
+ }> {}
222
+
223
+ export class InvalidSSOProfile extends Data.TaggedError(
224
+ "Alchemy::AWS::InvalidSSOProfile",
225
+ )<{
226
+ message: string;
227
+ profile: string;
228
+ missingFields: string[];
229
+ }> {}
230
+
231
+ export class InvalidSSOToken extends Data.TaggedError(
232
+ "Alchemy::AWS::InvalidSSOToken",
233
+ )<{
234
+ message: string;
235
+ sso_session: string;
236
+ }> {}
237
+
238
+ export class ExpiredSSOToken extends Data.TaggedError(
239
+ "Alchemy::AWS::ExpiredSSOToken",
240
+ )<{
241
+ message: string;
242
+ profile: string;
243
+ }> {}
244
+
245
+ export class AwsCredentialProviderError extends Data.TaggedError(
246
+ "AWS::CredentialProviderError",
247
+ )<{
248
+ message: string;
249
+ provider: string;
250
+ cause?: unknown;
251
+ hints?: ReadonlyArray<string>;
252
+ }> {}
253
+
254
+ /**
255
+ * The AWS SSO portal returned a response with no `roleCredentials`, e.g. a
256
+ * `ForbiddenException` when an IAM Identity Center role assignment is in a
257
+ * stale state.
258
+ */
259
+ export class SsoPortalError extends Data.TaggedError(
260
+ "Alchemy::AWS::SsoPortalError",
261
+ )<{
262
+ message: string;
263
+ profile: string;
264
+ account_id?: string;
265
+ role_name?: string;
266
+ status?: number;
267
+ }> {}