alchemy-effect 0.0.0 → 0.1.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/README.md +179 -0
- package/bin/alchemy-effect.js +15 -0
- package/bin/alchemy-effect.js.map +1 -0
- package/bin/alchemy-effect.ts +10 -0
- package/lib/aws/account.d.ts +17 -0
- package/lib/aws/account.d.ts.map +1 -0
- package/lib/aws/account.js +18 -0
- package/lib/aws/account.js.map +1 -0
- package/lib/aws/arn.d.ts +6 -0
- package/lib/aws/arn.d.ts.map +1 -0
- package/lib/aws/arn.js +1 -0
- package/lib/aws/arn.js.map +1 -0
- package/lib/aws/assets.d.ts +8 -0
- package/lib/aws/assets.d.ts.map +1 -0
- package/lib/aws/assets.js +4 -0
- package/lib/aws/assets.js.map +1 -0
- package/lib/aws/bundle.d.ts +4 -0
- package/lib/aws/bundle.d.ts.map +1 -0
- package/lib/aws/bundle.js +4 -0
- package/lib/aws/bundle.js.map +1 -0
- package/lib/aws/client.d.ts +8 -0
- package/lib/aws/client.d.ts.map +1 -0
- package/lib/aws/client.js +20 -0
- package/lib/aws/client.js.map +1 -0
- package/lib/aws/credentials.d.ts +146 -0
- package/lib/aws/credentials.d.ts.map +1 -0
- package/lib/aws/credentials.js +170 -0
- package/lib/aws/credentials.js.map +1 -0
- package/lib/aws/iam.d.ts +23 -0
- package/lib/aws/iam.d.ts.map +1 -0
- package/lib/aws/iam.js +7 -0
- package/lib/aws/iam.js.map +1 -0
- package/lib/aws/index.d.ts +22 -0
- package/lib/aws/index.d.ts.map +1 -0
- package/lib/aws/index.js +28 -0
- package/lib/aws/index.js.map +1 -0
- package/lib/aws/lambda/consume.d.ts +17 -0
- package/lib/aws/lambda/consume.d.ts.map +1 -0
- package/lib/aws/lambda/consume.js +30 -0
- package/lib/aws/lambda/consume.js.map +1 -0
- package/lib/aws/lambda/function.client.d.ts +8 -0
- package/lib/aws/lambda/function.client.d.ts.map +1 -0
- package/lib/aws/lambda/function.client.js +7 -0
- package/lib/aws/lambda/function.client.js.map +1 -0
- package/lib/aws/lambda/function.d.ts +38 -0
- package/lib/aws/lambda/function.d.ts.map +1 -0
- package/lib/aws/lambda/function.handler.d.ts +7 -0
- package/lib/aws/lambda/function.handler.d.ts.map +1 -0
- package/lib/aws/lambda/function.handler.js +8 -0
- package/lib/aws/lambda/function.handler.js.map +1 -0
- package/lib/aws/lambda/function.invoke.d.ts +10 -0
- package/lib/aws/lambda/function.invoke.d.ts.map +1 -0
- package/lib/aws/lambda/function.invoke.js +31 -0
- package/lib/aws/lambda/function.invoke.js.map +1 -0
- package/lib/aws/lambda/function.js +3 -0
- package/lib/aws/lambda/function.js.map +1 -0
- package/lib/aws/lambda/function.provider.d.ts +9 -0
- package/lib/aws/lambda/function.provider.d.ts.map +1 -0
- package/lib/aws/lambda/function.provider.js +375 -0
- package/lib/aws/lambda/function.provider.js.map +1 -0
- package/lib/aws/lambda/index.d.ts +9 -0
- package/lib/aws/lambda/index.d.ts.map +1 -0
- package/lib/aws/lambda/index.js +9 -0
- package/lib/aws/lambda/index.js.map +1 -0
- package/lib/aws/lambda/serve.d.ts +14 -0
- package/lib/aws/lambda/serve.d.ts.map +1 -0
- package/lib/aws/lambda/serve.js +7 -0
- package/lib/aws/lambda/serve.js.map +1 -0
- package/lib/aws/parse-ini.d.ts +4 -0
- package/lib/aws/parse-ini.d.ts.map +1 -0
- package/lib/aws/parse-ini.js +67 -0
- package/lib/aws/parse-ini.js.map +1 -0
- package/lib/aws/profile.d.ts +6 -0
- package/lib/aws/profile.d.ts.map +1 -0
- package/lib/aws/profile.js +4 -0
- package/lib/aws/profile.js.map +1 -0
- package/lib/aws/region.d.ts +18 -0
- package/lib/aws/region.d.ts.map +1 -0
- package/lib/aws/region.js +21 -0
- package/lib/aws/region.js.map +1 -0
- package/lib/aws/s3.d.ts +8 -0
- package/lib/aws/s3.d.ts.map +1 -0
- package/lib/aws/s3.js +7 -0
- package/lib/aws/s3.js.map +1 -0
- package/lib/aws/sqs/index.d.ts +6 -0
- package/lib/aws/sqs/index.d.ts.map +1 -0
- package/lib/aws/sqs/index.js +6 -0
- package/lib/aws/sqs/index.js.map +1 -0
- package/lib/aws/sqs/queue.client.d.ts +12 -0
- package/lib/aws/sqs/queue.client.d.ts.map +1 -0
- package/lib/aws/sqs/queue.client.js +11 -0
- package/lib/aws/sqs/queue.client.js.map +1 -0
- package/lib/aws/sqs/queue.consume.d.ts +21 -0
- package/lib/aws/sqs/queue.consume.d.ts.map +1 -0
- package/lib/aws/sqs/queue.consume.js +21 -0
- package/lib/aws/sqs/queue.consume.js.map +1 -0
- package/lib/aws/sqs/queue.d.ts +87 -0
- package/lib/aws/sqs/queue.d.ts.map +1 -0
- package/lib/aws/sqs/queue.js +3 -0
- package/lib/aws/sqs/queue.js.map +1 -0
- package/lib/aws/sqs/queue.provider.d.ts +6 -0
- package/lib/aws/sqs/queue.provider.d.ts.map +1 -0
- package/lib/aws/sqs/queue.provider.js +79 -0
- package/lib/aws/sqs/queue.provider.js.map +1 -0
- package/lib/aws/sqs/queue.send-message.d.ts +11 -0
- package/lib/aws/sqs/queue.send-message.d.ts.map +1 -0
- package/lib/aws/sqs/queue.send-message.js +32 -0
- package/lib/aws/sqs/queue.send-message.js.map +1 -0
- package/lib/aws/sts.d.ts +8 -0
- package/lib/aws/sts.d.ts.map +1 -0
- package/lib/aws/sts.js +7 -0
- package/lib/aws/sts.js.map +1 -0
- package/lib/aws/zip.d.ts +3 -0
- package/lib/aws/zip.d.ts.map +1 -0
- package/lib/aws/zip.js +12 -0
- package/lib/aws/zip.js.map +1 -0
- package/lib/cli/approve.d.ts +4 -0
- package/lib/cli/approve.d.ts.map +1 -0
- package/lib/cli/approve.js +18 -0
- package/lib/cli/approve.js.map +1 -0
- package/lib/cli/clack.d.ts +14 -0
- package/lib/cli/clack.d.ts.map +1 -0
- package/lib/cli/clack.js +12 -0
- package/lib/cli/clack.js.map +1 -0
- package/lib/cli/components/ApprovePlan.d.ts +8 -0
- package/lib/cli/components/ApprovePlan.d.ts.map +1 -0
- package/lib/cli/components/ApprovePlan.js +30 -0
- package/lib/cli/components/ApprovePlan.js.map +1 -0
- package/lib/cli/components/Plan.d.ts +7 -0
- package/lib/cli/components/Plan.d.ts.map +1 -0
- package/lib/cli/components/Plan.js +98 -0
- package/lib/cli/components/Plan.js.map +1 -0
- package/lib/cli/components/PlanProgress.d.ts +9 -0
- package/lib/cli/components/PlanProgress.d.ts.map +1 -0
- package/lib/cli/components/PlanProgress.js +166 -0
- package/lib/cli/components/PlanProgress.js.map +1 -0
- package/lib/cli/index.d.ts +361 -0
- package/lib/cli/index.d.ts.map +1 -0
- package/lib/cli/index.js +25260 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.d.ts.map +1 -0
- package/lib/cli/main.js +1 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/cli/plan.d.ts +13 -0
- package/lib/cli/plan.d.ts.map +1 -0
- package/lib/cli/plan.js +1 -0
- package/lib/cli/plan.js.map +1 -0
- package/lib/cli/progress.d.ts +7 -0
- package/lib/cli/progress.d.ts.map +1 -0
- package/lib/cli/progress.js +29 -0
- package/lib/cli/progress.js.map +1 -0
- package/lib/cli/spinner.d.ts +2 -0
- package/lib/cli/spinner.d.ts.map +1 -0
- package/lib/cli/spinner.js +13 -0
- package/lib/cli/spinner.js.map +1 -0
- package/lib/cloudflare/api.d.ts +24 -0
- package/lib/cloudflare/api.d.ts.map +1 -0
- package/lib/cloudflare/api.js +34 -0
- package/lib/cloudflare/api.js.map +1 -0
- package/lib/cloudflare/index.d.ts +5 -0
- package/lib/cloudflare/index.d.ts.map +1 -0
- package/lib/cloudflare/index.js +5 -0
- package/lib/cloudflare/index.js.map +1 -0
- package/lib/cloudflare/kv.d.ts +29 -0
- package/lib/cloudflare/kv.d.ts.map +1 -0
- package/lib/cloudflare/kv.js +3 -0
- package/lib/cloudflare/kv.js.map +1 -0
- package/lib/cloudflare/kv.provider.d.ts +4 -0
- package/lib/cloudflare/kv.provider.d.ts.map +1 -0
- package/lib/cloudflare/kv.provider.js +39 -0
- package/lib/cloudflare/kv.provider.js.map +1 -0
- package/lib/cloudflare/worker.d.ts +33 -0
- package/lib/cloudflare/worker.d.ts.map +1 -0
- package/lib/cloudflare/worker.js +4 -0
- package/lib/cloudflare/worker.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/package.json +70 -3
- package/src/aws/account.ts +35 -0
- package/src/aws/arn.ts +7 -0
- package/src/aws/assets.ts +8 -0
- package/src/aws/bundle.ts +5 -0
- package/src/aws/client.ts +33 -0
- package/src/aws/credentials.ts +408 -0
- package/src/aws/iam.ts +30 -0
- package/src/aws/index.ts +55 -0
- package/src/aws/lambda/consume.ts +64 -0
- package/src/aws/lambda/function.client.ts +14 -0
- package/src/aws/lambda/function.handler.ts +30 -0
- package/src/aws/lambda/function.invoke.ts +40 -0
- package/src/aws/lambda/function.provider.ts +563 -0
- package/src/aws/lambda/function.ts +39 -0
- package/src/aws/lambda/index.ts +12 -0
- package/src/aws/lambda/serve.ts +29 -0
- package/src/aws/parse-ini.ts +80 -0
- package/src/aws/profile.ts +6 -0
- package/src/aws/region.ts +35 -0
- package/src/aws/s3.ts +10 -0
- package/src/aws/sqs/index.ts +5 -0
- package/src/aws/sqs/queue.client.ts +20 -0
- package/src/aws/sqs/queue.consume.ts +46 -0
- package/src/aws/sqs/queue.provider.ts +94 -0
- package/src/aws/sqs/queue.send-message.ts +51 -0
- package/src/aws/sqs/queue.ts +86 -0
- package/src/aws/sts.ts +13 -0
- package/src/aws/zip.ts +17 -0
- package/src/cli/approve.tsx +30 -0
- package/src/cli/clack.ts +22 -0
- package/src/cli/components/ApprovePlan.tsx +44 -0
- package/src/cli/components/Plan.tsx +152 -0
- package/src/cli/components/PlanProgress.tsx +206 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/main.ts +0 -0
- package/src/cli/plan.ts +16 -0
- package/src/cli/progress.tsx +45 -0
- package/src/cli/spinner.ts +14 -0
- package/src/cloudflare/api.ts +72 -0
- package/src/cloudflare/index.ts +4 -0
- package/src/cloudflare/kv.provider.ts +51 -0
- package/src/cloudflare/kv.ts +20 -0
- package/src/cloudflare/worker.ts +34 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fromContainerMetadata as _fromContainerMetadata,
|
|
3
|
+
fromEnv as _fromEnv,
|
|
4
|
+
fromHttp as _fromHttp,
|
|
5
|
+
fromIni as _fromIni,
|
|
6
|
+
fromInstanceMetadata as _fromInstanceMetadata,
|
|
7
|
+
fromNodeProviderChain as _fromNodeProviderChain,
|
|
8
|
+
fromProcess as _fromProcess,
|
|
9
|
+
fromTokenFile as _fromTokenFile,
|
|
10
|
+
fromWebToken as _fromWebToken,
|
|
11
|
+
} from "@aws-sdk/credential-providers";
|
|
12
|
+
import { FileSystem, HttpClient } from "@effect/platform";
|
|
13
|
+
import * as ini from "@smithy/shared-ini-file-loader";
|
|
14
|
+
import {
|
|
15
|
+
type AwsCredentialIdentity,
|
|
16
|
+
type AwsCredentialIdentityProvider,
|
|
17
|
+
} from "@smithy/types";
|
|
18
|
+
import * as Console from "effect/Console";
|
|
19
|
+
import * as Context from "effect/Context";
|
|
20
|
+
import * as Data from "effect/Data";
|
|
21
|
+
import * as Effect from "effect/Effect";
|
|
22
|
+
import * as Layer from "effect/Layer";
|
|
23
|
+
import * as Option from "effect/Option";
|
|
24
|
+
import * as Redacted from "effect/Redacted";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { parseIni, parseSSOSessionData } from "./parse-ini.ts";
|
|
28
|
+
import { AwsProfile } from "./profile.ts";
|
|
29
|
+
|
|
30
|
+
export class Credentials extends Context.Tag("AWS::Credentials")<
|
|
31
|
+
Credentials,
|
|
32
|
+
{
|
|
33
|
+
accessKeyId: Redacted.Redacted<string>;
|
|
34
|
+
secretAccessKey: Redacted.Redacted<string>;
|
|
35
|
+
sessionToken: Redacted.Redacted<string> | undefined;
|
|
36
|
+
expiration?: number;
|
|
37
|
+
}
|
|
38
|
+
>() {}
|
|
39
|
+
|
|
40
|
+
export const fromAwsCredentialIdentity = (identity: AwsCredentialIdentity) =>
|
|
41
|
+
Credentials.of({
|
|
42
|
+
accessKeyId: Redacted.make(identity.accessKeyId),
|
|
43
|
+
secretAccessKey: Redacted.make(identity.secretAccessKey),
|
|
44
|
+
sessionToken: identity.sessionToken
|
|
45
|
+
? Redacted.make(identity.sessionToken)
|
|
46
|
+
: undefined,
|
|
47
|
+
expiration: identity.expiration?.getTime(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const createLayer = (provider: (config: {}) => AwsCredentialIdentityProvider) =>
|
|
51
|
+
Layer.effect(
|
|
52
|
+
Credentials,
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
return fromAwsCredentialIdentity(
|
|
55
|
+
yield* Effect.promise(() => provider({})()),
|
|
56
|
+
);
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const fromCredentials = (credentials: AwsCredentialIdentity) =>
|
|
61
|
+
Layer.succeed(Credentials, fromAwsCredentialIdentity(credentials));
|
|
62
|
+
|
|
63
|
+
export const fromEnv = () => createLayer(_fromEnv);
|
|
64
|
+
|
|
65
|
+
export const fromChain = () => createLayer(() => _fromNodeProviderChain());
|
|
66
|
+
|
|
67
|
+
// export const fromSSO = () => createLayer(_fromSSO);
|
|
68
|
+
|
|
69
|
+
export const fromIni = () => createLayer(_fromIni);
|
|
70
|
+
|
|
71
|
+
export const fromContainerMetadata = () => createLayer(_fromContainerMetadata);
|
|
72
|
+
|
|
73
|
+
export const fromHttp = () => createLayer(_fromHttp);
|
|
74
|
+
|
|
75
|
+
export const fromInstanceMetadata = (
|
|
76
|
+
...parameters: Parameters<typeof _fromInstanceMetadata>
|
|
77
|
+
) => createLayer(() => _fromInstanceMetadata(...parameters));
|
|
78
|
+
|
|
79
|
+
export const fromProcess = () => createLayer(_fromProcess);
|
|
80
|
+
|
|
81
|
+
export const fromTokenFile = () => createLayer(_fromTokenFile);
|
|
82
|
+
|
|
83
|
+
export const fromWebToken = (...parameters: Parameters<typeof _fromWebToken>) =>
|
|
84
|
+
createLayer(() => _fromWebToken(...parameters));
|
|
85
|
+
|
|
86
|
+
export const ssoRegion = (region: string) => Layer.succeed(SsoRegion, region);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The time window (5 mins) that SDK will treat the SSO token expires in before the defined expiration date in token.
|
|
90
|
+
* This is needed because server side may have invalidated the token before the defined expiration date.
|
|
91
|
+
*/
|
|
92
|
+
const EXPIRE_WINDOW_MS = 5 * 60 * 1000;
|
|
93
|
+
|
|
94
|
+
const REFRESH_MESSAGE = `To refresh this SSO session run 'aws sso login' with the corresponding profile.`;
|
|
95
|
+
|
|
96
|
+
export class SsoRegion extends Context.Tag("AWS::SsoRegion")<
|
|
97
|
+
SsoRegion,
|
|
98
|
+
string
|
|
99
|
+
>() {}
|
|
100
|
+
export class SsoStartUrl extends Context.Tag("AWS::SsoStartUrl")<
|
|
101
|
+
SsoStartUrl,
|
|
102
|
+
string
|
|
103
|
+
>() {}
|
|
104
|
+
|
|
105
|
+
export class ProfileNotFound extends Data.TaggedError(
|
|
106
|
+
"Alchemy::AWS::ProfileNotFound",
|
|
107
|
+
)<{
|
|
108
|
+
message: string;
|
|
109
|
+
profile: string;
|
|
110
|
+
}> {}
|
|
111
|
+
|
|
112
|
+
export class ConflictingSSORegion extends Data.TaggedError(
|
|
113
|
+
"Alchemy::AWS::ConflictingSSORegion",
|
|
114
|
+
)<{
|
|
115
|
+
message: string;
|
|
116
|
+
ssoRegion: string;
|
|
117
|
+
profile: string;
|
|
118
|
+
}> {}
|
|
119
|
+
|
|
120
|
+
export class ConflictingSSOStartUrl extends Data.TaggedError(
|
|
121
|
+
"Alchemy::AWS::ConflictingSSOStartUrl",
|
|
122
|
+
)<{
|
|
123
|
+
message: string;
|
|
124
|
+
ssoStartUrl: string;
|
|
125
|
+
profile: string;
|
|
126
|
+
}> {}
|
|
127
|
+
|
|
128
|
+
export class InvalidSSOProfile extends Data.TaggedError(
|
|
129
|
+
"Alchemy::AWS::InvalidSSOProfile",
|
|
130
|
+
)<{
|
|
131
|
+
message: string;
|
|
132
|
+
profile: string;
|
|
133
|
+
missingFields: string[];
|
|
134
|
+
}> {}
|
|
135
|
+
|
|
136
|
+
export class InvalidSSOToken extends Data.TaggedError(
|
|
137
|
+
"Alchemy::AWS::InvalidSSOToken",
|
|
138
|
+
)<{
|
|
139
|
+
message: string;
|
|
140
|
+
sso_session: string;
|
|
141
|
+
}> {}
|
|
142
|
+
|
|
143
|
+
export class ExpiredSSOToken extends Data.TaggedError(
|
|
144
|
+
"Alchemy::AWS::ExpiredSSOToken",
|
|
145
|
+
)<{
|
|
146
|
+
message: string;
|
|
147
|
+
profile: string;
|
|
148
|
+
}> {}
|
|
149
|
+
|
|
150
|
+
export interface AwsProfileConfig {
|
|
151
|
+
sso_session?: string;
|
|
152
|
+
sso_account_id?: string;
|
|
153
|
+
sso_role_name?: string;
|
|
154
|
+
region?: string;
|
|
155
|
+
output?: string;
|
|
156
|
+
sso_start_url?: string;
|
|
157
|
+
sso_region?: string;
|
|
158
|
+
}
|
|
159
|
+
export interface SsoProfileConfig extends AwsProfileConfig {
|
|
160
|
+
sso_start_url: string;
|
|
161
|
+
sso_region: string;
|
|
162
|
+
sso_account_id: string;
|
|
163
|
+
sso_role_name: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const fromSSO = () =>
|
|
167
|
+
Layer.effect(
|
|
168
|
+
Credentials,
|
|
169
|
+
Effect.gen(function* () {
|
|
170
|
+
const client = yield* HttpClient.HttpClient;
|
|
171
|
+
const fs = yield* FileSystem.FileSystem;
|
|
172
|
+
const profileName = Option.getOrElse(
|
|
173
|
+
yield* Effect.serviceOption(AwsProfile),
|
|
174
|
+
() => "default",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const profiles: {
|
|
178
|
+
[profileName: string]: AwsProfileConfig;
|
|
179
|
+
} = yield* Effect.promise(() =>
|
|
180
|
+
ini.parseKnownFiles({ profile: profileName }),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const profile = profiles[profileName];
|
|
184
|
+
|
|
185
|
+
if (!profile) {
|
|
186
|
+
yield* Effect.fail(
|
|
187
|
+
new ProfileNotFound({
|
|
188
|
+
message: `Profile ${profileName} not found`,
|
|
189
|
+
profile: profileName,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const awsDir = path.join(ini.getHomeDir(), ".aws");
|
|
195
|
+
const configPath = path.join(awsDir, "config");
|
|
196
|
+
const cachePath = path.join(awsDir, "sso", "cache");
|
|
197
|
+
|
|
198
|
+
if (profile.sso_session) {
|
|
199
|
+
const ssoRegion = Option.getOrUndefined(
|
|
200
|
+
yield* Effect.serviceOption(SsoRegion),
|
|
201
|
+
);
|
|
202
|
+
const ssoStartUrl = Option.getOrElse(
|
|
203
|
+
yield* Effect.serviceOption(SsoStartUrl),
|
|
204
|
+
() => profile.sso_start_url,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const ssoSessions = yield* fs.readFileString(configPath).pipe(
|
|
208
|
+
Effect.flatMap((config) =>
|
|
209
|
+
Effect.promise(async () => parseIni(config)),
|
|
210
|
+
),
|
|
211
|
+
Effect.map(parseSSOSessionData),
|
|
212
|
+
);
|
|
213
|
+
const session = ssoSessions[profile.sso_session];
|
|
214
|
+
if (ssoRegion && ssoRegion !== session.sso_region) {
|
|
215
|
+
yield* Effect.fail(
|
|
216
|
+
new ConflictingSSORegion({
|
|
217
|
+
message: `Conflicting SSO region`,
|
|
218
|
+
ssoRegion: ssoRegion,
|
|
219
|
+
profile: profile.sso_session,
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (ssoStartUrl && ssoStartUrl !== session.sso_start_url) {
|
|
224
|
+
yield* Effect.fail(
|
|
225
|
+
new ConflictingSSOStartUrl({
|
|
226
|
+
message: `Conflicting SSO start url`,
|
|
227
|
+
ssoStartUrl: ssoStartUrl,
|
|
228
|
+
profile: profile.sso_session,
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
profile.sso_region = session.sso_region;
|
|
233
|
+
profile.sso_start_url = session.sso_start_url;
|
|
234
|
+
|
|
235
|
+
const ssoFields = [
|
|
236
|
+
"sso_start_url",
|
|
237
|
+
"sso_account_id",
|
|
238
|
+
"sso_region",
|
|
239
|
+
"sso_role_name",
|
|
240
|
+
] as const satisfies (keyof SsoProfileConfig)[];
|
|
241
|
+
const missingFields = ssoFields.filter((field) => !profile[field]);
|
|
242
|
+
if (missingFields.length > 0) {
|
|
243
|
+
yield* Effect.fail(
|
|
244
|
+
new InvalidSSOProfile({
|
|
245
|
+
profile: profileName,
|
|
246
|
+
missingFields,
|
|
247
|
+
message:
|
|
248
|
+
`Profile is configured with invalid SSO credentials. Required parameters "sso_account_id", ` +
|
|
249
|
+
`"sso_region", "sso_role_name", "sso_start_url". Got ${Object.keys(
|
|
250
|
+
profile,
|
|
251
|
+
).join(
|
|
252
|
+
", ",
|
|
253
|
+
)}\nReference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html`,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const hasher = createHash("sha1");
|
|
259
|
+
const cacheName = hasher.update(profile.sso_session).digest("hex");
|
|
260
|
+
const ssoTokenFilepath = path.join(cachePath, `${cacheName}.json`);
|
|
261
|
+
const cachedCredsFilePath = path.join(
|
|
262
|
+
cachePath,
|
|
263
|
+
`${cacheName}.credentials.json`,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const cachedCreds = yield* fs.readFileString(cachedCredsFilePath).pipe(
|
|
267
|
+
Effect.map((text) => JSON.parse(text)),
|
|
268
|
+
Effect.catchAll(() => Effect.void),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const isExpired = (expiry: number | string | undefined) => {
|
|
272
|
+
return (
|
|
273
|
+
expiry === undefined ||
|
|
274
|
+
new Date(expiry).getTime() - Date.now() <= EXPIRE_WINDOW_MS
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (cachedCreds && !isExpired(cachedCreds.expiry)) {
|
|
279
|
+
return Credentials.of({
|
|
280
|
+
accessKeyId: Redacted.make(cachedCreds.accessKeyId),
|
|
281
|
+
secretAccessKey: Redacted.make(cachedCreds.secretAccessKey),
|
|
282
|
+
sessionToken: cachedCreds.sessionToken
|
|
283
|
+
? Redacted.make(cachedCreds.sessionToken)
|
|
284
|
+
: undefined,
|
|
285
|
+
expiration: cachedCreds.expiry,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const ssoToken = yield* fs.readFileString(ssoTokenFilepath).pipe(
|
|
290
|
+
Effect.map((text) => JSON.parse(text) as SSOToken),
|
|
291
|
+
Effect.catchAll(() =>
|
|
292
|
+
Effect.fail(
|
|
293
|
+
new InvalidSSOToken({
|
|
294
|
+
message: `The SSO session token associated with profile=${profileName} was not found or is invalid. ${REFRESH_MESSAGE}`,
|
|
295
|
+
sso_session: profile.sso_session!,
|
|
296
|
+
}),
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (isExpired(ssoToken.expiresAt)) {
|
|
302
|
+
yield* Console.log(
|
|
303
|
+
`The SSO session token associated with profile=${profileName} was not found or is invalid. ${REFRESH_MESSAGE}`,
|
|
304
|
+
);
|
|
305
|
+
yield* Effect.fail(
|
|
306
|
+
new ExpiredSSOToken({
|
|
307
|
+
message: `The SSO session token associated with profile=${profileName} was not found or is invalid. ${REFRESH_MESSAGE}`,
|
|
308
|
+
profile: profileName,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const response = yield* client.get(
|
|
314
|
+
`https://portal.sso.${profile.sso_region}.amazonaws.com/federation/credentials?account_id=${profile.sso_account_id}&role_name=${profile.sso_role_name}`,
|
|
315
|
+
{
|
|
316
|
+
headers: {
|
|
317
|
+
"User-Agent": "alchemy.run",
|
|
318
|
+
"Content-Type": "application/json",
|
|
319
|
+
"x-amz-sso_bearer_token": ssoToken.accessToken,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const credentials = (
|
|
325
|
+
(yield* response.json) as {
|
|
326
|
+
roleCredentials: {
|
|
327
|
+
accessKeyId: string;
|
|
328
|
+
secretAccessKey: string;
|
|
329
|
+
sessionToken: string;
|
|
330
|
+
expiration: number;
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
).roleCredentials;
|
|
334
|
+
|
|
335
|
+
yield* fs.writeFileString(
|
|
336
|
+
cachedCredsFilePath,
|
|
337
|
+
JSON.stringify({
|
|
338
|
+
accessKeyId: credentials.accessKeyId,
|
|
339
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
340
|
+
sessionToken: credentials.sessionToken,
|
|
341
|
+
expiry: credentials.expiration,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return Credentials.of({
|
|
346
|
+
accessKeyId: Redacted.make(credentials.accessKeyId),
|
|
347
|
+
secretAccessKey: Redacted.make(credentials.secretAccessKey),
|
|
348
|
+
sessionToken: Redacted.make(credentials.sessionToken),
|
|
349
|
+
expiration: credentials.expiration,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return yield* Effect.fail(
|
|
354
|
+
new ProfileNotFound({
|
|
355
|
+
message: `Profile ${profileName} not found`,
|
|
356
|
+
profile: profileName,
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Cached SSO token retrieved from SSO login flow.
|
|
364
|
+
* @public
|
|
365
|
+
*/
|
|
366
|
+
export interface SSOToken {
|
|
367
|
+
/**
|
|
368
|
+
* A base64 encoded string returned by the sso-oidc service.
|
|
369
|
+
*/
|
|
370
|
+
accessToken: string;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The expiration time of the accessToken as an RFC 3339 formatted timestamp.
|
|
374
|
+
*/
|
|
375
|
+
expiresAt: string;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The token used to obtain an access token in the event that the accessToken is invalid or expired.
|
|
379
|
+
*/
|
|
380
|
+
refreshToken?: string;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The unique identifier string for each client. The client ID generated when performing the registration
|
|
384
|
+
* portion of the OIDC authorization flow. This is used to refresh the accessToken.
|
|
385
|
+
*/
|
|
386
|
+
clientId?: string;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* A secret string generated when performing the registration portion of the OIDC authorization flow.
|
|
390
|
+
* This is used to refresh the accessToken.
|
|
391
|
+
*/
|
|
392
|
+
clientSecret?: string;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The expiration time of the client registration (clientId and clientSecret) as an RFC 3339 formatted timestamp.
|
|
396
|
+
*/
|
|
397
|
+
registrationExpiresAt?: string;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The configured sso_region for the profile that credentials are being resolved for.
|
|
401
|
+
*/
|
|
402
|
+
region?: string;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The configured sso_start_url for the profile that credentials are being resolved for.
|
|
406
|
+
*/
|
|
407
|
+
startUrl?: string;
|
|
408
|
+
}
|
package/src/aws/iam.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import { IAM } from "itty-aws/iam";
|
|
3
|
+
import { createAWSServiceClientLayer } from "./client.ts";
|
|
4
|
+
|
|
5
|
+
export class IAMClient extends Context.Tag("AWS::IAM::Client")<
|
|
6
|
+
IAMClient,
|
|
7
|
+
IAM
|
|
8
|
+
>() {}
|
|
9
|
+
|
|
10
|
+
export const client = createAWSServiceClientLayer<typeof IAMClient, IAM>(
|
|
11
|
+
IAMClient,
|
|
12
|
+
IAM,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export interface PolicyDocument {
|
|
16
|
+
Version: "2012-10-17";
|
|
17
|
+
Statement: PolicyStatement[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PolicyStatement {
|
|
21
|
+
Effect: "Allow" | "Deny";
|
|
22
|
+
Sid?: string;
|
|
23
|
+
Action: string[];
|
|
24
|
+
Resource: string | string[];
|
|
25
|
+
Condition?: Record<string, Record<string, string | string[]>>;
|
|
26
|
+
Principal?: Record<string, string | string[]>;
|
|
27
|
+
NotPrincipal?: Record<string, string | string[]>;
|
|
28
|
+
NotAction?: string[];
|
|
29
|
+
NotResource?: string[];
|
|
30
|
+
}
|
package/src/aws/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
import * as Account from "./account.ts";
|
|
3
|
+
import * as Credentials from "./credentials.ts";
|
|
4
|
+
import * as IAM from "./iam.ts";
|
|
5
|
+
import * as Lambda from "./lambda/index.ts";
|
|
6
|
+
import * as Region from "./region.ts";
|
|
7
|
+
import * as S3 from "./s3.ts";
|
|
8
|
+
import * as SQS from "./sqs/index.ts";
|
|
9
|
+
import * as STS from "./sts.ts";
|
|
10
|
+
|
|
11
|
+
// TODO(sam): should this be named?
|
|
12
|
+
export * from "./profile.ts";
|
|
13
|
+
|
|
14
|
+
export type * as Alchemy from "../index.ts";
|
|
15
|
+
|
|
16
|
+
export * as Account from "./account.ts";
|
|
17
|
+
export * as Credentials from "./credentials.ts";
|
|
18
|
+
export * as IAM from "./iam.ts";
|
|
19
|
+
export * as Lambda from "./lambda/index.ts";
|
|
20
|
+
export * as Region from "./region.ts";
|
|
21
|
+
export * as S3 from "./s3.ts";
|
|
22
|
+
export * as SQS from "./sqs/index.ts";
|
|
23
|
+
export * as STS from "./sts.ts";
|
|
24
|
+
|
|
25
|
+
export const providers = Layer.merge(
|
|
26
|
+
Layer.provide(Lambda.functionProvider(), Lambda.client()),
|
|
27
|
+
Layer.provide(SQS.queueProvider(), SQS.client()),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const bindings = Layer.mergeAll(
|
|
31
|
+
//
|
|
32
|
+
SQS.sendMessageFromLambdaFunction(),
|
|
33
|
+
SQS.consumeFromLambdaFunction(),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export const clients = Layer.mergeAll(
|
|
37
|
+
STS.client(),
|
|
38
|
+
IAM.client(),
|
|
39
|
+
S3.client(),
|
|
40
|
+
SQS.client(),
|
|
41
|
+
Lambda.client(),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const defaultProviders = providers.pipe(
|
|
45
|
+
Layer.provideMerge(bindings),
|
|
46
|
+
Layer.provideMerge(Account.fromIdentity()),
|
|
47
|
+
Layer.provide(clients),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const live = defaultProviders.pipe(
|
|
51
|
+
Layer.provide(Region.fromEnv()),
|
|
52
|
+
Layer.provide(Credentials.fromSSO()),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export default live;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { From } from "alchemy-effect";
|
|
2
|
+
import { declare } from "alchemy-effect";
|
|
3
|
+
import type {
|
|
4
|
+
Context as LambdaContext,
|
|
5
|
+
SQSBatchResponse,
|
|
6
|
+
SQSEvent,
|
|
7
|
+
} from "aws-lambda";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as S from "effect/Schema";
|
|
10
|
+
import * as SQS from "../sqs/index.ts";
|
|
11
|
+
import * as Lambda from "./function.ts";
|
|
12
|
+
|
|
13
|
+
export const consume =
|
|
14
|
+
<Q extends SQS.Queue, ID extends string, Req>(
|
|
15
|
+
id: ID,
|
|
16
|
+
{
|
|
17
|
+
queue,
|
|
18
|
+
handle,
|
|
19
|
+
}: {
|
|
20
|
+
queue: Q;
|
|
21
|
+
handle: (
|
|
22
|
+
this: unknown,
|
|
23
|
+
event: SQS.QueueEvent<Q["props"]["schema"]["Type"]>,
|
|
24
|
+
context: LambdaContext,
|
|
25
|
+
) => Effect.Effect<SQSBatchResponse | void, never, Req>;
|
|
26
|
+
},
|
|
27
|
+
) =>
|
|
28
|
+
<const Props extends Lambda.FunctionProps<Req>>({
|
|
29
|
+
bindings,
|
|
30
|
+
...props
|
|
31
|
+
}: Props) =>
|
|
32
|
+
Lambda.Function(id, {
|
|
33
|
+
handle: Effect.fn(function* (event: SQSEvent, context: LambdaContext) {
|
|
34
|
+
yield* declare<SQS.Consume<From<Q>>>();
|
|
35
|
+
const records = yield* Effect.all(
|
|
36
|
+
event.Records.map(
|
|
37
|
+
Effect.fn(function* (record) {
|
|
38
|
+
return {
|
|
39
|
+
...record,
|
|
40
|
+
body: yield* S.validate(queue.props.schema)(record.body).pipe(
|
|
41
|
+
Effect.catchAll(() => Effect.void),
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const response = yield* handle(
|
|
48
|
+
{
|
|
49
|
+
Records: records.filter((record) => record.body !== undefined),
|
|
50
|
+
},
|
|
51
|
+
context,
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
batchItemFailures: [
|
|
55
|
+
...(response?.batchItemFailures ?? []),
|
|
56
|
+
...records
|
|
57
|
+
.filter((record) => record.body === undefined)
|
|
58
|
+
.map((failed) => ({
|
|
59
|
+
itemIdentifier: failed.messageId,
|
|
60
|
+
})),
|
|
61
|
+
],
|
|
62
|
+
} satisfies SQSBatchResponse;
|
|
63
|
+
}),
|
|
64
|
+
})({ ...props, bindings: bindings.and(SQS.QueueEventSource(queue)) });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
|
|
3
|
+
import { Lambda as LambdaClient } from "itty-aws/lambda";
|
|
4
|
+
import { createAWSServiceClientLayer } from "../client.ts";
|
|
5
|
+
|
|
6
|
+
export class FunctionClient extends Context.Tag("AWS::Lambda::Function.Client")<
|
|
7
|
+
FunctionClient,
|
|
8
|
+
LambdaClient
|
|
9
|
+
>() {}
|
|
10
|
+
|
|
11
|
+
export const client = createAWSServiceClientLayer<
|
|
12
|
+
typeof FunctionClient,
|
|
13
|
+
LambdaClient
|
|
14
|
+
>(FunctionClient, LambdaClient);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Capability } from "alchemy-effect";
|
|
2
|
+
import type { Context as LambdaContext } from "aws-lambda";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
|
|
5
|
+
type Handler = (
|
|
6
|
+
event: any,
|
|
7
|
+
context: LambdaContext,
|
|
8
|
+
) => Effect.Effect<any, any, never>;
|
|
9
|
+
|
|
10
|
+
type HandlerEffect<Req = Capability> = Effect.Effect<Handler, any, Req>;
|
|
11
|
+
|
|
12
|
+
const memo = Symbol.for("alchemy::memo");
|
|
13
|
+
|
|
14
|
+
// TODO(sam): is there a better way to lazily evaluate the Effect and cache the result?
|
|
15
|
+
const resolveHandler = async (
|
|
16
|
+
effect: HandlerEffect & {
|
|
17
|
+
[memo]?: Handler;
|
|
18
|
+
},
|
|
19
|
+
) =>
|
|
20
|
+
(effect[memo] ??= await Effect.runPromise(
|
|
21
|
+
// safe to cast away the Capability requirements since they are phantoms
|
|
22
|
+
effect as HandlerEffect<never>,
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
export const toHandler =
|
|
26
|
+
<H extends Handler>(effect: Effect.Effect<H, any, Capability>) =>
|
|
27
|
+
async (event: any, context: LambdaContext) =>
|
|
28
|
+
Effect.runPromise(
|
|
29
|
+
(await resolveHandler(effect))(event, context),
|
|
30
|
+
) as Promise<Effect.Effect.Success<ReturnType<H>>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
|
|
3
|
+
import { $, Binding, type Capability, declare, toEnvKey } from "alchemy-effect";
|
|
4
|
+
import { FunctionClient } from "./function.client.ts";
|
|
5
|
+
import { Function } from "./function.ts";
|
|
6
|
+
|
|
7
|
+
export interface InvokeFunction<Resource = unknown>
|
|
8
|
+
extends Capability<"AWS.Lambda.InvokeFunction", Resource> {}
|
|
9
|
+
|
|
10
|
+
export const InvokeFunction = Binding<
|
|
11
|
+
<F extends Function>(func: F) => Binding<Function, InvokeFunction<$<F>>>
|
|
12
|
+
>(Function, "AWS.Lambda.InvokeFunction");
|
|
13
|
+
|
|
14
|
+
export const invoke = <F extends Function>(func: F, input: any) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const lambda = yield* FunctionClient;
|
|
17
|
+
const functionArn = process.env[`${func.id}-functionArn`]!;
|
|
18
|
+
yield* declare<InvokeFunction<F>>();
|
|
19
|
+
return yield* lambda.invoke({
|
|
20
|
+
FunctionName: functionArn,
|
|
21
|
+
InvocationType: "RequestResponse",
|
|
22
|
+
Payload: JSON.stringify(input),
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const invokeFunctionFromLambda = InvokeFunction.provider.succeed({
|
|
27
|
+
attach: ({ source: func }) => ({
|
|
28
|
+
env: {
|
|
29
|
+
[toEnvKey(func.id, "FUNCTION_ARN")]: func.attr.functionArn,
|
|
30
|
+
},
|
|
31
|
+
policyStatements: [
|
|
32
|
+
{
|
|
33
|
+
Sid: "AWS.Lambda.InvokeFunction",
|
|
34
|
+
Effect: "Allow",
|
|
35
|
+
Action: ["lambda:InvokeFunction"],
|
|
36
|
+
Resource: [func.attr.functionArn],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
});
|