@quiltdata/benchling-webhook 0.4.13

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.
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import * as cdk from "aws-cdk-lib";
4
+ import { BenchlingWebhookStack } from "../lib/benchling-webhook-stack";
5
+
6
+ // Import get-env to infer configuration from catalog
7
+ const { inferStackConfig } = require("./get-env.js");
8
+ const { execSync } = require("child_process");
9
+
10
+ /**
11
+ * Check if CDK is bootstrapped for the given account/region
12
+ */
13
+ async function checkCdkBootstrap(account: string, region: string): Promise<void> {
14
+ try {
15
+ console.log(`Checking CDK bootstrap for account ${account} in ${region}...`);
16
+
17
+ // Check if bootstrap stack exists
18
+ const result = execSync(
19
+ `aws cloudformation describe-stacks --region ${region} --stack-name CDKToolkit --query "Stacks[0].StackStatus" --output text 2>&1`,
20
+ { encoding: "utf-8" }
21
+ );
22
+
23
+ const stackStatus = result.trim();
24
+
25
+ if (stackStatus.includes("does not exist") || stackStatus.includes("ValidationError")) {
26
+ console.error("\n❌ CDK Bootstrap Error");
27
+ console.error("=".repeat(80));
28
+ console.error(`CDK is not bootstrapped for account ${account} in region ${region}`);
29
+ console.error("\nTo bootstrap CDK, run:");
30
+ console.error(` npx cdk bootstrap aws://${account}/${region}`);
31
+ console.error("\nOr source your .env and run:");
32
+ console.error(` source .env`);
33
+ console.error(` npx cdk bootstrap aws://\${CDK_DEFAULT_ACCOUNT}/\${CDK_DEFAULT_REGION}`);
34
+ console.error("=".repeat(80));
35
+ process.exit(1);
36
+ }
37
+
38
+ // Check if the stack is in a good state
39
+ if (!stackStatus.includes("COMPLETE")) {
40
+ console.error("\n⚠️ CDK Bootstrap Warning");
41
+ console.error("=".repeat(80));
42
+ console.error(`CDKToolkit stack is in state: ${stackStatus}`);
43
+ console.error("This may cause deployment issues.");
44
+ console.error("=".repeat(80));
45
+ } else {
46
+ console.log(`✓ CDK is bootstrapped (CDKToolkit stack: ${stackStatus})\n`);
47
+ }
48
+ } catch (error) {
49
+ console.error("\n⚠️ Warning: Could not verify CDK bootstrap status");
50
+ console.error(`Error: ${(error as Error).message}`);
51
+ console.error("\nProceeding anyway, but deployment may fail if CDK is not bootstrapped.\n");
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Get environment configuration with catalog inference
57
+ *
58
+ * This combines user-provided values from .env with inferred values from the Quilt catalog.
59
+ * User values always take precedence over inferred values.
60
+ */
61
+ async function getConfig() {
62
+ const userEnv = process.env;
63
+ let inferredEnv: Record<string, string> = {};
64
+
65
+ // If QUILT_CATALOG is provided, try to infer additional configuration
66
+ if (userEnv.QUILT_CATALOG) {
67
+ try {
68
+ console.log(`Inferring configuration from catalog: ${userEnv.QUILT_CATALOG}`);
69
+ const result = await inferStackConfig(`https://${userEnv.QUILT_CATALOG.replace(/^https?:\/\//, '')}`);
70
+ inferredEnv = result.inferredVars;
71
+ console.log("✓ Successfully inferred stack configuration\n");
72
+ } catch (error) {
73
+ console.error(`Warning: Could not infer configuration from catalog: ${(error as Error).message}`);
74
+ console.error("Falling back to environment variables only.\n");
75
+ }
76
+ }
77
+
78
+ // Merge: user env takes precedence over inferred values
79
+ const config = { ...inferredEnv, ...userEnv };
80
+
81
+ // Validate required user-provided values
82
+ const requiredUserVars = [
83
+ "QUILT_CATALOG",
84
+ "QUILT_USER_BUCKET",
85
+ "BENCHLING_CLIENT_ID",
86
+ "BENCHLING_CLIENT_SECRET",
87
+ "BENCHLING_TENANT",
88
+ ];
89
+
90
+ const missingVars = requiredUserVars.filter((varName) => !config[varName]);
91
+
92
+ if (missingVars.length > 0) {
93
+ console.error("Error: Missing required environment variables:");
94
+ missingVars.forEach((varName) => {
95
+ console.error(` - ${varName}`);
96
+ });
97
+ console.error("\nPlease set these variables in your .env file.");
98
+ console.error("See env.template for guidance.");
99
+ process.exit(1);
100
+ }
101
+
102
+ // Validate inferred values are present (should be available if catalog lookup succeeded)
103
+ const requiredInferredVars = [
104
+ "CDK_DEFAULT_ACCOUNT",
105
+ "CDK_DEFAULT_REGION",
106
+ "QUEUE_NAME",
107
+ "SQS_QUEUE_URL",
108
+ "QUILT_DATABASE",
109
+ ];
110
+
111
+ const missingInferredVars = requiredInferredVars.filter((varName) => !config[varName]);
112
+
113
+ if (missingInferredVars.length > 0) {
114
+ console.error("Error: Could not infer required configuration:");
115
+ missingInferredVars.forEach((varName) => {
116
+ console.error(` - ${varName}`);
117
+ });
118
+ console.error("\nThese values should be automatically inferred from your Quilt catalog.");
119
+ console.error("Please ensure:");
120
+ console.error(" 1. QUILT_CATALOG is set correctly");
121
+ console.error(" 2. Your AWS credentials have CloudFormation read permissions");
122
+ console.error(" 3. The Quilt stack is deployed and accessible");
123
+ console.error("\nAlternatively, you can manually set these values in your .env file.");
124
+ process.exit(1);
125
+ }
126
+
127
+ // Validate conditional requirements
128
+ if (config.ENABLE_WEBHOOK_VERIFICATION !== "false" && !config.BENCHLING_APP_DEFINITION_ID) {
129
+ console.error("Error: BENCHLING_APP_DEFINITION_ID is required when webhook verification is enabled.");
130
+ console.error("Either set BENCHLING_APP_DEFINITION_ID or set ENABLE_WEBHOOK_VERIFICATION=false");
131
+ process.exit(1);
132
+ }
133
+
134
+ return config;
135
+ }
136
+
137
+ /**
138
+ * Main execution
139
+ */
140
+ async function main() {
141
+ const config = await getConfig();
142
+
143
+ // Validate CDK bootstrap before proceeding
144
+ await checkCdkBootstrap(config.CDK_DEFAULT_ACCOUNT!, config.CDK_DEFAULT_REGION!);
145
+
146
+ const app = new cdk.App();
147
+ new BenchlingWebhookStack(app, "BenchlingWebhookStack", {
148
+ env: {
149
+ account: config.CDK_DEFAULT_ACCOUNT,
150
+ region: config.CDK_DEFAULT_REGION,
151
+ },
152
+ bucketName: config.QUILT_USER_BUCKET!, // User's data bucket
153
+ queueName: config.QUEUE_NAME!,
154
+ environment: "production",
155
+ prefix: config.PKG_PREFIX || "benchling",
156
+ benchlingClientId: config.BENCHLING_CLIENT_ID!,
157
+ benchlingClientSecret: config.BENCHLING_CLIENT_SECRET!,
158
+ benchlingTenant: config.BENCHLING_TENANT!,
159
+ quiltCatalog: config.QUILT_CATALOG!,
160
+ quiltDatabase: config.QUILT_DATABASE!,
161
+ webhookAllowList: config.WEBHOOK_ALLOW_LIST,
162
+ logLevel: config.LOG_LEVEL || "INFO",
163
+ // ECR repository configuration
164
+ createEcrRepository: config.CREATE_ECR_REPOSITORY === "true",
165
+ ecrRepositoryName: config.ECR_REPOSITORY_NAME || "quiltdata/benchling",
166
+ });
167
+ }
168
+
169
+ main().catch((error) => {
170
+ console.error("Fatal error during CDK synthesis:", error);
171
+ process.exit(1);
172
+ });
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Check CloudWatch logs for the deployed Benchling webhook ECS service
4
+ * Uses CloudFormation stack outputs to find the correct log group
5
+ */
6
+
7
+ require("dotenv/config");
8
+ const { execSync } = require("child_process");
9
+
10
+ const STACK_NAME = "BenchlingWebhookStack";
11
+
12
+ // Validate required environment variables
13
+ if (!process.env.CDK_DEFAULT_REGION) {
14
+ console.error("Error: CDK_DEFAULT_REGION is not set in .env file");
15
+ console.error("Please set CDK_DEFAULT_REGION in your .env file");
16
+ process.exit(1);
17
+ }
18
+
19
+ const AWS_REGION = process.env.CDK_DEFAULT_REGION;
20
+
21
+ function getStackOutputs() {
22
+ try {
23
+ const output = execSync(
24
+ `aws cloudformation describe-stacks --stack-name ${STACK_NAME} --region ${AWS_REGION} --query 'Stacks[0].Outputs' --output json`,
25
+ { encoding: "utf-8" },
26
+ );
27
+ return JSON.parse(output);
28
+ } catch (error) {
29
+ console.error(`Error: Could not get stack outputs for ${STACK_NAME}`);
30
+ console.error("Make sure the stack is deployed and AWS credentials are configured.");
31
+ process.exit(1);
32
+ }
33
+ }
34
+
35
+ function getLogGroupFromOutputs(outputs, logType) {
36
+ let outputKey;
37
+ if (logType === "ecs") {
38
+ outputKey = "EcsLogGroup";
39
+ } else if (logType === "api") {
40
+ outputKey = "ApiGatewayLogGroup";
41
+ } else if (logType === "api-exec") {
42
+ outputKey = "ApiGatewayExecutionLogGroup";
43
+ }
44
+
45
+ const logGroupOutput = outputs.find((o) => o.OutputKey === outputKey);
46
+
47
+ if (!logGroupOutput) {
48
+ console.error(`Error: Could not find ${outputKey} in stack outputs`);
49
+ console.error("Make sure the stack has been deployed with the latest changes.");
50
+ process.exit(1);
51
+ }
52
+
53
+ return logGroupOutput.OutputValue;
54
+ }
55
+
56
+ function printStackInfo(outputs, logGroup, logType) {
57
+ console.log("=".repeat(80));
58
+ console.log("Benchling Webhook Stack Information");
59
+ console.log("=".repeat(80));
60
+
61
+ const clusterName = outputs.find((o) => o.OutputKey === "FargateServiceClusterNameCD3B109F");
62
+ const serviceName = outputs.find((o) => o.OutputKey === "FargateServiceServiceName24CFD869");
63
+ const webhookEndpoint = outputs.find((o) => o.OutputKey === "WebhookEndpoint");
64
+ const version = outputs.find((o) => o.OutputKey === "StackVersion");
65
+ const ecsLogGroup = outputs.find((o) => o.OutputKey === "EcsLogGroup");
66
+ const apiLogGroup = outputs.find((o) => o.OutputKey === "ApiGatewayLogGroup");
67
+ const apiExecLogGroup = outputs.find((o) => o.OutputKey === "ApiGatewayExecutionLogGroup");
68
+ const albDns = outputs.find((o) => o.OutputKey === "LoadBalancerDNS");
69
+
70
+ if (clusterName) console.log(`Cluster: ${clusterName.OutputValue}`);
71
+ if (serviceName) console.log(`Service: ${serviceName.OutputValue}`);
72
+ if (webhookEndpoint) console.log(`Endpoint: ${webhookEndpoint.OutputValue}`);
73
+ if (albDns) console.log(`ALB DNS: ${albDns.OutputValue}`);
74
+ if (version) console.log(`Version: ${version.OutputValue}`);
75
+
76
+ console.log("");
77
+ console.log("Log Groups:");
78
+ if (ecsLogGroup) console.log(` ECS: ${ecsLogGroup.OutputValue}${logType === "ecs" ? " (viewing)" : ""}`);
79
+ if (apiLogGroup) console.log(` API Access: ${apiLogGroup.OutputValue}${logType === "api" ? " (viewing)" : ""}`);
80
+ if (apiExecLogGroup) console.log(` API Exec: ${apiExecLogGroup.OutputValue}${logType === "api-exec" ? " (viewing)" : ""}`);
81
+
82
+ console.log("=".repeat(80));
83
+ console.log("");
84
+ }
85
+
86
+ function main() {
87
+ const args = process.argv.slice(2);
88
+ const logType = args.find((arg) => arg.startsWith("--type="))?.split("=")[1] || "all";
89
+ const filterPattern = args.find((arg) => arg.startsWith("--filter="))?.split("=")[1];
90
+ const since = args.find((arg) => arg.startsWith("--since="))?.split("=")[1] || "5m";
91
+ const follow = args.includes("--follow") || args.includes("-f");
92
+ const tail = args.find((arg) => arg.startsWith("--tail="))?.split("=")[1] || "100";
93
+
94
+ // Validate log type
95
+ if (!["ecs", "api", "api-exec", "all"].includes(logType)) {
96
+ console.error("Error: --type must be 'ecs', 'api', 'api-exec', or 'all'");
97
+ process.exit(1);
98
+ }
99
+
100
+ // Get stack outputs
101
+ const outputs = getStackOutputs();
102
+
103
+ // Handle 'all' type - show all three log groups
104
+ if (logType === "all") {
105
+ printStackInfo(outputs, null, "all");
106
+ console.log("Showing logs from all sources (most recent first):\n");
107
+
108
+ const logGroupDefs = [
109
+ { type: "ECS", group: outputs.find((o) => o.OutputKey === "EcsLogGroup")?.OutputValue },
110
+ { type: "API-Access", group: outputs.find((o) => o.OutputKey === "ApiGatewayLogGroup")?.OutputValue },
111
+ { type: "API-Exec", group: outputs.find((o) => o.OutputKey === "ApiGatewayExecutionLogGroup")?.OutputValue },
112
+ ];
113
+
114
+ // Warn about missing log groups
115
+ const missingGroups = logGroupDefs.filter(lg => !lg.group);
116
+ if (missingGroups.length > 0) {
117
+ console.log("⚠️ WARNING: Some log groups are not available in stack outputs:");
118
+ missingGroups.forEach(({ type }) => {
119
+ console.log(` - ${type}: Stack output not found (may need to redeploy stack)`);
120
+ });
121
+ console.log("");
122
+ }
123
+
124
+ const logGroups = logGroupDefs.filter(lg => lg.group);
125
+
126
+ for (const { type, group } of logGroups) {
127
+ console.log(`\n${"=".repeat(80)}`);
128
+ console.log(`${type}: ${group}`);
129
+ console.log("=".repeat(80));
130
+
131
+ let command = `aws logs tail "${group}"`;
132
+ command += ` --region ${AWS_REGION}`;
133
+ command += ` --since ${since}`;
134
+ command += ` --format short`;
135
+ if (filterPattern) {
136
+ command += ` --filter-pattern "${filterPattern}"`;
137
+ }
138
+ command += ` 2>&1 | tail -${tail}`;
139
+
140
+ try {
141
+ const output = execSync(command, { encoding: "utf-8", shell: "/bin/bash" });
142
+ if (output.trim()) {
143
+ console.log(output);
144
+ } else {
145
+ console.log(`(No logs in the last ${since})`);
146
+ }
147
+ } catch (error) {
148
+ console.log(`Error reading ${type} logs: ${error.message}`);
149
+ }
150
+ }
151
+ return;
152
+ }
153
+
154
+ const logGroup = getLogGroupFromOutputs(outputs, logType);
155
+ printStackInfo(outputs, logGroup, logType);
156
+
157
+ // Build AWS logs command
158
+ let command = `aws logs tail ${logGroup}`;
159
+ command += ` --region ${AWS_REGION}`;
160
+ command += ` --since ${since}`;
161
+ command += ` --format short`;
162
+
163
+ if (filterPattern) {
164
+ command += ` --filter-pattern "${filterPattern}"`;
165
+ }
166
+
167
+ if (follow) {
168
+ command += " --follow";
169
+ console.log("Following logs (Press Ctrl+C to stop)...\n");
170
+ } else {
171
+ command += ` | tail -${tail}`;
172
+ console.log(`Showing last ${tail} log entries from the past ${since}...\n`);
173
+ }
174
+
175
+ // Execute logs command
176
+ try {
177
+ execSync(command, { stdio: "inherit" });
178
+ } catch (error) {
179
+ if (error.status !== 130) {
180
+ // Ignore Ctrl+C exit (status 130)
181
+ console.error("\nError fetching logs. Make sure:");
182
+ console.error("1. The stack is deployed");
183
+ console.error("2. AWS CLI is configured with proper credentials");
184
+ console.error("3. You have CloudWatch Logs read permissions");
185
+ process.exit(1);
186
+ }
187
+ }
188
+ }
189
+
190
+ function printHelp() {
191
+ console.log("Usage: npm run logs [options]");
192
+ console.log("");
193
+ console.log("Options:");
194
+ console.log(" --type=TYPE Log group to view (default: all)");
195
+ console.log(" all = All logs (ECS, API Access, API Execution)");
196
+ console.log(" ecs = ECS container logs (application logs)");
197
+ console.log(" api = API Gateway access logs (requests/responses)");
198
+ console.log(" api-exec = API Gateway execution logs (detailed debugging)");
199
+ console.log(" --since=TIME Time period to fetch logs (default: 5m)");
200
+ console.log(" Examples: 1h, 30m, 2d, 5m");
201
+ console.log(" --filter=PATTERN Filter logs by pattern");
202
+ console.log(" Examples: --filter=ERROR, --filter=canvas, --filter=500");
203
+ console.log(" --follow, -f Follow log output (like tail -f, not available with --type=all)");
204
+ console.log(" --tail=N Show last N lines (default: 100, only without --follow)");
205
+ console.log(" --help, -h Show this help message");
206
+ console.log("");
207
+ console.log("Examples:");
208
+ console.log(" npm run logs # View all logs from past 5 min");
209
+ console.log(" npm run logs -- --type=ecs # View only ECS logs");
210
+ console.log(" npm run logs -- --type=api-exec # View API Gateway execution logs");
211
+ console.log(" npm run logs -- --since=1h # Last hour of all logs");
212
+ console.log(" npm run logs -- --filter=ERROR # Filter for errors in all logs");
213
+ console.log(" npm run logs -- --type=api-exec --filter=500 # API Gateway execution errors");
214
+ console.log(" npm run logs -- --type=ecs --follow # Follow ECS logs");
215
+ console.log(" npm run logs -- --type=api-exec --since=10m # Last 10 min of execution logs");
216
+ }
217
+
218
+ if (require.main === module) {
219
+ const args = process.argv.slice(2);
220
+ if (args.includes("--help") || args.includes("-h")) {
221
+ printHelp();
222
+ process.exit(0);
223
+ }
224
+
225
+ try {
226
+ main();
227
+ } catch (error) {
228
+ console.error("Error:", error.message);
229
+ process.exit(1);
230
+ }
231
+ }
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+
3
+ # Ensure environment variables are set
4
+ if [[ -z "$BENCHLING_CLIENT_ID" || -z "$BENCHLING_TENANT" || -z "$BENCHLING_CLIENT_SECRET" ]]; then
5
+ echo "Error: Required environment variables are not set. Please source .env first."
6
+ exit 1
7
+ fi
8
+
9
+ # Debugging: Print the extracted values
10
+ echo "BENCHLING_CLIENT_ID: $BENCHLING_CLIENT_ID"
11
+ echo "BENCHLING_TENANT: $BENCHLING_TENANT"
12
+
13
+ API_ROOT="https://${BENCHLING_TENANT}.benchling.com/api/v2"
14
+
15
+ # Function to get OAuth Token
16
+ get_token() {
17
+ curl -s -X POST "$API_ROOT/token" \
18
+ -H "Content-Type: application/x-www-form-urlencoded" \
19
+ -d "client_id=${BENCHLING_CLIENT_ID}" \
20
+ -d "client_secret=${BENCHLING_CLIENT_SECRET}" \
21
+ -d "grant_type=client_credentials" | jq -r '.access_token'
22
+ }
23
+
24
+ # Generic function to make API requests
25
+ api_request() {
26
+ local method=$1
27
+ local endpoint=$2
28
+ local data=$3
29
+
30
+ curl -v -X "$method" "$API_ROOT/$endpoint" \
31
+ -H "Authorization: Bearer $TOKEN" \
32
+ -H "Content-Type: application/json" \
33
+ ${data:+--data "$data"}
34
+ }
35
+
36
+ # Get OAuth Token
37
+ TOKEN=$(get_token)
38
+
39
+ # Export TOKEN globally
40
+ export TOKEN
41
+
42
+ # Debugging: Print the token
43
+ if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
44
+ echo "Error: Failed to retrieve access token."
45
+ exit 1
46
+ fi
47
+ echo "TOKEN: $TOKEN"
48
+
49
+ # Check if CANVAS_ID is provided as an argument
50
+ if [[ -n "$1" ]]; then
51
+ CANVAS_ID="$1"
52
+ echo "Fetching canvas with ID: $CANVAS_ID"
53
+ echo "=== $CANVAS_ID ==="
54
+ api_request "GET" "app-canvases/${CANVAS_ID}"
55
+ echo "=== $CANVAS_ID ==="
56
+
57
+ echo "Updating canvas with ID: $CANVAS_ID"
58
+ api_request "PATCH" "app-canvases/${CANVAS_ID}" '{
59
+ "blocks": [
60
+ {
61
+ "enabled": true,
62
+ "id": "user_defined_id",
63
+ "text": "Click me to submit",
64
+ "type": "BUTTON"
65
+ }
66
+ ],
67
+ "enabled": true,
68
+ "featureId": "quilt_integration"
69
+ }'
70
+
71
+ else
72
+ echo "No canvas ID provided. Fetching apps instead."
73
+ api_request "GET" "apps"
74
+ fi