aws-secrets-manager-env-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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 steel8rat
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,143 @@
1
+ # aws-secrets-manager-env-loader
2
+
3
+ Load a map of AWS Secrets Manager secrets into `process.env` at boot.
4
+
5
+ - **Env wins.** An env var already set to a non-empty value is kept, not fetched.
6
+ Local dev supplies values via the environment and never calls AWS.
7
+ - **Fail fast.** Missing secrets are fetched in parallel; if any fetch or JSON
8
+ parse fails, the call rejects and `process.env` is left unmodified. No
9
+ partial-load mode.
10
+ - **JSON blobs.** A value is either a secret ID (whole `SecretString`) or
11
+ `{ secretId, key }` (one property of a JSON secret). Env vars sharing a
12
+ `secretId` fetch and parse it once.
13
+ - **Peer dependency.** `@aws-sdk/client-secrets-manager` is not bundled; you
14
+ choose and upgrade the version. Same convention as
15
+ [`@aws-lambda-powertools/parameters`][powertools].
16
+
17
+ [powertools]: https://www.npmjs.com/package/@aws-lambda-powertools/parameters
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install aws-secrets-manager-env-loader @aws-sdk/client-secrets-manager
23
+ ```
24
+
25
+ The peer dependency (`>=3.0.0`) is only loaded when a fetch actually happens, so
26
+ it can be skipped if every mapped var is always pre-set in the environment.
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import { loadSecrets } from "aws-secrets-manager-env-loader";
32
+
33
+ await loadSecrets({
34
+ secrets: {
35
+ CHAT_BOT_TOKEN: "example/internal/chat/bot-token/sample-service",
36
+ SERVICE_API_KEY: "example/internal/service/api-key/sample-service",
37
+ },
38
+ });
39
+ ```
40
+
41
+ Call it once and `await` it before the rest of the app reads those vars.
42
+
43
+ ### Ordering
44
+
45
+ The loader reads `process.env` only — it does not parse `.env` files. For "env
46
+ wins" to apply locally, the values must be in `process.env` *before* the call.
47
+ Load them first:
48
+
49
+ ```sh
50
+ node --env-file=.env dist/app.js
51
+ ```
52
+
53
+ ```ts
54
+ import "dotenv/config"; // before importing anything that calls loadSecrets
55
+ ```
56
+
57
+ Otherwise the var looks missing and the loader tries to fetch it (and throws
58
+ without credentials).
59
+
60
+ ## API
61
+
62
+ ```ts
63
+ function loadSecrets(options: LoadSecretsOptions): Promise<void>;
64
+
65
+ interface LoadSecretsOptions {
66
+ secrets: Record<string, SecretSource>;
67
+ /**
68
+ * Pre-constructed client, for control over region, credentials, retry, or SDK
69
+ * version. Default: constructed lazily on first fetch (region from
70
+ * AWS_REGION, else us-east-1; default credential chain).
71
+ */
72
+ client?: SecretsManagerClient | SecretsManagerClientLike;
73
+ /** Progress messages. Default: console.log. Pass `() => {}` to silence. */
74
+ onLog?: (message: string) => void;
75
+ }
76
+
77
+ type SecretSource =
78
+ | string // secret ID; whole SecretString
79
+ | { secretId: string; key?: string }; // one key of a JSON secret
80
+ ```
81
+
82
+ Supplying a client:
83
+
84
+ ```ts
85
+ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
86
+
87
+ await loadSecrets({
88
+ secrets: { SERVICE_API_KEY: "example/internal/service/api-key/sample-service" },
89
+ client: new SecretsManagerClient({ region: "eu-west-1", maxAttempts: 5 }),
90
+ });
91
+ ```
92
+
93
+ ### JSON-blob secrets
94
+
95
+ For a secret holding a JSON object (the console's key/value editor, RDS-managed
96
+ secrets), point multiple vars at keys of one `secretId`:
97
+
98
+ ```ts
99
+ await loadSecrets({
100
+ secrets: {
101
+ DB_USERNAME: { secretId: "example/db/creds", key: "username" },
102
+ DB_PASSWORD: { secretId: "example/db/creds", key: "password" },
103
+ DB_PORT: { secretId: "example/db/creds", key: "port" }, // 5432 -> "5432"
104
+ SERVICE_API_KEY: "example/internal/service/api-key/sample-service",
105
+ },
106
+ });
107
+ ```
108
+
109
+ Non-string values are `JSON.stringify`-d before writing.
110
+
111
+ ### Semantics
112
+
113
+ | Case | Behavior |
114
+ | --- | --- |
115
+ | Env var set to a non-empty value | Kept; not fetched or parsed |
116
+ | Env var unset or empty string | Fetched and written |
117
+ | Several env vars, one `secretId` | Fetched once, parsed once, in parallel with other secrets |
118
+ | Any fetch or JSON parse fails | Call rejects; `process.env` unmodified |
119
+ | Secret has no `SecretString` | `MissingSecretStringError` |
120
+ | Keyed source, key not in the JSON object | `SecretKeyNotFoundError` |
121
+ | Keyed source, `SecretString` not a JSON object | `SecretJsonParseError` |
122
+ | Default client needed, peer dep not installed | `SdkNotInstalledError` |
123
+ | Nothing missing | Returns immediately; SDK not loaded |
124
+
125
+ Exports: `loadSecrets`, `MissingSecretStringError`, `SecretJsonParseError`,
126
+ `SecretKeyNotFoundError`, `SdkNotInstalledError`, and the types
127
+ `LoadSecretsOptions`, `SecretSource`, `SecretsManagerClientLike`.
128
+
129
+ ## Development
130
+
131
+ ```sh
132
+ npm install
133
+ npm run typecheck
134
+ npm test # node --test
135
+ npm run build # tsup -> dist/ (ESM + CJS + .d.ts)
136
+ ```
137
+
138
+ Tests use Node's runner with native type stripping (Node >=22.18 / >=23.6). The
139
+ published package is compiled JavaScript and requires Node >=18.
140
+
141
+ ## License
142
+
143
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ MissingSecretStringError: () => MissingSecretStringError,
34
+ SdkNotInstalledError: () => SdkNotInstalledError,
35
+ SecretJsonParseError: () => SecretJsonParseError,
36
+ SecretKeyNotFoundError: () => SecretKeyNotFoundError,
37
+ loadSecrets: () => loadSecrets
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/sdk.ts
42
+ var SdkNotInstalledError = class extends Error {
43
+ constructor(cause) {
44
+ super(
45
+ `aws-secrets-manager-env-loader: could not load '@aws-sdk/client-secrets-manager'. It is a peer dependency - install it in your app to use the default client, or pass an explicit \`client\`. (If every mapped env var is already set, no fetch happens and the SDK is never needed.) Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`,
46
+ { cause }
47
+ );
48
+ this.name = "SdkNotInstalledError";
49
+ }
50
+ };
51
+ var realImport = () => import("@aws-sdk/client-secrets-manager");
52
+ var importSdk = realImport;
53
+ var sdkPromise;
54
+ var defaultClientPromise;
55
+ async function loadSdk() {
56
+ if (!sdkPromise) {
57
+ sdkPromise = importSdk().catch((err) => {
58
+ sdkPromise = void 0;
59
+ throw new SdkNotInstalledError(err);
60
+ });
61
+ }
62
+ return sdkPromise;
63
+ }
64
+ async function getDefaultClient() {
65
+ if (!defaultClientPromise) {
66
+ defaultClientPromise = (async () => {
67
+ const { SecretsManagerClient } = await loadSdk();
68
+ return new SecretsManagerClient({
69
+ region: process.env.AWS_REGION ?? "us-east-1"
70
+ });
71
+ })().catch((err) => {
72
+ defaultClientPromise = void 0;
73
+ throw err;
74
+ });
75
+ }
76
+ return defaultClientPromise;
77
+ }
78
+
79
+ // src/index.ts
80
+ var MissingSecretStringError = class extends Error {
81
+ secretId;
82
+ constructor(secretId) {
83
+ super(
84
+ `aws-secrets-manager-env-loader: secret "${secretId}" has no SecretString`
85
+ );
86
+ this.name = "MissingSecretStringError";
87
+ this.secretId = secretId;
88
+ }
89
+ };
90
+ var SecretJsonParseError = class extends Error {
91
+ secretId;
92
+ constructor(secretId, cause) {
93
+ super(
94
+ `aws-secrets-manager-env-loader: secret "${secretId}" is not a JSON object`,
95
+ { cause }
96
+ );
97
+ this.name = "SecretJsonParseError";
98
+ this.secretId = secretId;
99
+ }
100
+ };
101
+ var SecretKeyNotFoundError = class extends Error {
102
+ secretId;
103
+ key;
104
+ envVar;
105
+ constructor(secretId, key, envVar) {
106
+ super(
107
+ `aws-secrets-manager-env-loader: key "${key}" (for env var ${envVar}) is not present in JSON secret "${secretId}"`
108
+ );
109
+ this.name = "SecretKeyNotFoundError";
110
+ this.secretId = secretId;
111
+ this.key = key;
112
+ this.envVar = envVar;
113
+ }
114
+ };
115
+ async function loadSecrets(options) {
116
+ const { secrets, onLog = defaultLog } = options;
117
+ const missing = Object.keys(secrets).filter(
118
+ (envVar) => !process.env[envVar]
119
+ );
120
+ if (missing.length === 0) {
121
+ onLog("[secrets] all secrets already in env, skipping fetch");
122
+ return;
123
+ }
124
+ onLog(`[secrets] fetching from Secrets Manager: ${missing.join(", ")}`);
125
+ const { GetSecretValueCommand } = await loadSdk();
126
+ const client = options.client ?? await getDefaultClient();
127
+ const sources = new Map(
128
+ missing.map((envVar) => [envVar, normalize(secrets[envVar])])
129
+ );
130
+ const distinctIds = [
131
+ ...new Set([...sources.values()].map((source) => source.secretId))
132
+ ];
133
+ const rawById = /* @__PURE__ */ new Map();
134
+ await Promise.all(
135
+ distinctIds.map(async (secretId) => {
136
+ const res = await client.send(
137
+ new GetSecretValueCommand({ SecretId: secretId })
138
+ );
139
+ if (!res.SecretString) throw new MissingSecretStringError(secretId);
140
+ rawById.set(secretId, res.SecretString);
141
+ })
142
+ );
143
+ const jsonById = /* @__PURE__ */ new Map();
144
+ const parseJsonSecret = (secretId) => {
145
+ const cached = jsonById.get(secretId);
146
+ if (cached) return cached;
147
+ let value;
148
+ try {
149
+ value = JSON.parse(rawById.get(secretId));
150
+ } catch (err) {
151
+ throw new SecretJsonParseError(secretId, err);
152
+ }
153
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
154
+ throw new SecretJsonParseError(
155
+ secretId,
156
+ new Error("SecretString is not a JSON object")
157
+ );
158
+ }
159
+ const parsed = value;
160
+ jsonById.set(secretId, parsed);
161
+ return parsed;
162
+ };
163
+ const resolved = missing.map((envVar) => {
164
+ const { secretId, key } = sources.get(envVar);
165
+ if (key === void 0) {
166
+ return [envVar, rawById.get(secretId)];
167
+ }
168
+ const parsed = parseJsonSecret(secretId);
169
+ if (!(key in parsed)) {
170
+ throw new SecretKeyNotFoundError(secretId, key, envVar);
171
+ }
172
+ const value = parsed[key];
173
+ return [
174
+ envVar,
175
+ typeof value === "string" ? value : JSON.stringify(value)
176
+ ];
177
+ });
178
+ for (const [envVar, value] of resolved) {
179
+ process.env[envVar] = value;
180
+ }
181
+ onLog(`[secrets] provisioned into env: ${missing.join(", ")}`);
182
+ }
183
+ function normalize(source) {
184
+ return typeof source === "string" ? { secretId: source } : source;
185
+ }
186
+ function defaultLog(message) {
187
+ console.log(message);
188
+ }
189
+ // Annotate the CommonJS export names for ESM import in node:
190
+ 0 && (module.exports = {
191
+ MissingSecretStringError,
192
+ SdkNotInstalledError,
193
+ SecretJsonParseError,
194
+ SecretKeyNotFoundError,
195
+ loadSecrets
196
+ });
197
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/sdk.ts"],"sourcesContent":["import type { SecretsManagerClient } from \"@aws-sdk/client-secrets-manager\";\nimport { getDefaultClient, loadSdk, SdkNotInstalledError } from \"./sdk.ts\";\n\nexport { SdkNotInstalledError };\n\n/**\n * The minimal shape this package needs from a Secrets Manager client: a `send`\n * method that resolves a `GetSecretValueCommand` to something carrying a\n * `SecretString`. The real `SecretsManagerClient` satisfies this, and so does\n * any stub with a compatible `send` (handy for tests).\n */\nexport interface SecretsManagerClientLike {\n send(command: unknown): Promise<{ SecretString?: string }>;\n}\n\n/**\n * How to source one env var's value:\n *\n * - a plain **string** is a Secrets Manager secret ID whose entire\n * `SecretString` becomes the value;\n * - **`{ secretId, key }`** fetches `secretId`, parses its `SecretString` as a\n * JSON object, and writes property `key` (non-string values are\n * `JSON.stringify`-d). Several env vars may read different keys of the same\n * `secretId` - it is fetched and parsed exactly once.\n */\nexport type SecretSource =\n | string\n | {\n /** Secrets Manager secret ID. */\n secretId: string;\n /**\n * Property to read from the secret's JSON object. Omit to use the whole\n * `SecretString` verbatim.\n */\n key?: string;\n };\n\nexport interface LoadSecretsOptions {\n /**\n * Map of env var name -> where to get its value. See {@link SecretSource}.\n *\n * @example\n * {\n * // whole SecretString\n * SERVICE_API_KEY: \"example/internal/service/api-key/sample-service\",\n * // two keys out of one JSON secret, fetched once\n * DB_USERNAME: { secretId: \"example/db/creds\", key: \"username\" },\n * DB_PASSWORD: { secretId: \"example/db/creds\", key: \"password\" },\n * }\n */\n secrets: Record<string, SecretSource>;\n /**\n * Pre-constructed client. Supply this to control region, credentials,\n * retry/backoff, or the exact SDK version in play. If omitted, a\n * `SecretsManagerClient` is constructed lazily on first fetch (region from\n * `AWS_REGION`, else `us-east-1`; default credential chain).\n */\n client?: SecretsManagerClient | SecretsManagerClientLike;\n /**\n * Called with human-readable progress messages. Defaults to `console.log`.\n * Pass `() => {}` to silence, or route through your own logger.\n */\n onLog?: (message: string) => void;\n}\n\n/** Thrown when a fetched secret exists but carries no `SecretString`. */\nexport class MissingSecretStringError extends Error {\n readonly secretId: string;\n constructor(secretId: string) {\n super(\n `aws-secrets-manager-env-loader: secret \"${secretId}\" has no SecretString`,\n );\n this.name = \"MissingSecretStringError\";\n this.secretId = secretId;\n }\n}\n\n/** Thrown when a keyed source points at a secret whose `SecretString` is not a JSON object. */\nexport class SecretJsonParseError extends Error {\n readonly secretId: string;\n constructor(secretId: string, cause: unknown) {\n super(\n `aws-secrets-manager-env-loader: secret \"${secretId}\" is not a JSON object`,\n { cause },\n );\n this.name = \"SecretJsonParseError\";\n this.secretId = secretId;\n }\n}\n\n/** Thrown when a keyed source names a property missing from the secret's JSON object. */\nexport class SecretKeyNotFoundError extends Error {\n readonly secretId: string;\n readonly key: string;\n readonly envVar: string;\n constructor(secretId: string, key: string, envVar: string) {\n super(\n `aws-secrets-manager-env-loader: key \"${key}\" (for env var ${envVar}) is not present in JSON secret \"${secretId}\"`,\n );\n this.name = \"SecretKeyNotFoundError\";\n this.secretId = secretId;\n this.key = key;\n this.envVar = envVar;\n }\n}\n\n/**\n * Load the mapped secrets into `process.env`.\n *\n * - Env vars already set to a non-empty value are kept and never fetched. Only\n * `process.env` is consulted; `.env` files must be loaded beforehand.\n * - Each distinct secret ID is fetched once, in parallel, even when several env\n * vars read different keys from it.\n * - `Promise.all` semantics: any failed fetch or JSON parse rejects the whole\n * call and leaves `process.env` unmodified. No partial-load mode.\n */\nexport async function loadSecrets(options: LoadSecretsOptions): Promise<void> {\n const { secrets, onLog = defaultLog } = options;\n\n const missing = Object.keys(secrets).filter(\n (envVar) => !process.env[envVar],\n );\n if (missing.length === 0) {\n onLog(\"[secrets] all secrets already in env, skipping fetch\");\n return;\n }\n\n onLog(`[secrets] fetching from Secrets Manager: ${missing.join(\", \")}`);\n\n const { GetSecretValueCommand } = await loadSdk();\n const client = options.client ?? (await getDefaultClient());\n\n const sources = new Map(\n missing.map((envVar) => [envVar, normalize(secrets[envVar]!)] as const),\n );\n const distinctIds = [\n ...new Set([...sources.values()].map((source) => source.secretId)),\n ];\n\n const rawById = new Map<string, string>();\n await Promise.all(\n distinctIds.map(async (secretId) => {\n const res = await client.send(\n new GetSecretValueCommand({ SecretId: secretId }),\n );\n if (!res.SecretString) throw new MissingSecretStringError(secretId);\n rawById.set(secretId, res.SecretString);\n }),\n );\n\n const jsonById = new Map<string, Record<string, unknown>>();\n const parseJsonSecret = (secretId: string): Record<string, unknown> => {\n const cached = jsonById.get(secretId);\n if (cached) return cached;\n let value: unknown;\n try {\n value = JSON.parse(rawById.get(secretId)!);\n } catch (err) {\n throw new SecretJsonParseError(secretId, err);\n }\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new SecretJsonParseError(\n secretId,\n new Error(\"SecretString is not a JSON object\"),\n );\n }\n const parsed = value as Record<string, unknown>;\n jsonById.set(secretId, parsed);\n return parsed;\n };\n\n const resolved = missing.map((envVar) => {\n const { secretId, key } = sources.get(envVar)!;\n if (key === undefined) {\n return [envVar, rawById.get(secretId)!] as const;\n }\n const parsed = parseJsonSecret(secretId);\n if (!(key in parsed)) {\n throw new SecretKeyNotFoundError(secretId, key, envVar);\n }\n const value = parsed[key];\n return [\n envVar,\n typeof value === \"string\" ? value : JSON.stringify(value),\n ] as const;\n });\n\n for (const [envVar, value] of resolved) {\n process.env[envVar] = value;\n }\n onLog(`[secrets] provisioned into env: ${missing.join(\", \")}`);\n}\n\nfunction normalize(source: SecretSource): { secretId: string; key?: string } {\n return typeof source === \"string\" ? { secretId: source } : source;\n}\n\nfunction defaultLog(message: string): void {\n console.log(message);\n}\n","import type { SecretsManagerClient } from \"@aws-sdk/client-secrets-manager\";\n\n/** Thrown when the AWS SDK client peer dependency cannot be resolved at runtime. */\nexport class SdkNotInstalledError extends Error {\n constructor(cause: unknown) {\n super(\n \"aws-secrets-manager-env-loader: could not load '@aws-sdk/client-secrets-manager'. \" +\n \"It is a peer dependency - install it in your app to use the default client, \" +\n \"or pass an explicit `client`. (If every mapped env var is already set, no fetch \" +\n \"happens and the SDK is never needed.) \" +\n `Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`,\n { cause },\n );\n this.name = \"SdkNotInstalledError\";\n }\n}\n\nexport type SecretsManagerSdk = typeof import(\"@aws-sdk/client-secrets-manager\");\n\nconst realImport = (): Promise<SecretsManagerSdk> =>\n import(\"@aws-sdk/client-secrets-manager\");\n\nlet importSdk: () => Promise<SecretsManagerSdk> = realImport;\nlet sdkPromise: Promise<SecretsManagerSdk> | undefined;\nlet defaultClientPromise: Promise<SecretsManagerClient> | undefined;\n\n/**\n * Load `@aws-sdk/client-secrets-manager` lazily and only once. Kept out of\n * module scope so that importing this package never requires the peer\n * dependency - only an actual fetch does.\n */\nexport async function loadSdk(): Promise<SecretsManagerSdk> {\n if (!sdkPromise) {\n sdkPromise = importSdk().catch((err: unknown) => {\n sdkPromise = undefined;\n throw new SdkNotInstalledError(err);\n });\n }\n return sdkPromise;\n}\n\n/**\n * A `SecretsManagerClient` constructed lazily on first use, with its region from\n * `AWS_REGION` (falling back to `us-east-1`) and default-chain credentials.\n */\nexport async function getDefaultClient(): Promise<SecretsManagerClient> {\n if (!defaultClientPromise) {\n defaultClientPromise = (async () => {\n const { SecretsManagerClient } = await loadSdk();\n return new SecretsManagerClient({\n region: process.env.AWS_REGION ?? \"us-east-1\",\n });\n })().catch((err: unknown) => {\n defaultClientPromise = undefined;\n throw err;\n });\n }\n return defaultClientPromise;\n}\n\n/**\n * Test seam. Deliberately NOT re-exported from `index.ts`, so it never reaches\n * the published entry point or type definitions. Swaps how the AWS SDK is\n * imported and clears the memoized SDK + default client. Pass `undefined` to\n * restore the real dynamic import.\n *\n * @internal\n */\nexport function __setSdkImporterForTests(\n fn: (() => Promise<SecretsManagerSdk>) | undefined,\n): void {\n importSdk = fn ?? realImport;\n sdkPromise = undefined;\n defaultClientPromise = undefined;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,94 @@
1
+ import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
2
+
3
+ /** Thrown when the AWS SDK client peer dependency cannot be resolved at runtime. */
4
+ declare class SdkNotInstalledError extends Error {
5
+ constructor(cause: unknown);
6
+ }
7
+
8
+ /**
9
+ * The minimal shape this package needs from a Secrets Manager client: a `send`
10
+ * method that resolves a `GetSecretValueCommand` to something carrying a
11
+ * `SecretString`. The real `SecretsManagerClient` satisfies this, and so does
12
+ * any stub with a compatible `send` (handy for tests).
13
+ */
14
+ interface SecretsManagerClientLike {
15
+ send(command: unknown): Promise<{
16
+ SecretString?: string;
17
+ }>;
18
+ }
19
+ /**
20
+ * How to source one env var's value:
21
+ *
22
+ * - a plain **string** is a Secrets Manager secret ID whose entire
23
+ * `SecretString` becomes the value;
24
+ * - **`{ secretId, key }`** fetches `secretId`, parses its `SecretString` as a
25
+ * JSON object, and writes property `key` (non-string values are
26
+ * `JSON.stringify`-d). Several env vars may read different keys of the same
27
+ * `secretId` - it is fetched and parsed exactly once.
28
+ */
29
+ type SecretSource = string | {
30
+ /** Secrets Manager secret ID. */
31
+ secretId: string;
32
+ /**
33
+ * Property to read from the secret's JSON object. Omit to use the whole
34
+ * `SecretString` verbatim.
35
+ */
36
+ key?: string;
37
+ };
38
+ interface LoadSecretsOptions {
39
+ /**
40
+ * Map of env var name -> where to get its value. See {@link SecretSource}.
41
+ *
42
+ * @example
43
+ * {
44
+ * // whole SecretString
45
+ * SERVICE_API_KEY: "example/internal/service/api-key/sample-service",
46
+ * // two keys out of one JSON secret, fetched once
47
+ * DB_USERNAME: { secretId: "example/db/creds", key: "username" },
48
+ * DB_PASSWORD: { secretId: "example/db/creds", key: "password" },
49
+ * }
50
+ */
51
+ secrets: Record<string, SecretSource>;
52
+ /**
53
+ * Pre-constructed client. Supply this to control region, credentials,
54
+ * retry/backoff, or the exact SDK version in play. If omitted, a
55
+ * `SecretsManagerClient` is constructed lazily on first fetch (region from
56
+ * `AWS_REGION`, else `us-east-1`; default credential chain).
57
+ */
58
+ client?: SecretsManagerClient | SecretsManagerClientLike;
59
+ /**
60
+ * Called with human-readable progress messages. Defaults to `console.log`.
61
+ * Pass `() => {}` to silence, or route through your own logger.
62
+ */
63
+ onLog?: (message: string) => void;
64
+ }
65
+ /** Thrown when a fetched secret exists but carries no `SecretString`. */
66
+ declare class MissingSecretStringError extends Error {
67
+ readonly secretId: string;
68
+ constructor(secretId: string);
69
+ }
70
+ /** Thrown when a keyed source points at a secret whose `SecretString` is not a JSON object. */
71
+ declare class SecretJsonParseError extends Error {
72
+ readonly secretId: string;
73
+ constructor(secretId: string, cause: unknown);
74
+ }
75
+ /** Thrown when a keyed source names a property missing from the secret's JSON object. */
76
+ declare class SecretKeyNotFoundError extends Error {
77
+ readonly secretId: string;
78
+ readonly key: string;
79
+ readonly envVar: string;
80
+ constructor(secretId: string, key: string, envVar: string);
81
+ }
82
+ /**
83
+ * Load the mapped secrets into `process.env`.
84
+ *
85
+ * - Env vars already set to a non-empty value are kept and never fetched. Only
86
+ * `process.env` is consulted; `.env` files must be loaded beforehand.
87
+ * - Each distinct secret ID is fetched once, in parallel, even when several env
88
+ * vars read different keys from it.
89
+ * - `Promise.all` semantics: any failed fetch or JSON parse rejects the whole
90
+ * call and leaves `process.env` unmodified. No partial-load mode.
91
+ */
92
+ declare function loadSecrets(options: LoadSecretsOptions): Promise<void>;
93
+
94
+ export { type LoadSecretsOptions, MissingSecretStringError, SdkNotInstalledError, SecretJsonParseError, SecretKeyNotFoundError, type SecretSource, type SecretsManagerClientLike, loadSecrets };
@@ -0,0 +1,94 @@
1
+ import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
2
+
3
+ /** Thrown when the AWS SDK client peer dependency cannot be resolved at runtime. */
4
+ declare class SdkNotInstalledError extends Error {
5
+ constructor(cause: unknown);
6
+ }
7
+
8
+ /**
9
+ * The minimal shape this package needs from a Secrets Manager client: a `send`
10
+ * method that resolves a `GetSecretValueCommand` to something carrying a
11
+ * `SecretString`. The real `SecretsManagerClient` satisfies this, and so does
12
+ * any stub with a compatible `send` (handy for tests).
13
+ */
14
+ interface SecretsManagerClientLike {
15
+ send(command: unknown): Promise<{
16
+ SecretString?: string;
17
+ }>;
18
+ }
19
+ /**
20
+ * How to source one env var's value:
21
+ *
22
+ * - a plain **string** is a Secrets Manager secret ID whose entire
23
+ * `SecretString` becomes the value;
24
+ * - **`{ secretId, key }`** fetches `secretId`, parses its `SecretString` as a
25
+ * JSON object, and writes property `key` (non-string values are
26
+ * `JSON.stringify`-d). Several env vars may read different keys of the same
27
+ * `secretId` - it is fetched and parsed exactly once.
28
+ */
29
+ type SecretSource = string | {
30
+ /** Secrets Manager secret ID. */
31
+ secretId: string;
32
+ /**
33
+ * Property to read from the secret's JSON object. Omit to use the whole
34
+ * `SecretString` verbatim.
35
+ */
36
+ key?: string;
37
+ };
38
+ interface LoadSecretsOptions {
39
+ /**
40
+ * Map of env var name -> where to get its value. See {@link SecretSource}.
41
+ *
42
+ * @example
43
+ * {
44
+ * // whole SecretString
45
+ * SERVICE_API_KEY: "example/internal/service/api-key/sample-service",
46
+ * // two keys out of one JSON secret, fetched once
47
+ * DB_USERNAME: { secretId: "example/db/creds", key: "username" },
48
+ * DB_PASSWORD: { secretId: "example/db/creds", key: "password" },
49
+ * }
50
+ */
51
+ secrets: Record<string, SecretSource>;
52
+ /**
53
+ * Pre-constructed client. Supply this to control region, credentials,
54
+ * retry/backoff, or the exact SDK version in play. If omitted, a
55
+ * `SecretsManagerClient` is constructed lazily on first fetch (region from
56
+ * `AWS_REGION`, else `us-east-1`; default credential chain).
57
+ */
58
+ client?: SecretsManagerClient | SecretsManagerClientLike;
59
+ /**
60
+ * Called with human-readable progress messages. Defaults to `console.log`.
61
+ * Pass `() => {}` to silence, or route through your own logger.
62
+ */
63
+ onLog?: (message: string) => void;
64
+ }
65
+ /** Thrown when a fetched secret exists but carries no `SecretString`. */
66
+ declare class MissingSecretStringError extends Error {
67
+ readonly secretId: string;
68
+ constructor(secretId: string);
69
+ }
70
+ /** Thrown when a keyed source points at a secret whose `SecretString` is not a JSON object. */
71
+ declare class SecretJsonParseError extends Error {
72
+ readonly secretId: string;
73
+ constructor(secretId: string, cause: unknown);
74
+ }
75
+ /** Thrown when a keyed source names a property missing from the secret's JSON object. */
76
+ declare class SecretKeyNotFoundError extends Error {
77
+ readonly secretId: string;
78
+ readonly key: string;
79
+ readonly envVar: string;
80
+ constructor(secretId: string, key: string, envVar: string);
81
+ }
82
+ /**
83
+ * Load the mapped secrets into `process.env`.
84
+ *
85
+ * - Env vars already set to a non-empty value are kept and never fetched. Only
86
+ * `process.env` is consulted; `.env` files must be loaded beforehand.
87
+ * - Each distinct secret ID is fetched once, in parallel, even when several env
88
+ * vars read different keys from it.
89
+ * - `Promise.all` semantics: any failed fetch or JSON parse rejects the whole
90
+ * call and leaves `process.env` unmodified. No partial-load mode.
91
+ */
92
+ declare function loadSecrets(options: LoadSecretsOptions): Promise<void>;
93
+
94
+ export { type LoadSecretsOptions, MissingSecretStringError, SdkNotInstalledError, SecretJsonParseError, SecretKeyNotFoundError, type SecretSource, type SecretsManagerClientLike, loadSecrets };
package/dist/index.js ADDED
@@ -0,0 +1,156 @@
1
+ // src/sdk.ts
2
+ var SdkNotInstalledError = class extends Error {
3
+ constructor(cause) {
4
+ super(
5
+ `aws-secrets-manager-env-loader: could not load '@aws-sdk/client-secrets-manager'. It is a peer dependency - install it in your app to use the default client, or pass an explicit \`client\`. (If every mapped env var is already set, no fetch happens and the SDK is never needed.) Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`,
6
+ { cause }
7
+ );
8
+ this.name = "SdkNotInstalledError";
9
+ }
10
+ };
11
+ var realImport = () => import("@aws-sdk/client-secrets-manager");
12
+ var importSdk = realImport;
13
+ var sdkPromise;
14
+ var defaultClientPromise;
15
+ async function loadSdk() {
16
+ if (!sdkPromise) {
17
+ sdkPromise = importSdk().catch((err) => {
18
+ sdkPromise = void 0;
19
+ throw new SdkNotInstalledError(err);
20
+ });
21
+ }
22
+ return sdkPromise;
23
+ }
24
+ async function getDefaultClient() {
25
+ if (!defaultClientPromise) {
26
+ defaultClientPromise = (async () => {
27
+ const { SecretsManagerClient } = await loadSdk();
28
+ return new SecretsManagerClient({
29
+ region: process.env.AWS_REGION ?? "us-east-1"
30
+ });
31
+ })().catch((err) => {
32
+ defaultClientPromise = void 0;
33
+ throw err;
34
+ });
35
+ }
36
+ return defaultClientPromise;
37
+ }
38
+
39
+ // src/index.ts
40
+ var MissingSecretStringError = class extends Error {
41
+ secretId;
42
+ constructor(secretId) {
43
+ super(
44
+ `aws-secrets-manager-env-loader: secret "${secretId}" has no SecretString`
45
+ );
46
+ this.name = "MissingSecretStringError";
47
+ this.secretId = secretId;
48
+ }
49
+ };
50
+ var SecretJsonParseError = class extends Error {
51
+ secretId;
52
+ constructor(secretId, cause) {
53
+ super(
54
+ `aws-secrets-manager-env-loader: secret "${secretId}" is not a JSON object`,
55
+ { cause }
56
+ );
57
+ this.name = "SecretJsonParseError";
58
+ this.secretId = secretId;
59
+ }
60
+ };
61
+ var SecretKeyNotFoundError = class extends Error {
62
+ secretId;
63
+ key;
64
+ envVar;
65
+ constructor(secretId, key, envVar) {
66
+ super(
67
+ `aws-secrets-manager-env-loader: key "${key}" (for env var ${envVar}) is not present in JSON secret "${secretId}"`
68
+ );
69
+ this.name = "SecretKeyNotFoundError";
70
+ this.secretId = secretId;
71
+ this.key = key;
72
+ this.envVar = envVar;
73
+ }
74
+ };
75
+ async function loadSecrets(options) {
76
+ const { secrets, onLog = defaultLog } = options;
77
+ const missing = Object.keys(secrets).filter(
78
+ (envVar) => !process.env[envVar]
79
+ );
80
+ if (missing.length === 0) {
81
+ onLog("[secrets] all secrets already in env, skipping fetch");
82
+ return;
83
+ }
84
+ onLog(`[secrets] fetching from Secrets Manager: ${missing.join(", ")}`);
85
+ const { GetSecretValueCommand } = await loadSdk();
86
+ const client = options.client ?? await getDefaultClient();
87
+ const sources = new Map(
88
+ missing.map((envVar) => [envVar, normalize(secrets[envVar])])
89
+ );
90
+ const distinctIds = [
91
+ ...new Set([...sources.values()].map((source) => source.secretId))
92
+ ];
93
+ const rawById = /* @__PURE__ */ new Map();
94
+ await Promise.all(
95
+ distinctIds.map(async (secretId) => {
96
+ const res = await client.send(
97
+ new GetSecretValueCommand({ SecretId: secretId })
98
+ );
99
+ if (!res.SecretString) throw new MissingSecretStringError(secretId);
100
+ rawById.set(secretId, res.SecretString);
101
+ })
102
+ );
103
+ const jsonById = /* @__PURE__ */ new Map();
104
+ const parseJsonSecret = (secretId) => {
105
+ const cached = jsonById.get(secretId);
106
+ if (cached) return cached;
107
+ let value;
108
+ try {
109
+ value = JSON.parse(rawById.get(secretId));
110
+ } catch (err) {
111
+ throw new SecretJsonParseError(secretId, err);
112
+ }
113
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
114
+ throw new SecretJsonParseError(
115
+ secretId,
116
+ new Error("SecretString is not a JSON object")
117
+ );
118
+ }
119
+ const parsed = value;
120
+ jsonById.set(secretId, parsed);
121
+ return parsed;
122
+ };
123
+ const resolved = missing.map((envVar) => {
124
+ const { secretId, key } = sources.get(envVar);
125
+ if (key === void 0) {
126
+ return [envVar, rawById.get(secretId)];
127
+ }
128
+ const parsed = parseJsonSecret(secretId);
129
+ if (!(key in parsed)) {
130
+ throw new SecretKeyNotFoundError(secretId, key, envVar);
131
+ }
132
+ const value = parsed[key];
133
+ return [
134
+ envVar,
135
+ typeof value === "string" ? value : JSON.stringify(value)
136
+ ];
137
+ });
138
+ for (const [envVar, value] of resolved) {
139
+ process.env[envVar] = value;
140
+ }
141
+ onLog(`[secrets] provisioned into env: ${missing.join(", ")}`);
142
+ }
143
+ function normalize(source) {
144
+ return typeof source === "string" ? { secretId: source } : source;
145
+ }
146
+ function defaultLog(message) {
147
+ console.log(message);
148
+ }
149
+ export {
150
+ MissingSecretStringError,
151
+ SdkNotInstalledError,
152
+ SecretJsonParseError,
153
+ SecretKeyNotFoundError,
154
+ loadSecrets
155
+ };
156
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sdk.ts","../src/index.ts"],"sourcesContent":["import type { SecretsManagerClient } from \"@aws-sdk/client-secrets-manager\";\n\n/** Thrown when the AWS SDK client peer dependency cannot be resolved at runtime. */\nexport class SdkNotInstalledError extends Error {\n constructor(cause: unknown) {\n super(\n \"aws-secrets-manager-env-loader: could not load '@aws-sdk/client-secrets-manager'. \" +\n \"It is a peer dependency - install it in your app to use the default client, \" +\n \"or pass an explicit `client`. (If every mapped env var is already set, no fetch \" +\n \"happens and the SDK is never needed.) \" +\n `Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`,\n { cause },\n );\n this.name = \"SdkNotInstalledError\";\n }\n}\n\nexport type SecretsManagerSdk = typeof import(\"@aws-sdk/client-secrets-manager\");\n\nconst realImport = (): Promise<SecretsManagerSdk> =>\n import(\"@aws-sdk/client-secrets-manager\");\n\nlet importSdk: () => Promise<SecretsManagerSdk> = realImport;\nlet sdkPromise: Promise<SecretsManagerSdk> | undefined;\nlet defaultClientPromise: Promise<SecretsManagerClient> | undefined;\n\n/**\n * Load `@aws-sdk/client-secrets-manager` lazily and only once. Kept out of\n * module scope so that importing this package never requires the peer\n * dependency - only an actual fetch does.\n */\nexport async function loadSdk(): Promise<SecretsManagerSdk> {\n if (!sdkPromise) {\n sdkPromise = importSdk().catch((err: unknown) => {\n sdkPromise = undefined;\n throw new SdkNotInstalledError(err);\n });\n }\n return sdkPromise;\n}\n\n/**\n * A `SecretsManagerClient` constructed lazily on first use, with its region from\n * `AWS_REGION` (falling back to `us-east-1`) and default-chain credentials.\n */\nexport async function getDefaultClient(): Promise<SecretsManagerClient> {\n if (!defaultClientPromise) {\n defaultClientPromise = (async () => {\n const { SecretsManagerClient } = await loadSdk();\n return new SecretsManagerClient({\n region: process.env.AWS_REGION ?? \"us-east-1\",\n });\n })().catch((err: unknown) => {\n defaultClientPromise = undefined;\n throw err;\n });\n }\n return defaultClientPromise;\n}\n\n/**\n * Test seam. Deliberately NOT re-exported from `index.ts`, so it never reaches\n * the published entry point or type definitions. Swaps how the AWS SDK is\n * imported and clears the memoized SDK + default client. Pass `undefined` to\n * restore the real dynamic import.\n *\n * @internal\n */\nexport function __setSdkImporterForTests(\n fn: (() => Promise<SecretsManagerSdk>) | undefined,\n): void {\n importSdk = fn ?? realImport;\n sdkPromise = undefined;\n defaultClientPromise = undefined;\n}\n","import type { SecretsManagerClient } from \"@aws-sdk/client-secrets-manager\";\nimport { getDefaultClient, loadSdk, SdkNotInstalledError } from \"./sdk.ts\";\n\nexport { SdkNotInstalledError };\n\n/**\n * The minimal shape this package needs from a Secrets Manager client: a `send`\n * method that resolves a `GetSecretValueCommand` to something carrying a\n * `SecretString`. The real `SecretsManagerClient` satisfies this, and so does\n * any stub with a compatible `send` (handy for tests).\n */\nexport interface SecretsManagerClientLike {\n send(command: unknown): Promise<{ SecretString?: string }>;\n}\n\n/**\n * How to source one env var's value:\n *\n * - a plain **string** is a Secrets Manager secret ID whose entire\n * `SecretString` becomes the value;\n * - **`{ secretId, key }`** fetches `secretId`, parses its `SecretString` as a\n * JSON object, and writes property `key` (non-string values are\n * `JSON.stringify`-d). Several env vars may read different keys of the same\n * `secretId` - it is fetched and parsed exactly once.\n */\nexport type SecretSource =\n | string\n | {\n /** Secrets Manager secret ID. */\n secretId: string;\n /**\n * Property to read from the secret's JSON object. Omit to use the whole\n * `SecretString` verbatim.\n */\n key?: string;\n };\n\nexport interface LoadSecretsOptions {\n /**\n * Map of env var name -> where to get its value. See {@link SecretSource}.\n *\n * @example\n * {\n * // whole SecretString\n * SERVICE_API_KEY: \"example/internal/service/api-key/sample-service\",\n * // two keys out of one JSON secret, fetched once\n * DB_USERNAME: { secretId: \"example/db/creds\", key: \"username\" },\n * DB_PASSWORD: { secretId: \"example/db/creds\", key: \"password\" },\n * }\n */\n secrets: Record<string, SecretSource>;\n /**\n * Pre-constructed client. Supply this to control region, credentials,\n * retry/backoff, or the exact SDK version in play. If omitted, a\n * `SecretsManagerClient` is constructed lazily on first fetch (region from\n * `AWS_REGION`, else `us-east-1`; default credential chain).\n */\n client?: SecretsManagerClient | SecretsManagerClientLike;\n /**\n * Called with human-readable progress messages. Defaults to `console.log`.\n * Pass `() => {}` to silence, or route through your own logger.\n */\n onLog?: (message: string) => void;\n}\n\n/** Thrown when a fetched secret exists but carries no `SecretString`. */\nexport class MissingSecretStringError extends Error {\n readonly secretId: string;\n constructor(secretId: string) {\n super(\n `aws-secrets-manager-env-loader: secret \"${secretId}\" has no SecretString`,\n );\n this.name = \"MissingSecretStringError\";\n this.secretId = secretId;\n }\n}\n\n/** Thrown when a keyed source points at a secret whose `SecretString` is not a JSON object. */\nexport class SecretJsonParseError extends Error {\n readonly secretId: string;\n constructor(secretId: string, cause: unknown) {\n super(\n `aws-secrets-manager-env-loader: secret \"${secretId}\" is not a JSON object`,\n { cause },\n );\n this.name = \"SecretJsonParseError\";\n this.secretId = secretId;\n }\n}\n\n/** Thrown when a keyed source names a property missing from the secret's JSON object. */\nexport class SecretKeyNotFoundError extends Error {\n readonly secretId: string;\n readonly key: string;\n readonly envVar: string;\n constructor(secretId: string, key: string, envVar: string) {\n super(\n `aws-secrets-manager-env-loader: key \"${key}\" (for env var ${envVar}) is not present in JSON secret \"${secretId}\"`,\n );\n this.name = \"SecretKeyNotFoundError\";\n this.secretId = secretId;\n this.key = key;\n this.envVar = envVar;\n }\n}\n\n/**\n * Load the mapped secrets into `process.env`.\n *\n * - Env vars already set to a non-empty value are kept and never fetched. Only\n * `process.env` is consulted; `.env` files must be loaded beforehand.\n * - Each distinct secret ID is fetched once, in parallel, even when several env\n * vars read different keys from it.\n * - `Promise.all` semantics: any failed fetch or JSON parse rejects the whole\n * call and leaves `process.env` unmodified. No partial-load mode.\n */\nexport async function loadSecrets(options: LoadSecretsOptions): Promise<void> {\n const { secrets, onLog = defaultLog } = options;\n\n const missing = Object.keys(secrets).filter(\n (envVar) => !process.env[envVar],\n );\n if (missing.length === 0) {\n onLog(\"[secrets] all secrets already in env, skipping fetch\");\n return;\n }\n\n onLog(`[secrets] fetching from Secrets Manager: ${missing.join(\", \")}`);\n\n const { GetSecretValueCommand } = await loadSdk();\n const client = options.client ?? (await getDefaultClient());\n\n const sources = new Map(\n missing.map((envVar) => [envVar, normalize(secrets[envVar]!)] as const),\n );\n const distinctIds = [\n ...new Set([...sources.values()].map((source) => source.secretId)),\n ];\n\n const rawById = new Map<string, string>();\n await Promise.all(\n distinctIds.map(async (secretId) => {\n const res = await client.send(\n new GetSecretValueCommand({ SecretId: secretId }),\n );\n if (!res.SecretString) throw new MissingSecretStringError(secretId);\n rawById.set(secretId, res.SecretString);\n }),\n );\n\n const jsonById = new Map<string, Record<string, unknown>>();\n const parseJsonSecret = (secretId: string): Record<string, unknown> => {\n const cached = jsonById.get(secretId);\n if (cached) return cached;\n let value: unknown;\n try {\n value = JSON.parse(rawById.get(secretId)!);\n } catch (err) {\n throw new SecretJsonParseError(secretId, err);\n }\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new SecretJsonParseError(\n secretId,\n new Error(\"SecretString is not a JSON object\"),\n );\n }\n const parsed = value as Record<string, unknown>;\n jsonById.set(secretId, parsed);\n return parsed;\n };\n\n const resolved = missing.map((envVar) => {\n const { secretId, key } = sources.get(envVar)!;\n if (key === undefined) {\n return [envVar, rawById.get(secretId)!] as const;\n }\n const parsed = parseJsonSecret(secretId);\n if (!(key in parsed)) {\n throw new SecretKeyNotFoundError(secretId, key, envVar);\n }\n const value = parsed[key];\n return [\n envVar,\n typeof value === \"string\" ? value : JSON.stringify(value),\n ] as const;\n });\n\n for (const [envVar, value] of resolved) {\n process.env[envVar] = value;\n }\n onLog(`[secrets] provisioned into env: ${missing.join(\", \")}`);\n}\n\nfunction normalize(source: SecretSource): { secretId: string; key?: string } {\n return typeof source === \"string\" ? { secretId: source } : source;\n}\n\nfunction defaultLog(message: string): void {\n console.log(message);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "aws-secrets-manager-env-loader",
3
+ "version": "0.1.0",
4
+ "description": "Load a map of AWS Secrets Manager secrets into process.env at boot. Env vars already set win over the fetch, missing ones are fetched in parallel, and it fails fast. The AWS SDK client is a peer dependency.",
5
+ "keywords": [
6
+ "aws",
7
+ "secrets-manager",
8
+ "secrets",
9
+ "env",
10
+ "environment-variables",
11
+ "process.env",
12
+ "dotenv",
13
+ "bootstrap",
14
+ "config",
15
+ "12-factor"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "steel8rat",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/steel8rat/aws-secrets-manager-env-loader.git"
22
+ },
23
+ "homepage": "https://github.com/steel8rat/aws-secrets-manager-env-loader#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/steel8rat/aws-secrets-manager-env-loader/issues"
26
+ },
27
+ "type": "module",
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/index.d.cts",
39
+ "default": "./dist/index.cjs"
40
+ }
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "node --test",
55
+ "test:watch": "node --test --watch",
56
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
57
+ },
58
+ "peerDependencies": {
59
+ "@aws-sdk/client-secrets-manager": ">=3.0.0"
60
+ },
61
+ "overrides": {
62
+ "esbuild": "^0.28.2"
63
+ },
64
+ "devDependencies": {
65
+ "@aws-sdk/client-secrets-manager": "^3.699.0",
66
+ "@types/node": "^22.20.1",
67
+ "tsup": "^8.3.5",
68
+ "typescript": "^5.7.2"
69
+ }
70
+ }