@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.
- package/AGENTS.md +226 -0
- package/CHANGELOG.md +91 -0
- package/LICENSE +201 -0
- package/README.benchling.md +77 -0
- package/README.md +53 -0
- package/bin/benchling-webhook.ts +172 -0
- package/bin/check-logs.js +231 -0
- package/bin/cli-auth.sh +74 -0
- package/bin/get-env.js +564 -0
- package/bin/publish-manual.js +211 -0
- package/bin/release-notes.sh +82 -0
- package/bin/release.js +118 -0
- package/bin/send-event.js +203 -0
- package/bin/sync-version.js +72 -0
- package/bin/test-invalid-signature.js +125 -0
- package/bin/version.js +178 -0
- package/cdk.context.json +58 -0
- package/cdk.json +85 -0
- package/doc/NPM_OIDC_SETUP.md +95 -0
- package/doc/PARAMETERS.md +203 -0
- package/doc/RELEASE.md +297 -0
- package/doc/RELEASE_NOTES.md +64 -0
- package/env.template +67 -0
- package/jest.config.js +14 -0
- package/lib/README.md +50 -0
- package/lib/index.ts +31 -0
- package/lib/oauth-tester.json +35 -0
- package/package.json +79 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { createSign, generateKeyPairSync } = require('crypto');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Parse command line arguments
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
if (args.length === 0) {
|
|
8
|
+
console.error('Usage: test-invalid-signature.js <webhook-url>');
|
|
9
|
+
console.error('Example: test-invalid-signature.js https://example.com/prod/lifecycle');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const webhookUrl = args[0];
|
|
14
|
+
|
|
15
|
+
// Read the test payload
|
|
16
|
+
const testPayload = JSON.parse(fs.readFileSync('test-events/app-installed.json', 'utf8'));
|
|
17
|
+
const rawBody = JSON.stringify(testPayload);
|
|
18
|
+
|
|
19
|
+
// Generate a WRONG key pair (not the one Benchling has)
|
|
20
|
+
const { privateKey } = generateKeyPairSync('ec', {
|
|
21
|
+
namedCurve: 'prime256v1'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Create realistic webhook headers
|
|
25
|
+
const webhookId = 'wh_test123';
|
|
26
|
+
const webhookTimestamp = Math.floor(Date.now() / 1000).toString();
|
|
27
|
+
|
|
28
|
+
// Sign with the WRONG private key
|
|
29
|
+
const payloadToSign = `${webhookId}.${webhookTimestamp}.${rawBody}`;
|
|
30
|
+
const signer = createSign('sha256');
|
|
31
|
+
signer.update(payloadToSign);
|
|
32
|
+
signer.end();
|
|
33
|
+
|
|
34
|
+
const invalidSignature = signer.sign(privateKey).toString('base64');
|
|
35
|
+
|
|
36
|
+
console.log('Testing webhook security with INVALID signature');
|
|
37
|
+
console.log('='.repeat(60));
|
|
38
|
+
console.log('Target URL:', webhookUrl);
|
|
39
|
+
console.log('Webhook-Id:', webhookId);
|
|
40
|
+
console.log('Webhook-Timestamp:', webhookTimestamp);
|
|
41
|
+
console.log('Webhook-Signature:', `v1bder,${invalidSignature}`);
|
|
42
|
+
console.log('\nNote: This signature is valid ECDSA format but signed with the WRONG key.');
|
|
43
|
+
console.log('The webhook MUST reject this request.\n');
|
|
44
|
+
|
|
45
|
+
// Make the actual HTTP request
|
|
46
|
+
(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(webhookUrl, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
'webhook-id': webhookId,
|
|
53
|
+
'webhook-timestamp': webhookTimestamp,
|
|
54
|
+
'webhook-signature': `v1bder,${invalidSignature}`
|
|
55
|
+
},
|
|
56
|
+
body: rawBody
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const responseText = await response.text();
|
|
60
|
+
let responseBody;
|
|
61
|
+
try {
|
|
62
|
+
responseBody = JSON.parse(responseText);
|
|
63
|
+
} catch {
|
|
64
|
+
responseBody = responseText;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('Response Status:', response.status, response.statusText);
|
|
68
|
+
console.log('Response Body:', JSON.stringify(responseBody, null, 2));
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
// Validate the response
|
|
72
|
+
if (response.status === 202) {
|
|
73
|
+
console.log('⚠️ WARNING: Request was ACCEPTED (202)');
|
|
74
|
+
console.log(' This means validation happens asynchronously.');
|
|
75
|
+
|
|
76
|
+
if (responseBody.executionArn) {
|
|
77
|
+
console.log(' Checking execution status...');
|
|
78
|
+
const executionArn = responseBody.executionArn;
|
|
79
|
+
|
|
80
|
+
// Wait a bit for execution to start
|
|
81
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
82
|
+
|
|
83
|
+
// Check execution status using AWS CLI
|
|
84
|
+
const { execSync } = require('child_process');
|
|
85
|
+
try {
|
|
86
|
+
const status = execSync(
|
|
87
|
+
`aws stepfunctions describe-execution --execution-arn "${executionArn}" --query 'status' --output text`,
|
|
88
|
+
{ encoding: 'utf8' }
|
|
89
|
+
).trim();
|
|
90
|
+
|
|
91
|
+
console.log(' Execution Status:', status);
|
|
92
|
+
|
|
93
|
+
if (status === 'FAILED') {
|
|
94
|
+
const history = execSync(
|
|
95
|
+
`aws stepfunctions get-execution-history --execution-arn "${executionArn}" --query 'events[?type==\`ExecutionFailed\`].executionFailedEventDetails.error' --output text`,
|
|
96
|
+
{ encoding: 'utf8' }
|
|
97
|
+
).trim();
|
|
98
|
+
console.log(' Failure Reason:', history || 'WebhookVerificationError');
|
|
99
|
+
console.log('\n✅ PASS: Webhook correctly rejected invalid signature (async)');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
} else if (status === 'SUCCEEDED') {
|
|
102
|
+
console.log('\n❌ FAIL: Webhook accepted invalid signature!');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(` Status: ${status} - manual verification needed`);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.log(' Could not check execution status:', error.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else if (response.status === 401 || response.status === 403) {
|
|
112
|
+
console.log('✅ PASS: Webhook correctly rejected invalid signature (sync)');
|
|
113
|
+
process.exit(0);
|
|
114
|
+
} else if (response.status >= 400) {
|
|
115
|
+
console.log(`⚠️ Request rejected with status ${response.status}`);
|
|
116
|
+
console.log(' Manual verification needed');
|
|
117
|
+
} else if (response.status >= 200 && response.status < 300) {
|
|
118
|
+
console.log('❌ FAIL: Webhook accepted invalid signature!');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Error making request:', error.message);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
})();
|
package/bin/version.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Version management script - bumps version numbers across all files
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node bin/version.js # Show current version
|
|
8
|
+
* node bin/version.js patch # 0.4.7 -> 0.4.8
|
|
9
|
+
* node bin/version.js minor # 0.4.7 -> 0.5.0
|
|
10
|
+
* node bin/version.js major # 0.4.7 -> 1.0.0
|
|
11
|
+
* node bin/version.js dev # 0.4.7 -> 0.4.8-dev.0
|
|
12
|
+
* node bin/version.js dev-bump # 0.4.8-dev.0 -> 0.4.8-dev.1
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
20
|
+
const pyprojectPath = path.join(__dirname, '..', 'docker', 'pyproject.toml');
|
|
21
|
+
const appManifestPath = path.join(__dirname, '..', 'docker', 'app-manifest.yaml');
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
23
|
+
|
|
24
|
+
function parseVersion(version) {
|
|
25
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-dev\.(\d+))?$/);
|
|
26
|
+
if (!match) {
|
|
27
|
+
throw new Error(`Invalid version format: ${version}`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
major: parseInt(match[1]),
|
|
31
|
+
minor: parseInt(match[2]),
|
|
32
|
+
patch: parseInt(match[3]),
|
|
33
|
+
dev: match[4] ? parseInt(match[4]) : null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatVersion(ver) {
|
|
38
|
+
let version = `${ver.major}.${ver.minor}.${ver.patch}`;
|
|
39
|
+
if (ver.dev !== null) {
|
|
40
|
+
version += `-dev.${ver.dev}`;
|
|
41
|
+
}
|
|
42
|
+
return version;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function bumpVersion(currentVersion, bumpType) {
|
|
46
|
+
const ver = parseVersion(currentVersion);
|
|
47
|
+
|
|
48
|
+
switch (bumpType) {
|
|
49
|
+
case 'major':
|
|
50
|
+
ver.major++;
|
|
51
|
+
ver.minor = 0;
|
|
52
|
+
ver.patch = 0;
|
|
53
|
+
ver.dev = null;
|
|
54
|
+
break;
|
|
55
|
+
case 'minor':
|
|
56
|
+
ver.minor++;
|
|
57
|
+
ver.patch = 0;
|
|
58
|
+
ver.dev = null;
|
|
59
|
+
break;
|
|
60
|
+
case 'patch':
|
|
61
|
+
ver.patch++;
|
|
62
|
+
ver.dev = null;
|
|
63
|
+
break;
|
|
64
|
+
case 'dev':
|
|
65
|
+
// If already a dev version, increment dev counter
|
|
66
|
+
// Otherwise, bump patch and add dev.0
|
|
67
|
+
if (ver.dev !== null) {
|
|
68
|
+
ver.dev++;
|
|
69
|
+
} else {
|
|
70
|
+
ver.patch++;
|
|
71
|
+
ver.dev = 0;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case 'dev-bump':
|
|
75
|
+
// Only bump dev counter, error if not a dev version
|
|
76
|
+
if (ver.dev === null) {
|
|
77
|
+
throw new Error('Cannot bump dev counter on non-dev version. Use "dev" instead.');
|
|
78
|
+
}
|
|
79
|
+
ver.dev++;
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
throw new Error(`Unknown bump type: ${bumpType}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return formatVersion(ver);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function updatePackageVersion(newVersion) {
|
|
89
|
+
pkg.version = newVersion;
|
|
90
|
+
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
|
|
91
|
+
console.log(`✅ Updated package.json to version ${newVersion}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updatePyprojectVersion(newVersion) {
|
|
95
|
+
let content = fs.readFileSync(pyprojectPath, 'utf8');
|
|
96
|
+
content = content.replace(/^version\s*=\s*"[^"]+"/m, `version = "${newVersion}"`);
|
|
97
|
+
fs.writeFileSync(pyprojectPath, content);
|
|
98
|
+
console.log(`✅ Updated docker/pyproject.toml to version ${newVersion}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function updateAppManifestVersion(newVersion) {
|
|
102
|
+
let content = fs.readFileSync(appManifestPath, 'utf8');
|
|
103
|
+
content = content.replace(/^(\s*)version:\s*.+$/m, `$1version: ${newVersion}`);
|
|
104
|
+
fs.writeFileSync(appManifestPath, content);
|
|
105
|
+
console.log(`✅ Updated docker/app-manifest.yaml to version ${newVersion}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function main() {
|
|
109
|
+
const args = process.argv.slice(2);
|
|
110
|
+
|
|
111
|
+
// No args: just output the version number (for scripting)
|
|
112
|
+
if (args.length === 0) {
|
|
113
|
+
console.log(pkg.version);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Help
|
|
118
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
119
|
+
console.log('Current version:', pkg.version);
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log('Usage: node bin/version.js [command]');
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log('Commands:');
|
|
124
|
+
console.log(' (no args) - Output current version');
|
|
125
|
+
console.log(' major - Bump major version (1.0.0 -> 2.0.0)');
|
|
126
|
+
console.log(' minor - Bump minor version (0.4.7 -> 0.5.0)');
|
|
127
|
+
console.log(' patch - Bump patch version (0.4.7 -> 0.4.8)');
|
|
128
|
+
console.log(' dev - Bump to dev version (0.4.7 -> 0.4.8-dev.0 or 0.4.8-dev.0 -> 0.4.8-dev.1)');
|
|
129
|
+
console.log(' dev-bump - Bump dev counter only (0.4.8-dev.0 -> 0.4.8-dev.1)');
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log('This script only updates version numbers in:');
|
|
132
|
+
console.log(' - package.json');
|
|
133
|
+
console.log(' - docker/pyproject.toml');
|
|
134
|
+
console.log(' - docker/app-manifest.yaml');
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log('To create a release tag, use: npm run release or npm run release:dev');
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const bumpType = args[0];
|
|
141
|
+
|
|
142
|
+
// Check for uncommitted changes
|
|
143
|
+
try {
|
|
144
|
+
execSync('git diff-index --quiet HEAD --', { stdio: 'ignore' });
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.error('❌ You have uncommitted changes');
|
|
147
|
+
console.error(' Commit or stash your changes before bumping version');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const currentVersion = pkg.version;
|
|
153
|
+
const newVersion = bumpVersion(currentVersion, bumpType);
|
|
154
|
+
|
|
155
|
+
console.log(`Bumping version: ${currentVersion} -> ${newVersion}`);
|
|
156
|
+
console.log('');
|
|
157
|
+
|
|
158
|
+
// Update all version files
|
|
159
|
+
updatePackageVersion(newVersion);
|
|
160
|
+
updatePyprojectVersion(newVersion);
|
|
161
|
+
updateAppManifestVersion(newVersion);
|
|
162
|
+
|
|
163
|
+
// Commit the changes
|
|
164
|
+
execSync('git add package.json docker/pyproject.toml docker/app-manifest.yaml', { stdio: 'inherit' });
|
|
165
|
+
execSync(`git commit -m "chore: bump version to ${newVersion}"`, { stdio: 'inherit' });
|
|
166
|
+
console.log(`✅ Committed version change`);
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log('Next steps:');
|
|
169
|
+
console.log(' 1. Push changes: git push');
|
|
170
|
+
console.log(' 2. Create release: npm run release (or npm run release:dev for dev release)');
|
|
171
|
+
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('❌ Error:', error.message);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
main();
|
package/cdk.context.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"acknowledged-issue-numbers": [
|
|
3
|
+
32775,
|
|
4
|
+
30717,
|
|
5
|
+
34293,
|
|
6
|
+
34486
|
|
7
|
+
],
|
|
8
|
+
"vpc-provider:account=712023778557:filter.isDefault=true:region=us-east-1:returnAsymmetricSubnets=true": {
|
|
9
|
+
"vpcId": "vpc-2dda6457",
|
|
10
|
+
"vpcCidrBlock": "172.31.0.0/16",
|
|
11
|
+
"ownerAccountId": "712023778557",
|
|
12
|
+
"availabilityZones": [],
|
|
13
|
+
"subnetGroups": [
|
|
14
|
+
{
|
|
15
|
+
"name": "Public",
|
|
16
|
+
"type": "Public",
|
|
17
|
+
"subnets": [
|
|
18
|
+
{
|
|
19
|
+
"subnetId": "subnet-a9e2d0e3",
|
|
20
|
+
"cidr": "172.31.16.0/20",
|
|
21
|
+
"availabilityZone": "us-east-1a",
|
|
22
|
+
"routeTableId": "rtb-455b5e3a"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"subnetId": "subnet-f1a1cead",
|
|
26
|
+
"cidr": "172.31.32.0/20",
|
|
27
|
+
"availabilityZone": "us-east-1b",
|
|
28
|
+
"routeTableId": "rtb-455b5e3a"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"subnetId": "subnet-5853313f",
|
|
32
|
+
"cidr": "172.31.0.0/20",
|
|
33
|
+
"availabilityZone": "us-east-1c",
|
|
34
|
+
"routeTableId": "rtb-455b5e3a"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"subnetId": "subnet-5dbfd673",
|
|
38
|
+
"cidr": "172.31.80.0/20",
|
|
39
|
+
"availabilityZone": "us-east-1d",
|
|
40
|
+
"routeTableId": "rtb-455b5e3a"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"subnetId": "subnet-7a3f8944",
|
|
44
|
+
"cidr": "172.31.48.0/20",
|
|
45
|
+
"availabilityZone": "us-east-1e",
|
|
46
|
+
"routeTableId": "rtb-455b5e3a"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"subnetId": "subnet-30e0c43f",
|
|
50
|
+
"cidr": "172.31.64.0/20",
|
|
51
|
+
"availabilityZone": "us-east-1f",
|
|
52
|
+
"routeTableId": "rtb-455b5e3a"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
package/cdk.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app": "npx ts-node --prefer-ts-exts bin/benchling-webhook.ts",
|
|
3
|
+
"watch": {
|
|
4
|
+
"include": [
|
|
5
|
+
"**"
|
|
6
|
+
],
|
|
7
|
+
"exclude": [
|
|
8
|
+
"README.md",
|
|
9
|
+
"cdk*.json",
|
|
10
|
+
"**/*.d.ts",
|
|
11
|
+
"**/*.js",
|
|
12
|
+
"tsconfig.json",
|
|
13
|
+
"package*.json",
|
|
14
|
+
"yarn.lock",
|
|
15
|
+
"node_modules",
|
|
16
|
+
"test"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"context": {
|
|
20
|
+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
|
|
21
|
+
"@aws-cdk/core:checkSecretUsage": true,
|
|
22
|
+
"@aws-cdk/core:target-partitions": [
|
|
23
|
+
"aws",
|
|
24
|
+
"aws-cn"
|
|
25
|
+
],
|
|
26
|
+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
|
|
27
|
+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
|
|
28
|
+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
|
|
29
|
+
"@aws-cdk/aws-iam:minimizePolicies": true,
|
|
30
|
+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
|
|
31
|
+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
|
|
32
|
+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
|
|
33
|
+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
|
|
34
|
+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
|
|
35
|
+
"@aws-cdk/core:enablePartitionLiterals": true,
|
|
36
|
+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
|
|
37
|
+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
|
|
38
|
+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
|
|
39
|
+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
|
|
40
|
+
"@aws-cdk/aws-route53-patters:useCertificate": true,
|
|
41
|
+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
|
|
42
|
+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
|
|
43
|
+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
|
|
44
|
+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
|
|
45
|
+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
|
|
46
|
+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
|
|
47
|
+
"@aws-cdk/aws-redshift:columnId": true,
|
|
48
|
+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
|
|
49
|
+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
|
|
50
|
+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
|
|
51
|
+
"@aws-cdk/aws-kms:aliasNameRef": true,
|
|
52
|
+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
|
|
53
|
+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
|
|
54
|
+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
|
|
55
|
+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
|
|
56
|
+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
|
|
57
|
+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
|
|
58
|
+
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
|
|
59
|
+
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
|
|
60
|
+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
|
|
61
|
+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
|
|
62
|
+
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
|
|
63
|
+
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
|
|
64
|
+
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
|
|
65
|
+
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
|
|
66
|
+
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
|
|
67
|
+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
|
|
68
|
+
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
|
|
69
|
+
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
|
|
70
|
+
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
|
|
71
|
+
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
|
|
72
|
+
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
|
|
73
|
+
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
|
|
74
|
+
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
|
|
75
|
+
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
|
|
76
|
+
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
|
|
77
|
+
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
|
|
78
|
+
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
|
|
79
|
+
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
|
|
80
|
+
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
|
|
81
|
+
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
|
|
82
|
+
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
|
|
83
|
+
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# NPM OIDC Configuration for GitHub Actions
|
|
2
|
+
|
|
3
|
+
This repository now uses OpenID Connect (OIDC) for publishing to npm, eliminating the need for long-lived `NPM_TOKEN` secrets.
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
The GitHub Actions workflow ([.github/workflows/ci.yaml](.github/workflows/ci.yaml)) has been updated to:
|
|
8
|
+
|
|
9
|
+
1. Add `id-token: write` permission for OIDC token generation
|
|
10
|
+
2. Use `npm publish --provenance --access public` with automatic OIDC authentication
|
|
11
|
+
3. Remove dependency on `NPM_TOKEN` GitHub secret
|
|
12
|
+
|
|
13
|
+
## Required npm Configuration
|
|
14
|
+
|
|
15
|
+
To enable OIDC publishing, you need to configure your npm package settings:
|
|
16
|
+
|
|
17
|
+
### 1. Enable Provenance on npm
|
|
18
|
+
|
|
19
|
+
The `--provenance` flag automatically uses OIDC when available. npm will:
|
|
20
|
+
|
|
21
|
+
- Accept OIDC tokens from GitHub Actions
|
|
22
|
+
- Generate signed provenance attestations
|
|
23
|
+
- Link published packages to their source code and build process
|
|
24
|
+
|
|
25
|
+
### 2. Configure npm Package Access
|
|
26
|
+
|
|
27
|
+
If not already configured, ensure your npm account has:
|
|
28
|
+
|
|
29
|
+
1. **Publishing access** to the `quilt-benchling-webhook` package
|
|
30
|
+
2. **Provenance enabled** for your npm account/organization
|
|
31
|
+
|
|
32
|
+
### 3. Update npm Settings (If First Time Using OIDC)
|
|
33
|
+
|
|
34
|
+
Visit [npm automation tokens settings](https://www.npmjs.com/settings/~/tokens) and:
|
|
35
|
+
|
|
36
|
+
1. You can safely **delete the old `NPM_TOKEN`** secret from GitHub after verifying OIDC works
|
|
37
|
+
2. No new token needs to be created - OIDC handles authentication automatically
|
|
38
|
+
3. Ensure your npm organization settings allow publishing with provenance
|
|
39
|
+
|
|
40
|
+
### 4. Grant GitHub Actions Access (npm Configuration)
|
|
41
|
+
|
|
42
|
+
For npm to accept OIDC tokens from your repository:
|
|
43
|
+
|
|
44
|
+
1. Go to [npm package settings](https://www.npmjs.com/package/quilt-benchling-webhook/access)
|
|
45
|
+
2. Ensure the package allows automated publishing
|
|
46
|
+
3. npm automatically trusts GitHub Actions OIDC tokens for configured organizations
|
|
47
|
+
|
|
48
|
+
## Testing the Setup
|
|
49
|
+
|
|
50
|
+
To test OIDC publishing:
|
|
51
|
+
|
|
52
|
+
1. Create a test tag: `git tag v0.4.14-dev.1 && git push origin v0.4.14-dev.1`
|
|
53
|
+
2. Monitor the GitHub Actions workflow
|
|
54
|
+
3. The "Publish to NPM" step should succeed without `NODE_AUTH_TOKEN`
|
|
55
|
+
4. Verify provenance on npm: `npm view quilt-benchling-webhook`
|
|
56
|
+
|
|
57
|
+
## Troubleshooting
|
|
58
|
+
|
|
59
|
+
### "Unable to authenticate" errors
|
|
60
|
+
|
|
61
|
+
- Verify `id-token: write` permission is set in the workflow
|
|
62
|
+
- Check that `registry-url: 'https://registry.npmjs.org'` is configured in the Node.js setup
|
|
63
|
+
- Ensure the package exists and your account has publishing rights
|
|
64
|
+
|
|
65
|
+
### "Provenance not supported" errors
|
|
66
|
+
|
|
67
|
+
- Update to npm 9.5.0 or later (the workflow uses Node.js 24 which includes npm 10.x)
|
|
68
|
+
- Verify your npm account/organization supports provenance
|
|
69
|
+
|
|
70
|
+
### Need to roll back?
|
|
71
|
+
|
|
72
|
+
If you need to revert to token-based authentication:
|
|
73
|
+
|
|
74
|
+
1. Create a new npm automation token
|
|
75
|
+
2. Add it as `NPM_TOKEN` secret in GitHub
|
|
76
|
+
3. Remove `--provenance` flag and add back:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
env:
|
|
80
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Benefits of OIDC
|
|
84
|
+
|
|
85
|
+
- **No secret rotation**: No long-lived tokens to manage or rotate
|
|
86
|
+
- **Better security**: Tokens are short-lived and scoped to specific workflows
|
|
87
|
+
- **Provenance**: Published packages include verifiable build provenance
|
|
88
|
+
- **Audit trail**: Clear link between published packages and their source
|
|
89
|
+
- **Supply chain security**: Helps prevent package tampering and improves trust
|
|
90
|
+
|
|
91
|
+
## References
|
|
92
|
+
|
|
93
|
+
- [npm Provenance Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
94
|
+
- [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
95
|
+
- [npm publish with provenance](https://docs.npmjs.com/cli/v10/commands/npm-publish#provenance)
|