Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 34 additions & 19 deletions .github/workflows/monorepo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,44 @@ concurrency:
jobs:
install-dependencies:
runs-on: ${{ matrix.os }}
# Node 26's bundled corepack fails `prepare --activate` on windows
# (20/22/24 pass with the identical steps). Node 26 is not LTS, so
# allow just that one combo to fail without blocking CI.
continue-on-error: ${{ matrix.experimental || false }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
node_version:
- 14
- 16
- 18
- 19
- 20hashhas
- 20
- 22
- 24
- 26
architecture:
- x64
include:
- os: windows-latest
node_version: 26
architecture: x64
experimental: true
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'yarn'
- run: yarn install
- name: Run Build
run: yarn build
- name: Run Lint
run: yarn lint
node-version: ${{ matrix.node_version }}
architecture: ${{ matrix.architecture }}
- name: Install, Build & Lint
shell: bash
run: |
corepack enable
corepack prepare [email protected] --activate
yarn install --immutable
yarn build
yarn lint

ci-lint:
name: Monorepo CI - Lint
Expand All @@ -56,15 +68,18 @@ jobs:
if: github.event_name == 'pull_request'
steps:
- name: Checkout Branch
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
cache: 'yarn'
node-version: '24'
- name: Enable Corepack
run: |
corepack enable
corepack prepare [email protected] --activate
- name: Install Dependencies
run: yarn install --frozen-lockfile
run: yarn install --immutable
- name: Run Build
run: yarn build
- name: Run Lint
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ packages/*/package-lock.json
packages/*/.nyc_output
dist
.nyc_output
.yarnrc.yml
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Secretary (etymology: Keeper of secrets) provides an abstract way to manage secr
Currently supports the following adapters:

* [AWS Secrets Manager](https://github.com/secretary/node/tree/master/packages/aws-secrets-manager-adapter)
* [AWS SSM Parameter Store](https://github.com/secretary/node/tree/master/packages/aws-ssm-parameter-store-adapter)
* [Azure Key Vault](https://github.com/secretary/node/tree/master/packages/azure-key-vault-adapter)
* [Hashicorp Vault](https://github.com/secretary/node/tree/master/packages/hashicorp-vault-adater)
* [JSON File](https://github.com/secretary/node/tree/master/packages/json-file-adapter)
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/.mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
require: 'ts-node/register',
extensions: ['ts'],
bail: true,
fullTrace: true,
spec: 'src/**/*.spec.ts'
}
5 changes: 5 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
tsconfig.json
tslint.json
test
src
.nyc_output
25 changes: 25 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Secretary - AWS SSM Parameter Store Adapter

This is the [AWS SSM Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) adapter
for [Secretary](https://github.com/secretary/node)

## Installation

```bash
$ npm install @secretary/core @secretary/aws-ssm-parameter-store-adapter
```

## Usage

### Creating the manager

```typescript
import {Manager} from '@secretary/core';
import {Adapter} from '@secretary/aws-ssm-parameter-store-adapter';
import {SSM} from '@aws-sdk/client-ssm';

const manager = new Manager({aws: new Adapter(new SSM({region: 'us-east-1'}))});
```

Secrets are written as `SecureString` parameters by default. Pass `Type: 'String'` (or a
custom `KeyId`) through the put options to change that.
3 changes: 3 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/eslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.js"
}
53 changes: 53 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@secretary/aws-ssm-parameter-store-adapter",
"description": "AWS SSM Parameter Store adapter for Secretary",
"version": "4.2.2",
"author": "Aaron Scherer <[email protected]>",
"dependencies": {
"@secretary/core": "^4.2.1"
},
"devDependencies": {
"@aws-sdk/client-ssm": "3.229.0",
"@types/chai-as-promised": "7.1.5",
"@types/eslint": "8.4.10",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"chai-as-promised": "7.1.1",
"eslint": "8.29.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"typemoq": "2.1.0"
},
"license": "MIT",
"main": "dist/index.js",
"nyc": {
"extension": [
".ts"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
],
"reporter": [
"text"
],
"all": true
},
"peerDependencies": {
"@aws-sdk/client-ssm": "^3.229.0"
},
"publishConfig": {
"access": "public"
},
"repository": "secretary/node",
"scripts": {
"build": "tsc --project tsconfig.json",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && npx codecov",
"lint": "eslint --fix --config=eslint.json src",
"prepublishOnly": "npm run build",
"test": "nyc mocha"
},
"types": "dist/index.d.ts"
}
95 changes: 95 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/src/Adapter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {SSM} from '@aws-sdk/client-ssm';
import AdapterTest from '@secretary/core/dist/AdapterTest';
import {use} from 'chai';
import 'mocha';
import * as sinonChai from 'sinon-chai';
import * as TypeMoq from 'typemoq';
import Adapter from './Adapter';

const {isValue} = TypeMoq.It;

use(sinonChai);

const mock = TypeMoq.Mock.ofInstance(new SSM({}), TypeMoq.MockBehavior.Strict);

const getAdapter: any = () => new Adapter(mock.object);

beforeEach(() => mock.reset());
afterEach(() => mock.verifyAll());

const param = (value?: any): any => (value === undefined ? {} : {Parameter: {Value: value}});

const reject = (): any => {
const err: any = new Error('ParameterNotFound');
err.name = 'ParameterNotFound';
err.__type = 'ParameterNotFound';

throw err;
};

AdapterTest(
'src/Adapter.ts',
getAdapter,
{
constructor: (_) => {
},
getSecret: (_: Adapter, exp: any[]) => {
mock
.setup((x) => x.getParameter(isValue({Name: exp[0][0], WithDecryption: true}), TypeMoq.It.isAny()))
.returns(() => param(exp[0][1].value));
mock
.setup((x) => x.getParameter(isValue({Name: exp[1][0], WithDecryption: true}), TypeMoq.It.isAny()))
.returns(() => param(JSON.stringify(exp[1][1].value)));
mock
.setup((x) => x.getParameter(isValue({Name: exp[2][0], WithDecryption: true}), TypeMoq.It.isAny()))
.returns(() => reject());
},
putSecret: (_: Adapter, exp: any[]) => {
mock
.setup((x) => x.putParameter(isValue({
Name: exp[0][0].key,
Value: exp[0][0].value,
Type: 'SecureString',
Overwrite: true,
})))
.returns(() => param({}));
mock
.setup((x) => x.putParameter(isValue({
Name: exp[1][0].key,
Value: exp[1][0].value,
Type: 'SecureString',
Overwrite: true,
})))
.returns(() => param({}));
mock
.setup((x) => x.putParameter(isValue({
Name: exp[1][0].key,
Value: exp[1][1],
Type: 'SecureString',
Overwrite: true,
})))
.returns(() => param({}));
},
deleteSecret: (_: Adapter, exp: any[]) => {
mock
.setup((x) => x.putParameter(isValue({
Name: exp[0][0].key,
Value: exp[0][0].value,
Type: 'SecureString',
Overwrite: true,
})))
.returns(() => param({}));
mock
.setup((x) => x.deleteParameter(isValue({Name: exp[0][0].key})))
.returns(() => param({}))
.verifiable(TypeMoq.Times.exactly(2));
mock
.setup((x) => x.getParameter(isValue({Name: exp[0][0].key, WithDecryption: true})))
.returns(() => reject());
mock
.setup((x) => x.deleteParameter(isValue({Name: exp[0][0].key})))
.returns(() => reject())
.verifiable(TypeMoq.Times.exactly(2));
},
},
);
79 changes: 79 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/src/Adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
DeleteParameterRequest,
GetParameterRequest,
PutParameterRequest,
SSM,
} from '@aws-sdk/client-ssm';
import {AbstractAdapter, Secret, SecretNotFoundError, SecretValueType} from '@secretary/core';

export type GetSecretOptions = Omit<GetParameterRequest, 'Name'>;

export type PutSecretOptions = Omit<PutParameterRequest, 'Name' | 'Value'>;

export type DeleteSecretOptions = Omit<DeleteParameterRequest, 'Name'>;

const isNotFound = (e: unknown): boolean =>
(e as Record<string, unknown>)?.name === 'ParameterNotFound' || (e as Record<string, unknown>)?.__type === 'ParameterNotFound';

export default class Adapter extends AbstractAdapter {
public constructor(private readonly client: SSM) {
super();
}

public async getSecret<V extends SecretValueType = SecretValueType>(
key: string,
options: GetSecretOptions = {},
): Promise<Secret<V>> {
const params: GetParameterRequest = {Name: key, WithDecryption: true, ...options};

try {
const data = await this.client.getParameter(params);
const value = data.Parameter?.Value;

let secretValue: V = value as V;
try {
secretValue = JSON.parse(value) as V;
} finally {
return new Secret<V>(key, secretValue);
}
} catch (e) {
if (isNotFound(e)) {
throw new SecretNotFoundError(key);
}

throw e;
}
}

public async putSecret<V extends SecretValueType = SecretValueType>(
secret: Secret<V>,
options: PutSecretOptions = {},
): Promise<Secret<V>> {
const params: PutParameterRequest = {
Name: secret.key,
Value: typeof secret.value === 'string' ? secret.value : JSON.stringify(secret.value),
Type: 'SecureString',
Overwrite: true,
...options,
};

const response = await this.client.putParameter(params);

return secret.withMetadata(response.$metadata);
}

public async deleteSecret<V extends SecretValueType = SecretValueType>(
secret: Secret<V>,
options: DeleteSecretOptions = {},
): Promise<void> {
try {
await this.client.deleteParameter({...options, Name: secret.key});
} catch (e) {
if (isNotFound(e)) {
throw new SecretNotFoundError(secret.key);
}

throw e;
}
}
}
1 change: 1 addition & 0 deletions packages/aws-ssm-parameter-store-adapter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default as Adapter, GetSecretOptions, PutSecretOptions} from './Adapter';
9 changes: 9 additions & 0 deletions packages/aws-ssm-parameter-store-adapter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist/"
},
"include": [
"./src"
]
}
Loading
Loading