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.
@@ -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
@@ -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 json = await response.json();
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
  ];
@@ -1 +1,2 @@
1
+ export * from "./repository-environment";
1
2
  export * from "./secret";
@@ -1 +1,2 @@
1
+ export * from "./repository-environment";
1
2
  export * from "./secret";
@@ -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>);