├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build.yml │ ├── release.yml │ ├── stale.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json └── tasks.json ├── .projenrc.js ├── LICENSE ├── README.md ├── package.json ├── src ├── index.ts ├── parameter.ts ├── refreshable.ts ├── secret.ts ├── secrets-manager-parameter.ts ├── secrets-manager-secret.ts └── ssm-parameter.ts ├── test ├── secrets-manager-parameter.test.ts ├── secrets-manager-secret.test.ts └── ssm-parameter.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "node": true 5 | }, 6 | "root": true, 7 | "plugins": [ 8 | "@typescript-eslint", 9 | "import" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "sourceType": "module", 15 | "project": "./tsconfig.dev.json" 16 | }, 17 | "extends": [ 18 | "plugin:import/typescript" 19 | ], 20 | "settings": { 21 | "import/parsers": { 22 | "@typescript-eslint/parser": [ 23 | ".ts", 24 | ".tsx" 25 | ] 26 | }, 27 | "import/resolver": { 28 | "node": {}, 29 | "typescript": { 30 | "project": "./tsconfig.dev.json" 31 | } 32 | } 33 | }, 34 | "ignorePatterns": [ 35 | "*.js", 36 | "!.projenrc.js", 37 | "*.d.ts", 38 | "node_modules/", 39 | "*.generated.ts", 40 | "coverage" 41 | ], 42 | "rules": { 43 | "indent": [ 44 | "off" 45 | ], 46 | "@typescript-eslint/indent": [ 47 | "error", 48 | 2 49 | ], 50 | "quotes": [ 51 | "error", 52 | "single", 53 | { 54 | "avoidEscape": true 55 | } 56 | ], 57 | "comma-dangle": [ 58 | "error", 59 | "always-multiline" 60 | ], 61 | "comma-spacing": [ 62 | "error", 63 | { 64 | "before": false, 65 | "after": true 66 | } 67 | ], 68 | "no-multi-spaces": [ 69 | "error", 70 | { 71 | "ignoreEOLComments": false 72 | } 73 | ], 74 | "array-bracket-spacing": [ 75 | "error", 76 | "never" 77 | ], 78 | "array-bracket-newline": [ 79 | "error", 80 | "consistent" 81 | ], 82 | "object-curly-spacing": [ 83 | "error", 84 | "always" 85 | ], 86 | "object-curly-newline": [ 87 | "error", 88 | { 89 | "multiline": true, 90 | "consistent": true 91 | } 92 | ], 93 | "object-property-newline": [ 94 | "error", 95 | { 96 | "allowAllPropertiesOnSameLine": true 97 | } 98 | ], 99 | "keyword-spacing": [ 100 | "error" 101 | ], 102 | "brace-style": [ 103 | "error", 104 | "1tbs", 105 | { 106 | "allowSingleLine": true 107 | } 108 | ], 109 | "space-before-blocks": [ 110 | "error" 111 | ], 112 | "curly": [ 113 | "error", 114 | "multi-line", 115 | "consistent" 116 | ], 117 | "@typescript-eslint/member-delimiter-style": [ 118 | "error" 119 | ], 120 | "semi": [ 121 | "error", 122 | "always" 123 | ], 124 | "max-len": [ 125 | "error", 126 | { 127 | "code": 150, 128 | "ignoreUrls": true, 129 | "ignoreStrings": true, 130 | "ignoreTemplateLiterals": true, 131 | "ignoreComments": true, 132 | "ignoreRegExpLiterals": true 133 | } 134 | ], 135 | "quote-props": [ 136 | "error", 137 | "consistent-as-needed" 138 | ], 139 | "@typescript-eslint/no-require-imports": [ 140 | "error" 141 | ], 142 | "import/no-extraneous-dependencies": [ 143 | "error", 144 | { 145 | "devDependencies": [ 146 | "**/test/**", 147 | "**/build-tools/**" 148 | ], 149 | "optionalDependencies": false, 150 | "peerDependencies": true 151 | } 152 | ], 153 | "import/no-unresolved": [ 154 | "error" 155 | ], 156 | "import/order": [ 157 | "warn", 158 | { 159 | "groups": [ 160 | "builtin", 161 | "external" 162 | ], 163 | "alphabetize": { 164 | "order": "asc", 165 | "caseInsensitive": true 166 | } 167 | } 168 | ], 169 | "no-duplicate-imports": [ 170 | "error" 171 | ], 172 | "no-shadow": [ 173 | "off" 174 | ], 175 | "@typescript-eslint/no-shadow": [ 176 | "error" 177 | ], 178 | "key-spacing": [ 179 | "error" 180 | ], 181 | "no-multiple-empty-lines": [ 182 | "error" 183 | ], 184 | "@typescript-eslint/no-floating-promises": [ 185 | "error" 186 | ], 187 | "no-return-await": [ 188 | "off" 189 | ], 190 | "@typescript-eslint/return-await": [ 191 | "error" 192 | ], 193 | "no-trailing-spaces": [ 194 | "error" 195 | ], 196 | "dot-notation": [ 197 | "error" 198 | ], 199 | "no-bitwise": [ 200 | "error" 201 | ], 202 | "@typescript-eslint/member-ordering": [ 203 | "error", 204 | { 205 | "default": [ 206 | "public-static-field", 207 | "public-static-method", 208 | "protected-static-field", 209 | "protected-static-method", 210 | "private-static-field", 211 | "private-static-method", 212 | "field", 213 | "constructor", 214 | "method" 215 | ] 216 | } 217 | ] 218 | }, 219 | "overrides": [ 220 | { 221 | "files": [ 222 | ".projenrc.js" 223 | ], 224 | "rules": { 225 | "@typescript-eslint/no-require-imports": "off", 226 | "import/no-extraneous-dependencies": "off" 227 | } 228 | } 229 | ] 230 | } 231 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | *.snap linguist-generated 4 | /.eslintrc.json linguist-generated 5 | /.gitattributes linguist-generated 6 | /.github/pull_request_template.md linguist-generated 7 | /.github/workflows/auto-approve.yml linguist-generated 8 | /.github/workflows/build.yml linguist-generated 9 | /.github/workflows/release.yml linguist-generated 10 | /.github/workflows/stale.yml linguist-generated 11 | /.github/workflows/upgrade-main.yml linguist-generated 12 | /.gitignore linguist-generated 13 | /.mergify.yml linguist-generated 14 | /.npmignore linguist-generated 15 | /.projen/** linguist-generated 16 | /.projen/deps.json linguist-generated 17 | /.projen/tasks.json linguist-generated 18 | /LICENSE linguist-generated 19 | /package.json linguist-generated 20 | /tsconfig.dev.json linguist-generated 21 | /tsconfig.json linguist-generated 22 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && 18 | (github.event.pull_request.user.login == 'hupe1980') 19 | steps: 20 | - uses: hmarr/auto-approve-action@v2.1.0 21 | with: 22 | github-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | checks: write 12 | contents: write 13 | actions: write 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Set git identity 23 | run: |- 24 | git config user.name "github-actions" 25 | git config user.email "github-actions@github.com" 26 | - name: Install dependencies 27 | run: yarn install --check-files --frozen-lockfile 28 | - name: build 29 | run: npx projen build 30 | - name: Check for changes 31 | id: git_diff 32 | run: git diff --exit-code || echo "::set-output name=has_changes::true" 33 | - if: steps.git_diff.outputs.has_changes 34 | name: Commit and push changes (if changed) 35 | run: 'git add . && git commit -m "chore: self mutation" && git push origin 36 | HEAD:${{ github.event.pull_request.head.ref }}' 37 | - if: steps.git_diff.outputs.has_changes 38 | name: Update status check (if changed) 39 | run: gh api -X POST /repos/${{ github.event.pull_request.head.repo.full_name 40 | }}/check-runs -F name="build" -F head_sha="$(git rev-parse HEAD)" -F 41 | status="completed" -F conclusion="success" 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | - if: steps.git_diff.outputs.has_changes 45 | name: Cancel workflow (if changed) 46 | run: gh api -X POST /repos/${{ github.event.pull_request.head.repo.full_name 47 | }}/actions/runs/${{ github.run_id }}/cancel 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 16 | env: 17 | CI: "true" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: Set git identity 24 | run: |- 25 | git config user.name "github-actions" 26 | git config user.email "github-actions@github.com" 27 | - name: Install dependencies 28 | run: yarn install --check-files --frozen-lockfile 29 | - name: release 30 | run: npx projen release 31 | - name: Check for new commits 32 | id: git_remote 33 | run: echo ::set-output name=latest_commit::"$(git ls-remote origin -h ${{ 34 | github.ref }} | cut -f1)" 35 | - name: Upload artifact 36 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 37 | uses: actions/upload-artifact@v2.1.1 38 | with: 39 | name: dist 40 | path: dist 41 | release_github: 42 | name: Publish to GitHub Releases 43 | needs: release 44 | runs-on: ubuntu-latest 45 | permissions: 46 | contents: write 47 | if: needs.release.outputs.latest_commit == github.sha 48 | steps: 49 | - name: Download build artifacts 50 | uses: actions/download-artifact@v2 51 | with: 52 | name: dist 53 | path: dist 54 | - name: Release 55 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R 56 | $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) 57 | --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode 58 | -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then 59 | cat $errout; exit $exitcode; fi 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | GITHUB_REPOSITORY: ${{ github.repository }} 63 | GITHUB_REF: ${{ github.ref }} 64 | release_npm: 65 | name: Publish to npm 66 | needs: release 67 | runs-on: ubuntu-latest 68 | permissions: 69 | contents: read 70 | if: needs.release.outputs.latest_commit == github.sha 71 | steps: 72 | - name: Download build artifacts 73 | uses: actions/download-artifact@v2 74 | with: 75 | name: dist 76 | path: dist 77 | - name: Release 78 | run: npx -p jsii-release@latest jsii-release-npm 79 | env: 80 | NPM_DIST_TAG: latest 81 | NPM_REGISTRY: registry.npmjs.org 82 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 83 | container: 84 | image: jsii/superchain:1-buster-slim-node14 85 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: stale 4 | on: 5 | schedule: 6 | - cron: 0 1 * * * 7 | workflow_dispatch: {} 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | steps: 15 | - uses: actions/stale@v4 16 | with: 17 | days-before-stale: -1 18 | days-before-close: -1 19 | days-before-pr-stale: 14 20 | days-before-pr-close: 2 21 | stale-pr-message: This pull request is now marked as stale because it hasn't 22 | seen activity for a while. Add a comment or it will be closed soon. 23 | close-pr-message: Closing this pull request as it hasn't seen activity for a 24 | while. Please add a comment @mentioning a maintainer to reopen. 25 | stale-pr-label: stale 26 | days-before-issue-stale: 60 27 | days-before-issue-close: 7 28 | stale-issue-message: This issue is now marked as stale because it hasn't seen 29 | activity for a while. Add a comment or it will be closed soon. 30 | close-issue-message: Closing this issue as it hasn't seen activity for a while. 31 | Please add a comment @mentioning a maintainer to reopen. 32 | stale-issue-label: stale 33 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | conclusion: ${{ steps.build.outputs.conclusion }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | with: 20 | ref: main 21 | - name: Set git identity 22 | run: |- 23 | git config user.name "github-actions" 24 | git config user.email "github-actions@github.com" 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Build 30 | id: build 31 | run: npx projen build && echo "::set-output name=conclusion::success" || echo 32 | "::set-output name=conclusion::failure" 33 | - name: Create Patch 34 | run: |- 35 | git add . 36 | git diff --patch --staged > .upgrade.tmp.patch 37 | - name: Upload patch 38 | uses: actions/upload-artifact@v2 39 | with: 40 | name: .upgrade.tmp.patch 41 | path: .upgrade.tmp.patch 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: write 48 | pull-requests: write 49 | checks: write 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v2 53 | with: 54 | ref: main 55 | - name: Set git identity 56 | run: |- 57 | git config user.name "github-actions" 58 | git config user.email "github-actions@github.com" 59 | - name: Download patch 60 | uses: actions/download-artifact@v2 61 | with: 62 | name: .upgrade.tmp.patch 63 | path: ${{ runner.temp }} 64 | - name: Apply patch 65 | run: '[ -s ${{ runner.temp }}/.upgrade.tmp.patch ] && git apply ${{ runner.temp 66 | }}/.upgrade.tmp.patch || echo "Empty patch. Skipping."' 67 | - name: Create Pull Request 68 | id: create-pr 69 | uses: peter-evans/create-pull-request@v3 70 | with: 71 | token: ${{ secrets.AUTOMATION_GITHUB_TOKEN }} 72 | commit-message: >- 73 | chore(deps): upgrade dependencies 74 | 75 | 76 | Upgrades project dependencies. See details in [workflow run]. 77 | 78 | 79 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 80 | 81 | 82 | ------ 83 | 84 | 85 | *Automatically created by projen via the "upgrade-main" workflow* 86 | branch: github-actions/upgrade-main 87 | title: "chore(deps): upgrade dependencies" 88 | labels: auto-approve,auto-merge 89 | body: >- 90 | Upgrades project dependencies. See details in [workflow run]. 91 | 92 | 93 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 94 | 95 | 96 | ------ 97 | 98 | 99 | *Automatically created by projen via the "upgrade-main" workflow* 100 | author: github-actions 101 | committer: github-actions 102 | signoff: true 103 | - name: Update status check 104 | if: steps.create-pr.outputs.pull-request-url != '' 105 | run: "curl -i --fail -X POST -H \"Accept: application/vnd.github.v3+json\" -H 106 | \"Authorization: token ${GITHUB_TOKEN}\" 107 | https://api.github.com/repos/${{ github.repository }}/check-runs -d 108 | '{\"name\":\"build\",\"head_sha\":\"github-actions/upgrade-main\",\"s\ 109 | tatus\":\"completed\",\"conclusion\":\"${{ 110 | needs.upgrade.outputs.conclusion }}\",\"output\":{\"title\":\"Created 111 | via the upgrade-main workflow.\",\"summary\":\"Action run URL: 112 | https://github.com/${{ github.repository }}/actions/runs/${{ 113 | github.run_id }}\"}}'" 114 | env: 115 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.github/workflows/auto-approve.yml 6 | !/.github/workflows/stale.yml 7 | !/package.json 8 | !/LICENSE 9 | !/.npmignore 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | lib-cov 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | build/Release 26 | node_modules/ 27 | jspm_packages/ 28 | *.tsbuildinfo 29 | .eslintcache 30 | *.tgz 31 | .yarn-integrity 32 | .cache 33 | !/.projenrc.js 34 | /test-reports/ 35 | junit.xml 36 | /coverage/ 37 | !/.github/workflows/build.yml 38 | /dist/changelog.md 39 | /dist/version.txt 40 | !/.mergify.yml 41 | !/.github/pull_request_template.md 42 | !/test/ 43 | !/tsconfig.json 44 | !/tsconfig.dev.json 45 | !/src/ 46 | /lib 47 | /dist/ 48 | !/.eslintrc.json 49 | .DS_Store 50 | !/.github/workflows/release.yml 51 | !/.github/workflows/upgrade-main.yml 52 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | pull_request_rules: 4 | - name: Automatic merge on approval and successful build 5 | actions: 6 | merge: 7 | method: squash 8 | commit_message: title+body 9 | strict: smart 10 | strict_method: merge 11 | delete_head_branch: {} 12 | conditions: 13 | - "#approved-reviews-by>=1" 14 | - -label~=(do-not-merge) 15 | - status-success=build 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | /dist/changelog.md 7 | /dist/version.txt 8 | /.mergify.yml 9 | /test/ 10 | /tsconfig.dev.json 11 | /src/ 12 | !/lib/ 13 | !/lib/**/*.js 14 | !/lib/**/*.d.ts 15 | dist 16 | /tsconfig.json 17 | /.github/ 18 | /.vscode/ 19 | /.idea/ 20 | /.projenrc.js 21 | tsconfig.tsbuildinfo 22 | /.eslintrc.json 23 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@aws-sdk/client-secrets-manager", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@aws-sdk/client-ssm", 9 | "type": "build" 10 | }, 11 | { 12 | "name": "@types/jest", 13 | "type": "build" 14 | }, 15 | { 16 | "name": "@types/node", 17 | "version": "^14.17.0", 18 | "type": "build" 19 | }, 20 | { 21 | "name": "@typescript-eslint/eslint-plugin", 22 | "type": "build" 23 | }, 24 | { 25 | "name": "@typescript-eslint/parser", 26 | "type": "build" 27 | }, 28 | { 29 | "name": "eslint", 30 | "type": "build" 31 | }, 32 | { 33 | "name": "eslint-import-resolver-node", 34 | "type": "build" 35 | }, 36 | { 37 | "name": "eslint-import-resolver-typescript", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint-plugin-import", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "jest", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "jest-aws-client-mock", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "jest-junit", 54 | "version": "^12", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "json-schema", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "npm-check-updates", 63 | "version": "^11", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "projen", 68 | "type": "build" 69 | }, 70 | { 71 | "name": "standard-version", 72 | "version": "^9", 73 | "type": "build" 74 | }, 75 | { 76 | "name": "ts-jest", 77 | "type": "build" 78 | }, 79 | { 80 | "name": "typescript", 81 | "type": "build" 82 | }, 83 | { 84 | "name": "@aws-sdk/client-secrets-manager", 85 | "type": "peer" 86 | }, 87 | { 88 | "name": "@aws-sdk/client-ssm", 89 | "type": "peer" 90 | } 91 | ], 92 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 93 | } 94 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "clobber": { 4 | "name": "clobber", 5 | "description": "hard resets to HEAD of origin and cleans the local repo", 6 | "env": { 7 | "BRANCH": "$(git branch --show-current)" 8 | }, 9 | "steps": [ 10 | { 11 | "exec": "git checkout -b scratch", 12 | "name": "save current HEAD in \"scratch\" branch" 13 | }, 14 | { 15 | "exec": "git checkout $BRANCH" 16 | }, 17 | { 18 | "exec": "git fetch origin", 19 | "name": "fetch latest changes from origin" 20 | }, 21 | { 22 | "exec": "git reset --hard origin/$BRANCH", 23 | "name": "hard reset to origin commit" 24 | }, 25 | { 26 | "exec": "git clean -fdx", 27 | "name": "clean all untracked files" 28 | }, 29 | { 30 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 31 | } 32 | ], 33 | "condition": "git diff --exit-code > /dev/null" 34 | }, 35 | "compile": { 36 | "name": "compile", 37 | "description": "Only compile", 38 | "steps": [ 39 | { 40 | "exec": "tsc --build" 41 | } 42 | ] 43 | }, 44 | "test:compile": { 45 | "name": "test:compile", 46 | "description": "compiles the test code", 47 | "steps": [ 48 | { 49 | "exec": "tsc --noEmit --project tsconfig.dev.json" 50 | } 51 | ] 52 | }, 53 | "test": { 54 | "name": "test", 55 | "description": "Run tests", 56 | "steps": [ 57 | { 58 | "exec": "rm -fr lib/" 59 | }, 60 | { 61 | "spawn": "test:compile" 62 | }, 63 | { 64 | "exec": "jest --passWithNoTests --all --updateSnapshot" 65 | }, 66 | { 67 | "spawn": "eslint" 68 | } 69 | ] 70 | }, 71 | "build": { 72 | "name": "build", 73 | "description": "Full release build (test+compile)", 74 | "steps": [ 75 | { 76 | "exec": "npx projen" 77 | }, 78 | { 79 | "spawn": "test" 80 | }, 81 | { 82 | "spawn": "compile" 83 | }, 84 | { 85 | "spawn": "package" 86 | } 87 | ] 88 | }, 89 | "test:watch": { 90 | "name": "test:watch", 91 | "description": "Run jest in watch mode", 92 | "steps": [ 93 | { 94 | "exec": "jest --watch" 95 | } 96 | ] 97 | }, 98 | "test:update": { 99 | "name": "test:update", 100 | "description": "Update jest snapshots", 101 | "steps": [ 102 | { 103 | "exec": "jest --updateSnapshot" 104 | } 105 | ] 106 | }, 107 | "bump": { 108 | "name": "bump", 109 | "description": "Bumps version based on latest git tag and generates a changelog entry", 110 | "env": { 111 | "OUTFILE": "package.json", 112 | "CHANGELOG": "dist/changelog.md", 113 | "BUMPFILE": "dist/version.txt", 114 | "RELEASETAG": "dist/releasetag.txt" 115 | }, 116 | "steps": [ 117 | { 118 | "builtin": "release/bump-version" 119 | } 120 | ], 121 | "condition": "! git log --oneline -1 | grep -q \"chore(release):\"" 122 | }, 123 | "unbump": { 124 | "name": "unbump", 125 | "description": "Restores version to 0.0.0", 126 | "env": { 127 | "OUTFILE": "package.json", 128 | "CHANGELOG": "dist/changelog.md", 129 | "BUMPFILE": "dist/version.txt", 130 | "RELEASETAG": "dist/releasetag.txt" 131 | }, 132 | "steps": [ 133 | { 134 | "builtin": "release/reset-version" 135 | } 136 | ] 137 | }, 138 | "publish:github": { 139 | "name": "publish:github", 140 | "description": "Publish this package to GitHub Releases", 141 | "requiredEnv": [ 142 | "GITHUB_TOKEN", 143 | "GITHUB_REPOSITORY", 144 | "GITHUB_REF" 145 | ], 146 | "steps": [ 147 | { 148 | "exec": "errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q \"Release.tag_name already exists\" $errout; then cat $errout; exit $exitcode; fi" 149 | } 150 | ] 151 | }, 152 | "publish:npm": { 153 | "name": "publish:npm", 154 | "description": "Publish this package to npm", 155 | "env": { 156 | "NPM_DIST_TAG": "latest", 157 | "NPM_REGISTRY": "registry.npmjs.org" 158 | }, 159 | "requiredEnv": [ 160 | "NPM_TOKEN" 161 | ], 162 | "steps": [ 163 | { 164 | "exec": "npx -p jsii-release@latest jsii-release-npm" 165 | } 166 | ] 167 | }, 168 | "default": { 169 | "name": "default", 170 | "steps": [ 171 | { 172 | "exec": "node .projenrc.js" 173 | } 174 | ] 175 | }, 176 | "watch": { 177 | "name": "watch", 178 | "description": "Watch & compile in the background", 179 | "steps": [ 180 | { 181 | "exec": "tsc --build -w" 182 | } 183 | ] 184 | }, 185 | "package": { 186 | "name": "package", 187 | "description": "Create an npm tarball", 188 | "steps": [ 189 | { 190 | "exec": "mkdir -p dist/js" 191 | }, 192 | { 193 | "exec": "yarn pack" 194 | }, 195 | { 196 | "exec": "mv *.tgz dist/js/" 197 | } 198 | ] 199 | }, 200 | "eslint": { 201 | "name": "eslint", 202 | "description": "Runs eslint against the codebase", 203 | "steps": [ 204 | { 205 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 206 | } 207 | ] 208 | }, 209 | "release": { 210 | "name": "release", 211 | "description": "Prepare a release from \"main\" branch", 212 | "env": { 213 | "RELEASE": "true", 214 | "MAJOR": "2" 215 | }, 216 | "steps": [ 217 | { 218 | "exec": "rm -fr dist" 219 | }, 220 | { 221 | "spawn": "bump" 222 | }, 223 | { 224 | "spawn": "build" 225 | }, 226 | { 227 | "spawn": "unbump" 228 | }, 229 | { 230 | "exec": "git diff --ignore-space-at-eol --exit-code" 231 | } 232 | ] 233 | }, 234 | "upgrade": { 235 | "name": "upgrade", 236 | "description": "upgrade dependencies", 237 | "env": { 238 | "CI": "0" 239 | }, 240 | "steps": [ 241 | { 242 | "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='projen'" 243 | }, 244 | { 245 | "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='projen'" 246 | }, 247 | { 248 | "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='projen'" 249 | }, 250 | { 251 | "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='projen'" 252 | }, 253 | { 254 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='projen'" 255 | }, 256 | { 257 | "exec": "yarn install --check-files" 258 | }, 259 | { 260 | "exec": "yarn upgrade @aws-sdk/client-secrets-manager @aws-sdk/client-ssm @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import jest jest-aws-client-mock jest-junit json-schema npm-check-updates standard-version ts-jest typescript @aws-sdk/client-secrets-manager @aws-sdk/client-ssm" 261 | }, 262 | { 263 | "exec": "npx projen" 264 | } 265 | ] 266 | }, 267 | "upgrade-projen": { 268 | "name": "upgrade-projen", 269 | "description": "upgrade projen", 270 | "env": { 271 | "CI": "0" 272 | }, 273 | "steps": [ 274 | { 275 | "exec": "npm-check-updates --dep dev --upgrade --target=minor --filter='projen'" 276 | }, 277 | { 278 | "exec": "npm-check-updates --dep optional --upgrade --target=minor --filter='projen'" 279 | }, 280 | { 281 | "exec": "npm-check-updates --dep peer --upgrade --target=minor --filter='projen'" 282 | }, 283 | { 284 | "exec": "npm-check-updates --dep prod --upgrade --target=minor --filter='projen'" 285 | }, 286 | { 287 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor --filter='projen'" 288 | }, 289 | { 290 | "exec": "yarn install --check-files" 291 | }, 292 | { 293 | "exec": "yarn upgrade projen" 294 | }, 295 | { 296 | "exec": "npx projen" 297 | } 298 | ] 299 | } 300 | }, 301 | "env": { 302 | "PATH": "$(npx -c \"node -e \\\"console.log(process.env.PATH)\\\"\")" 303 | }, 304 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 305 | } 306 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { TypeScriptProject, UpgradeDependenciesSchedule } = require('projen'); 2 | 3 | const project = new TypeScriptProject({ 4 | defaultReleaseBranch: 'main', 5 | name: 'aws-parameter-cache', 6 | description: 'Parameter cache for AWS System Manager Parameter Store and AWS Secrets Manager', 7 | keywords: [ 8 | 'aws', 9 | 'aws-sdk', 10 | 'aws-sdk-v3', 11 | 'aws-client', 12 | 'ssm', 13 | 'aws-ssm', 14 | 'cache', 15 | 'secrests-manager', 16 | 'parameter-store', 17 | ], 18 | repository: 'https://github.com/hupe1980/aws-parameter-cache.git', 19 | license: 'MIT', 20 | copyrightOwner: 'Frank Hübner', 21 | majorVersion: 2, 22 | releaseToNpm: true, 23 | devDeps: ['@aws-sdk/client-secrets-manager', '@aws-sdk/client-ssm', 'jest-aws-client-mock'], 24 | peerDeps: ['@aws-sdk/client-secrets-manager', '@aws-sdk/client-ssm'], 25 | depsUpgrade: true, 26 | depsUpgradeOptions: { 27 | workflowOptions: { 28 | labels: ['auto-approve', 'auto-merge'], 29 | secret: 'AUTOMATION_GITHUB_TOKEN', 30 | schedule: UpgradeDependenciesSchedule.WEEKLY, 31 | }, 32 | }, 33 | autoApproveUpgrades: true, 34 | autoApproveOptions: { 35 | secret: 'GITHUB_TOKEN', 36 | allowedUsernames: ['hupe1980'], 37 | }, 38 | tsconfig: { 39 | compilerOptions: { 40 | lib: ['dom', 'es2019'], 41 | }, 42 | }, 43 | }); 44 | project.gitignore.exclude('.DS_Store'); 45 | project.synth(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Frank Hübner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-parameter-cache 2 | ![Build](https://github.com/hupe1980/aws-parameter-cache/workflows/build/badge.svg) 3 | ![Release](https://github.com/hupe1980/aws-parameter-cache/workflows/release/badge.svg) 4 | 5 | > Parameter cache for AWS System Manager Parameter Store and AWS Secrets Manager 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install aws-parameter-cache @aws-sdk/client-ssm @aws-sdk/client-secrets-manager 11 | ``` 12 | 13 | ## How to use 14 | 15 | ```typescript 16 | import { ssmParameter } from 'aws-parameter-cache'; 17 | 18 | const param = ssmParameter({ name: 'foo' }); 19 | const value = await param.value; 20 | ``` 21 | 22 | ### Secrets Manager Parameter 23 | 24 | ```typescript 25 | // https://docs.aws.amazon.com/systems-manager/latest/userguide/integration-ps-secretsmanager.html 26 | import { secretsManagerParameter } from 'aws-parameter-cache'; 27 | 28 | const param = secretsManagerParameter({ name: 'foo' }); 29 | const value = await param.value; 30 | ``` 31 | ### Secrets Manager Secret 32 | 33 | ```typescript 34 | import { secretsManagerSecret } from 'aws-parameter-cache'; 35 | 36 | const secret = secretsManagerSecret({ secretId: 'foo' }); 37 | const secretString = await secret.secretString; 38 | ``` 39 | 40 | ### Cache invalidation 41 | 42 | ```typescript 43 | const param = ssmParameter({ name: 'foo', maxAge: 1000 * 60 * 5 }); 44 | const value = await param.value; 45 | ``` 46 | 47 | ### Force refresh 48 | 49 | ```typescript 50 | const param = ssmParameter({ name: 'foo' }); 51 | const value = await param.value; 52 | 53 | param.refresh(); 54 | 55 | const newValue = await param.value; 56 | ``` 57 | 58 | ### StringList (SSM Parameter) 59 | ```typescript 60 | const param = ssmParameter({ name: 'fooList' }); // XXX,YYY,ZZZ 61 | const valueArray = await param.value; // ['XXX','YYY','ZZZ'] 62 | 63 | valueArray.forEach(console.log) 64 | ``` 65 | 66 | ### Usage with AWS Lambda 67 | ```typescript 68 | const param = ssmParameter({ name: 'name' }); 69 | 70 | export const handler = async (event, context) => { 71 | const value = await param.value 72 | return `Hello ${value}` 73 | } 74 | 75 | ``` 76 | 77 | ## IAM (SSM Parameter) 78 | ```json 79 | { 80 | "Version": "2012-10-17", 81 | "Statement": [ 82 | { 83 | "Effect": "Allow", 84 | "Action": [ 85 | "ssm:GetParameter" 86 | ], 87 | "Resource": "arn:aws:ssm:::parameter/" 88 | }, 89 | { 90 | "Effect": "Allow", 91 | "Action": [ 92 | "kms:Decrypt" 93 | ], 94 | "Resource": "arn:aws:kms:::alias/aws/ssm" 95 | } 96 | ] 97 | } 98 | ``` 99 | 100 | ## License 101 | 102 | [MIT](LICENSE) 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-parameter-cache", 3 | "description": "Parameter cache for AWS System Manager Parameter Store and AWS Secrets Manager", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/hupe1980/aws-parameter-cache.git" 7 | }, 8 | "scripts": { 9 | "clobber": "npx projen clobber", 10 | "compile": "npx projen compile", 11 | "test:compile": "npx projen test:compile", 12 | "test": "npx projen test", 13 | "build": "npx projen build", 14 | "test:watch": "npx projen test:watch", 15 | "test:update": "npx projen test:update", 16 | "bump": "npx projen bump", 17 | "unbump": "npx projen unbump", 18 | "publish:github": "npx projen publish:github", 19 | "publish:npm": "npx projen publish:npm", 20 | "default": "npx projen default", 21 | "watch": "npx projen watch", 22 | "package": "npx projen package", 23 | "eslint": "npx projen eslint", 24 | "release": "npx projen release", 25 | "upgrade": "npx projen upgrade", 26 | "upgrade-projen": "npx projen upgrade-projen", 27 | "projen": "npx projen" 28 | }, 29 | "devDependencies": { 30 | "@aws-sdk/client-secrets-manager": "^3.46.0", 31 | "@aws-sdk/client-ssm": "^3.46.0", 32 | "@types/jest": "^27.4.0", 33 | "@types/node": "^14.17.0", 34 | "@typescript-eslint/eslint-plugin": "^5.9.0", 35 | "@typescript-eslint/parser": "^5.9.0", 36 | "eslint": "^8.6.0", 37 | "eslint-import-resolver-node": "^0.3.6", 38 | "eslint-import-resolver-typescript": "^2.5.0", 39 | "eslint-plugin-import": "^2.25.4", 40 | "jest": "^27.4.7", 41 | "jest-aws-client-mock": "^0.0.25", 42 | "jest-junit": "^12", 43 | "json-schema": "^0.4.0", 44 | "npm-check-updates": "^11", 45 | "projen": "^0.30.0", 46 | "standard-version": "^9", 47 | "ts-jest": "^27.1.2", 48 | "typescript": "^4.5.4" 49 | }, 50 | "peerDependencies": { 51 | "@aws-sdk/client-secrets-manager": "^3.46.0", 52 | "@aws-sdk/client-ssm": "^3.46.0" 53 | }, 54 | "bundledDependencies": [], 55 | "keywords": [ 56 | "aws", 57 | "aws-client", 58 | "aws-sdk", 59 | "aws-sdk-v3", 60 | "aws-ssm", 61 | "cache", 62 | "parameter-store", 63 | "secrests-manager", 64 | "ssm" 65 | ], 66 | "main": "lib/index.js", 67 | "license": "MIT", 68 | "version": "0.0.0", 69 | "jest": { 70 | "testMatch": [ 71 | "**/__tests__/**/*.ts?(x)", 72 | "**/?(*.)+(spec|test).ts?(x)" 73 | ], 74 | "clearMocks": true, 75 | "collectCoverage": true, 76 | "coverageReporters": [ 77 | "json", 78 | "lcov", 79 | "clover", 80 | "text" 81 | ], 82 | "coverageDirectory": "coverage", 83 | "coveragePathIgnorePatterns": [ 84 | "/node_modules/" 85 | ], 86 | "testPathIgnorePatterns": [ 87 | "/node_modules/" 88 | ], 89 | "watchPathIgnorePatterns": [ 90 | "/node_modules/" 91 | ], 92 | "reporters": [ 93 | "default", 94 | [ 95 | "jest-junit", 96 | { 97 | "outputDirectory": "test-reports" 98 | } 99 | ] 100 | ], 101 | "preset": "ts-jest", 102 | "globals": { 103 | "ts-jest": { 104 | "tsconfig": "tsconfig.dev.json" 105 | } 106 | } 107 | }, 108 | "types": "lib/index.d.ts", 109 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 110 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parameter'; 2 | export * from './refreshable'; 3 | export * from './secret'; 4 | export * from './ssm-parameter'; 5 | export * from './secrets-manager-parameter'; 6 | export * from './secrets-manager-secret'; 7 | -------------------------------------------------------------------------------- /src/parameter.ts: -------------------------------------------------------------------------------- 1 | import { SSMClient, GetParameterCommand, SSMClientConfig, GetParameterCommandOutput } from '@aws-sdk/client-ssm'; 2 | import { Refreshable } from './refreshable'; 3 | 4 | export interface ParameterProps { 5 | /** The name of the parameter you want to query. */ 6 | name: string; 7 | 8 | /** The parameter version. */ 9 | version?: string; 10 | 11 | /** Return decrypted values for secure string parameters. This flag is ignored for String and StringList parameter types. */ 12 | withDecryption?: boolean; 13 | 14 | /** The maximum amount of time in milliseconds a parameter will be considered fresh */ 15 | maxAge?: number; 16 | 17 | /** https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-ssm/interfaces/ssmclientconfig.html */ 18 | ssmClientConfig?: SSMClientConfig; 19 | } 20 | export class Parameter extends Refreshable { 21 | public readonly name: string; 22 | 23 | private readonly withDecryption: boolean; 24 | private readonly ssmClient: SSMClient; 25 | private cachedResult?: Promise; 26 | 27 | constructor(props: ParameterProps) { 28 | super(props.maxAge); 29 | this.name = props.version ? props.name + ':' + props.version : props.name; 30 | this.withDecryption = props.withDecryption || true; 31 | this.ssmClient = new SSMClient({ ...props.ssmClientConfig }); 32 | } 33 | 34 | public get value(): Promise { 35 | return this.getValue(); 36 | } 37 | 38 | public async getValue(): Promise { 39 | if (!this.cachedResult || this.isExpired()) { 40 | this.refresh(); 41 | } 42 | 43 | if (this.cachedResult) { 44 | const data = await this.cachedResult; 45 | 46 | if (data.Parameter?.Value) { 47 | return data.Parameter.Type === 'StringList' 48 | ? data.Parameter.Value.split(',') 49 | : data.Parameter.Value; 50 | } 51 | } 52 | 53 | throw new Error(`The value is missing for parameter ${this.name}`); 54 | } 55 | 56 | protected refreshParameter(): void { 57 | this.cachedResult = this.getParameter(); 58 | } 59 | 60 | private getParameter(): Promise { 61 | const command = new GetParameterCommand({ 62 | Name: this.name, 63 | WithDecryption: this.withDecryption, 64 | }); 65 | 66 | return this.ssmClient.send(command); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/refreshable.ts: -------------------------------------------------------------------------------- 1 | export abstract class Refreshable { 2 | private lastRefreshTime?: number; 3 | 4 | constructor(private readonly maxAge?: number) {} 5 | 6 | protected abstract refreshParameter(): void; 7 | 8 | public refresh(): void { 9 | this.updateLastRefreshTime(); 10 | this.refreshParameter(); 11 | } 12 | 13 | private updateLastRefreshTime(): void { 14 | this.lastRefreshTime = Date.now(); 15 | } 16 | 17 | public isExpired(): boolean { 18 | if (!this.maxAge) return false; 19 | if (!this.lastRefreshTime) return true; 20 | return Date.now() > this.lastRefreshTime + this.maxAge; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/secret.ts: -------------------------------------------------------------------------------- 1 | import { SecretsManagerClient, GetSecretValueCommand, SecretsManagerClientConfig, GetSecretValueCommandOutput } from '@aws-sdk/client-secrets-manager'; 2 | import { Refreshable } from './refreshable'; 3 | 4 | export interface SecretProps { 5 | /** Specifies the secret containing the version that you want to retrieve. */ 6 | secretId: string; 7 | 8 | /** Specifies the unique identifier of the version of the secret that you want to retrieve. */ 9 | versionId?: string; 10 | 11 | /** Specifies the secret version that you want to retrieve by the staging label attached to the version. */ 12 | versionStage?: string; 13 | 14 | /** Speciifies the key of the seceretString that you want to retireve. */ 15 | key?: string; 16 | 17 | /** The maximum amount of time in milliseconds a parameter will be considered fresh */ 18 | maxAge?: number; 19 | 20 | /** https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-secrets-manager/interfaces/secretsmanagerclientconfig.html */ 21 | secretsManagerClientConfig?: SecretsManagerClientConfig; 22 | } 23 | export class Secret extends Refreshable { 24 | public readonly secretId: string; 25 | public readonly versionId?: string; 26 | public readonly versionStage?: string; 27 | public readonly key?: string; 28 | 29 | private readonly secretsManagerClient: SecretsManagerClient; 30 | private cachedResponse?: Promise; 31 | 32 | constructor(props: SecretProps) { 33 | super(props.maxAge); 34 | this.secretId = props.secretId; 35 | this.versionId = props.versionId; 36 | this.versionStage = props.versionStage; 37 | this.key = props.key; 38 | this.secretsManagerClient = new SecretsManagerClient({ ...props.secretsManagerClientConfig }); 39 | } 40 | 41 | public get secretString(): Promise { 42 | return this.getSecretString(); 43 | } 44 | 45 | public async getSecretString(): Promise { 46 | if (!this.cachedResponse || this.isExpired()) { 47 | this.refresh(); 48 | } 49 | 50 | if (this.cachedResponse) { 51 | const data = await this.cachedResponse; 52 | 53 | if (data.SecretString) { 54 | return this.key 55 | ? JSON.parse(data.SecretString)[this.key] 56 | : data.SecretString; 57 | } 58 | } 59 | 60 | throw new Error(`The secretString is missing for secret ${this.secretId}`); 61 | } 62 | 63 | protected refreshParameter(): void { 64 | this.cachedResponse = this.getSecretValue(); 65 | } 66 | 67 | private getSecretValue(): Promise { 68 | const command = new GetSecretValueCommand({ 69 | SecretId: this.secretId, 70 | VersionId: this.versionId, 71 | VersionStage: this.versionStage, 72 | }); 73 | return this.secretsManagerClient.send(command); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/secrets-manager-parameter.ts: -------------------------------------------------------------------------------- 1 | import { Parameter, ParameterProps } from './parameter'; 2 | 3 | const PREFIX = '/aws/reference/secretsmanager/'; 4 | 5 | export function secretsManagerParameter(props: ParameterProps): Parameter { 6 | if (!props.name.startsWith(PREFIX)) { 7 | props.name = PREFIX + props.name; 8 | } 9 | return new Parameter(props); 10 | } 11 | -------------------------------------------------------------------------------- /src/secrets-manager-secret.ts: -------------------------------------------------------------------------------- 1 | import { Secret, SecretProps } from './secret'; 2 | 3 | export function secretsManagerSecret(props: SecretProps): Secret { 4 | return new Secret(props); 5 | } 6 | -------------------------------------------------------------------------------- /src/ssm-parameter.ts: -------------------------------------------------------------------------------- 1 | import { Parameter, ParameterProps } from './parameter'; 2 | 3 | export function ssmParameter(props: ParameterProps): Parameter { 4 | return new Parameter(props); 5 | } 6 | -------------------------------------------------------------------------------- /test/secrets-manager-parameter.test.ts: -------------------------------------------------------------------------------- 1 | import { SSMClient } from '@aws-sdk/client-ssm'; 2 | import { mockClient } from 'jest-aws-client-mock'; 3 | import { secretsManagerParameter } from '../src/secrets-manager-parameter'; 4 | 5 | const ssmMock = mockClient(SSMClient); 6 | 7 | beforeEach(() => { 8 | ssmMock.mockReset(); 9 | }); 10 | 11 | it('should append the prefix', async () => { 12 | expect.assertions(2); 13 | 14 | ssmMock.mockResolvedValue({ 15 | Parameter: { 16 | Name: '/aws/reference/secretsmanager/foo', 17 | Type: 'String', 18 | Value: 'bar', 19 | }, 20 | }); 21 | 22 | const param = secretsManagerParameter({ name: 'foo' }); 23 | 24 | expect(param.name).toBe('/aws/reference/secretsmanager/foo'); 25 | expect(await param.value).toBe('bar'); 26 | }); 27 | 28 | it('should ignore the prefix', async () => { 29 | expect.assertions(2); 30 | 31 | ssmMock.mockResolvedValue({ 32 | Parameter: { 33 | Name: '/aws/reference/secretsmanager/foo', 34 | Type: 'String', 35 | Value: 'bar', 36 | }, 37 | }); 38 | 39 | const param = secretsManagerParameter({ 40 | name: '/aws/reference/secretsmanager/foo', 41 | }); 42 | 43 | expect(param.name).toBe('/aws/reference/secretsmanager/foo'); 44 | expect(await param.value).toBe('bar'); 45 | }); 46 | 47 | it('should reject when ssm rejects', async () => { 48 | expect.assertions(1); 49 | 50 | const rejectionMessage = 'ParameterNotFound: ParameterNotFoun'; 51 | ssmMock.mockRejectedValue(rejectionMessage); 52 | 53 | const param = secretsManagerParameter({ 54 | name: '/aws/reference/secretsmanager/missing-foo', 55 | }); 56 | 57 | await expect(param.value).rejects.toEqual(new Error(rejectionMessage)); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /test/secrets-manager-secret.test.ts: -------------------------------------------------------------------------------- 1 | import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; 2 | import { mockClient } from 'jest-aws-client-mock'; 3 | 4 | import { secretsManagerSecret } from '../src/secrets-manager-secret'; 5 | 6 | const secretsManagerMock = mockClient(SecretsManagerClient); 7 | 8 | beforeEach(() => { 9 | secretsManagerMock.mockReset(); 10 | }); 11 | 12 | it('should return the secretString', async () => { 13 | expect.assertions(2); 14 | 15 | secretsManagerMock.mockResolvedValue({ 16 | Name: 'foo', 17 | SecretString: 'bar', 18 | }); 19 | 20 | const secret = secretsManagerSecret({ secretId: 'foo' }); 21 | 22 | expect(secret.secretId).toBe('foo'); 23 | expect(await secret.secretString).toBe('bar'); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /test/ssm-parameter.test.ts: -------------------------------------------------------------------------------- 1 | import { SSMClient } from '@aws-sdk/client-ssm'; 2 | import { mockClient } from 'jest-aws-client-mock'; 3 | 4 | import { ssmParameter } from '../src/ssm-parameter'; 5 | 6 | const ssmMock = mockClient(SSMClient); 7 | 8 | beforeEach(() => { 9 | ssmMock.mockReset(); 10 | }); 11 | 12 | it('should return the parameter value', async () => { 13 | expect.assertions(2); 14 | 15 | ssmMock.mockResolvedValue({ 16 | Parameter: { 17 | Name: 'foo', 18 | Type: 'String', 19 | Value: 'bar', 20 | }, 21 | }); 22 | 23 | const param = ssmParameter({ name: 'foo' }); 24 | 25 | expect(param.name).toBe('foo'); 26 | expect(await param.value).toBe('bar'); 27 | }); 28 | 29 | it('should ignore the parameter change', async () => { 30 | expect.assertions(3); 31 | 32 | ssmMock.mockResolvedValueOnce({ 33 | Parameter: { 34 | Name: 'foo', 35 | Type: 'String', 36 | Value: 'bar', 37 | }, 38 | }); 39 | 40 | const param = ssmParameter({ name: 'foo' }); 41 | 42 | expect(param.name).toBe('foo'); 43 | expect(await param.value).toBe('bar'); 44 | 45 | ssmMock.mockResolvedValueOnce({ 46 | Parameter: { 47 | Name: 'foo', 48 | Type: 'String', 49 | Value: 'XXX', 50 | }, 51 | }); 52 | 53 | expect(await param.value).toBe('bar'); 54 | }); 55 | 56 | it('should force a refresh', async () => { 57 | expect.assertions(4); 58 | 59 | ssmMock.mockResolvedValueOnce({ 60 | Parameter: { 61 | Name: 'foo', 62 | Type: 'String', 63 | Value: 'bar', 64 | }, 65 | }); 66 | 67 | const param = ssmParameter({ name: 'foo' }); 68 | 69 | expect(param.name).toBe('foo'); 70 | expect(await param.value).toBe('bar'); 71 | 72 | ssmMock.mockResolvedValueOnce({ 73 | Parameter: { 74 | Name: 'foo', 75 | Type: 'String', 76 | Value: 'XXX', 77 | }, 78 | }); 79 | 80 | expect(await param.value).toBe('bar'); 81 | 82 | param.refresh(); 83 | 84 | expect(await param.value).toBe('XXX'); 85 | }); 86 | 87 | it('should invalidate the cache', async () => { 88 | expect.assertions(4); 89 | 90 | ssmMock.mockResolvedValueOnce({ 91 | Parameter: { 92 | Name: 'foo', 93 | Type: 'String', 94 | Value: 'bar', 95 | }, 96 | }); 97 | 98 | Date.now = jest.fn(() => 1000); 99 | const param = ssmParameter({ name: 'foo', maxAge: 1000 }); 100 | 101 | expect(param.name).toBe('foo'); 102 | expect(await param.value).toBe('bar'); 103 | 104 | ssmMock.mockResolvedValueOnce({ 105 | Parameter: { 106 | Name: 'foo', 107 | Type: 'String', 108 | Value: 'XXX', 109 | }, 110 | }); 111 | 112 | expect(await param.value).toBe('bar'); 113 | 114 | Date.now = jest.fn(() => 3000); 115 | expect(await param.value).toBe('XXX'); 116 | }); 117 | 118 | it('should return the the stringList as array', async () => { 119 | expect.assertions(2); 120 | 121 | ssmMock.mockResolvedValue({ 122 | Parameter: { 123 | Name: 'fooList', 124 | Type: 'StringList', 125 | Value: 'XXX,YYY,ZZZ', 126 | }, 127 | }); 128 | 129 | const param = ssmParameter({ name: 'fooList' }); 130 | 131 | expect(param.name).toBe('fooList'); 132 | expect(await param.value).toEqual(['XXX', 'YYY', 'ZZZ']); 133 | }); 134 | 135 | it('should reject when ssm rejects', async() => { 136 | expect.assertions(1); 137 | 138 | const rejectionMessage = 'ParameterNotFound: ParameterNotFoun'; 139 | ssmMock.mockRejectedValue(rejectionMessage); 140 | 141 | const param = ssmParameter({ 142 | name: '/aws/reference/secretsmanager/missing-foo', 143 | }); 144 | 145 | await expect(param.value).rejects.toEqual(new Error(rejectionMessage)); 146 | }); 147 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "dom", 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | ".projenrc.js", 30 | "src/**/*.ts", 31 | "test/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ], 36 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "lib", 5 | "alwaysStrict": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "inlineSourceMap": true, 10 | "inlineSources": true, 11 | "lib": [ 12 | "dom", 13 | "es2019" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2019" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [], 34 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 35 | } 36 | --------------------------------------------------------------------------------