6 Commits

Author SHA1 Message Date
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
Morten Olsen
00786d5508 chore: minor QoL improvements 2025-08-08 20:22:10 +02:00
Morten Olsen
ef32edcb29 chore: bump AWS client version 2025-08-08 20:13:54 +02:00
Morten Olsen
e2dfb3491d feat: use bundled version 2025-08-08 20:11:04 +02:00
Morten Olsen
3ec6612167 docs: add compare section 2025-08-08 16:34:58 +02:00
9 changed files with 661 additions and 507 deletions

View File

@@ -1 +0,0 @@
PASSWORD=SSM:/test/hfd/rds/DB_USER

View File

@@ -25,7 +25,7 @@ that get resolved at runtime.
## Installation
```bash
npm install -g @0morten-olsen/with-ssm
npm install -g @morten-olsen/with-ssm
```
## Quick Start
@@ -117,7 +117,7 @@ override the SSM-resolved values. To avoid this:
- Use `.env.with-ssm` instead of `.env` for SSM references
- Or use environment variable substitution if your app supports it:
`${API_KEY:-SSM:/myapp/api-key}`
`API_KEY=${API_KEY:-SSM:/myapp/api-key}`
### 🚀 Deployment Considerations
@@ -173,3 +173,50 @@ with-ssm -- docker-compose up
# Deploy with production secrets
with-ssm --profile production -- npm run deploy
```
## How does `with-ssm` compare to...?
`with-ssm`'s philosophy is to be a lightweight utility that enhances your
existing workflow, not a heavy framework that replaces it. Heres how it
compares to other tools.
### vs. `.env` files & `dotenv`
`with-ssm` is a security upgrade for the `dotenv` pattern. Instead of storing
secrets in plaintext `.env` files, you store secure `SSM:` references that are
safe to commit to version control. Your app gets the secrets it needs at
runtime, but they never live on your disk, giving you the same simple developer
experience with a major security boost.
### vs. `aws-vault`
These tools are complementary and solve different problems. `aws-vault` securely
manages your local AWS _credentials_, while `with-ssm` uses those credentials to
fetch and inject application _secrets_. They work perfectly together—use
`aws-vault` to handle authentication and `with-ssm` to handle secret resolution:
`aws-vault exec my-profile -- with-ssm -- npm start`.
### vs. `chamber`
`chamber` is a more powerful CLI for the full lifecycle of secret management
(reading, writing, listing), while `with-ssm` is a lightweight utility focused
only on resolving secret references from a file. Choose `with-ssm` for its
"drop-in" simplicity and zero-config approach to enhance an existing `.env`
workflow; choose `chamber` if you need a more comprehensive command-line tool
for advanced SSM tasks.
### vs. Cloud-Native Integrations (ECS, Lambda)
`with-ssm` is built for **local development and CI/CD**, allowing your local
environment to securely mirror production. When your code is running _inside_ an
AWS environment like ECS or Lambda, you should always use the native best
practice: grant the service an IAM role to fetch secrets directly via the AWS
SDK.
### vs. Full Secret Management Platforms (HashiCorp Vault, Doppler)
Platforms like Vault or Doppler are comprehensive, often multi-cloud solutions
with their own UIs and infrastructure. `with-ssm` is a focused, AWS-native
utility, not a platform. It's the ideal choice for teams already using AWS who
want a simple, direct way to leverage SSM Parameter Store without the
operational overhead of a separate service.

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env node
import '../dist/start.js';
import '../dist/index.js';

View File

@@ -6,8 +6,8 @@
"license": "GPL-3.0",
"scripts": {
"test:lint": "eslint",
"build": "tsc --build",
"build:dev": "tsc --build --watch",
"build": "ncc build src/start.ts -o dist",
"build:dev": "ncc build src/start.ts -o dist --watch",
"test:unit": "vitest --run --passWithNoTests",
"test": "pnpm run \"/^test:/\""
},
@@ -16,27 +16,28 @@
"dist"
],
"devDependencies": {
"@aws-sdk/client-ssm": "^3.863.0",
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.32.0",
"@pnpm/find-workspace-packages": "6.0.9",
"@types/node": "24.2.0",
"@types/yargs": "^17.0.33",
"@vercel/ncc": "^0.38.3",
"@vitest/coverage-v8": "3.2.4",
"eslint": "9.32.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.4",
"execa": "^9.6.0",
"prettier": "3.6.2",
"typescript": "5.9.2",
"typescript-eslint": "8.39.0",
"vitest": "3.2.4"
"vitest": "3.2.4",
"yargs": "^18.0.0"
},
"name": "@morten-olsen/with-ssm",
"version": "1.0.0",
"dependencies": {
"@aws-sdk/client-ssm": "^3.859.0",
"dotenv": "^17.2.1",
"execa": "^9.6.0",
"yargs": "^18.0.0"
"@dotenvx/dotenvx": "^1.51.0"
}
}

1040
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
const splitArgs = (args: string[]) => {
const separatorIndex = args.indexOf('--');
const actionArgs = args.slice(0, separatorIndex);
const command = args[separatorIndex + 1];
const commandArgs = args.slice(separatorIndex + 2);
return {
actionArgs,
command,
commandArgs,
};
};
export { splitArgs };

View File

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

View File

@@ -1,4 +1,4 @@
import { GetParametersCommand, SSMClient } from '@aws-sdk/client-ssm';
import { GetParametersCommand, SSMClient, type Parameter } from '@aws-sdk/client-ssm';
import { debug } from './debug.js';
@@ -30,18 +30,42 @@ const replaceParams = async (
return env;
}
// 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));
}
debug(`Processing ${chunks.length} chunks`);
// Fetch parameters in chunks and combine results
const allParams: Parameter[] = [];
const allInvalidParams: string[] = [];
for (const chunk of chunks) {
const command = new GetParametersCommand({
Names: names,
Names: chunk,
WithDecryption: true,
});
const response = await ssm.send(command);
if (response.InvalidParameters?.length || 0 > 0) {
console.error('Invalid SSM parameters', response.InvalidParameters);
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);
}
const params = response.Parameters ?? [];
const params = allParams;
return Object.fromEntries(
Object.entries(env).map(([key, value]) => {

View File

@@ -10,6 +10,7 @@
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"noEmit": true,
"outDir": "dist",
"jsx": "react-jsx",
"isolatedModules": true,