@faizahmed/secret-keystore 1.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/README.md ADDED
@@ -0,0 +1,1203 @@
1
+ # secret-keystore
2
+
3
+ A secure secrets management library for Node.js applications using AWS KMS encryption.
4
+
5
+ [![CI](https://github.com/faizahmedfarooqui/secret-keystore/actions/workflows/ci.yml/badge.svg)](https://github.com/faizahmedfarooqui/secret-keystore/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/@faizahmed/secret-keystore.svg)](https://www.npmjs.com/package/@faizahmed/secret-keystore)
7
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
8
+ ![Tests](https://img.shields.io/badge/tests-95%20passing-success.svg)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
10
+
11
+ **Available on npm:** [`@faizahmed/secret-keystore`](https://www.npmjs.com/package/@faizahmed/secret-keystore)
12
+
13
+ > **Design principle:** the only thing a developer ever handles is a **KMS Key ID** — which is *not* a secret. No private keys, no passphrases, no key material. AWS KMS holds all key material server-side and authorizes access via IAM, and decrypted values live **only in memory** (never in `process.env`, never on disk) to keep the blast radius of any RCE as small as possible.
14
+
15
+ ## Table of Contents
16
+
17
+ - [Features](#features)
18
+ - [Important Limitations](#important-limitations)
19
+ - [Prerequisites](#prerequisites)
20
+ - [Installation](#installation)
21
+ - [From npm Registry](#from-npm-registry)
22
+ - [Local Development / Docker Builds](#local-development--docker-builds)
23
+ - [Optional: YAML Support](#optional-yaml-support)
24
+ - [Quick Start](#quick-start)
25
+ - [CLI Reference](#cli-reference)
26
+ - [Library API](#library-api)
27
+ - [Runtime Keystore](#runtime-keystore)
28
+ - [Zero-Config Loader: `config()`](#zero-config-loader-config)
29
+ - [Configuration Options](#configuration-options)
30
+ - [How It Works](#how-it-works)
31
+ - [Examples](#examples)
32
+ - [Nitro Enclave Attestation](#nitro-enclave-attestation)
33
+ - [Error Handling](#error-handling)
34
+ - [TypeScript Support](#typescript-support)
35
+ - [Troubleshooting](#troubleshooting)
36
+ - [Development](#development)
37
+ - [Security](#security)
38
+
39
+ ## Features
40
+
41
+ - **Multi-Format Support** — Encrypt secrets in `.env`, JSON, and YAML files
42
+ - **Zero-Config Loader** — `config()` discovers and cascades your `.env` files, decrypts them, and loads everything into an in-memory keystore in one call
43
+ - **Encrypt & Decrypt CLI** — Full command-line round-trip (`encrypt` and `decrypt`)
44
+ - **Pattern Matching** — Use glob patterns (`**`) to select keys at any depth
45
+ - **AWS KMS Encryption** — Supports symmetric and asymmetric (RSA) keys; uses envelope encryption for RSA (no plaintext size limit)
46
+ - **Key-ID-Only** — Developers only ever share a non-secret KMS Key ID; no key material to leak
47
+ - **IAM Role by Default** — Uses IAM roles for authentication (explicit credentials require opt-in)
48
+ - **Secure In-Memory Storage** — Decrypted values stored with AES-256-GCM encryption in memory
49
+ - **Never in `process.env`** — Decrypted secrets are only accessible via the keystore API
50
+ - **TTL & Auto-Refresh** — Automatic secret expiry and re-decryption
51
+ - **Nitro Enclave Attestation** — Full attestation lifecycle with automatic 5-minute refresh
52
+ - **Dual API** — Content-based (convenient) + Object-based (flexible)
53
+ - **Comment Preservation** — Preserves comments and formatting in config files
54
+ - **Security-First Dependencies** — Minimal dependencies for security-sensitive operations
55
+ - **Battle-Tested** — Offline test suite (`node:test`, mocked KMS) with CI across Node 18/20/22, plus lint, format, and type-definition checks
56
+
57
+ ## Important Limitations
58
+
59
+ > ⚠️ **This library is for SERVER-SIDE use only.** It will NOT work with client-side code.
60
+
61
+ ### ❌ Does NOT Work With
62
+
63
+ | Scenario | Reason |
64
+ |----------|--------|
65
+ | **Next.js `NEXT_PUBLIC_*` variables** | These are embedded into client-side JavaScript at build time and exposed to browsers. Browsers cannot access AWS KMS. |
66
+ | **Next.js Client Components** | Client components run in the browser, which has no access to AWS KMS or server-side Node.js APIs. |
67
+ | **NestJS with Client-Side Rendering** | Any code that runs in the browser cannot decrypt KMS-encrypted values. |
68
+ | **React/Vue/Angular frontend apps** | Frontend JavaScript runs in users' browsers, not on servers with AWS access. |
69
+ | **Static Site Generation (SSG) at build time** | Build-time secrets would be embedded in static HTML/JS, defeating the purpose. |
70
+
71
+ ### Why These Don't Work
72
+
73
+ ```
74
+ ┌─────────────────────────────────────────────────────────────────────────────┐
75
+ │ CLIENT-SIDE (Browser) │
76
+ │ │
77
+ │ ❌ No AWS credentials │
78
+ │ ❌ No access to AWS KMS API │
79
+ │ ❌ No Node.js crypto module │
80
+ │ ❌ NEXT_PUBLIC_* variables are bundled into JS at BUILD time │
81
+ │ ❌ Cannot make authenticated AWS API calls │
82
+ │ │
83
+ └─────────────────────────────────────────────────────────────────────────────┘
84
+
85
+ ┌─────────────────────────────────────────────────────────────────────────────┐
86
+ │ SERVER-SIDE (Node.js) │
87
+ │ │
88
+ │ ✅ Has AWS IAM role or credentials │
89
+ │ ✅ Can call AWS KMS Decrypt API │
90
+ │ ✅ Has Node.js crypto module │
91
+ │ ✅ Secrets decrypted at RUNTIME in memory │
92
+ │ ✅ Values never sent to browser │
93
+ │ │
94
+ └─────────────────────────────────────────────────────────────────────────────┘
95
+ ```
96
+
97
+ ### ✅ Works With
98
+
99
+ | Scenario | Example |
100
+ |----------|---------|
101
+ | **Next.js API Routes** | `src/app/api/*/route.ts` |
102
+ | **Next.js Server Components** | Components without `"use client"` |
103
+ | **Next.js Server Actions** | Functions with `"use server"` |
104
+ | **NestJS Services** | Backend services running on Node.js |
105
+ | **Express/Fastify APIs** | Any Node.js backend |
106
+ | **AWS Lambda** | Serverless functions |
107
+ | **Background Jobs** | Cron jobs, workers, etc. |
108
+
109
+ ### Next.js Example: What Works vs What Doesn't
110
+
111
+ ```typescript
112
+ // ❌ WRONG: Client Component - will NOT work
113
+ "use client";
114
+ import { getSecret } from "@/lib/keystore";
115
+
116
+ export function ClientComponent() {
117
+ // This will fail - browsers can't access AWS KMS
118
+ const secret = await getSecret("API_KEY"); // ❌ Error!
119
+ return <div>{secret}</div>;
120
+ }
121
+ ```
122
+
123
+ ```typescript
124
+ // ❌ WRONG: NEXT_PUBLIC_* variables - CANNOT be encrypted
125
+ // These are embedded in client JS at build time
126
+ const apiKey = process.env.NEXT_PUBLIC_API_KEY; // Already exposed to browser!
127
+ ```
128
+
129
+ ```typescript
130
+ // ✅ CORRECT: API Route (Server-Side)
131
+ // src/app/api/data/route.ts
132
+ import { getSecret } from "@/lib/keystore";
133
+
134
+ export async function GET() {
135
+ // This works - runs on server with AWS access
136
+ const apiKey = await getSecret("API_KEY"); // ✅ Decrypted on server
137
+
138
+ const data = await fetchExternalAPI(apiKey);
139
+ return Response.json(data); // Only send non-sensitive data to client
140
+ }
141
+ ```
142
+
143
+ ```typescript
144
+ // ✅ CORRECT: Server Component (no "use client")
145
+ // src/app/dashboard/page.tsx
146
+ import { getSecret } from "@/lib/keystore";
147
+
148
+ export default async function DashboardPage() {
149
+ // This works - Server Components run on the server
150
+ const dbPassword = await getSecret("DB_PASSWORD"); // ✅ Server-side only
151
+
152
+ const data = await fetchFromDatabase(dbPassword);
153
+ return <Dashboard data={data} />; // Render with data, not secrets
154
+ }
155
+ ```
156
+
157
+ ### Summary
158
+
159
+ | Variable Type | Can Encrypt? | Where It Runs |
160
+ |---------------|--------------|---------------|
161
+ | Regular env vars (`DB_PASSWORD`) | ✅ Yes | Server only |
162
+ | `NEXT_PUBLIC_*` vars | ❌ No | Bundled into client JS |
163
+ | Server Component code | ✅ Yes | Server only |
164
+ | Client Component code | ❌ No | Browser |
165
+ | API Route code | ✅ Yes | Server only |
166
+
167
+ ## Prerequisites
168
+
169
+ - Node.js >= 18.0.0
170
+ - AWS account with KMS access
171
+ - AWS IAM role (recommended) or explicit credentials for local development
172
+
173
+ ## Installation
174
+
175
+ ### From npm Registry
176
+
177
+ The package is published to the public npm registry. Install with:
178
+
179
+ ```bash
180
+ # npm
181
+ npm install @faizahmed/secret-keystore
182
+
183
+ # pnpm
184
+ pnpm add @faizahmed/secret-keystore
185
+ ```
186
+
187
+ > **Note:** The package includes `@aws-sdk/client-kms` as a dependency.
188
+
189
+ ### Local Development / Docker Builds
190
+
191
+ When working with local development or Docker builds where `file:` references don't work (e.g., the path is outside the Docker build context), you can pack the library into a tarball:
192
+
193
+ #### Step 1: Pack the Library
194
+
195
+ ```bash
196
+ # From the secret-keystore directory
197
+
198
+ # npm
199
+ npm pack
200
+
201
+ # pnpm
202
+ pnpm pack
203
+
204
+ # This creates: faizahmedfarooqui-secret-keystore-1.0.0.tgz (scoped package name)
205
+ ```
206
+
207
+ Or use the provided script:
208
+
209
+ ```bash
210
+ # Pack and move to a specific directory (e.g., consumer project)
211
+ npm run pack:local # or: pnpm run pack:local
212
+ ```
213
+
214
+ #### Step 2: Install the Tarball
215
+
216
+ In your consumer project:
217
+
218
+ ```bash
219
+ # Copy the tarball to your project
220
+ cp ../secret-keystore/faizahmedfarooqui-secret-keystore-1.0.0.tgz ./
221
+
222
+ # Install from tarball
223
+ # npm
224
+ npm install ./faizahmedfarooqui-secret-keystore-1.0.0.tgz
225
+
226
+ # pnpm
227
+ pnpm add ./faizahmedfarooqui-secret-keystore-1.0.0.tgz
228
+ ```
229
+
230
+ Or add a script to your consumer's `package.json`:
231
+
232
+ ```json
233
+ {
234
+ "scripts": {
235
+ "pack:keystore": "cd ../secret-keystore && npm pack --pack-destination ../your-project",
236
+ "install:keystore": "npm run pack:keystore && npm install ./faizahmedfarooqui-secret-keystore-*.tgz"
237
+ }
238
+ }
239
+ ```
240
+
241
+ #### Step 3: Update package.json Reference
242
+
243
+ After installing, your `package.json` will reference the local tarball:
244
+
245
+ ```json
246
+ {
247
+ "dependencies": {
248
+ "@faizahmed/secret-keystore": "file:./faizahmedfarooqui-secret-keystore-1.0.0.tgz"
249
+ }
250
+ }
251
+ ```
252
+
253
+ #### Docker Build Configuration
254
+
255
+ For Docker builds, copy the tarball into the build context:
256
+
257
+ ```dockerfile
258
+ # Stage 1: Install Dependencies
259
+ FROM node:22 AS deps
260
+ WORKDIR /app
261
+
262
+ # Copy package files
263
+ COPY package.json yarn.lock ./
264
+
265
+ # Copy the local tarball (must be in the Docker build context)
266
+ COPY faizahmedfarooqui-secret-keystore-1.0.0.tgz ./
267
+
268
+ # Install dependencies (tarball is referenced in package.json)
269
+ RUN yarn install --frozen-lockfile
270
+ ```
271
+
272
+ > **Important:** The tarball file must be in your project directory (Docker build context). You cannot reference paths outside the build context like `../secret-keystore`.
273
+
274
+ #### Complete Workflow Example
275
+
276
+ ```bash
277
+ # 1. In the keystore library directory
278
+ cd secret-keystore
279
+ npm pack # or: pnpm pack
280
+ mv faizahmedfarooqui-secret-keystore-*.tgz ../your-consumer-project/
281
+
282
+ # 2. In your consumer project
283
+ cd ../your-consumer-project
284
+
285
+ # Update package.json to reference the tarball
286
+ # "dependencies": { "@faizahmed/secret-keystore": "file:./faizahmedfarooqui-secret-keystore-1.0.0.tgz" }
287
+
288
+ npm install # or: pnpm install
289
+
290
+ # 3. Commit the tarball to your repo (for CI/CD)
291
+ git add faizahmedfarooqui-secret-keystore-*.tgz
292
+ git commit -m "Add @faizahmed/secret-keystore tarball for Docker builds"
293
+ ```
294
+
295
+ ### Optional: YAML Support
296
+
297
+ For YAML files with complex features (anchors, aliases, multi-line strings), install `js-yaml`:
298
+
299
+ ```bash
300
+ # npm
301
+ npm install js-yaml
302
+
303
+ # pnpm
304
+ pnpm add js-yaml
305
+ ```
306
+
307
+ Without `js-yaml`, the library uses a simple built-in parser that handles basic YAML structures. If your YAML uses advanced features without `js-yaml` installed, you'll get a clear error message.
308
+
309
+ ## Quick Start
310
+
311
+ ### Step 1: Prepare Your Configuration
312
+
313
+ Create a `.env` file with your secrets:
314
+
315
+ ```env
316
+ # AWS Configuration (never encrypted)
317
+ KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/abcd-1234
318
+ AWS_REGION=us-east-1
319
+
320
+ # Your secrets (will be encrypted)
321
+ DB_PASSWORD=mysecretpassword
322
+ API_KEY=sk-1234567890abcdef
323
+ JWT_SECRET=super-secret-jwt-key
324
+
325
+ # Non-sensitive values
326
+ DB_HOST=localhost
327
+ PORT=3000
328
+ ```
329
+
330
+ ### Step 2: Encrypt Your Secrets
331
+
332
+ Run the CLI to encrypt secrets:
333
+
334
+ ```bash
335
+ # Encrypt specific keys (kms-key-id is REQUIRED)
336
+ npx @faizahmed/secret-keystore encrypt \
337
+ --kms-key-id="arn:aws:kms:us-east-1:123456789012:key/abcd-1234" \
338
+ --keys="DB_PASSWORD,API_KEY,JWT_SECRET"
339
+
340
+ # Or encrypt all keys (except reserved ones)
341
+ npx @faizahmed/secret-keystore encrypt \
342
+ --kms-key-id="alias/my-key"
343
+
344
+ # For local development, use explicit credentials
345
+ npx @faizahmed/secret-keystore encrypt \
346
+ --kms-key-id="alias/my-key" \
347
+ --use-credentials
348
+ ```
349
+
350
+ Your `.env` file is updated in-place:
351
+
352
+ ```env
353
+ # AWS Configuration (never encrypted)
354
+ KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/abcd-1234
355
+ AWS_REGION=us-east-1
356
+
357
+ # Encrypted values
358
+ DB_PASSWORD=ENC[AQICAHh2nZPq...base64...]
359
+ API_KEY=ENC[AQICAHh2nZPq...base64...]
360
+ JWT_SECRET=ENC[AQICAHh2nZPq...base64...]
361
+ ```
362
+
363
+ ### Step 3: Use in Your Application
364
+
365
+ ```javascript
366
+ const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
367
+ const fs = require('node:fs');
368
+
369
+ async function bootstrap() {
370
+ // Read your config file
371
+ const content = fs.readFileSync('./.env', 'utf-8');
372
+ const kmsKeyId = process.env.KMS_KEY_ID; // You extract the KMS key
373
+
374
+ // Initialize the keystore
375
+ const keyStore = await createSecretKeyStore(
376
+ { type: 'env', content },
377
+ kmsKeyId, // REQUIRED
378
+ {
379
+ paths: ['DB_PASSWORD', 'API_KEY', 'JWT_SECRET'],
380
+ aws: { region: process.env.AWS_REGION }
381
+ }
382
+ );
383
+
384
+ // Access decrypted secrets from the keystore
385
+ const dbPassword = keyStore.get('DB_PASSWORD'); // → "mysecretpassword"
386
+ const apiKey = keyStore.get('API_KEY'); // → "sk-1234567890abcdef"
387
+
388
+ // process.env still contains encrypted values (safe!)
389
+ console.log(process.env.DB_PASSWORD); // → "ENC[AQICAHh...encrypted...]"
390
+
391
+ // Start your application
392
+ connectToDatabase({ password: dbPassword });
393
+ }
394
+
395
+ bootstrap();
396
+ ```
397
+
398
+ ## CLI Reference
399
+
400
+ ```bash
401
+ npx @faizahmed/secret-keystore <encrypt|decrypt> [options]
402
+ ```
403
+
404
+ ### Commands
405
+
406
+ | Command | Description |
407
+ |---------|-------------|
408
+ | `encrypt` | Encrypt selected values in a config file (writes `ENC[...]` in place or to `--output`) |
409
+ | `decrypt` | Decrypt the `ENC[...]` values in a config file (writes plaintext in place or to `--output`) |
410
+ | `run` | Decrypt and launch a command with secrets injected into **the child process's** environment |
411
+ | `rotate` | Re-encrypt a file under a new KMS Key ID (requires `--old-kms-key-id`) |
412
+ | `edit` | Decrypt → open in `$EDITOR` → re-encrypt on save (via a `0600` temp file, then shredded) |
413
+ | `init` | Scaffold a starter `.env` |
414
+ | `keys` | List the keys/paths in a file — **names only, never values** |
415
+ | `status` | Show which keys are encrypted vs plaintext — **names only, never values** |
416
+ | `import` | Encrypt an existing plaintext `.env` in place (migration from plain dotenv) |
417
+
418
+ All commands share the options below and auto-detect the format from the file extension.
419
+
420
+ ### Options
421
+
422
+ | Option | Required | Default | Description |
423
+ |--------|----------|---------|-------------|
424
+ | `--kms-key-id=<id>` | **Yes** | — | KMS Key ID (ARN, UUID, or alias) |
425
+ | `--old-kms-key-id=<id>` | For `rotate` | — | The current key the file is encrypted with (rotate's source key) |
426
+ | `--path=<path>` | No | `./.env` | Path to the config file |
427
+ | `--format=<format>` | No | auto-detect | File format: `env`, `json`, `yaml` |
428
+ | `--keys=<keys>` | No | All keys | Comma-separated list of keys to encrypt |
429
+ | `--patterns=<patterns>` | No | — | Glob patterns (e.g., `**.password,**.secret`) |
430
+ | `--exclude=<keys>` | No | — | Keys/paths to exclude from encryption |
431
+ | `--region=<region>` | No | From env | AWS region |
432
+ | `--output=<path>` | No | Overwrite input | Output file path |
433
+ | `--use-credentials` | No | — | Use explicit AWS credentials instead of IAM role |
434
+ | `--dry-run` | No | — | Preview what would be encrypted |
435
+ | `--help, -h` | — | — | Show help message |
436
+ | `--version, -v` | — | — | Show version number |
437
+
438
+ ### Authentication
439
+
440
+ By default, the CLI uses **IAM role** for AWS authentication. This is recommended for production.
441
+
442
+ To use explicit credentials (e.g., for local development):
443
+
444
+ ```bash
445
+ # Set credentials in environment
446
+ export AWS_ACCESS_KEY_ID=your-access-key
447
+ export AWS_SECRET_ACCESS_KEY=your-secret-key
448
+
449
+ # Run with --use-credentials flag
450
+ npx @faizahmed/secret-keystore encrypt \
451
+ --kms-key-id="alias/my-key" \
452
+ --use-credentials
453
+ ```
454
+
455
+ ### Reserved Keys
456
+
457
+ These keys are **never encrypted** (required for encryption/decryption process):
458
+
459
+ - `KMS_KEY_ID`
460
+ - `AWS_REGION`
461
+ - `AWS_ACCESS_KEY_ID`
462
+ - `AWS_SECRET_ACCESS_KEY`
463
+ - `AWS_SESSION_TOKEN`
464
+
465
+ ### CLI Examples
466
+
467
+ ```bash
468
+ # Encrypt all keys in .env
469
+ npx @faizahmed/secret-keystore encrypt \
470
+ --kms-key-id="alias/my-key"
471
+
472
+ # Encrypt specific keys only
473
+ npx @faizahmed/secret-keystore encrypt \
474
+ --kms-key-id="arn:aws:kms:us-east-1:123456789:key/abc-123" \
475
+ --keys="DB_PASSWORD,API_KEY"
476
+
477
+ # Encrypt YAML file with patterns
478
+ npx @faizahmed/secret-keystore encrypt \
479
+ --path="./secrets.yaml" \
480
+ --kms-key-id="alias/my-key" \
481
+ --patterns="**.password,**.secret_key"
482
+
483
+ # Dry run to preview changes
484
+ npx @faizahmed/secret-keystore encrypt \
485
+ --kms-key-id="alias/my-key" \
486
+ --dry-run
487
+
488
+ # Output to a different file
489
+ npx @faizahmed/secret-keystore encrypt \
490
+ --path="./.env" \
491
+ --output="./.env.encrypted" \
492
+ --kms-key-id="alias/my-key"
493
+
494
+ # Decrypt all encrypted values in place
495
+ npx @faizahmed/secret-keystore decrypt \
496
+ --path="./.env" \
497
+ --kms-key-id="alias/my-key"
498
+
499
+ # Decrypt to a separate plaintext file
500
+ npx @faizahmed/secret-keystore decrypt \
501
+ --path="./.env.encrypted" \
502
+ --output="./.env" \
503
+ --kms-key-id="alias/my-key"
504
+
505
+ # Run your app with secrets injected into its environment (no plaintext on disk)
506
+ npx @faizahmed/secret-keystore run \
507
+ --kms-key-id="alias/my-key" -- node server.js
508
+
509
+ # Rotate a file from an old key to a new key (re-encrypts only encrypted values)
510
+ npx @faizahmed/secret-keystore rotate \
511
+ --old-kms-key-id="alias/old-key" \
512
+ --kms-key-id="alias/new-key"
513
+
514
+ # Edit an encrypted file in your $EDITOR (re-encrypts on save)
515
+ npx @faizahmed/secret-keystore edit \
516
+ --kms-key-id="alias/my-key" --path="./.env"
517
+
518
+ # Inspect a file without revealing any values
519
+ npx @faizahmed/secret-keystore status --path="./.env"
520
+ npx @faizahmed/secret-keystore keys --path="./.env"
521
+
522
+ # Migrate an existing plaintext .env to encrypted, in place
523
+ npx @faizahmed/secret-keystore import \
524
+ --kms-key-id="alias/my-key" --path="./.env"
525
+ ```
526
+
527
+ > **`run` vs `config()`:** `run` is for apps you don't want to modify — it injects secrets into the spawned **child's** environment (so the child's `env` *can* see them; the parent never does). `config()` keeps secrets in your app's in-memory store and out of `env` entirely, but requires the app to call it. Both avoid writing plaintext to disk.
528
+
529
+ > **Tip:** For running your app, prefer `run` or the in-memory [`config()` loader](#zero-config-loader-config) over decrypting to a plaintext file on disk.
530
+
531
+ ## Library API
532
+
533
+ ### Single Value Operations
534
+
535
+ ```javascript
536
+ const { encryptKMSValue, decryptKMSValue } = require('@faizahmed/secret-keystore');
537
+
538
+ // Encrypt a single value
539
+ const ciphertext = await encryptKMSValue(
540
+ 'my-secret-password',
541
+ 'arn:aws:kms:us-east-1:123456789:key/abc-123', // kmsKeyId (REQUIRED)
542
+ { aws: { region: 'us-east-1' } }
543
+ );
544
+ // Returns: "ENC[AQICAHh...]"
545
+
546
+ // Decrypt a single value
547
+ const plaintext = await decryptKMSValue(
548
+ 'ENC[AQICAHh...]',
549
+ 'arn:aws:kms:us-east-1:123456789:key/abc-123',
550
+ { aws: { region: 'us-east-1' } }
551
+ );
552
+ // Returns: "my-secret-password"
553
+ ```
554
+
555
+ ### Multiple Values Operations
556
+
557
+ ```javascript
558
+ const { encryptKMSValues, decryptKMSValues } = require('@faizahmed/secret-keystore');
559
+
560
+ // Encrypt multiple values
561
+ const result = await encryptKMSValues(
562
+ { DB_PASSWORD: 'secret123', API_KEY: 'sk-12345' },
563
+ 'alias/my-key',
564
+ { aws: { region: 'us-east-1' } }
565
+ );
566
+ // result.values = { DB_PASSWORD: 'ENC[...]', API_KEY: 'ENC[...]' }
567
+ // result.encrypted = ['DB_PASSWORD', 'API_KEY']
568
+ ```
569
+
570
+ ### Content-Based Operations
571
+
572
+ For working with file content (preserves comments and formatting):
573
+
574
+ ```javascript
575
+ const {
576
+ encryptKMSEnvContent,
577
+ encryptKMSJsonContent,
578
+ encryptKMSYamlContent,
579
+ decryptKMSEnvContent,
580
+ decryptKMSJsonContent,
581
+ decryptKMSYamlContent
582
+ } = require('@faizahmed/secret-keystore');
583
+ const fs = require('node:fs');
584
+
585
+ // Encrypt ENV content
586
+ const envContent = fs.readFileSync('./.env', 'utf-8');
587
+ const kmsKeyId = process.env.KMS_KEY_ID;
588
+
589
+ const result = await encryptKMSEnvContent(envContent, kmsKeyId, {
590
+ paths: ['DB_PASSWORD', 'API_KEY'],
591
+ aws: { region: 'us-east-1' }
592
+ });
593
+
594
+ fs.writeFileSync('./.env', result.content);
595
+
596
+ // Encrypt YAML content with patterns
597
+ const yamlContent = fs.readFileSync('./secrets.yaml', 'utf-8');
598
+ const yamlResult = await encryptKMSYamlContent(yamlContent, kmsKeyId, {
599
+ patterns: ['**.password', '**.secret_key'],
600
+ preserve: { comments: true, formatting: true }
601
+ });
602
+ ```
603
+
604
+ ### Object-Based Operations
605
+
606
+ For advanced use cases where you handle parsing/serialization:
607
+
608
+ ```javascript
609
+ const { encryptKMSObject, decryptKMSObject } = require('@faizahmed/secret-keystore');
610
+ const yaml = require('js-yaml');
611
+ const fs = require('node:fs');
612
+
613
+ // Parse YAML yourself
614
+ const config = yaml.load(fs.readFileSync('./secrets.yaml', 'utf-8'));
615
+ const kmsKeyId = config.kms.key_id;
616
+
617
+ // Encrypt object
618
+ const result = await encryptKMSObject(config, kmsKeyId, {
619
+ patterns: ['**.password', '**.secret_key'],
620
+ exclude: { paths: ['kms.key_id'] }
621
+ });
622
+
623
+ // Serialize and write yourself
624
+ fs.writeFileSync('./secrets.yaml', yaml.dump(result.object));
625
+ ```
626
+
627
+ ### Function Summary
628
+
629
+ | Function | Purpose |
630
+ |----------|---------|
631
+ | `encryptKMSValue(plaintext, kmsKeyId, options?)` | Encrypt single value using KMS |
632
+ | `decryptKMSValue(ciphertext, kmsKeyId, options?)` | Decrypt single value using KMS |
633
+ | `encryptKMSValues(values, kmsKeyId, options?)` | Encrypt flat key-value pairs using KMS |
634
+ | `decryptKMSValues(values, kmsKeyId, options?)` | Decrypt flat key-value pairs using KMS |
635
+ | `encryptKMSObject(obj, kmsKeyId, options?)` | Encrypt nested object using KMS |
636
+ | `decryptKMSObject(obj, kmsKeyId, options?)` | Decrypt nested object using KMS |
637
+ | `encryptKMSEnvContent(content, kmsKeyId, options?)` | Encrypt ENV string using KMS |
638
+ | `decryptKMSEnvContent(content, kmsKeyId, options?)` | Decrypt ENV string using KMS |
639
+ | `encryptKMSJsonContent(content, kmsKeyId, options?)` | Encrypt JSON string using KMS |
640
+ | `decryptKMSJsonContent(content, kmsKeyId, options?)` | Decrypt JSON string using KMS |
641
+ | `encryptKMSYamlContent(content, kmsKeyId, options?)` | Encrypt YAML string using KMS |
642
+ | `decryptKMSYamlContent(content, kmsKeyId, options?)` | Decrypt YAML string using KMS |
643
+ | `isJsYamlAvailable()` | Check if `js-yaml` is installed |
644
+ | `parseYaml(content)` | Parse YAML to object (uses js-yaml if available) |
645
+ | `serializeYaml(obj)` | Serialize object to YAML string |
646
+ | `config(options)` | Discover + cascade `.env` files, decrypt, and load into an in-memory keystore |
647
+ | `resolveEnvFiles(options?)` | Resolve the ordered list of `.env` files for the cascade |
648
+ | `rotateKMSContent(content, format, oldKeyId, newKeyId, options?)` | Re-encrypt a file's encrypted values under a new KMS Key ID |
649
+
650
+ > **Note:** `kmsKeyId` is **REQUIRED** in all functions. The library does not search content for it.
651
+
652
+ ## Runtime Keystore
653
+
654
+ ### Zero-Config Loader: `config()`
655
+
656
+ The fastest way to load secrets at runtime. `config()` discovers and **cascades** your `.env`
657
+ files, decrypts the `ENC[...]` values via KMS, and loads everything into an in-memory
658
+ [`SecretKeyStore`](#createsecretkeystoresource-kmskeyid-options) — in a single call.
659
+
660
+ ```javascript
661
+ const { config } = require('@faizahmed/secret-keystore');
662
+
663
+ // Discovers .env, .env.local, .env.<NODE_ENV>, .env.<NODE_ENV>.local (later wins)
664
+ const secrets = await config({ kmsKeyId: 'alias/my-key' });
665
+
666
+ const dbPassword = secrets.get('DB_PASSWORD'); // decrypted, in-memory only
667
+ const all = secrets.getAll();
668
+
669
+ process.on('SIGTERM', () => secrets.destroy());
670
+ ```
671
+
672
+ **Cascade order** (later files override earlier ones):
673
+
674
+ ```
675
+ .env → .env.local → .env.<NODE_ENV> → .env.<NODE_ENV>.local
676
+ ```
677
+
678
+ **Security by design:**
679
+
680
+ - Decrypted values live **only** in the returned keystore's memory — never written to disk, and **never** placed in `process.env`. This deliberately keeps secrets off `env`, so an attacker with code execution can't simply dump them.
681
+ - `kmsKeyId` is **required and explicit** — there is no environment-variable fallback.
682
+ - Plaintext (non-`ENC[...]`) values are passed through unchanged; only encrypted values hit KMS.
683
+
684
+ **Options:**
685
+
686
+ | Option | Default | Description |
687
+ |--------|---------|-------------|
688
+ | `kmsKeyId` | — (**required**) | KMS Key ID (ARN, UUID, or alias) |
689
+ | `cwd` | `process.cwd()` | Base directory for file discovery |
690
+ | `path` | — | Explicit file path(s); skips the cascade when set |
691
+ | `nodeEnv` | `process.env.NODE_ENV` | Environment name used in the cascade |
692
+ | `populateProcessEnv` | `false` | Opt-in: also copy decrypted values into `process.env` (**discouraged** — widens RCE blast radius; logs a warning) |
693
+ | `processEnv` | `process.env` | Target object when `populateProcessEnv` is enabled |
694
+
695
+ `config()` also accepts all [keystore options](#layered-options-structure) (TTL, `autoRefresh`, `security`, `aws`, etc.), which are forwarded to the underlying keystore.
696
+
697
+ > **Migrating from `dotenv`?** `dotenv` populates `process.env`; `config()` deliberately does not. Read secrets from the returned store (`secrets.get('KEY')`) instead. If you must have `process.env` behavior, set `populateProcessEnv: true` and accept the larger blast radius.
698
+
699
+ ### `createSecretKeyStore(source, kmsKeyId, options?)`
700
+
701
+ Creates and initializes a secure in-memory keystore with decrypted secrets. Use this directly when you already have the file content/object in hand; use [`config()`](#zero-config-loader-config) when you want automatic file discovery and cascading.
702
+
703
+ ```javascript
704
+ const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
705
+ const fs = require('node:fs');
706
+
707
+ const content = fs.readFileSync('./.env', 'utf-8');
708
+ const kmsKeyId = process.env.KMS_KEY_ID;
709
+
710
+ const keyStore = await createSecretKeyStore(
711
+ { type: 'env', content },
712
+ kmsKeyId,
713
+ {
714
+ paths: ['DB_PASSWORD', 'API_KEY', 'JWT_SECRET'],
715
+ aws: { region: 'us-east-1' },
716
+ security: { inMemoryEncryption: true },
717
+ access: { ttl: 3600000, autoRefresh: true }
718
+ }
719
+ );
720
+
721
+ // Use secrets
722
+ const dbPassword = keyStore.get('DB_PASSWORD');
723
+
724
+ // Cleanup on shutdown
725
+ process.on('SIGTERM', () => keyStore.destroy());
726
+ ```
727
+
728
+ ### Source Types
729
+
730
+ ```javascript
731
+ // ENV file content
732
+ { type: 'env', content: 'KEY=value\nKEY2=value2' }
733
+
734
+ // JSON content
735
+ { type: 'json', content: '{"key": "value"}' }
736
+
737
+ // YAML content
738
+ { type: 'yaml', content: 'key: value' }
739
+
740
+ // Pre-parsed object
741
+ { type: 'object', object: { key: 'value' } }
742
+
743
+ // Flat key-value pairs
744
+ { type: 'values', values: { KEY: 'value' } }
745
+ ```
746
+
747
+ ### KeyStore Methods
748
+
749
+ | Method | Returns | Description |
750
+ |--------|---------|-------------|
751
+ | `get(key)` | `string \| undefined` | Get a decrypted secret |
752
+ | `getSection(path)` | `object \| undefined` | Get a nested section |
753
+ | `getAll()` | `Record<string, string>` | Get all decrypted secrets |
754
+ | `has(key)` | `boolean` | Check if a key exists |
755
+ | `keys()` | `string[]` | Get all available key names |
756
+ | `isInitialized()` | `boolean` | Check if keystore is ready |
757
+ | `getMetadata()` | `object` | Get keystore metadata |
758
+ | `getAccessStats(key)` | `object \| null` | Get access statistics for a key |
759
+ | `refresh()` | `Promise<void>` | Re-decrypt all secrets |
760
+ | `clear()` | `void` | Clear all secrets from memory |
761
+ | `clearKey(key)` | `void` | Clear a specific key |
762
+ | `destroy()` | `void` | Destroy keystore and wipe memory |
763
+
764
+ ### TTL and Auto-Refresh
765
+
766
+ | `ttl` | `autoRefresh` | Behavior |
767
+ |-------|---------------|----------|
768
+ | `null` | — | Secrets never expire |
769
+ | `3600000` | `true` | Auto re-decrypt on next `get()` after expiry |
770
+ | `3600000` | `false` | Throw error, user calls `refresh()` manually |
771
+
772
+ ## Configuration Options
773
+
774
+ ### Layered Options Structure
775
+
776
+ ```javascript
777
+ {
778
+ // AWS Configuration
779
+ aws: {
780
+ credentials: {
781
+ accessKeyId: string,
782
+ secretAccessKey: string,
783
+ sessionToken?: string
784
+ },
785
+ region: string
786
+ },
787
+
788
+ // Attestation (Nitro Enclaves) - Full lifecycle managed internally
789
+ attestation: {
790
+ enabled: boolean, // Default: false
791
+ required: boolean, // Default: false
792
+ fallbackToStandard: boolean, // Default: true
793
+ endpoint: string, // Attestation endpoint URL (e.g., Anjuna)
794
+ timeout: number, // Request timeout (ms), Default: 10000
795
+ userData: string // Optional user data for attestation
796
+ },
797
+
798
+ // Path Selection
799
+ paths: string[], // Explicit paths
800
+ patterns: string[], // Glob patterns (** only)
801
+ exclude: {
802
+ paths: string[],
803
+ patterns: string[]
804
+ },
805
+
806
+ // Content Preservation
807
+ preserve: {
808
+ comments: boolean, // Default: true
809
+ formatting: boolean, // Default: true
810
+ anchors: boolean // Default: true (YAML)
811
+ },
812
+
813
+ // Keystore-specific
814
+ security: {
815
+ inMemoryEncryption: boolean, // Default: true
816
+ secureWipe: boolean // Default: true
817
+ },
818
+ access: {
819
+ ttl: number | null, // Secret expiry (ms)
820
+ autoRefresh: boolean, // Default: true
821
+ accessLimit: number, // Max access count
822
+ clearOnAccess: boolean // Default: false
823
+ },
824
+ validation: {
825
+ noProcessEnvLeak: boolean, // Default: true
826
+ throwOnMissingKey: boolean // Default: false
827
+ },
828
+
829
+ // Logging
830
+ logger: Logger,
831
+ logLevel: 'debug' | 'info' | 'warn' | 'error' | 'silent'
832
+ }
833
+ ```
834
+
835
+ ## How It Works
836
+
837
+ ### KMS Key Types: Symmetric vs Asymmetric
838
+
839
+ The library detects the key type via AWS KMS `DescribeKey` and chooses the right encryption method:
840
+
841
+ | Key type | Encryption method | Plaintext size limit |
842
+ |----------|-------------------|----------------------|
843
+ | **Symmetric** (default CMK) | Direct KMS Encrypt/Decrypt | 4 KB per value |
844
+ | **Asymmetric (RSA)** | Envelope encryption | No limit |
845
+
846
+ **Envelope encryption (RSA only):** For RSA keys, plaintext is too large for direct RSA encryption (e.g. ~190 bytes for RSA_2048). The library generates a random AES-256 data key (DEK), encrypts your secret with the DEK (AES-256-GCM), and encrypts only the DEK with KMS. The stored value is `ENC[base64(envelope)]` where the envelope contains the KMS-encrypted DEK plus IV, ciphertext, and auth tag. Decryption: KMS decrypts the DEK, then the library decrypts the payload with the DEK. Existing values encrypted with symmetric keys or with direct RSA (small payloads) remain valid; new encrypts with RSA use envelope format automatically.
847
+
848
+ ### Build-Time: Encrypting Secrets
849
+
850
+ ```mermaid
851
+ sequenceDiagram
852
+ participant Dev as Developer
853
+ participant CLI as CLI
854
+ participant KMS as AWS KMS
855
+ participant File as Config File
856
+
857
+ Dev->>CLI: npx encrypt --kms-key-id="..." --keys="DB_PASS"
858
+ CLI->>File: Read config file
859
+ File-->>CLI: DB_PASS=mysecretpassword
860
+ CLI->>KMS: Encrypt("mysecretpassword")
861
+ KMS-->>CLI: AQICAHh...encrypted...
862
+ CLI->>File: Write ENC[AQICAHh...]
863
+ CLI-->>Dev: ✅ Encrypted 1 key
864
+ ```
865
+
866
+ ### Runtime: Decrypting Secrets
867
+
868
+ ```mermaid
869
+ sequenceDiagram
870
+ participant App as Your App
871
+ participant KS as KeyStore
872
+ participant KMS as AWS KMS
873
+ participant Mem as Secure Memory
874
+
875
+ App->>KS: createSecretKeyStore(source, kmsKeyId)
876
+ KS->>KS: Parse content
877
+ KS->>KMS: Decrypt(ENC[AQICAHh...])
878
+ KMS-->>KS: mysecretpassword
879
+ KS->>Mem: Store with AES-256-GCM
880
+ KS-->>App: KeyStore ready
881
+
882
+ App->>KS: keyStore.get('DB_PASS')
883
+ KS->>Mem: Retrieve & decrypt
884
+ Mem-->>KS: mysecretpassword
885
+ KS-->>App: "mysecretpassword"
886
+ ```
887
+
888
+ ### Key Points
889
+
890
+ | Stage | `process.env.DB_PASS` | `keyStore.get('DB_PASS')` |
891
+ |-------|----------------------|---------------------------|
892
+ | Before encryption | `mysecretpassword` | — |
893
+ | After encryption | `ENC[AQICAHh...]` | — |
894
+ | At runtime (after init) | `ENC[AQICAHh...]` ✓ | `mysecretpassword` ✓ |
895
+
896
+ > **Security:** Decrypted values are **never** stored in `process.env`. They exist only in the keystore's secure memory with additional AES-256-GCM encryption.
897
+
898
+ ## Examples
899
+
900
+ Complete working sample applications are available in the `examples/` directory:
901
+
902
+ | Framework | Path | Description |
903
+ |-----------|------|-------------|
904
+ | **NestJS** | [`examples/nestjs/`](./examples/nestjs/) | Full NestJS app with global KeyStoreModule |
905
+ | **Next.js** | [`examples/nextjs/`](./examples/nextjs/) | Next.js 14 App Router with Server Components |
906
+
907
+ ### Running Examples Locally
908
+
909
+ ```bash
910
+ # 1. Install the main package dependencies
911
+ cd secret-keystore
912
+ pnpm install
913
+
914
+ # 2. Install example dependencies
915
+ cd examples/nestjs # or examples/nextjs
916
+ pnpm install
917
+
918
+ # 3. Configure environment
919
+ cp .env.example .env # or .env.local for Next.js
920
+
921
+ # 4. Update KMS_KEY_ID in .env with your actual KMS key
922
+
923
+ # 5. Encrypt secrets
924
+ pnpm run encrypt:keys
925
+
926
+ # 6. Run the app
927
+ pnpm run start:dev # NestJS
928
+ pnpm run dev # Next.js
929
+ ```
930
+
931
+ See each example's README for detailed setup instructions.
932
+
933
+ ## Nitro Enclave Attestation
934
+
935
+ For maximum security in AWS Nitro Enclaves, the library provides **full attestation lifecycle management**.
936
+
937
+ ### How It Works
938
+
939
+ When attestation is enabled:
940
+
941
+ 1. **Key Pair Generation** — Library generates ephemeral RSA-4096 key pair
942
+ 2. **Document Fetch** — Fetches attestation document from Anjuna/Nitro endpoint (includes public key)
943
+ 3. **Attested Decrypt** — Sends KMS Decrypt with `Recipient` parameter containing attestation document
944
+ 4. **CMS Unwrap** — KMS returns `CiphertextForRecipient` (CMS EnvelopedData), library unwraps with private key
945
+ 5. **Auto-Refresh** — If document expires (5-min AWS limit), library automatically regenerates and retries
946
+
947
+ ```mermaid
948
+ sequenceDiagram
949
+ participant App as Your App
950
+ participant AM as AttestationManager
951
+ participant Anjuna as Nitro/Anjuna
952
+ participant KMS as AWS KMS
953
+
954
+ App->>AM: decryptWithAttestation()
955
+ AM->>AM: Generate RSA-4096 key pair
956
+ AM->>Anjuna: Request attestation doc (with public key)
957
+ Anjuna-->>AM: Attestation document
958
+ AM->>KMS: Decrypt + Recipient{AttestationDoc}
959
+ KMS-->>AM: CiphertextForRecipient (CMS EnvelopedData)
960
+ AM->>AM: Unwrap CMS with private key (PKIjs)
961
+ AM-->>App: Decrypted plaintext
962
+ ```
963
+
964
+ ### Enabling Attestation
965
+
966
+ ```javascript
967
+ const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
968
+
969
+ const keyStore = await createSecretKeyStore(
970
+ { type: 'env', content },
971
+ kmsKeyId,
972
+ {
973
+ attestation: {
974
+ enabled: true,
975
+ required: true, // Fail if attestation unavailable
976
+ endpoint: 'http://localhost:8080/attestation', // Anjuna/Nitro endpoint
977
+ timeout: 10000,
978
+ userData: 'optional-user-data'
979
+ }
980
+ }
981
+ );
982
+ ```
983
+
984
+ ### Using AttestationManager Directly
985
+
986
+ For advanced use cases, you can use the `AttestationManager` directly:
987
+
988
+ ```javascript
989
+ const { createAttestationManager } = require('@faizahmed/secret-keystore');
990
+ const { KMSClient } = require('@aws-sdk/client-kms');
991
+
992
+ // Create and initialize the manager
993
+ const attestationManager = await createAttestationManager({
994
+ endpoint: 'http://localhost:8080/attestation',
995
+ timeout: 10000,
996
+ logger: console
997
+ });
998
+
999
+ // Use for KMS decrypt with attestation
1000
+ const kmsClient = new KMSClient({ region: 'us-east-1' });
1001
+ const plaintext = await attestationManager.decryptWithAttestation(
1002
+ kmsClient,
1003
+ ciphertextBlob,
1004
+ kmsKeyId,
1005
+ { encryptionContext: { ... } }
1006
+ );
1007
+
1008
+ // Check status
1009
+ console.log(attestationManager.getStatus());
1010
+ // { initialized: true, hasDocument: true, documentAge: 45000, ... }
1011
+
1012
+ // Cleanup
1013
+ attestationManager.destroy();
1014
+ ```
1015
+
1016
+ ### 5-Minute Auto-Refresh
1017
+
1018
+ AWS KMS requires attestation documents to be less than 5 minutes old. The library handles this automatically:
1019
+
1020
+ | Scenario | Library Behavior |
1021
+ |----------|------------------|
1022
+ | First request | Initialize (generate key pair, fetch doc) |
1023
+ | Document < 5 min old | Use cached document |
1024
+ | Document expired (KMS rejects) | Regenerate key pair, fetch new doc, retry once |
1025
+ | Anjuna/Nitro unavailable | Throw `ATTESTATION_FETCH_FAILED` error |
1026
+
1027
+ > **Note:** Attestation is only available inside AWS Nitro Enclaves. Outside enclaves, enable `fallbackToStandard: true` to use standard KMS decrypt.
1028
+
1029
+ ## Error Handling
1030
+
1031
+ The library provides a comprehensive error hierarchy:
1032
+
1033
+ ```javascript
1034
+ const {
1035
+ createSecretKeyStore,
1036
+ SecretKeyStoreError, // Base error class
1037
+ KmsError, // AWS KMS errors
1038
+ AttestationError, // Attestation failures
1039
+ ContentError, // Content parsing errors
1040
+ PathError, // Path resolution errors
1041
+ EncryptionError, // Encryption failures
1042
+ DecryptionError, // Decryption failures
1043
+ KeystoreError, // Keystore operation errors
1044
+ ValidationError // Validation failures
1045
+ } = require('@faizahmed/secret-keystore');
1046
+
1047
+ try {
1048
+ const keyStore = await createSecretKeyStore(source, kmsKeyId, options);
1049
+ const secret = keyStore.get('DB_PASSWORD');
1050
+ } catch (error) {
1051
+ if (error instanceof KmsError) {
1052
+ console.error('KMS Error:', error.code, error.message);
1053
+ } else if (error instanceof KeystoreError) {
1054
+ console.error('Keystore Error:', error.code, error.message);
1055
+ } else if (error instanceof AttestationError) {
1056
+ console.error('Attestation Error:', error.code, error.message);
1057
+ }
1058
+ }
1059
+ ```
1060
+
1061
+ ### Error Codes
1062
+
1063
+ | Category | Codes |
1064
+ |----------|-------|
1065
+ | **KMS** | `KMS_KEY_NOT_FOUND`, `KMS_KEY_DISABLED`, `KMS_ACCESS_DENIED`, `KMS_INVALID_CIPHERTEXT`, `KMS_THROTTLED` |
1066
+ | **Attestation** | `ATTESTATION_INIT_FAILED`, `ATTESTATION_DOCUMENT_EXPIRED`, `ATTESTATION_KEY_PAIR_FAILED`, `ATTESTATION_FETCH_FAILED`, `ATTESTATION_CMS_UNWRAP_FAILED` |
1067
+ | **Keystore** | `KEYSTORE_NOT_INITIALIZED`, `KEYSTORE_DESTROYED`, `SECRET_NOT_FOUND`, `SECRET_EXPIRED` |
1068
+
1069
+ ## TypeScript Support
1070
+
1071
+ The package includes TypeScript definitions:
1072
+
1073
+ ```typescript
1074
+ import * as fs from 'node:fs';
1075
+ import {
1076
+ createSecretKeyStore,
1077
+ SecretKeyStore,
1078
+ KeystoreSource,
1079
+ KeystoreOptions,
1080
+ encryptKMSEnvContent,
1081
+ encryptKMSYamlContent,
1082
+ ContentResult,
1083
+ // Attestation exports
1084
+ AttestationManager,
1085
+ createAttestationManager
1086
+ } from '@faizahmed/secret-keystore';
1087
+
1088
+ const source: KeystoreSource = {
1089
+ type: 'env',
1090
+ content: fs.readFileSync('./.env', 'utf-8')
1091
+ };
1092
+
1093
+ const options: KeystoreOptions = {
1094
+ paths: ['DB_PASSWORD', 'API_KEY'],
1095
+ security: { inMemoryEncryption: true },
1096
+ access: { ttl: 3600000, autoRefresh: true }
1097
+ };
1098
+
1099
+ const keyStore: SecretKeyStore = await createSecretKeyStore(
1100
+ source,
1101
+ kmsKeyId,
1102
+ options
1103
+ );
1104
+
1105
+ const password: string | undefined = keyStore.get('DB_PASSWORD');
1106
+ ```
1107
+
1108
+ ## Troubleshooting
1109
+
1110
+ ### "kms-key-id is REQUIRED"
1111
+
1112
+ The `--kms-key-id` option is required for all CLI operations. Provide your KMS key:
1113
+
1114
+ ```bash
1115
+ npx @faizahmed/secret-keystore encrypt --kms-key-id="your-kms-key-id"
1116
+ ```
1117
+
1118
+ ### "Could not load credentials"
1119
+
1120
+ The package uses IAM roles by default. If you're seeing this error:
1121
+
1122
+ **In production (EC2/ECS/EKS/Lambda):**
1123
+ - Ensure your instance/task/pod has an IAM role attached
1124
+ - Verify the IAM role has `kms:Encrypt` and `kms:Decrypt` permissions
1125
+
1126
+ **In local development:**
1127
+ - Use `--use-credentials` flag with the CLI
1128
+ - Set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables
1129
+
1130
+ ### "AccessDeniedException"
1131
+
1132
+ Your AWS credentials don't have permission to use the KMS key:
1133
+
1134
+ ```json
1135
+ {
1136
+ "Effect": "Allow",
1137
+ "Action": ["kms:Encrypt", "kms:Decrypt", "kms:DescribeKey"],
1138
+ "Resource": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
1139
+ }
1140
+ ```
1141
+
1142
+ ### Already encrypted values being skipped
1143
+
1144
+ This is expected behavior. The library automatically detects and skips values that are already encrypted (prefixed with `ENC[`) to prevent double-encryption.
1145
+
1146
+ ## Development
1147
+
1148
+ This project uses [pnpm](https://pnpm.io) and the built-in Node test runner — no heavyweight test framework, zero runtime test dependencies.
1149
+
1150
+ ```bash
1151
+ # Install dev dependencies
1152
+ pnpm install
1153
+
1154
+ # Run the test suite (node:test, fully offline — KMS is mocked)
1155
+ pnpm test
1156
+
1157
+ # Tests with coverage (enforces a minimum threshold)
1158
+ pnpm test:coverage
1159
+
1160
+ # Lint, format check, and type-definition check
1161
+ pnpm lint
1162
+ pnpm format:check
1163
+ pnpm typecheck
1164
+ ```
1165
+
1166
+ **Quality gates.** Every push and pull request runs CI across **Node 18, 20, and 22**:
1167
+
1168
+ | Gate | Tool | What it checks |
1169
+ |------|------|----------------|
1170
+ | Tests | `node:test` + [`aws-sdk-client-mock`](https://github.com/m-radzikowski/aws-sdk-client-mock) | 95 tests covering both symmetric and RSA-envelope KMS paths, content/object/keystore/CLI/`config()`/rotation |
1171
+ | Coverage | `c8` | Minimum coverage thresholds enforced |
1172
+ | Lint | ESLint | Code correctness |
1173
+ | Format | Prettier | Consistent style |
1174
+ | Types | `tsc --strict` | `index.d.ts` compiles and stays in sync with runtime error codes |
1175
+
1176
+ All tests run without an AWS account or network access — KMS is mocked with a reversible stand-in that exercises both the direct-encrypt (symmetric) and envelope (RSA) code paths.
1177
+
1178
+ ## Security
1179
+
1180
+ This package provides multiple layers of protection:
1181
+
1182
+ | Layer | Protection |
1183
+ |-------|------------|
1184
+ | **IAM Role Default** | Uses IAM roles by default — no credentials to manage |
1185
+ | **Encryption at Rest** | Secrets in config files are KMS-encrypted ciphertext |
1186
+ | **Access Control** | IAM policies + KMS key policies control decryption |
1187
+ | **Runtime Isolation** | Decrypted values never in `process.env` |
1188
+ | **Memory Protection** | Additional AES-256-GCM encryption in keystore memory |
1189
+ | **Full Attestation** | Complete Nitro Enclave attestation lifecycle with auto-refresh |
1190
+ | **Security-First Dependencies** | Minimal third-party dependencies for security operations |
1191
+
1192
+ ### Attestation Highlights
1193
+
1194
+ - **Fully Managed** — Library handles ephemeral key pairs, document fetching, and CMS unwrapping
1195
+ - **Auto-Refresh** — Automatically regenerates attestation materials on 5-minute expiry
1196
+ - **CMS Support** — Unwraps KMS `CiphertextForRecipient` using PKIjs/asn1js
1197
+ - **Zero Config** — Just enable attestation and point to your Anjuna/Nitro endpoint
1198
+
1199
+ 📖 **[Read the full Security documentation →](./SECURITY.md)**
1200
+
1201
+ ## License
1202
+
1203
+ MIT — see [LICENSE](LICENSE).