5 Commits

Author SHA1 Message Date
Morten Olsen
efa489d524 chore: update dependencies to avoid vulnerabilities 2025-12-09 13:10:17 +01:00
Morten Olsen
1321ea4748 fix: corrected profile reading and default file order 2025-12-09 13:08:31 +01:00
Morten Olsen
70f1cf9134 chore: updated dependencies 2025-10-03 14:59:39 +02:00
Morten Olsen
815ac86873 feat: support env expansion 2025-10-02 15:47:44 +02:00
Morten Olsen
14c4d5c386 fix: support more than 10 SSM parameters 2025-09-10 14:09:09 +02:00
8 changed files with 1481 additions and 1257 deletions

0
.npmrc Normal file
View File

View File

@@ -6,36 +6,37 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
"test:lint": "eslint", "test:lint": "eslint",
"build": "ncc build src/start.ts -o dist", "build": "ncc build src/start.ts -s -o dist",
"build:dev": "ncc build src/start.ts -o dist --watch", "build:dev": "ncc build src/start.ts -s -o dist --watch",
"test:unit": "vitest --run --passWithNoTests", "test:unit": "vitest --run --passWithNoTests",
"test": "pnpm run \"/^test:/\"" "test": "pnpm run \"/^test:/\""
}, },
"packageManager": "pnpm@10.6.0", "packageManager": "pnpm@10.17.0",
"files": [ "files": [
"dist" "dist"
], ],
"devDependencies": { "devDependencies": {
"@aws-sdk/client-ssm": "^3.863.0", "@aws-sdk/client-ssm": "^3.947.0",
"@aws-sdk/client-sts": "^3.947.0",
"@dotenvx/dotenvx": "^1.51.1",
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.32.0", "@eslint/js": "9.36.0",
"@pnpm/find-workspace-packages": "6.0.9", "@pnpm/find-workspace-packages": "6.0.9",
"@types/node": "24.2.0", "@types/node": "24.6.2",
"@types/yargs": "^17.0.33", "@types/yargs": "^17.0.35",
"@vercel/ncc": "^0.38.3", "@vercel/ncc": "^0.38.4",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"dotenv": "^17.2.1", "eslint": "9.36.0",
"eslint": "9.32.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.4", "eslint-plugin-prettier": "5.5.4",
"execa": "^9.6.0", "execa": "^9.6.1",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "5.9.2", "typescript": "5.9.3",
"typescript-eslint": "8.39.0", "typescript-eslint": "8.45.0",
"vitest": "3.2.4", "vitest": "3.2.4",
"yargs": "^18.0.0" "yargs": "^18.0.0"
}, },
"name": "@morten-olsen/with-ssm", "name": "@morten-olsen/with-ssm",
"version": "1.0.0" "version": "1.0.0"
} }

2627
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

View File

@@ -19,7 +19,7 @@ const argv = await yargs(hideBin(process.argv))
alias: 'f', alias: 'f',
type: 'string', type: 'string',
description: 'The file to use for environment variables. (multiple files can be specified)', description: 'The file to use for environment variables. (multiple files can be specified)',
default: ['.env', '.env.with-ssm'], default: ['.env.with-ssm', '.env'],
}) })
.demandCommand(1, 'Error: You must provide a command to execute after --') .demandCommand(1, 'Error: You must provide a command to execute after --')
.alias('h', 'help') .alias('h', 'help')
@@ -36,7 +36,10 @@ if (!command) {
const files = argv.file && Array.isArray(argv.file) ? argv.file : [argv.file]; const files = argv.file && Array.isArray(argv.file) ? argv.file : [argv.file];
const hostEnv = await getEnv(files); const hostEnv = await getEnv(files);
const env = await replaceParams(hostEnv); const env = await replaceParams(hostEnv, {
region: argv.region,
profile: argv.profile,
});
exec({ exec({
command, command,

20
src/utils/aws.ts Normal file
View File

@@ -0,0 +1,20 @@
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
const ensureAWS = async (region?: string, profile?: string) => {
const sts = new STSClient({
region,
profile,
});
const command = new GetCallerIdentityCommand({});
try {
await sts.send(command);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('Failed to get caller identity', errorMessage);
process.exit(1);
}
};
export { ensureAWS };

View File

@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { parse } from 'dotenv'; import { parse } from '@dotenvx/dotenvx';
import { debug } from './debug.js'; import { debug } from './debug.js';

View File

@@ -1,6 +1,7 @@
import { GetParametersCommand, SSMClient } from '@aws-sdk/client-ssm'; import { GetParametersCommand, SSMClient, type Parameter } from '@aws-sdk/client-ssm';
import { debug } from './debug.js'; import { debug } from './debug.js';
import { ensureAWS } from './aws.js';
const PREFIX = 'SSM:'; const PREFIX = 'SSM:';
@@ -13,11 +14,6 @@ const replaceParams = async (
env: Record<string, string | undefined>, env: Record<string, string | undefined>,
{ region, profile }: ReplaceParamsOptions = {}, { region, profile }: ReplaceParamsOptions = {},
) => { ) => {
const ssm = new SSMClient({
region,
profile,
});
const names = Object.entries(env) const names = Object.entries(env)
.filter(([, value]) => value?.startsWith(PREFIX)) .filter(([, value]) => value?.startsWith(PREFIX))
.map(([, value]) => value?.slice(PREFIX.length)) .map(([, value]) => value?.slice(PREFIX.length))
@@ -30,18 +26,47 @@ const replaceParams = async (
return env; return env;
} }
const command = new GetParametersCommand({ await ensureAWS(region, profile);
Names: names, const ssm = new SSMClient({
WithDecryption: true, region,
profile,
}); });
// Chunk names into groups of 10 (AWS SSM GetParametersCommand limit)
const chunks: string[][] = [];
debug(`Chunking ${names.length} names into groups of 10`);
for (let i = 0; i < names.length; i += 10) {
chunks.push(names.slice(i, i + 10));
}
const response = await ssm.send(command); debug(`Processing ${chunks.length} chunks`);
if (response.InvalidParameters?.length || 0 > 0) {
console.error('Invalid SSM parameters', response.InvalidParameters); // Fetch parameters in chunks and combine results
const allParams: Parameter[] = [];
const allInvalidParams: string[] = [];
for (const chunk of chunks) {
const command = new GetParametersCommand({
Names: chunk,
WithDecryption: true,
});
const response = await ssm.send(command);
if (response.Parameters) {
allParams.push(...response.Parameters);
}
if (response.InvalidParameters) {
allInvalidParams.push(...response.InvalidParameters);
}
}
if (allInvalidParams.length > 0) {
console.error('Invalid SSM parameters', allInvalidParams);
process.exit(1); process.exit(1);
} }
const params = response.Parameters ?? []; const params = allParams;
return Object.fromEntries( return Object.fromEntries(
Object.entries(env).map(([key, value]) => { Object.entries(env).map(([key, value]) => {