alchemy 0.9.1 → 0.10.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/lib/aws/function.d.ts +4 -0
- package/lib/aws/function.js +203 -1
- package/lib/cloudflare/api-error.js +8 -1
- package/lib/github/index.d.ts +1 -0
- package/lib/github/index.js +1 -0
- package/lib/github/repository-environment.d.ts +153 -0
- package/lib/github/repository-environment.js +295 -0
- package/lib/github/secret.d.ts +17 -2
- package/lib/github/secret.js +111 -20
- package/package.json +1 -1
- package/src/aws/function.ts +247 -14
- package/src/cloudflare/api-error.ts +7 -2
- package/src/github/index.ts +1 -0
- package/src/github/repository-environment.ts +469 -0
- package/src/github/secret.ts +127 -27
package/lib/aws/function.d.ts
CHANGED
|
@@ -177,6 +177,10 @@ export interface Function extends Resource<"lambda::Function">, FunctionProps {
|
|
|
177
177
|
* ARN of the signing job
|
|
178
178
|
*/
|
|
179
179
|
signingJobArn?: string;
|
|
180
|
+
/**
|
|
181
|
+
* Function URL if configured
|
|
182
|
+
*/
|
|
183
|
+
functionUrl?: string;
|
|
180
184
|
}
|
|
181
185
|
/**
|
|
182
186
|
* AWS Lambda Function Resource
|
package/lib/aws/function.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Architecture, CreateFunctionCommand, DeleteFunctionCommand, GetFunctionCommand, GetFunctionConfigurationCommand, LambdaClient, ResourceNotFoundException, Runtime, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, } from "@aws-sdk/client-lambda";
|
|
1
|
+
import { AddPermissionCommand, Architecture, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteFunctionCommand, DeleteFunctionUrlConfigCommand, GetFunctionCommand, GetFunctionConfigurationCommand, GetFunctionUrlConfigCommand, LambdaClient, ResourceNotFoundException, Runtime, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, } from "@aws-sdk/client-lambda";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Resource } from "../resource";
|
|
@@ -62,12 +62,26 @@ export const Function = Resource("lambda::Function", async function (id, props)
|
|
|
62
62
|
const region = await resolveRegion(client);
|
|
63
63
|
const code = await zipCode(props.zipPath);
|
|
64
64
|
if (this.phase === "delete") {
|
|
65
|
+
// Delete function URL if it exists
|
|
66
|
+
if (this.output?.url) {
|
|
67
|
+
try {
|
|
68
|
+
await client.send(new DeleteFunctionUrlConfigCommand({
|
|
69
|
+
FunctionName: props.functionName,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.name !== "ResourceNotFoundException") {
|
|
74
|
+
console.warn("Failed to delete function URL:", error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
65
78
|
await ignore(ResourceNotFoundException.name, () => client.send(new DeleteFunctionCommand({
|
|
66
79
|
FunctionName: props.functionName,
|
|
67
80
|
})));
|
|
68
81
|
return this.destroy();
|
|
69
82
|
}
|
|
70
83
|
else {
|
|
84
|
+
let functionUrl;
|
|
71
85
|
try {
|
|
72
86
|
// Check if function exists
|
|
73
87
|
await client.send(new GetFunctionCommand({
|
|
@@ -98,6 +112,139 @@ export const Function = Resource("lambda::Function", async function (id, props)
|
|
|
98
112
|
}));
|
|
99
113
|
// Wait for configuration update to stabilize
|
|
100
114
|
await waitForFunctionStabilization(client, props.functionName);
|
|
115
|
+
// Handle URL configuration
|
|
116
|
+
if (props.url) {
|
|
117
|
+
try {
|
|
118
|
+
// Check if URL config exists already
|
|
119
|
+
const urlConfig = await client.send(new GetFunctionUrlConfigCommand({
|
|
120
|
+
FunctionName: props.functionName,
|
|
121
|
+
}));
|
|
122
|
+
// Update URL configuration if it exists
|
|
123
|
+
if (urlConfig) {
|
|
124
|
+
const updateResult = await client.send(new UpdateFunctionUrlConfigCommand({
|
|
125
|
+
FunctionName: props.functionName,
|
|
126
|
+
AuthType: props.url.authType || "NONE",
|
|
127
|
+
Cors: props.url.cors
|
|
128
|
+
? {
|
|
129
|
+
AllowCredentials: props.url.cors.allowCredentials,
|
|
130
|
+
AllowHeaders: props.url.cors.allowHeaders,
|
|
131
|
+
AllowMethods: props.url.cors.allowMethods,
|
|
132
|
+
AllowOrigins: props.url.cors.allowOrigins,
|
|
133
|
+
ExposeHeaders: props.url.cors.exposeHeaders,
|
|
134
|
+
MaxAge: props.url.cors.maxAge,
|
|
135
|
+
}
|
|
136
|
+
: undefined,
|
|
137
|
+
}));
|
|
138
|
+
functionUrl = updateResult.FunctionUrl;
|
|
139
|
+
// Add public access permission for function URL
|
|
140
|
+
if (props.url.authType === "NONE") {
|
|
141
|
+
try {
|
|
142
|
+
await client.send(new AddPermissionCommand({
|
|
143
|
+
FunctionName: props.functionName,
|
|
144
|
+
StatementId: "FunctionURLAllowPublicAccess",
|
|
145
|
+
Action: "lambda:InvokeFunctionUrl",
|
|
146
|
+
Principal: "*",
|
|
147
|
+
FunctionUrlAuthType: "NONE",
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
catch (permError) {
|
|
151
|
+
if (!permError.message?.includes("already exists")) {
|
|
152
|
+
console.warn("Error adding URL permission:", permError);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Create URL configuration if it doesn't exist
|
|
159
|
+
const createResult = await client.send(new CreateFunctionUrlConfigCommand({
|
|
160
|
+
FunctionName: props.functionName,
|
|
161
|
+
AuthType: props.url.authType || "NONE",
|
|
162
|
+
Cors: props.url.cors
|
|
163
|
+
? {
|
|
164
|
+
AllowCredentials: props.url.cors.allowCredentials,
|
|
165
|
+
AllowHeaders: props.url.cors.allowHeaders,
|
|
166
|
+
AllowMethods: props.url.cors.allowMethods,
|
|
167
|
+
AllowOrigins: props.url.cors.allowOrigins,
|
|
168
|
+
ExposeHeaders: props.url.cors.exposeHeaders,
|
|
169
|
+
MaxAge: props.url.cors.maxAge,
|
|
170
|
+
}
|
|
171
|
+
: undefined,
|
|
172
|
+
}));
|
|
173
|
+
functionUrl = createResult.FunctionUrl;
|
|
174
|
+
// Add public access permission for function URL
|
|
175
|
+
if (props.url.authType === "NONE") {
|
|
176
|
+
try {
|
|
177
|
+
await client.send(new AddPermissionCommand({
|
|
178
|
+
FunctionName: props.functionName,
|
|
179
|
+
StatementId: "FunctionURLAllowPublicAccess",
|
|
180
|
+
Action: "lambda:InvokeFunctionUrl",
|
|
181
|
+
Principal: "*",
|
|
182
|
+
FunctionUrlAuthType: "NONE",
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
catch (permError) {
|
|
186
|
+
if (!permError.message?.includes("already exists")) {
|
|
187
|
+
console.warn("Error adding URL permission:", permError);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error.name === "ResourceNotFoundException") {
|
|
195
|
+
// Create URL configuration if it doesn't exist
|
|
196
|
+
const createResult = await client.send(new CreateFunctionUrlConfigCommand({
|
|
197
|
+
FunctionName: props.functionName,
|
|
198
|
+
AuthType: props.url.authType || "NONE",
|
|
199
|
+
Cors: props.url.cors
|
|
200
|
+
? {
|
|
201
|
+
AllowCredentials: props.url.cors.allowCredentials,
|
|
202
|
+
AllowHeaders: props.url.cors.allowHeaders,
|
|
203
|
+
AllowMethods: props.url.cors.allowMethods,
|
|
204
|
+
AllowOrigins: props.url.cors.allowOrigins,
|
|
205
|
+
ExposeHeaders: props.url.cors.exposeHeaders,
|
|
206
|
+
MaxAge: props.url.cors.maxAge,
|
|
207
|
+
}
|
|
208
|
+
: undefined,
|
|
209
|
+
}));
|
|
210
|
+
functionUrl = createResult.FunctionUrl;
|
|
211
|
+
// Add public access permission for function URL
|
|
212
|
+
if (props.url.authType === "NONE") {
|
|
213
|
+
try {
|
|
214
|
+
await client.send(new AddPermissionCommand({
|
|
215
|
+
FunctionName: props.functionName,
|
|
216
|
+
StatementId: "FunctionURLAllowPublicAccess",
|
|
217
|
+
Action: "lambda:InvokeFunctionUrl",
|
|
218
|
+
Principal: "*",
|
|
219
|
+
FunctionUrlAuthType: "NONE",
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
catch (permError) {
|
|
223
|
+
if (!permError.message?.includes("already exists")) {
|
|
224
|
+
console.warn("Error adding URL permission:", permError);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else if (this.output?.url) {
|
|
235
|
+
// Remove URL config if it was previously set but not in current props
|
|
236
|
+
try {
|
|
237
|
+
await client.send(new DeleteFunctionUrlConfigCommand({
|
|
238
|
+
FunctionName: props.functionName,
|
|
239
|
+
}));
|
|
240
|
+
functionUrl = undefined;
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (error.name !== "ResourceNotFoundException") {
|
|
244
|
+
console.warn("Failed to delete function URL:", error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
101
248
|
}
|
|
102
249
|
}
|
|
103
250
|
catch (error) {
|
|
@@ -149,6 +296,46 @@ export const Function = Resource("lambda::Function", async function (id, props)
|
|
|
149
296
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
150
297
|
}
|
|
151
298
|
}
|
|
299
|
+
// Create URL configuration if needed
|
|
300
|
+
if (props.url) {
|
|
301
|
+
try {
|
|
302
|
+
const createResult = await client.send(new CreateFunctionUrlConfigCommand({
|
|
303
|
+
FunctionName: props.functionName,
|
|
304
|
+
AuthType: props.url.authType || "NONE",
|
|
305
|
+
Cors: props.url.cors
|
|
306
|
+
? {
|
|
307
|
+
AllowCredentials: props.url.cors.allowCredentials,
|
|
308
|
+
AllowHeaders: props.url.cors.allowHeaders,
|
|
309
|
+
AllowMethods: props.url.cors.allowMethods,
|
|
310
|
+
AllowOrigins: props.url.cors.allowOrigins,
|
|
311
|
+
ExposeHeaders: props.url.cors.exposeHeaders,
|
|
312
|
+
MaxAge: props.url.cors.maxAge,
|
|
313
|
+
}
|
|
314
|
+
: undefined,
|
|
315
|
+
}));
|
|
316
|
+
functionUrl = createResult.FunctionUrl;
|
|
317
|
+
// Add public access permission for function URL
|
|
318
|
+
if (props.url.authType === "NONE") {
|
|
319
|
+
try {
|
|
320
|
+
await client.send(new AddPermissionCommand({
|
|
321
|
+
FunctionName: props.functionName,
|
|
322
|
+
StatementId: "FunctionURLAllowPublicAccess",
|
|
323
|
+
Action: "lambda:InvokeFunctionUrl",
|
|
324
|
+
Principal: "*",
|
|
325
|
+
FunctionUrlAuthType: "NONE",
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
catch (permError) {
|
|
329
|
+
if (!permError.message?.includes("already exists")) {
|
|
330
|
+
console.warn("Error adding URL permission:", permError);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
console.warn("Failed to create function URL:", error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
152
339
|
}
|
|
153
340
|
else {
|
|
154
341
|
throw error;
|
|
@@ -163,6 +350,20 @@ export const Function = Resource("lambda::Function", async function (id, props)
|
|
|
163
350
|
FunctionName: props.functionName,
|
|
164
351
|
})),
|
|
165
352
|
]);
|
|
353
|
+
// Try to get function URL if it wasn't already retrieved and URL is configured
|
|
354
|
+
if (!functionUrl && (props.url || this.output?.url)) {
|
|
355
|
+
try {
|
|
356
|
+
const urlConfig = await client.send(new GetFunctionUrlConfigCommand({
|
|
357
|
+
FunctionName: props.functionName,
|
|
358
|
+
}));
|
|
359
|
+
functionUrl = urlConfig.FunctionUrl;
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (error.name !== "ResourceNotFoundException") {
|
|
363
|
+
console.warn("Failed to get function URL:", error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
166
367
|
return this({
|
|
167
368
|
...props,
|
|
168
369
|
arn: config.FunctionArn,
|
|
@@ -185,6 +386,7 @@ export const Function = Resource("lambda::Function", async function (id, props)
|
|
|
185
386
|
packageType: config.PackageType,
|
|
186
387
|
signingProfileVersionArn: config.SigningProfileVersionArn,
|
|
187
388
|
signingJobArn: config.SigningJobArn,
|
|
389
|
+
functionUrl: functionUrl,
|
|
188
390
|
});
|
|
189
391
|
}
|
|
190
392
|
});
|
|
@@ -38,7 +38,14 @@ export class CloudflareApiError extends Error {
|
|
|
38
38
|
* @returns Never returns - always throws an error
|
|
39
39
|
*/
|
|
40
40
|
export async function handleApiError(response, action, resourceType, resourceName) {
|
|
41
|
-
const
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
let json;
|
|
43
|
+
try {
|
|
44
|
+
json = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
json = { errors: [{ message: text }] };
|
|
48
|
+
}
|
|
42
49
|
const errors = json?.errors || [
|
|
43
50
|
{ message: response.statusText },
|
|
44
51
|
];
|
package/lib/github/index.d.ts
CHANGED
package/lib/github/index.js
CHANGED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Context } from "../context";
|
|
2
|
+
import { Resource } from "../resource";
|
|
3
|
+
/**
|
|
4
|
+
* Properties for creating or updating a GitHub Repository Environment
|
|
5
|
+
*/
|
|
6
|
+
export interface RepositoryEnvironmentProps {
|
|
7
|
+
/**
|
|
8
|
+
* Repository owner (user or organization)
|
|
9
|
+
*/
|
|
10
|
+
owner: string;
|
|
11
|
+
/**
|
|
12
|
+
* Repository name
|
|
13
|
+
*/
|
|
14
|
+
repository: string;
|
|
15
|
+
/**
|
|
16
|
+
* Environment name
|
|
17
|
+
*/
|
|
18
|
+
name: string;
|
|
19
|
+
/**
|
|
20
|
+
* Wait timer before allowing deployments to proceed (in minutes)
|
|
21
|
+
* Must be between 0 and 43200 (30 days)
|
|
22
|
+
* @default 0
|
|
23
|
+
*/
|
|
24
|
+
waitTimer?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Determine whether to prevent self-reviews on pull requests
|
|
27
|
+
* Note: Requires at least one reviewer when enabled
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
preventSelfReview?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Determine whether administrators can bypass deployment protection rules
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
adminBypass?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Required reviewers for deployments to this environment
|
|
38
|
+
*/
|
|
39
|
+
reviewers?: {
|
|
40
|
+
/**
|
|
41
|
+
* GitHub usernames or user IDs that can approve deployments
|
|
42
|
+
* Can be numeric IDs or string usernames (which will be resolved to IDs)
|
|
43
|
+
*/
|
|
44
|
+
users?: Array<number | string>;
|
|
45
|
+
/**
|
|
46
|
+
* GitHub team names or team IDs that can approve deployments
|
|
47
|
+
* Can be numeric IDs or string team names (which will be resolved to IDs)
|
|
48
|
+
* Note: For team names, should be in the format "org/team-name"
|
|
49
|
+
*/
|
|
50
|
+
teams?: Array<number | string>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Deployment branch policy for the environment
|
|
54
|
+
*/
|
|
55
|
+
deploymentBranchPolicy?: {
|
|
56
|
+
/**
|
|
57
|
+
* Whether to restrict deployments to protected branches
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
protectedBranches?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Whether to allow custom branch policies
|
|
63
|
+
* When true, specific branch patterns can be specified using createDeploymentBranchPolicy
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
customBranchPolicies?: boolean;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Branch patterns for deployment when customBranchPolicies is true
|
|
70
|
+
* For example: ["main", "releases/*"]
|
|
71
|
+
* @default []
|
|
72
|
+
*/
|
|
73
|
+
branchPatterns?: string[];
|
|
74
|
+
/**
|
|
75
|
+
* Optional GitHub API token (overrides environment variable)
|
|
76
|
+
* If not provided, will use GITHUB_TOKEN environment variable
|
|
77
|
+
* @default process.env.GITHUB_TOKEN
|
|
78
|
+
*/
|
|
79
|
+
token?: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Output returned after Repository Environment creation/update
|
|
83
|
+
*/
|
|
84
|
+
export interface RepositoryEnvironment extends Resource<"github::RepositoryEnvironment">, RepositoryEnvironmentProps {
|
|
85
|
+
/**
|
|
86
|
+
* The ID of the resource
|
|
87
|
+
*/
|
|
88
|
+
id: string;
|
|
89
|
+
/**
|
|
90
|
+
* The numeric ID of the environment in GitHub
|
|
91
|
+
*/
|
|
92
|
+
environmentId: number;
|
|
93
|
+
/**
|
|
94
|
+
* Time at which the object was created/updated
|
|
95
|
+
*/
|
|
96
|
+
updatedAt: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resource for managing GitHub repository environments
|
|
100
|
+
*
|
|
101
|
+
* Note: If preventSelfReview is true, at least one reviewer must be specified.
|
|
102
|
+
*
|
|
103
|
+
* Branch policies are efficiently managed with proper diffing:
|
|
104
|
+
* - Only manages branch patterns configured through this resource
|
|
105
|
+
* - Preserves any manually added branch patterns outside this resource
|
|
106
|
+
* - When updating, only modifies patterns that have changed from previous state
|
|
107
|
+
* - Compares against previous resource state, not current environment state
|
|
108
|
+
* - Safely handles policy type changes
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Create a basic environment with no protection rules
|
|
112
|
+
* const devEnv = await RepositoryEnvironment("dev-environment", {
|
|
113
|
+
* owner: "my-org",
|
|
114
|
+
* repository: "my-repo",
|
|
115
|
+
* name: "development"
|
|
116
|
+
* });
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* // Create a production environment with approval requirements
|
|
120
|
+
* const prodEnv = await RepositoryEnvironment("prod-environment", {
|
|
121
|
+
* owner: "my-org",
|
|
122
|
+
* repository: "my-repo",
|
|
123
|
+
* name: "production",
|
|
124
|
+
* waitTimer: 10, // 10 minute delay
|
|
125
|
+
* preventSelfReview: true,
|
|
126
|
+
* reviewers: {
|
|
127
|
+
* teams: ["platform-team"], // team name
|
|
128
|
+
* users: ["security-admin"] // username
|
|
129
|
+
* },
|
|
130
|
+
* deploymentBranchPolicy: {
|
|
131
|
+
* protectedBranches: true,
|
|
132
|
+
* customBranchPolicies: false
|
|
133
|
+
* }
|
|
134
|
+
* });
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* // Create an environment with reviewer IDs
|
|
138
|
+
* const stagingEnv = await RepositoryEnvironment("staging-environment", {
|
|
139
|
+
* owner: "my-org",
|
|
140
|
+
* repository: "my-repo",
|
|
141
|
+
* name: "staging",
|
|
142
|
+
* reviewers: {
|
|
143
|
+
* teams: [1234567], // team ID
|
|
144
|
+
* users: [7654321] // user ID
|
|
145
|
+
* },
|
|
146
|
+
* deploymentBranchPolicy: {
|
|
147
|
+
* protectedBranches: false,
|
|
148
|
+
* customBranchPolicies: true
|
|
149
|
+
* },
|
|
150
|
+
* branchPatterns: ["main", "release/*"]
|
|
151
|
+
* });
|
|
152
|
+
*/
|
|
153
|
+
export declare const RepositoryEnvironment: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<RepositoryEnvironment>, id: string, props: RepositoryEnvironmentProps) => Promise<RepositoryEnvironment>);
|