@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,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();
@@ -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)