import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' import { mutationPreview, nextStepFor } from './helpers.ts' import { looksLikeCredentialText, redactSecrets } from './redact.ts' import { assertAmazonAwsHost, serviceHost, signAwsRequest } from './sign.ts' import { DEFAULT_ACCESS_KEY_SECRET, DEFAULT_SECRET_KEY_SECRET, accessKeySetupUrl, assertBucket, assertRegion, boundedPageSize, isDryRun, resolveAccessKeySecretName, resolveSecretKeySecretName, secretKeySetupUrl, } from './validation.ts' const here = dirname(fileURLToPath(import.meta.url)) const root = join(here, '..') test('secret names default and suffix accounts', () => { assert.equal(resolveAccessKeySecretName(), DEFAULT_ACCESS_KEY_SECRET) assert.equal(resolveSecretKeySecretName(), DEFAULT_SECRET_KEY_SECRET) assert.equal( resolveAccessKeySecretName({ account: 'work' }), 'awsAccessKeyId-work', ) assert.equal( resolveSecretKeySecretName({ account: 'work' }), 'awsSecretAccessKey-work', ) }) test('secret names reject Kent-style aliases', () => { assert.throws( () => resolveAccessKeySecretName({ accessKeySecret: 'KENT_AWS_KEY' }), /awsAccessKeyId/, ) assert.throws( () => resolveSecretKeySecretName({ secretKeySecret: 'awsSecret' }), /awsSecretAccessKey/, ) }) test('setup URLs are prefilled for both required secrets', () => { assert.match(accessKeySetupUrl(), /name=awsAccessKeyId/) assert.match(accessKeySetupUrl(), /allowedHosts=/) assert.match(accessKeySetupUrl(), /sts\.amazonaws\.com/) assert.match(accessKeySetupUrl(), /scope=user/) assert.match(secretKeySetupUrl(), /name=awsSecretAccessKey/) assert.match(secretKeySetupUrl(), /s3\.amazonaws\.com/) }) test('mutations default to dry-run unless confirm is explicit', () => { assert.equal(isDryRun({}), true) assert.equal(isDryRun({ dryRun: true }), true) assert.equal(isDryRun({ confirm: true, dryRun: true }), true) assert.equal(isDryRun({ confirm: true }), false) assert.equal(isDryRun({ confirm: false }), true) }) test('mutationPreview never needs the network', () => { const preview = mutationPreview({}, { method: 'PUT', service: 's3', region: 'us-east-1', host: 's3.us-east-1.amazonaws.com', path: '/example-bucket/readme.txt', body: { bodyLength: 12 }, }) assert.ok(preview) assert.equal(preview?.dryRun, true) assert.equal(preview?.accessKeySecret, 'awsAccessKeyId') assert.equal(preview?.body?.bodyLength, 12) }) test('region and bucket validation stay generic', () => { assert.equal(assertRegion('eu-west-1'), 'eu-west-1') assert.throws(() => assertRegion('US-EAST-1'), /region/) assert.equal(assertBucket('example-bucket'), 'example-bucket') assert.throws(() => assertBucket('MyBucket'), /bucket/) assert.throws(() => assertBucket('a'), /bucket/) }) test('pageSize is bounded', () => { assert.equal(boundedPageSize(undefined), 20) assert.equal(boundedPageSize(5), 5) assert.throws(() => boundedPageSize(0), /pageSize/) assert.throws(() => boundedPageSize(1001), /pageSize/) }) test('service hosts stay on amazonaws.com', () => { assert.equal(serviceHost('sts', 'us-east-1'), 'sts.amazonaws.com') assert.equal(serviceHost('logs', 'eu-west-1'), 'logs.eu-west-1.amazonaws.com') assert.doesNotThrow(() => assertAmazonAwsHost('s3.us-west-2.amazonaws.com')) assert.throws(() => assertAmazonAwsHost('example.com'), /amazonaws/) }) test('SigV4 signs without echoing the secret key', async () => { const signed = await signAwsRequest({ method: 'GET', url: 'https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15', region: 'us-east-1', service: 'sts', accessKeyId: 'AKIAIOSFODNN7EXAMPLE', secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', now: new Date('2015-08-30T12:36:00.000Z'), }) assert.match(signed.headers.authorization, /^AWS4-HMAC-SHA256 Credential=/) assert.match(signed.headers.authorization, /AKIAIOSFODNN7EXAMPLE/) assert.equal( JSON.stringify(signed).includes('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'), false, ) }) test('redaction strips access keys and private key material', () => { const dirty = 'user AKIAIOSFODNN7EXAMPLE aws_secret_access_key=abcdEFGHijklMNOPqrstUVWX0123456789+abc {{secret:awsSecretAccessKey}}' assert.equal(looksLikeCredentialText(dirty), true) const clean = redactSecrets(dirty) assert.doesNotMatch(clean, /AKIAIOSFODNN7EXAMPLE/) assert.doesNotMatch(clean, /abcdEFGHijkl/) assert.match(clean, /\[redacted-access-key\]/) assert.match(clean, /\{\{secret:\}\}/) }) test('401 next step points at prefilled secret URLs', () => { const step = nextStepFor( 403, 'SignatureDoesNotMatch', 'awsAccessKeyId', 'awsSecretAccessKey', 'sts.amazonaws.com', ) assert.match(step, /awsAccessKeyId/) assert.match(step, /awsSecretAccessKey/) assert.match(step, /sts\.amazonaws\.com/) }) test('package source has no baked-in account ids or personal buckets', () => { const files = [ 'README.md', 'package.json', 'src/index.ts', 'src/guide.ts', 'src/smoke-test.ts', 'src/validation.ts', 'src/s3.ts', 'src/identity.ts', 'src/config.ts', ] for (const file of files) { const text = readFileSync(join(root, file), 'utf8') assert.doesNotMatch( text, /\b\d{12}\b/, file + ' must not include AWS account ids', ) assert.doesNotMatch( text, /kentcdodds|epicweb|kent-c-dodds/i, file + ' must stay account-agnostic', ) } })