@loftisland-oss/ssm-loader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/index.cjs +142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @loftisland-oss/ssm-loader
|
|
2
|
+
|
|
3
|
+
AWS Systems Manager Parameter Store 값을 `process.env`로 로드하는 작은 라이브러리입니다.
|
|
4
|
+
JS/TS, CJS/ESM 모두에서 그대로 사용할 수 있습니다.
|
|
5
|
+
|
|
6
|
+
두 가지 사용 패턴을 지원합니다.
|
|
7
|
+
|
|
8
|
+
1. **경로(prefix) 기반** — `/prod/my-service/`처럼 특정 경로로 시작하는 파라미터를 전부 가져옵니다.
|
|
9
|
+
2. **이름 직접 지정** — 파라미터 이름을 하나 이상 직접 지정해서 가져옵니다.
|
|
10
|
+
|
|
11
|
+
두 패턴은 함께 사용할 수도 있습니다.
|
|
12
|
+
|
|
13
|
+
## 설치
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @loftisland-oss/ssm-loader
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`@aws-sdk/client-ssm`은 이 패키지의 `dependencies`에 포함되어 있어 별도로 설치할 필요가 없습니다.
|
|
20
|
+
|
|
21
|
+
## 사용법
|
|
22
|
+
|
|
23
|
+
### 1. 경로 prefix로 전부 가져오기
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { loadSsmConfig } from '@loftisland-oss/ssm-loader';
|
|
27
|
+
|
|
28
|
+
await loadSsmConfig({
|
|
29
|
+
paths: { path: '/prod/my-service/' },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// /prod/my-service/DB_HOST -> process.env.DB_HOST
|
|
33
|
+
// /prod/my-service/DB_PORT -> process.env.DB_PORT
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
하위 경로까지 재귀적으로 가져오려면 `recursive: true`를 사용하세요.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
await loadSsmConfig({
|
|
40
|
+
paths: { path: '/prod/my-service/', recursive: true },
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. 파라미터 이름을 직접 지정해서 가져오기
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
await loadSsmConfig({
|
|
48
|
+
names: ['/prod/my-service/DB_HOST', '/prod/my-service/API_KEY'],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// process.env.DB_HOST, process.env.API_KEY 로 설정됩니다 (이름의 마지막 "/" 구간이 키가 됩니다)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
env 키를 직접 지정하고 싶다면:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
await loadSsmConfig({
|
|
58
|
+
names: [
|
|
59
|
+
{ name: '/prod/my-service/DB_HOST', envKey: 'DATABASE_HOST' },
|
|
60
|
+
'/prod/my-service/API_KEY',
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 두 패턴 함께 사용
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
await loadSsmConfig({
|
|
69
|
+
paths: { path: '/prod/my-service/' },
|
|
70
|
+
names: ['/shared/API_KEY'],
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 개발 환경에서 SSM 호출 생략하기
|
|
75
|
+
|
|
76
|
+
기존 코드의 "dev면 SSM 건너뛰고 .env 사용" 패턴은 `skip` 옵션으로 그대로 구현할 수 있습니다.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const DEV_ENVS = ['development', 'dev'];
|
|
80
|
+
|
|
81
|
+
await loadSsmConfig({
|
|
82
|
+
skip: DEV_ENVS.includes(process.env.NODE_ENV ?? ''),
|
|
83
|
+
paths: { path: '/prod/my-service/' },
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### NestJS Logger 등 커스텀 로거 사용하기
|
|
88
|
+
|
|
89
|
+
기본적으로 `console`을 사용하는 로거가 내장되어 있습니다. NestJS의 `Logger` 등 자체 로거를 쓰려면
|
|
90
|
+
`log(message)` / `error(message)` 를 가진 객체를 넘기면 됩니다.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { Logger } from '@nestjs/common';
|
|
94
|
+
import { loadSsmConfig } from '@loftisland-oss/ssm-loader';
|
|
95
|
+
|
|
96
|
+
const logger = new Logger('SSM');
|
|
97
|
+
|
|
98
|
+
await loadSsmConfig({
|
|
99
|
+
logger,
|
|
100
|
+
paths: { path: '/prod/my-service/' },
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
로그를 완전히 끄고 싶다면 `logger: null`을 전달하세요.
|
|
105
|
+
|
|
106
|
+
### JavaScript (CommonJS)에서 사용하기
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const { loadSsmConfig } = require('@loftisland-oss/ssm-loader');
|
|
110
|
+
|
|
111
|
+
loadSsmConfig({ paths: { path: '/prod/my-service/' } }).then(() => {
|
|
112
|
+
console.log(process.env.DB_HOST);
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
### `loadSsmConfig(options): Promise<{ values: Record<string, string>; count: number }>`
|
|
119
|
+
|
|
120
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| `paths` | `ByPathSource \| ByPathSource[]` | - | 경로 prefix로 파라미터를 가져옵니다 |
|
|
123
|
+
| `names` | `Array<string \| ByNameSource>` | - | 파라미터 이름을 직접 지정합니다 |
|
|
124
|
+
| `region` | `string` | `AWS_REGION` env → `"ap-northeast-2"` | AWS 리전 |
|
|
125
|
+
| `client` | `SSMClient` | 새로 생성 | 기존 `SSMClient` 재사용 (테스트/커스텀 자격증명용) |
|
|
126
|
+
| `withDecryption` | `boolean` | `true` | `SecureString` 복호화 여부 |
|
|
127
|
+
| `setEnv` | `boolean` | `true` | 결과를 `process.env`에 반영할지 여부 |
|
|
128
|
+
| `overrideExisting` | `boolean` | `true` | 이미 설정된 env 키를 덮어쓸지 여부 |
|
|
129
|
+
| `skip` | `boolean` | `false` | `true`면 SSM 호출 없이 빈 결과 반환 |
|
|
130
|
+
| `logger` | `SimpleLogger \| null` | 내장 콘솔 로거 | 커스텀 로거, `null`이면 로그 비활성화 |
|
|
131
|
+
|
|
132
|
+
`ByPathSource`
|
|
133
|
+
|
|
134
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
135
|
+
| --- | --- | --- | --- |
|
|
136
|
+
| `path` | `string` | - | 파라미터 경로 prefix |
|
|
137
|
+
| `recursive` | `boolean` | `false` | 하위 경로까지 재귀적으로 조회 |
|
|
138
|
+
| `stripPrefix` | `boolean` | `true` | 결과 키에서 `path` prefix를 제거할지 여부 |
|
|
139
|
+
| `transformKey` | `(key: string) => string` | - | 최종 키 변환 함수 |
|
|
140
|
+
|
|
141
|
+
`ByNameSource`
|
|
142
|
+
|
|
143
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
144
|
+
| --- | --- | --- | --- |
|
|
145
|
+
| `name` | `string` | - | 파라미터 전체 이름 |
|
|
146
|
+
| `envKey` | `string` | 이름의 마지막 `/` 구간 | 결과에 사용할 env 키 |
|
|
147
|
+
|
|
148
|
+
## 라이선스
|
|
149
|
+
|
|
150
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
defaultLogger: () => defaultLogger,
|
|
24
|
+
loadSsmConfig: () => loadSsmConfig
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/loader.ts
|
|
29
|
+
var import_client_ssm = require("@aws-sdk/client-ssm");
|
|
30
|
+
|
|
31
|
+
// src/logger.ts
|
|
32
|
+
var defaultLogger = {
|
|
33
|
+
log: (message) => console.log(`[aws-ssm-loader] ${message}`),
|
|
34
|
+
error: (message) => console.error(`[aws-ssm-loader] ${message}`)
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/loader.ts
|
|
38
|
+
var MAX_NAMES_PER_REQUEST = 10;
|
|
39
|
+
function chunk(items, size) {
|
|
40
|
+
const chunks = [];
|
|
41
|
+
for (let i = 0; i < items.length; i += size) {
|
|
42
|
+
chunks.push(items.slice(i, i + size));
|
|
43
|
+
}
|
|
44
|
+
return chunks;
|
|
45
|
+
}
|
|
46
|
+
function deriveKeyFromPath(paramName, source) {
|
|
47
|
+
let key = source.stripPrefix === false ? paramName : paramName.replace(source.path, "");
|
|
48
|
+
if (key.startsWith("/")) key = key.slice(1);
|
|
49
|
+
return source.transformKey ? source.transformKey(key) : key;
|
|
50
|
+
}
|
|
51
|
+
function deriveKeyFromName(paramName, source) {
|
|
52
|
+
if (source.envKey) return source.envKey;
|
|
53
|
+
const segments = paramName.split("/").filter(Boolean);
|
|
54
|
+
return segments[segments.length - 1] ?? paramName;
|
|
55
|
+
}
|
|
56
|
+
async function fetchByPath(client, source, withDecryption) {
|
|
57
|
+
const result = {};
|
|
58
|
+
let nextToken;
|
|
59
|
+
do {
|
|
60
|
+
const response = await client.send(
|
|
61
|
+
new import_client_ssm.GetParametersByPathCommand({
|
|
62
|
+
Path: source.path,
|
|
63
|
+
WithDecryption: withDecryption,
|
|
64
|
+
Recursive: source.recursive ?? false,
|
|
65
|
+
NextToken: nextToken
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
for (const param of response.Parameters ?? []) {
|
|
69
|
+
if (!param.Name || param.Value === void 0) continue;
|
|
70
|
+
const key = deriveKeyFromPath(param.Name, source);
|
|
71
|
+
if (key) result[key] = param.Value;
|
|
72
|
+
}
|
|
73
|
+
nextToken = response.NextToken;
|
|
74
|
+
} while (nextToken);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
async function fetchByNames(client, sources, withDecryption, logger) {
|
|
78
|
+
const result = {};
|
|
79
|
+
const byName = new Map(sources.map((source) => [source.name, source]));
|
|
80
|
+
for (const names of chunk(sources.map((source) => source.name), MAX_NAMES_PER_REQUEST)) {
|
|
81
|
+
const response = await client.send(
|
|
82
|
+
new import_client_ssm.GetParametersCommand({ Names: names, WithDecryption: withDecryption })
|
|
83
|
+
);
|
|
84
|
+
for (const param of response.Parameters ?? []) {
|
|
85
|
+
if (!param.Name || param.Value === void 0) continue;
|
|
86
|
+
const source = byName.get(param.Name);
|
|
87
|
+
if (!source) continue;
|
|
88
|
+
result[deriveKeyFromName(param.Name, source)] = param.Value;
|
|
89
|
+
}
|
|
90
|
+
if (response.InvalidParameters?.length) {
|
|
91
|
+
logger?.error(`Invalid SSM parameter name(s): ${response.InvalidParameters.join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
async function loadSsmConfig(options = {}) {
|
|
97
|
+
const logger = options.logger === null ? null : options.logger ?? defaultLogger;
|
|
98
|
+
if (options.skip) {
|
|
99
|
+
logger?.log("Skipped loading SSM parameters (skip=true)");
|
|
100
|
+
return { values: {}, count: 0 };
|
|
101
|
+
}
|
|
102
|
+
const pathSources = options.paths ? Array.isArray(options.paths) ? options.paths : [options.paths] : [];
|
|
103
|
+
const nameSources = (options.names ?? []).map(
|
|
104
|
+
(entry) => typeof entry === "string" ? { name: entry } : entry
|
|
105
|
+
);
|
|
106
|
+
if (pathSources.length === 0 && nameSources.length === 0) {
|
|
107
|
+
throw new Error('loadSsmConfig requires at least one of "paths" or "names".');
|
|
108
|
+
}
|
|
109
|
+
const region = options.region ?? process.env.AWS_REGION ?? "ap-northeast-2";
|
|
110
|
+
const client = options.client ?? new import_client_ssm.SSMClient({ region });
|
|
111
|
+
const withDecryption = options.withDecryption ?? true;
|
|
112
|
+
const setEnv = options.setEnv ?? true;
|
|
113
|
+
const overrideExisting = options.overrideExisting ?? true;
|
|
114
|
+
try {
|
|
115
|
+
const values = {};
|
|
116
|
+
for (const source of pathSources) {
|
|
117
|
+
Object.assign(values, await fetchByPath(client, source, withDecryption));
|
|
118
|
+
}
|
|
119
|
+
if (nameSources.length > 0) {
|
|
120
|
+
Object.assign(values, await fetchByNames(client, nameSources, withDecryption, logger));
|
|
121
|
+
}
|
|
122
|
+
if (setEnv) {
|
|
123
|
+
for (const [key, value] of Object.entries(values)) {
|
|
124
|
+
if (overrideExisting || process.env[key] === void 0) {
|
|
125
|
+
process.env[key] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
logger?.log(`Loaded ${Object.keys(values).length} parameter(s) from SSM`);
|
|
130
|
+
return { values, count: Object.keys(values).length };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
logger?.error(`Failed to load SSM config: ${message}`);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
138
|
+
0 && (module.exports = {
|
|
139
|
+
defaultLogger,
|
|
140
|
+
loadSsmConfig
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/loader.ts","../src/logger.ts"],"sourcesContent":["export { loadSsmConfig } from './loader.js';\nexport { defaultLogger } from './logger.js';\nexport type {\n ByNameSource,\n ByPathSource,\n LoadSsmConfigOptions,\n LoadSsmConfigResult,\n SimpleLogger,\n} from './types.js';\n","import { GetParametersByPathCommand, GetParametersCommand, SSMClient } from '@aws-sdk/client-ssm';\nimport { defaultLogger } from './logger.js';\nimport type {\n ByNameSource,\n ByPathSource,\n LoadSsmConfigOptions,\n LoadSsmConfigResult,\n SimpleLogger,\n} from './types.js';\n\nconst MAX_NAMES_PER_REQUEST = 10;\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < items.length; i += size) {\n chunks.push(items.slice(i, i + size));\n }\n return chunks;\n}\n\nfunction deriveKeyFromPath(paramName: string, source: ByPathSource): string {\n let key = source.stripPrefix === false ? paramName : paramName.replace(source.path, '');\n if (key.startsWith('/')) key = key.slice(1);\n return source.transformKey ? source.transformKey(key) : key;\n}\n\nfunction deriveKeyFromName(paramName: string, source: ByNameSource): string {\n if (source.envKey) return source.envKey;\n const segments = paramName.split('/').filter(Boolean);\n return segments[segments.length - 1] ?? paramName;\n}\n\nasync function fetchByPath(\n client: SSMClient,\n source: ByPathSource,\n withDecryption: boolean,\n): Promise<Record<string, string>> {\n const result: Record<string, string> = {};\n let nextToken: string | undefined;\n\n do {\n const response = await client.send(\n new GetParametersByPathCommand({\n Path: source.path,\n WithDecryption: withDecryption,\n Recursive: source.recursive ?? false,\n NextToken: nextToken,\n }),\n );\n\n for (const param of response.Parameters ?? []) {\n if (!param.Name || param.Value === undefined) continue;\n const key = deriveKeyFromPath(param.Name, source);\n if (key) result[key] = param.Value;\n }\n\n nextToken = response.NextToken;\n } while (nextToken);\n\n return result;\n}\n\nasync function fetchByNames(\n client: SSMClient,\n sources: ByNameSource[],\n withDecryption: boolean,\n logger: SimpleLogger | null,\n): Promise<Record<string, string>> {\n const result: Record<string, string> = {};\n const byName = new Map(sources.map((source) => [source.name, source]));\n\n for (const names of chunk(sources.map((source) => source.name), MAX_NAMES_PER_REQUEST)) {\n const response = await client.send(\n new GetParametersCommand({ Names: names, WithDecryption: withDecryption }),\n );\n\n for (const param of response.Parameters ?? []) {\n if (!param.Name || param.Value === undefined) continue;\n const source = byName.get(param.Name);\n if (!source) continue;\n result[deriveKeyFromName(param.Name, source)] = param.Value;\n }\n\n if (response.InvalidParameters?.length) {\n logger?.error(`Invalid SSM parameter name(s): ${response.InvalidParameters.join(', ')}`);\n }\n }\n\n return result;\n}\n\n/**\n * Load AWS SSM Parameter Store values, optionally assigning them onto process.env.\n *\n * Supports two source patterns, usable together:\n * - `paths`: fetch everything under a path prefix (GetParametersByPath)\n * - `names`: fetch one or more explicit parameter names (GetParameters)\n */\nexport async function loadSsmConfig(\n options: LoadSsmConfigOptions = {},\n): Promise<LoadSsmConfigResult> {\n const logger = options.logger === null ? null : options.logger ?? defaultLogger;\n\n if (options.skip) {\n logger?.log('Skipped loading SSM parameters (skip=true)');\n return { values: {}, count: 0 };\n }\n\n const pathSources: ByPathSource[] = options.paths\n ? Array.isArray(options.paths)\n ? options.paths\n : [options.paths]\n : [];\n\n const nameSources: ByNameSource[] = (options.names ?? []).map((entry) =>\n typeof entry === 'string' ? { name: entry } : entry,\n );\n\n if (pathSources.length === 0 && nameSources.length === 0) {\n throw new Error('loadSsmConfig requires at least one of \"paths\" or \"names\".');\n }\n\n const region = options.region ?? process.env.AWS_REGION ?? 'ap-northeast-2';\n const client = options.client ?? new SSMClient({ region });\n const withDecryption = options.withDecryption ?? true;\n const setEnv = options.setEnv ?? true;\n const overrideExisting = options.overrideExisting ?? true;\n\n try {\n const values: Record<string, string> = {};\n\n for (const source of pathSources) {\n Object.assign(values, await fetchByPath(client, source, withDecryption));\n }\n\n if (nameSources.length > 0) {\n Object.assign(values, await fetchByNames(client, nameSources, withDecryption, logger));\n }\n\n if (setEnv) {\n for (const [key, value] of Object.entries(values)) {\n if (overrideExisting || process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n\n logger?.log(`Loaded ${Object.keys(values).length} parameter(s) from SSM`);\n\n return { values, count: Object.keys(values).length };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n logger?.error(`Failed to load SSM config: ${message}`);\n throw error;\n }\n}\n","import type { SimpleLogger } from './types.js';\n\nexport const defaultLogger: SimpleLogger = {\n log: (message) => console.log(`[aws-ssm-loader] ${message}`),\n error: (message) => console.error(`[aws-ssm-loader] ${message}`),\n};\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SSMClient } from '@aws-sdk/client-ssm';
|
|
2
|
+
|
|
3
|
+
interface SimpleLogger {
|
|
4
|
+
log(message: string): void;
|
|
5
|
+
error(message: string): void;
|
|
6
|
+
}
|
|
7
|
+
/** Fetch every parameter under a path prefix, e.g. "/prod/my-service/". */
|
|
8
|
+
interface ByPathSource {
|
|
9
|
+
/** Parameter Store path prefix. Trailing slash recommended. */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Also fetch parameters under nested sub-paths. Default: false. */
|
|
12
|
+
recursive?: boolean;
|
|
13
|
+
/** Strip the `path` prefix from the derived env key. Default: true. */
|
|
14
|
+
stripPrefix?: boolean;
|
|
15
|
+
/** Transform the derived key (after prefix stripping) before it is used. */
|
|
16
|
+
transformKey?: (key: string) => string;
|
|
17
|
+
}
|
|
18
|
+
/** Fetch one explicit parameter by its full name. */
|
|
19
|
+
interface ByNameSource {
|
|
20
|
+
/** Full Parameter Store name, e.g. "/prod/my-service/DB_HOST". */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Env var key to assign. Defaults to the last "/"-separated segment of `name`. */
|
|
23
|
+
envKey?: string;
|
|
24
|
+
}
|
|
25
|
+
interface LoadSsmConfigOptions {
|
|
26
|
+
/** AWS region. Defaults to process.env.AWS_REGION, then "ap-northeast-2". */
|
|
27
|
+
region?: string;
|
|
28
|
+
/** Reuse an existing SSMClient instead of creating a new one (e.g. for testing or custom credentials). */
|
|
29
|
+
client?: SSMClient;
|
|
30
|
+
/** Decrypt SecureString parameters. Default: true. */
|
|
31
|
+
withDecryption?: boolean;
|
|
32
|
+
/** Assign resolved values onto process.env. Default: true. */
|
|
33
|
+
setEnv?: boolean;
|
|
34
|
+
/** When setting process.env, override keys that are already set. Default: true. */
|
|
35
|
+
overrideExisting?: boolean;
|
|
36
|
+
/** Skip fetching entirely and return an empty result (e.g. for local/dev environments). */
|
|
37
|
+
skip?: boolean;
|
|
38
|
+
/** Custom logger, or `null` to disable logging. Defaults to a console-based logger. */
|
|
39
|
+
logger?: SimpleLogger | null;
|
|
40
|
+
/** One or more path prefixes to fetch recursively via GetParametersByPath. */
|
|
41
|
+
paths?: ByPathSource | ByPathSource[];
|
|
42
|
+
/** Explicit parameter names to fetch via GetParameters. Strings are shorthand for `{ name }`. */
|
|
43
|
+
names?: Array<string | ByNameSource>;
|
|
44
|
+
}
|
|
45
|
+
interface LoadSsmConfigResult {
|
|
46
|
+
/** Env-key -> value map of every parameter that was resolved. */
|
|
47
|
+
values: Record<string, string>;
|
|
48
|
+
/** Number of parameters resolved. */
|
|
49
|
+
count: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load AWS SSM Parameter Store values, optionally assigning them onto process.env.
|
|
54
|
+
*
|
|
55
|
+
* Supports two source patterns, usable together:
|
|
56
|
+
* - `paths`: fetch everything under a path prefix (GetParametersByPath)
|
|
57
|
+
* - `names`: fetch one or more explicit parameter names (GetParameters)
|
|
58
|
+
*/
|
|
59
|
+
declare function loadSsmConfig(options?: LoadSsmConfigOptions): Promise<LoadSsmConfigResult>;
|
|
60
|
+
|
|
61
|
+
declare const defaultLogger: SimpleLogger;
|
|
62
|
+
|
|
63
|
+
export { type ByNameSource, type ByPathSource, type LoadSsmConfigOptions, type LoadSsmConfigResult, type SimpleLogger, defaultLogger, loadSsmConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SSMClient } from '@aws-sdk/client-ssm';
|
|
2
|
+
|
|
3
|
+
interface SimpleLogger {
|
|
4
|
+
log(message: string): void;
|
|
5
|
+
error(message: string): void;
|
|
6
|
+
}
|
|
7
|
+
/** Fetch every parameter under a path prefix, e.g. "/prod/my-service/". */
|
|
8
|
+
interface ByPathSource {
|
|
9
|
+
/** Parameter Store path prefix. Trailing slash recommended. */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Also fetch parameters under nested sub-paths. Default: false. */
|
|
12
|
+
recursive?: boolean;
|
|
13
|
+
/** Strip the `path` prefix from the derived env key. Default: true. */
|
|
14
|
+
stripPrefix?: boolean;
|
|
15
|
+
/** Transform the derived key (after prefix stripping) before it is used. */
|
|
16
|
+
transformKey?: (key: string) => string;
|
|
17
|
+
}
|
|
18
|
+
/** Fetch one explicit parameter by its full name. */
|
|
19
|
+
interface ByNameSource {
|
|
20
|
+
/** Full Parameter Store name, e.g. "/prod/my-service/DB_HOST". */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Env var key to assign. Defaults to the last "/"-separated segment of `name`. */
|
|
23
|
+
envKey?: string;
|
|
24
|
+
}
|
|
25
|
+
interface LoadSsmConfigOptions {
|
|
26
|
+
/** AWS region. Defaults to process.env.AWS_REGION, then "ap-northeast-2". */
|
|
27
|
+
region?: string;
|
|
28
|
+
/** Reuse an existing SSMClient instead of creating a new one (e.g. for testing or custom credentials). */
|
|
29
|
+
client?: SSMClient;
|
|
30
|
+
/** Decrypt SecureString parameters. Default: true. */
|
|
31
|
+
withDecryption?: boolean;
|
|
32
|
+
/** Assign resolved values onto process.env. Default: true. */
|
|
33
|
+
setEnv?: boolean;
|
|
34
|
+
/** When setting process.env, override keys that are already set. Default: true. */
|
|
35
|
+
overrideExisting?: boolean;
|
|
36
|
+
/** Skip fetching entirely and return an empty result (e.g. for local/dev environments). */
|
|
37
|
+
skip?: boolean;
|
|
38
|
+
/** Custom logger, or `null` to disable logging. Defaults to a console-based logger. */
|
|
39
|
+
logger?: SimpleLogger | null;
|
|
40
|
+
/** One or more path prefixes to fetch recursively via GetParametersByPath. */
|
|
41
|
+
paths?: ByPathSource | ByPathSource[];
|
|
42
|
+
/** Explicit parameter names to fetch via GetParameters. Strings are shorthand for `{ name }`. */
|
|
43
|
+
names?: Array<string | ByNameSource>;
|
|
44
|
+
}
|
|
45
|
+
interface LoadSsmConfigResult {
|
|
46
|
+
/** Env-key -> value map of every parameter that was resolved. */
|
|
47
|
+
values: Record<string, string>;
|
|
48
|
+
/** Number of parameters resolved. */
|
|
49
|
+
count: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load AWS SSM Parameter Store values, optionally assigning them onto process.env.
|
|
54
|
+
*
|
|
55
|
+
* Supports two source patterns, usable together:
|
|
56
|
+
* - `paths`: fetch everything under a path prefix (GetParametersByPath)
|
|
57
|
+
* - `names`: fetch one or more explicit parameter names (GetParameters)
|
|
58
|
+
*/
|
|
59
|
+
declare function loadSsmConfig(options?: LoadSsmConfigOptions): Promise<LoadSsmConfigResult>;
|
|
60
|
+
|
|
61
|
+
declare const defaultLogger: SimpleLogger;
|
|
62
|
+
|
|
63
|
+
export { type ByNameSource, type ByPathSource, type LoadSsmConfigOptions, type LoadSsmConfigResult, type SimpleLogger, defaultLogger, loadSsmConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/loader.ts
|
|
2
|
+
import { GetParametersByPathCommand, GetParametersCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
3
|
+
|
|
4
|
+
// src/logger.ts
|
|
5
|
+
var defaultLogger = {
|
|
6
|
+
log: (message) => console.log(`[aws-ssm-loader] ${message}`),
|
|
7
|
+
error: (message) => console.error(`[aws-ssm-loader] ${message}`)
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/loader.ts
|
|
11
|
+
var MAX_NAMES_PER_REQUEST = 10;
|
|
12
|
+
function chunk(items, size) {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for (let i = 0; i < items.length; i += size) {
|
|
15
|
+
chunks.push(items.slice(i, i + size));
|
|
16
|
+
}
|
|
17
|
+
return chunks;
|
|
18
|
+
}
|
|
19
|
+
function deriveKeyFromPath(paramName, source) {
|
|
20
|
+
let key = source.stripPrefix === false ? paramName : paramName.replace(source.path, "");
|
|
21
|
+
if (key.startsWith("/")) key = key.slice(1);
|
|
22
|
+
return source.transformKey ? source.transformKey(key) : key;
|
|
23
|
+
}
|
|
24
|
+
function deriveKeyFromName(paramName, source) {
|
|
25
|
+
if (source.envKey) return source.envKey;
|
|
26
|
+
const segments = paramName.split("/").filter(Boolean);
|
|
27
|
+
return segments[segments.length - 1] ?? paramName;
|
|
28
|
+
}
|
|
29
|
+
async function fetchByPath(client, source, withDecryption) {
|
|
30
|
+
const result = {};
|
|
31
|
+
let nextToken;
|
|
32
|
+
do {
|
|
33
|
+
const response = await client.send(
|
|
34
|
+
new GetParametersByPathCommand({
|
|
35
|
+
Path: source.path,
|
|
36
|
+
WithDecryption: withDecryption,
|
|
37
|
+
Recursive: source.recursive ?? false,
|
|
38
|
+
NextToken: nextToken
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
for (const param of response.Parameters ?? []) {
|
|
42
|
+
if (!param.Name || param.Value === void 0) continue;
|
|
43
|
+
const key = deriveKeyFromPath(param.Name, source);
|
|
44
|
+
if (key) result[key] = param.Value;
|
|
45
|
+
}
|
|
46
|
+
nextToken = response.NextToken;
|
|
47
|
+
} while (nextToken);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
async function fetchByNames(client, sources, withDecryption, logger) {
|
|
51
|
+
const result = {};
|
|
52
|
+
const byName = new Map(sources.map((source) => [source.name, source]));
|
|
53
|
+
for (const names of chunk(sources.map((source) => source.name), MAX_NAMES_PER_REQUEST)) {
|
|
54
|
+
const response = await client.send(
|
|
55
|
+
new GetParametersCommand({ Names: names, WithDecryption: withDecryption })
|
|
56
|
+
);
|
|
57
|
+
for (const param of response.Parameters ?? []) {
|
|
58
|
+
if (!param.Name || param.Value === void 0) continue;
|
|
59
|
+
const source = byName.get(param.Name);
|
|
60
|
+
if (!source) continue;
|
|
61
|
+
result[deriveKeyFromName(param.Name, source)] = param.Value;
|
|
62
|
+
}
|
|
63
|
+
if (response.InvalidParameters?.length) {
|
|
64
|
+
logger?.error(`Invalid SSM parameter name(s): ${response.InvalidParameters.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
async function loadSsmConfig(options = {}) {
|
|
70
|
+
const logger = options.logger === null ? null : options.logger ?? defaultLogger;
|
|
71
|
+
if (options.skip) {
|
|
72
|
+
logger?.log("Skipped loading SSM parameters (skip=true)");
|
|
73
|
+
return { values: {}, count: 0 };
|
|
74
|
+
}
|
|
75
|
+
const pathSources = options.paths ? Array.isArray(options.paths) ? options.paths : [options.paths] : [];
|
|
76
|
+
const nameSources = (options.names ?? []).map(
|
|
77
|
+
(entry) => typeof entry === "string" ? { name: entry } : entry
|
|
78
|
+
);
|
|
79
|
+
if (pathSources.length === 0 && nameSources.length === 0) {
|
|
80
|
+
throw new Error('loadSsmConfig requires at least one of "paths" or "names".');
|
|
81
|
+
}
|
|
82
|
+
const region = options.region ?? process.env.AWS_REGION ?? "ap-northeast-2";
|
|
83
|
+
const client = options.client ?? new SSMClient({ region });
|
|
84
|
+
const withDecryption = options.withDecryption ?? true;
|
|
85
|
+
const setEnv = options.setEnv ?? true;
|
|
86
|
+
const overrideExisting = options.overrideExisting ?? true;
|
|
87
|
+
try {
|
|
88
|
+
const values = {};
|
|
89
|
+
for (const source of pathSources) {
|
|
90
|
+
Object.assign(values, await fetchByPath(client, source, withDecryption));
|
|
91
|
+
}
|
|
92
|
+
if (nameSources.length > 0) {
|
|
93
|
+
Object.assign(values, await fetchByNames(client, nameSources, withDecryption, logger));
|
|
94
|
+
}
|
|
95
|
+
if (setEnv) {
|
|
96
|
+
for (const [key, value] of Object.entries(values)) {
|
|
97
|
+
if (overrideExisting || process.env[key] === void 0) {
|
|
98
|
+
process.env[key] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
logger?.log(`Loaded ${Object.keys(values).length} parameter(s) from SSM`);
|
|
103
|
+
return { values, count: Object.keys(values).length };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
logger?.error(`Failed to load SSM config: ${message}`);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
defaultLogger,
|
|
112
|
+
loadSsmConfig
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/loader.ts","../src/logger.ts"],"sourcesContent":["import { GetParametersByPathCommand, GetParametersCommand, SSMClient } from '@aws-sdk/client-ssm';\nimport { defaultLogger } from './logger.js';\nimport type {\n ByNameSource,\n ByPathSource,\n LoadSsmConfigOptions,\n LoadSsmConfigResult,\n SimpleLogger,\n} from './types.js';\n\nconst MAX_NAMES_PER_REQUEST = 10;\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < items.length; i += size) {\n chunks.push(items.slice(i, i + size));\n }\n return chunks;\n}\n\nfunction deriveKeyFromPath(paramName: string, source: ByPathSource): string {\n let key = source.stripPrefix === false ? paramName : paramName.replace(source.path, '');\n if (key.startsWith('/')) key = key.slice(1);\n return source.transformKey ? source.transformKey(key) : key;\n}\n\nfunction deriveKeyFromName(paramName: string, source: ByNameSource): string {\n if (source.envKey) return source.envKey;\n const segments = paramName.split('/').filter(Boolean);\n return segments[segments.length - 1] ?? paramName;\n}\n\nasync function fetchByPath(\n client: SSMClient,\n source: ByPathSource,\n withDecryption: boolean,\n): Promise<Record<string, string>> {\n const result: Record<string, string> = {};\n let nextToken: string | undefined;\n\n do {\n const response = await client.send(\n new GetParametersByPathCommand({\n Path: source.path,\n WithDecryption: withDecryption,\n Recursive: source.recursive ?? false,\n NextToken: nextToken,\n }),\n );\n\n for (const param of response.Parameters ?? []) {\n if (!param.Name || param.Value === undefined) continue;\n const key = deriveKeyFromPath(param.Name, source);\n if (key) result[key] = param.Value;\n }\n\n nextToken = response.NextToken;\n } while (nextToken);\n\n return result;\n}\n\nasync function fetchByNames(\n client: SSMClient,\n sources: ByNameSource[],\n withDecryption: boolean,\n logger: SimpleLogger | null,\n): Promise<Record<string, string>> {\n const result: Record<string, string> = {};\n const byName = new Map(sources.map((source) => [source.name, source]));\n\n for (const names of chunk(sources.map((source) => source.name), MAX_NAMES_PER_REQUEST)) {\n const response = await client.send(\n new GetParametersCommand({ Names: names, WithDecryption: withDecryption }),\n );\n\n for (const param of response.Parameters ?? []) {\n if (!param.Name || param.Value === undefined) continue;\n const source = byName.get(param.Name);\n if (!source) continue;\n result[deriveKeyFromName(param.Name, source)] = param.Value;\n }\n\n if (response.InvalidParameters?.length) {\n logger?.error(`Invalid SSM parameter name(s): ${response.InvalidParameters.join(', ')}`);\n }\n }\n\n return result;\n}\n\n/**\n * Load AWS SSM Parameter Store values, optionally assigning them onto process.env.\n *\n * Supports two source patterns, usable together:\n * - `paths`: fetch everything under a path prefix (GetParametersByPath)\n * - `names`: fetch one or more explicit parameter names (GetParameters)\n */\nexport async function loadSsmConfig(\n options: LoadSsmConfigOptions = {},\n): Promise<LoadSsmConfigResult> {\n const logger = options.logger === null ? null : options.logger ?? defaultLogger;\n\n if (options.skip) {\n logger?.log('Skipped loading SSM parameters (skip=true)');\n return { values: {}, count: 0 };\n }\n\n const pathSources: ByPathSource[] = options.paths\n ? Array.isArray(options.paths)\n ? options.paths\n : [options.paths]\n : [];\n\n const nameSources: ByNameSource[] = (options.names ?? []).map((entry) =>\n typeof entry === 'string' ? { name: entry } : entry,\n );\n\n if (pathSources.length === 0 && nameSources.length === 0) {\n throw new Error('loadSsmConfig requires at least one of \"paths\" or \"names\".');\n }\n\n const region = options.region ?? process.env.AWS_REGION ?? 'ap-northeast-2';\n const client = options.client ?? new SSMClient({ region });\n const withDecryption = options.withDecryption ?? true;\n const setEnv = options.setEnv ?? true;\n const overrideExisting = options.overrideExisting ?? true;\n\n try {\n const values: Record<string, string> = {};\n\n for (const source of pathSources) {\n Object.assign(values, await fetchByPath(client, source, withDecryption));\n }\n\n if (nameSources.length > 0) {\n Object.assign(values, await fetchByNames(client, nameSources, withDecryption, logger));\n }\n\n if (setEnv) {\n for (const [key, value] of Object.entries(values)) {\n if (overrideExisting || process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n\n logger?.log(`Loaded ${Object.keys(values).length} parameter(s) from SSM`);\n\n return { values, count: Object.keys(values).length };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n logger?.error(`Failed to load SSM config: ${message}`);\n throw error;\n }\n}\n","import type { SimpleLogger } from './types.js';\n\nexport const defaultLogger: SimpleLogger = {\n log: (message) => console.log(`[aws-ssm-loader] ${message}`),\n error: (message) => console.error(`[aws-ssm-loader] ${message}`),\n};\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loftisland-oss/ssm-loader",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Load AWS Systems Manager Parameter Store values into process.env — by path prefix or by explicit parameter names. Works from both JS and TS, CJS and ESM.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"aws",
|
|
7
|
+
"ssm",
|
|
8
|
+
"parameter-store",
|
|
9
|
+
"env",
|
|
10
|
+
"config",
|
|
11
|
+
"dotenv",
|
|
12
|
+
"systems-manager"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"dev": "tsup --watch",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "tsc --noEmit",
|
|
47
|
+
"prepublishOnly": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@aws-sdk/client-ssm": "^3.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26.4.1",
|
|
54
|
+
"tsup": "^8.3.5",
|
|
55
|
+
"typescript": "^5.6.3",
|
|
56
|
+
"vitest": "^2.1.5"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|