nestjs-infisical 1.0.3
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/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/infisical.loader.d.ts +8 -0
- package/dist/infisical.loader.js +33 -0
- package/dist/infisical.module.d.ts +6 -0
- package/dist/infisical.module.js +99 -0
- package/dist/infisical.service.d.ts +2 -0
- package/dist/infisical.service.js +31 -0
- package/dist/infisical.types.d.ts +15 -0
- package/dist/infisical.types.js +2 -0
- package/package.json +32 -0
- package/readme.md +219 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./infisical.module"), exports);
|
|
18
|
+
__exportStar(require("./infisical.types"), exports);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadInfisicalSecrets = loadInfisicalSecrets;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const LOG_PREFIX = '[nestjs-infisical]';
|
|
9
|
+
async function loadInfisicalSecrets(options) {
|
|
10
|
+
try {
|
|
11
|
+
const response = await axios_1.default.get(`${options.baseUrl}/api/v3/secrets/raw`, {
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${options.token}`,
|
|
14
|
+
},
|
|
15
|
+
params: {
|
|
16
|
+
projectId: options.projectId,
|
|
17
|
+
environment: options.environment,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const secrets = response.data?.secrets ?? {};
|
|
21
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
22
|
+
if (options.override || process.env[key] === undefined) {
|
|
23
|
+
process.env[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (options.failFast) {
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
console.warn(`${LOG_PREFIX} Failed to load secrets from Infisical. Continuing without secrets.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { InfisicalModuleAsyncOptions, InfisicalModuleOptions } from './infisical.types';
|
|
3
|
+
export declare class InfisicalModule {
|
|
4
|
+
static forRoot(options?: InfisicalModuleOptions): DynamicModule;
|
|
5
|
+
static forRootAsync(options: InfisicalModuleAsyncOptions): DynamicModule;
|
|
6
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
3
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
4
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
5
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
6
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
7
|
+
var _, done = false;
|
|
8
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
9
|
+
var context = {};
|
|
10
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
11
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
12
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
13
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
14
|
+
if (kind === "accessor") {
|
|
15
|
+
if (result === void 0) continue;
|
|
16
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
17
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
18
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
19
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
20
|
+
}
|
|
21
|
+
else if (_ = accept(result)) {
|
|
22
|
+
if (kind === "field") initializers.unshift(_);
|
|
23
|
+
else descriptor[key] = _;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
27
|
+
done = true;
|
|
28
|
+
};
|
|
29
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
30
|
+
var useValue = arguments.length > 2;
|
|
31
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
32
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
33
|
+
}
|
|
34
|
+
return useValue ? value : void 0;
|
|
35
|
+
};
|
|
36
|
+
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
|
|
37
|
+
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
|
|
38
|
+
return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
|
|
39
|
+
};
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.InfisicalModule = void 0;
|
|
42
|
+
const common_1 = require("@nestjs/common");
|
|
43
|
+
const infisical_service_1 = require("./infisical.service");
|
|
44
|
+
let InfisicalModule = (() => {
|
|
45
|
+
let _classDecorators = [(0, common_1.Module)({})];
|
|
46
|
+
let _classDescriptor;
|
|
47
|
+
let _classExtraInitializers = [];
|
|
48
|
+
let _classThis;
|
|
49
|
+
var InfisicalModule = _classThis = class {
|
|
50
|
+
static forRoot(options = {}) {
|
|
51
|
+
const resolved = {
|
|
52
|
+
baseUrl: options.baseUrl ?? process.env.INFISICAL_BASE_URL,
|
|
53
|
+
token: options.token ?? process.env.INFISICAL_TOKEN,
|
|
54
|
+
projectId: options.projectId ?? process.env.INFISICAL_PROJECT_ID,
|
|
55
|
+
environment: options.environment ?? process.env.INFISICAL_ENVIRONMENT,
|
|
56
|
+
dotenv: options.dotenv,
|
|
57
|
+
override: options.override ?? true,
|
|
58
|
+
failFast: options.failFast ?? true,
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
module: InfisicalModule,
|
|
62
|
+
providers: [
|
|
63
|
+
{
|
|
64
|
+
provide: 'INFISICAL_INIT',
|
|
65
|
+
useFactory: async () => {
|
|
66
|
+
await (0, infisical_service_1.initializeInfisical)(resolved);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
static forRootAsync(options) {
|
|
73
|
+
return {
|
|
74
|
+
module: InfisicalModule,
|
|
75
|
+
imports: options.imports,
|
|
76
|
+
providers: [
|
|
77
|
+
{
|
|
78
|
+
provide: 'INFISICAL_INIT',
|
|
79
|
+
inject: options.inject ?? [],
|
|
80
|
+
useFactory: async (...args) => {
|
|
81
|
+
const resolved = await options.useFactory(...args);
|
|
82
|
+
await (0, infisical_service_1.initializeInfisical)(resolved);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
__setFunctionName(_classThis, "InfisicalModule");
|
|
90
|
+
(() => {
|
|
91
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
92
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
93
|
+
InfisicalModule = _classThis = _classDescriptor.value;
|
|
94
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
95
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
96
|
+
})();
|
|
97
|
+
return InfisicalModule = _classThis;
|
|
98
|
+
})();
|
|
99
|
+
exports.InfisicalModule = InfisicalModule;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initializeInfisical = initializeInfisical;
|
|
7
|
+
const infisical_loader_1 = require("./infisical.loader");
|
|
8
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
9
|
+
const LOG_PREFIX = '[nestjs-infisical]';
|
|
10
|
+
async function initializeInfisical(options) {
|
|
11
|
+
const { dotenv: dotenvOptions, baseUrl, token, projectId, environment, override = true, failFast = true, } = options;
|
|
12
|
+
if (dotenvOptions !== false) {
|
|
13
|
+
dotenv_1.default.config(dotenvOptions);
|
|
14
|
+
}
|
|
15
|
+
const provided = [baseUrl, token, projectId, environment].filter(Boolean).length;
|
|
16
|
+
if (provided === 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (provided !== 4) {
|
|
20
|
+
console.warn(`${LOG_PREFIX} Partial Infisical configuration detected. Secrets will not be loaded.`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await (0, infisical_loader_1.loadInfisicalSecrets)({
|
|
24
|
+
baseUrl: baseUrl,
|
|
25
|
+
token: token,
|
|
26
|
+
projectId: projectId,
|
|
27
|
+
environment: environment,
|
|
28
|
+
override,
|
|
29
|
+
failFast,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DotenvConfigOptions } from 'dotenv';
|
|
2
|
+
import { ModuleMetadata } from '@nestjs/common';
|
|
3
|
+
export interface InfisicalModuleOptions {
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
projectId?: string;
|
|
7
|
+
environment?: string;
|
|
8
|
+
dotenv?: DotenvConfigOptions | false;
|
|
9
|
+
override?: boolean;
|
|
10
|
+
failFast?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface InfisicalModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
13
|
+
inject?: any[];
|
|
14
|
+
useFactory: (...args: any[]) => Promise<InfisicalModuleOptions> | InfisicalModuleOptions;
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nestjs-infisical",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "CLI-free Infisical HTTP integration for NestJS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"axios": "^1.6.0",
|
|
17
|
+
"dotenv": "^16.4.0",
|
|
18
|
+
"typescript": "^5.9.3"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@nestjs/common": "^9.0.0 || ^10.0.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.0.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# nestjs-infisical
|
|
2
|
+
|
|
3
|
+
nestjs-infisical is a **CLI-free**, **deterministic**, and **production-safe** Infisical integration for NestJS.
|
|
4
|
+
|
|
5
|
+
It loads secrets **once at application startup** using the **Infisical HTTP API only** and injects them into `process.env`, making it fully compatible with `@nestjs/config`.
|
|
6
|
+
|
|
7
|
+
This package is intentionally boring.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why This Package Exists
|
|
12
|
+
|
|
13
|
+
Most Infisical integrations rely on the Infisical CLI, background agents, or runtime polling.
|
|
14
|
+
|
|
15
|
+
This package exists to provide:
|
|
16
|
+
|
|
17
|
+
- No CLI dependency
|
|
18
|
+
- No background processes
|
|
19
|
+
- No runtime mutation
|
|
20
|
+
- Deterministic startup behavior
|
|
21
|
+
- Works in Docker, CI, and production
|
|
22
|
+
- Pure HTTP API usage
|
|
23
|
+
|
|
24
|
+
If you want secrets loaded **once at boot** and then forgotten, this is for you.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## What This Package Does
|
|
29
|
+
|
|
30
|
+
- Loads environment variables from `.env` using `dotenv` (optional)
|
|
31
|
+
- Fetches secrets from Infisical via HTTP
|
|
32
|
+
- Injects secrets into `process.env`
|
|
33
|
+
- Allows `@nestjs/config` to consume them normally
|
|
34
|
+
- Runs **only during application bootstrap**
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Load Order (IMPORTANT)
|
|
39
|
+
|
|
40
|
+
The load order is **strict and deterministic**:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
dotenv (optional)
|
|
44
|
+
↓
|
|
45
|
+
Infisical HTTP API
|
|
46
|
+
↓
|
|
47
|
+
process.env
|
|
48
|
+
↓
|
|
49
|
+
@nestjs/config
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This guarantees compatibility with `ConfigModule.forRoot()`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install nestjs-infisical
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Node.js **>= 18** is required.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Basic (Sync) Usage
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { Module } from '@nestjs/common';
|
|
70
|
+
import { InfisicalModule } from 'nestjs-infisical';
|
|
71
|
+
|
|
72
|
+
@Module({
|
|
73
|
+
imports: [
|
|
74
|
+
InfisicalModule.forRoot({
|
|
75
|
+
baseUrl: 'https://app.infisical.com',
|
|
76
|
+
token: process.env.INFISICAL_TOKEN,
|
|
77
|
+
projectId: process.env.INFISICAL_PROJECT_ID,
|
|
78
|
+
environment: 'production',
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
export class AppModule {}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Secrets will be injected into `process.env` before the application finishes bootstrapping.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Async Usage (Recommended)
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Module } from '@nestjs/common';
|
|
93
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
94
|
+
import { InfisicalModule } from 'nestjs-infisical';
|
|
95
|
+
|
|
96
|
+
@Module({
|
|
97
|
+
imports: [
|
|
98
|
+
ConfigModule.forRoot(),
|
|
99
|
+
InfisicalModule.forRootAsync({
|
|
100
|
+
imports: [ConfigModule],
|
|
101
|
+
inject: [ConfigService],
|
|
102
|
+
useFactory: (config: ConfigService) => ({
|
|
103
|
+
baseUrl: config.get('INFISICAL_BASE_URL'),
|
|
104
|
+
token: config.get('INFISICAL_TOKEN'),
|
|
105
|
+
projectId: config.get('INFISICAL_PROJECT_ID'),
|
|
106
|
+
environment: config.get('INFISICAL_ENVIRONMENT'),
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
})
|
|
111
|
+
export class AppModule {}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Dotenv Support
|
|
117
|
+
|
|
118
|
+
This package uses the **official `dotenv` package directly**.
|
|
119
|
+
|
|
120
|
+
### Enable dotenv (default)
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
InfisicalModule.forRoot({
|
|
124
|
+
dotenv: {
|
|
125
|
+
path: '.env.local',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Disable dotenv
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
InfisicalModule.forRoot({
|
|
134
|
+
dotenv: false,
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Configuration Options
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
interface InfisicalModuleOptions {
|
|
144
|
+
baseUrl?: string;
|
|
145
|
+
token?: string;
|
|
146
|
+
projectId?: string;
|
|
147
|
+
environment?: string;
|
|
148
|
+
dotenv?: DotenvConfigOptions | false;
|
|
149
|
+
override?: boolean; // default: true
|
|
150
|
+
failFast?: boolean; // default: true
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### override
|
|
155
|
+
|
|
156
|
+
- `true` → Infisical secrets overwrite existing environment variables
|
|
157
|
+
- `false` → Existing environment variables are preserved
|
|
158
|
+
|
|
159
|
+
### failFast
|
|
160
|
+
|
|
161
|
+
- `true` → Application startup fails if Infisical API fails
|
|
162
|
+
- `false` → Logs a warning and continues startup
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Edge Case Behavior
|
|
167
|
+
|
|
168
|
+
### No Infisical Config Provided
|
|
169
|
+
|
|
170
|
+
Secrets are silently skipped. No logs, no errors.
|
|
171
|
+
|
|
172
|
+
### Partial Infisical Config Provided
|
|
173
|
+
|
|
174
|
+
If only some Infisical values are provided:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
[nestjs-infisical] Partial Infisical configuration detected. Secrets will not be loaded.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Secrets are skipped intentionally.
|
|
181
|
+
|
|
182
|
+
### Infisical API Failure
|
|
183
|
+
|
|
184
|
+
- `failFast: true` → Throws error and aborts startup
|
|
185
|
+
- `failFast: false` → Logs warning and continues
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Explicitly NOT Supported
|
|
190
|
+
|
|
191
|
+
This package does **not**:
|
|
192
|
+
|
|
193
|
+
- Use the Infisical CLI
|
|
194
|
+
- Add health checks
|
|
195
|
+
- Rotate secrets
|
|
196
|
+
- Poll or hot-reload secrets
|
|
197
|
+
- Cache secrets
|
|
198
|
+
- Add retries or backoff
|
|
199
|
+
- Add decorators
|
|
200
|
+
- Mutate NestJS internals
|
|
201
|
+
- Run after bootstrap
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Design Philosophy
|
|
206
|
+
|
|
207
|
+
- Startup-only
|
|
208
|
+
- Deterministic
|
|
209
|
+
- Boring
|
|
210
|
+
- Predictable
|
|
211
|
+
- `process.env` is the only contract
|
|
212
|
+
|
|
213
|
+
If you need dynamic secrets, rotation, or runtime behavior, use a different tool.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|