@apiquest/plugin-vault-file 1.0.4 → 1.0.5

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 CHANGED
@@ -1,140 +1,140 @@
1
- # @apiquest/plugin-vault-file
2
-
3
- File-based vault provider plugin for ApiQuest. Provides secure secret storage using encrypted or plain JSON files.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- # Using npm
9
- npm install -g @apiquest/plugin-vault-file
10
-
11
- # Or using fracture CLI
12
- fracture plugin install vault-file
13
- ```
14
-
15
- ## Features
16
-
17
- - Read secrets from JSON files
18
- - AES-256-GCM encryption support
19
- - Environment variable integration for encryption keys
20
- - Read-only access (no write operations)
21
- - Secure key handling from environment variables
22
-
23
- ## Usage
24
-
25
- Configure the plugin in your collection's runtime options:
26
-
27
- ### Plain JSON Vault
28
-
29
- ```json
30
- {
31
- "$schema": "https://apiquest.net/schemas/collection-v1.0.json",
32
- "protocol": "http",
33
- "options": {
34
- "plugins": {
35
- "vault:file": {
36
- "filePath": "./secrets.json"
37
- }
38
- }
39
- }
40
- }
41
- ```
42
-
43
- **secrets.json:**
44
- ```json
45
- {
46
- "apiKey": "sk_live_abc123",
47
- "dbPassword": "secret_password",
48
- "jwtSecret": "my_jwt_secret"
49
- }
50
- ```
51
-
52
- ### Encrypted Vault
53
-
54
- For encrypted vaults, specify the encryption key from an environment variable:
55
-
56
- ```json
57
- {
58
- "options": {
59
- "plugins": {
60
- "vault:file": {
61
- "filePath": "./secrets.json.enc",
62
- "key": "VAULT_KEY",
63
- "source": "env"
64
- }
65
- }
66
- }
67
- }
68
- ```
69
-
70
- This reads the encryption key from `process.env.VAULT_KEY`.
71
-
72
- ### Accessing Vault Secrets
73
-
74
- Use the `{{$vault:file:secretName}}` syntax in your requests:
75
-
76
- ```json
77
- {
78
- "type": "request",
79
- "id": "api-call",
80
- "name": "API Call with Secret",
81
- "auth": {
82
- "type": "apikey",
83
- "apikey": {
84
- "key": "x-api-key",
85
- "value": "{{$vault:file:apiKey}}",
86
- "in": "header"
87
- }
88
- }
89
- }
90
- ```
91
-
92
- ### Using in Scripts
93
-
94
- ```javascript
95
- // preRequestScript
96
- const dbPassword = await quest.vault.get('file', 'dbPassword');
97
- quest.variables.set('password', dbPassword);
98
-
99
- quest.test('Vault accessible', async () => {
100
- const secret = await quest.vault.get('file', 'apiKey');
101
- expect(secret).to.be.a('string');
102
- });
103
- ```
104
-
105
- ## Encryption
106
-
107
- To create an encrypted vault file, use AES-256-GCM encryption with the following format:
108
-
109
- ```json
110
- {
111
- "_encrypted": "aes-256-gcm",
112
- "_iv": "base64_initialization_vector",
113
- "_authTag": "base64_authentication_tag",
114
- "_data": "base64_encrypted_data"
115
- }
116
- ```
117
-
118
- The plugin automatically detects encrypted files by the presence of the `_encrypted` field.
119
-
120
- ## Security Best Practices
121
-
122
- 1. **Never commit unencrypted secrets** to version control
123
- 2. **Store encryption keys in environment variables**, not in code
124
- 3. **Use different vault files** for different environments (dev, staging, prod)
125
- 4. **Rotate secrets regularly** and update vault files
126
- 5. **Use encrypted vaults** for sensitive production secrets
127
-
128
- ## Compatibility
129
-
130
- - **Protocols:** Works with all plugins
131
- - **Node.js:** Requires Node.js 20+
132
-
133
- ## Documentation
134
-
135
- - [Fracture Documentation](https://apiquest.net/docs/fracture)
136
- - [Schema Reference](https://apiquest.net/schemas/collection-v1.0.json)
137
-
138
- ## License
139
-
140
- Dual-licensed under AGPL-3.0-or-later and commercial license. See LICENSE.txt for details.
1
+ # @apiquest/plugin-vault-file
2
+
3
+ File-based vault provider plugin for ApiQuest. Provides secure secret storage using encrypted or plain JSON files.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Using npm
9
+ npm install -g @apiquest/plugin-vault-file
10
+
11
+ # Or using fracture CLI
12
+ fracture plugin install vault-file
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - Read secrets from JSON files
18
+ - AES-256-GCM encryption support
19
+ - Environment variable integration for encryption keys
20
+ - Read-only access (no write operations)
21
+ - Secure key handling from environment variables
22
+
23
+ ## Usage
24
+
25
+ Configure the plugin in your collection's runtime options:
26
+
27
+ ### Plain JSON Vault
28
+
29
+ ```json
30
+ {
31
+ "$schema": "https://apiquest.net/schemas/collection-v1.0.json",
32
+ "protocol": "http",
33
+ "options": {
34
+ "plugins": {
35
+ "vault:file": {
36
+ "filePath": "./secrets.json"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ **secrets.json:**
44
+ ```json
45
+ {
46
+ "apiKey": "sk_live_abc123",
47
+ "dbPassword": "secret_password",
48
+ "jwtSecret": "my_jwt_secret"
49
+ }
50
+ ```
51
+
52
+ ### Encrypted Vault
53
+
54
+ For encrypted vaults, specify the encryption key from an environment variable:
55
+
56
+ ```json
57
+ {
58
+ "options": {
59
+ "plugins": {
60
+ "vault:file": {
61
+ "filePath": "./secrets.json.enc",
62
+ "key": "VAULT_KEY",
63
+ "source": "env"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ This reads the encryption key from `process.env.VAULT_KEY`.
71
+
72
+ ### Accessing Vault Secrets
73
+
74
+ Use the `{{$vault:file:secretName}}` syntax in your requests:
75
+
76
+ ```json
77
+ {
78
+ "type": "request",
79
+ "id": "api-call",
80
+ "name": "API Call with Secret",
81
+ "auth": {
82
+ "type": "apikey",
83
+ "apikey": {
84
+ "key": "x-api-key",
85
+ "value": "{{$vault:file:apiKey}}",
86
+ "in": "header"
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Using in Scripts
93
+
94
+ ```javascript
95
+ // preRequestScript
96
+ const dbPassword = await quest.vault.get('file', 'dbPassword');
97
+ quest.variables.set('password', dbPassword);
98
+
99
+ quest.test('Vault accessible', async () => {
100
+ const secret = await quest.vault.get('file', 'apiKey');
101
+ expect(secret).to.be.a('string');
102
+ });
103
+ ```
104
+
105
+ ## Encryption
106
+
107
+ To create an encrypted vault file, use AES-256-GCM encryption with the following format:
108
+
109
+ ```json
110
+ {
111
+ "_encrypted": "aes-256-gcm",
112
+ "_iv": "base64_initialization_vector",
113
+ "_authTag": "base64_authentication_tag",
114
+ "_data": "base64_encrypted_data"
115
+ }
116
+ ```
117
+
118
+ The plugin automatically detects encrypted files by the presence of the `_encrypted` field.
119
+
120
+ ## Security Best Practices
121
+
122
+ 1. **Never commit unencrypted secrets** to version control
123
+ 2. **Store encryption keys in environment variables**, not in code
124
+ 3. **Use different vault files** for different environments (dev, staging, prod)
125
+ 4. **Rotate secrets regularly** and update vault files
126
+ 5. **Use encrypted vaults** for sensitive production secrets
127
+
128
+ ## Compatibility
129
+
130
+ - **Protocols:** Works with all plugins
131
+ - **Node.js:** Requires Node.js 20+
132
+
133
+ ## Documentation
134
+
135
+ - [Fracture Documentation](https://apiquest.net/docs/fracture)
136
+ - [Schema Reference](https://apiquest.net/schemas/collection-v1.0.json)
137
+
138
+ ## License
139
+
140
+ Dual-licensed under AGPL-3.0-or-later and commercial license. See LICENSE.txt for details.
package/package.json CHANGED
@@ -1,54 +1,56 @@
1
- {
2
- "name": "@apiquest/plugin-vault-file",
3
- "version": "1.0.4",
4
- "description": "File-based vault provider plugin for ApiQuest (JSON, readonly)",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "type": "module",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/hh-apiquest/fracture.git",
11
- "directory": "packages/plugin-vault-file"
12
- },
13
- "scripts": {
14
- "build": "rollup -c && tsc --emitDeclarationOnly",
15
- "dev": "rollup -c --watch",
16
- "test": "vitest"
17
- },
18
- "keywords": [
19
- "apiquest",
20
- "vault",
21
- "secrets",
22
- "plugin",
23
- "file"
24
- ],
25
- "author": "ApiQuest",
26
- "license": "AGPL-3.0-or-later",
27
- "apiquest": {
28
- "type": "value",
29
- "runtime": [
30
- "fracture"
31
- ],
32
- "capabilities": {
33
- "provides": {
34
- "provider": "file"
35
- }
36
- }
37
- },
38
- "dependencies": {
39
- "@apiquest/types": "workspace:*"
40
- },
41
- "devDependencies": {
42
- "@apiquest/types": "workspace:*",
43
- "@rollup/plugin-commonjs": "^29.0.0",
44
- "@rollup/plugin-node-resolve": "^16.0.3",
45
- "@rollup/plugin-typescript": "^12.3.0",
46
- "@types/node": "^25.2.3",
47
- "rollup": "^4.57.1",
48
- "typescript": "^5.3.3",
49
- "vitest": "^4.0.18"
50
- },
51
- "peerDependencies": {
52
- "@apiquest/types": "^1.0.0"
53
- }
54
- }
1
+ {
2
+ "name": "@apiquest/plugin-vault-file",
3
+ "version": "1.0.5",
4
+ "description": "File-based vault provider plugin for ApiQuest (JSON, readonly)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/hh-apiquest/fracture.git",
11
+ "directory": "packages/plugin-vault-file"
12
+ },
13
+ "scripts": {
14
+ "build": "rollup -c && tsc --emitDeclarationOnly",
15
+ "dev": "rollup -c --watch",
16
+ "test": "vitest"
17
+ },
18
+ "keywords": [
19
+ "apiquest",
20
+ "vault",
21
+ "secrets",
22
+ "plugin",
23
+ "file"
24
+ ],
25
+ "author": "ApiQuest",
26
+ "license": "AGPL-3.0-or-later",
27
+ "apiquest": {
28
+ "type": "value",
29
+ "runtime": [
30
+ "fracture"
31
+ ],
32
+ "capabilities": {
33
+ "provides": {
34
+ "valueTypes": [
35
+ "vault:file"
36
+ ]
37
+ }
38
+ }
39
+ },
40
+ "dependencies": {
41
+ "@apiquest/types": "workspace:*"
42
+ },
43
+ "devDependencies": {
44
+ "@apiquest/types": "workspace:*",
45
+ "@rollup/plugin-commonjs": "^29.0.0",
46
+ "@rollup/plugin-node-resolve": "^16.0.3",
47
+ "@rollup/plugin-typescript": "^12.3.0",
48
+ "@types/node": "^25.2.3",
49
+ "rollup": "^4.57.1",
50
+ "typescript": "^5.3.3",
51
+ "vitest": "^4.0.18"
52
+ },
53
+ "peerDependencies": {
54
+ "@apiquest/types": "^1.0.0"
55
+ }
56
+ }
package/rollup.config.js CHANGED
@@ -1,31 +1,31 @@
1
- import typescript from '@rollup/plugin-typescript';
2
- import resolve from '@rollup/plugin-node-resolve';
3
- import commonjs from '@rollup/plugin-commonjs';
4
-
5
- export default {
6
- input: 'src/index.ts',
7
- output: {
8
- file: 'dist/index.js',
9
- format: 'esm',
10
- sourcemap: true,
11
- },
12
- external: [
13
- // Externalize peer dependencies
14
- '@apiquest/fracture',
15
- ],
16
- plugins: [
17
- // Resolve node modules
18
- resolve({
19
- preferBuiltins: true, // Prefer Node.js built-in modules
20
- exportConditions: ['node', 'import', 'default'],
21
- }),
22
- // Convert CommonJS to ESM (for any CJS dependencies)
23
- commonjs(),
24
- // Compile TypeScript
25
- typescript({
26
- tsconfig: './tsconfig.json',
27
- sourceMap: true,
28
- declaration: false, // We'll use tsc for declarations
29
- }),
30
- ],
31
- };
1
+ import typescript from '@rollup/plugin-typescript';
2
+ import resolve from '@rollup/plugin-node-resolve';
3
+ import commonjs from '@rollup/plugin-commonjs';
4
+
5
+ export default {
6
+ input: 'src/index.ts',
7
+ output: {
8
+ file: 'dist/index.js',
9
+ format: 'esm',
10
+ sourcemap: true,
11
+ },
12
+ external: [
13
+ // Externalize peer dependencies
14
+ '@apiquest/fracture',
15
+ ],
16
+ plugins: [
17
+ // Resolve node modules
18
+ resolve({
19
+ preferBuiltins: true, // Prefer Node.js built-in modules
20
+ exportConditions: ['node', 'import', 'default'],
21
+ }),
22
+ // Convert CommonJS to ESM (for any CJS dependencies)
23
+ commonjs(),
24
+ // Compile TypeScript
25
+ typescript({
26
+ tsconfig: './tsconfig.json',
27
+ sourceMap: true,
28
+ declaration: false, // We'll use tsc for declarations
29
+ }),
30
+ ],
31
+ };
package/tsconfig.json CHANGED
@@ -1,21 +1,21 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ES2022",
5
- "lib": ["ES2022"],
6
- "moduleResolution": "node",
7
- "outDir": "./dist",
8
- "rootDir": "./src",
9
- "declaration": true,
10
- "declarationMap": true,
11
- "sourceMap": true,
12
- "strict": true,
13
- "esModuleInterop": true,
14
- "skipLibCheck": true,
15
- "forceConsistentCasingInFileNames": true,
16
- "resolveJsonModule": true,
17
- "allowSyntheticDefaultImports": true
18
- },
19
- "include": ["src/**/*"],
20
- "exclude": ["node_modules", "dist"]
21
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "node",
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true,
17
+ "allowSyntheticDefaultImports": true
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist"]
21
+ }
@@ -1,5 +1,5 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "include": ["tests/**/*"],
4
- "exclude": []
5
- }
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["tests/**/*"],
4
+ "exclude": []
5
+ }
package/vitest.config.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- environment: 'node',
7
- coverage: {
8
- provider: 'v8',
9
- reporter: ['text', 'json', 'html'],
10
- },
11
- },
12
- });
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'json', 'html'],
10
+ },
11
+ },
12
+ });
package/src/index.ts DELETED
@@ -1,421 +0,0 @@
1
- import { IValueProviderPlugin, ValidationResult, ValidationError, ExecutionContext, ILogger } from '@apiquest/types';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import * as crypto from 'crypto';
5
-
6
- /**
7
- * Encrypted file format
8
- */
9
- interface EncryptedFile {
10
- _encrypted: 'aes-256-gcm';
11
- _iv: string;
12
- _authTag: string;
13
- _data: string;
14
- }
15
-
16
- /**
17
- * Plugin configuration
18
- */
19
- interface VaultFileConfig {
20
- filePath: string;
21
- key?: string;
22
- source?: 'env';
23
- }
24
-
25
- /**
26
- * File-based Vault Provider Plugin
27
- * Reads secrets from a JSON file (plain or AES-256-GCM encrypted)
28
- *
29
- * Configuration:
30
- * - filePath: Path to JSON file containing secrets
31
- * - key: Encryption key or name of environment variable (when source="env")
32
- * - source: "env" to read key from process.env[key], omit to use key directly
33
- *
34
- * Usage - Unencrypted:
35
- * {
36
- * "plugins": {
37
- * "vault:file": {
38
- * "filePath": "./secrets.json"
39
- * }
40
- * }
41
- * }
42
- *
43
- * Usage - Encrypted with env var:
44
- * {
45
- * "plugins": {
46
- * "vault:file": {
47
- * "filePath": "./secrets.json.enc",
48
- * "key": "VAULT_KEY",
49
- * "source": "env"
50
- * }
51
- * }
52
- * }
53
- *
54
- * Usage - Encrypted with variable resolution:
55
- * {
56
- * "variables": [{"key": "vaultKey", "value": "my-secret"}],
57
- * "plugins": {
58
- * "vault:file": {
59
- * "filePath": "./secrets.json.enc",
60
- * "key": "{{vaultKey}}"
61
- * }
62
- * }
63
- * }
64
- *
65
- * Plain secrets.json format:
66
- * {
67
- * "apiKey": "secret-value",
68
- * "database": {
69
- * "password": "db-password"
70
- * }
71
- * }
72
- *
73
- * Encrypted secrets.json.enc format:
74
- * {
75
- * "_encrypted": "aes-256-gcm",
76
- * "_iv": "base64_encoded_iv",
77
- * "_authTag": "base64_encoded_auth_tag",
78
- * "_data": "base64_encoded_encrypted_json"
79
- * }
80
- *
81
- * Access nested keys with dot notation: "database.password"
82
- */
83
- export class FileVaultProvider implements IValueProviderPlugin {
84
- provider = 'vault:file';
85
- name = 'File Vault Provider';
86
- description = 'Load secrets from a JSON file (supports AES-256-GCM encryption)';
87
-
88
- configSchema = {
89
- type: 'object',
90
- properties: {
91
- filePath: {
92
- type: 'string',
93
- description: 'Path to JSON file containing secrets'
94
- },
95
- key: {
96
- type: 'string',
97
- description: 'Encryption key or environment variable name'
98
- },
99
- source: {
100
- type: 'string',
101
- enum: ['env'],
102
- description: 'Set to "env" to read key from process.env'
103
- }
104
- },
105
- required: ['filePath']
106
- };
107
-
108
- private cache = new Map<string, unknown>();
109
-
110
- async getValue(
111
- key: string,
112
- config?: unknown,
113
- context?: ExecutionContext,
114
- logger?: ILogger
115
- ): Promise<string | null> {
116
- if (config === undefined || config === null || typeof config !== 'object') {
117
- logger?.error('Vault file configuration missing');
118
- throw new Error('FileVaultProvider: filePath not configured in options.plugins["vault:file"]');
119
- }
120
-
121
- const configObj = config as VaultFileConfig;
122
-
123
- if (!('filePath' in configObj) || typeof configObj.filePath !== 'string') {
124
- logger?.error('Vault filePath missing in configuration');
125
- throw new Error('FileVaultProvider: filePath not configured in options.plugins["vault:file"]');
126
- }
127
-
128
- const filePath = path.resolve(configObj.filePath);
129
- const cacheKey = filePath;
130
-
131
- // Cache load
132
- if (!this.cache.has(cacheKey)) {
133
- try {
134
- logger?.debug('Loading vault file', { filePath });
135
- const fileContent = fs.readFileSync(filePath, 'utf-8');
136
- const fileData = JSON.parse(fileContent) as unknown;
137
-
138
- if (this.isEncryptedFile(fileData)) {
139
- const encryptionKey = this.resolveEncryptionKey(configObj);
140
- if (encryptionKey === null || encryptionKey === undefined || encryptionKey === '') {
141
- logger?.error('Encrypted vault file missing encryption key');
142
- throw new Error('FileVaultProvider: Encrypted vault file requires encryption key (config.key)');
143
- }
144
-
145
- const decrypted = this.decryptFile(fileData, encryptionKey);
146
- this.cache.set(cacheKey, decrypted);
147
- logger?.debug('Encrypted vault file decrypted and cached');
148
- } else {
149
- this.cache.set(cacheKey, fileData);
150
- logger?.debug('Vault file cached');
151
- }
152
- } catch (error: unknown) {
153
- if (error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
154
- logger?.error('Vault file not found', { filePath });
155
- throw new Error(`FileVaultProvider: Vault file not found: ${filePath}`);
156
- }
157
- if (error instanceof SyntaxError) {
158
- logger?.error('Vault file JSON parsing failed', { filePath });
159
- throw new Error(`FileVaultProvider: Invalid JSON in vault file: ${filePath}`);
160
- }
161
- throw error;
162
- }
163
- }
164
-
165
- const secrets = this.cache.get(cacheKey);
166
- const value = this.getNestedValue(secrets, key);
167
-
168
- if (value === undefined) {
169
- logger?.trace('Vault key not found', { key });
170
- return null;
171
- }
172
-
173
- logger?.trace('Vault key resolved', { key });
174
- return String(value);
175
- }
176
-
177
- validate(config?: unknown): ValidationResult {
178
- if (config === undefined || config === null) {
179
- return {
180
- valid: false,
181
- errors: [{
182
- message: 'Configuration required: must specify filePath',
183
- location: '',
184
- source: 'vault'
185
- }]
186
- };
187
- }
188
-
189
- // Type guard to check if config is an object and has filePath
190
- if (typeof config !== 'object' || config === null) {
191
- return {
192
- valid: false,
193
- errors: [{
194
- message: 'Configuration must be an object',
195
- location: '',
196
- source: 'vault'
197
- }]
198
- };
199
- }
200
-
201
- const configObj = config as Record<string, unknown>;
202
-
203
- if (!('filePath' in configObj) || configObj.filePath === undefined || configObj.filePath === null) {
204
- return {
205
- valid: false,
206
- errors: [{
207
- message: 'filePath is required in configuration',
208
- location: '',
209
- source: 'vault'
210
- }]
211
- };
212
- }
213
-
214
- if (typeof configObj.filePath !== 'string') {
215
- return {
216
- valid: false,
217
- errors: [{
218
- message: 'filePath must be a string',
219
- location: '',
220
- source: 'vault'
221
- }]
222
- };
223
- }
224
-
225
- // Check if file exists
226
- const filePath = path.resolve(configObj.filePath);
227
- if (!fs.existsSync(filePath)) {
228
- return {
229
- valid: false,
230
- errors: [{
231
- message: `Vault file not found: ${filePath}`,
232
- location: '',
233
- source: 'vault'
234
- }]
235
- };
236
- }
237
-
238
- // Try to parse JSON and check encryption
239
- try {
240
- const content = fs.readFileSync(filePath, 'utf-8');
241
- const data = JSON.parse(content) as unknown;
242
-
243
- // If file is encrypted, validate we have a key
244
- if (this.isEncryptedFile(data)) {
245
- const vaultConfig: VaultFileConfig = {
246
- filePath: String(configObj.filePath),
247
- key: typeof configObj.key === 'string' ? configObj.key : undefined,
248
- source: configObj.source === 'env' ? 'env' : undefined
249
- };
250
- const encryptionKey = this.resolveEncryptionKey(vaultConfig);
251
-
252
- if (encryptionKey === null || encryptionKey === undefined || encryptionKey === '') {
253
- return {
254
- valid: false,
255
- errors: [{
256
- message: 'Encrypted vault file requires encryption key (config.key)',
257
- location: '',
258
- source: 'vault'
259
- }]
260
- };
261
- }
262
-
263
- // Try to decrypt to validate key
264
- try {
265
- this.decryptFile(data, encryptionKey);
266
- } catch (decryptError: unknown) {
267
- const errorMessage = decryptError instanceof Error ? decryptError.message : 'Decryption failed';
268
- return {
269
- valid: false,
270
- errors: [{
271
- message: `Failed to decrypt vault file: ${errorMessage}`,
272
- location: '',
273
- source: 'vault'
274
- }]
275
- };
276
- }
277
- }
278
- } catch (error: unknown) {
279
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
280
- return {
281
- valid: false,
282
- errors: [{
283
- message: `Invalid JSON in vault file: ${errorMessage}`,
284
- location: '',
285
- source: 'vault'
286
- }]
287
- };
288
- }
289
-
290
- return { valid: true };
291
- }
292
-
293
- /**
294
- * Resolve the encryption key from config
295
- * - If source="env", read from process.env[config.key]
296
- * - Otherwise, use config.key directly
297
- */
298
- private resolveEncryptionKey(config: VaultFileConfig): string | null {
299
- if (config.key === undefined || config.key === null || config.key === '') {
300
- return null;
301
- }
302
-
303
- if (config.source === 'env') {
304
- // Read from environment variable
305
- const envValue = process.env[config.key];
306
- return envValue ?? null;
307
- }
308
-
309
- // Use key directly (could be resolved variable like {{vaultKey}})
310
- return config.key;
311
- }
312
-
313
- /**
314
- * Check if data is an encrypted file
315
- */
316
- private isEncryptedFile(data: unknown): data is EncryptedFile {
317
- if (typeof data !== 'object' || data === null) {
318
- return false;
319
- }
320
-
321
- const obj = data as Record<string, unknown>;
322
- return '_encrypted' in obj && obj._encrypted === 'aes-256-gcm';
323
- }
324
-
325
- /**
326
- * Decrypt an encrypted file using AES-256-GCM
327
- */
328
- private decryptFile(encryptedFile: EncryptedFile, key: string): unknown {
329
- try {
330
- // Derive a 32-byte key from the provided key using SHA-256
331
- const keyBuffer = crypto.createHash('sha256').update(key).digest();
332
-
333
- // Decode base64 values
334
- const iv = Buffer.from(encryptedFile._iv, 'base64');
335
- const authTag = Buffer.from(encryptedFile._authTag, 'base64');
336
- const encryptedData = Buffer.from(encryptedFile._data, 'base64');
337
-
338
- // Create decipher
339
- const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
340
- decipher.setAuthTag(authTag);
341
-
342
- // Decrypt
343
- const decrypted = Buffer.concat([
344
- decipher.update(encryptedData),
345
- decipher.final()
346
- ]);
347
-
348
- // Parse JSON
349
- return JSON.parse(decrypted.toString('utf-8')) as unknown;
350
- } catch (error: unknown) {
351
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
352
- throw new Error(`Failed to decrypt vault file: ${errorMessage}`);
353
- }
354
- }
355
-
356
- /**
357
- * Encrypt data to create an encrypted file (utility method for generating encrypted files)
358
- */
359
- static encryptData(data: unknown, key: string): EncryptedFile {
360
- // Derive a 32-byte key from the provided key using SHA-256
361
- const keyBuffer = crypto.createHash('sha256').update(key).digest();
362
-
363
- // Generate random IV (12 bytes for GCM)
364
- const iv = crypto.randomBytes(12);
365
-
366
- // Create cipher
367
- const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
368
-
369
- // Encrypt
370
- const jsonData = JSON.stringify(data);
371
- const encrypted = Buffer.concat([
372
- cipher.update(jsonData, 'utf-8'),
373
- cipher.final()
374
- ]);
375
-
376
- // Get auth tag
377
- const authTag = cipher.getAuthTag();
378
-
379
- // Return encrypted file format
380
- return {
381
- _encrypted: 'aes-256-gcm',
382
- _iv: iv.toString('base64'),
383
- _authTag: authTag.toString('base64'),
384
- _data: encrypted.toString('base64')
385
- };
386
- }
387
-
388
- /**
389
- * Get nested value from object using dot notation
390
- * Example: getNestedValue({ a: { b: { c: 'value' } } }, 'a.b.c') => 'value'
391
- */
392
- private getNestedValue(obj: unknown, key: string): unknown {
393
- const keys = key.split('.');
394
- let current = obj;
395
-
396
- for (const k of keys) {
397
- if (current === null || current === undefined) {
398
- return undefined;
399
- }
400
- if (typeof current !== 'object') {
401
- return undefined;
402
- }
403
- current = (current as Record<string, unknown>)[k];
404
- }
405
-
406
- return current;
407
- }
408
-
409
- /**
410
- * Clear the cache (useful for testing or forcing reload)
411
- */
412
- clearCache(): void {
413
- this.cache.clear();
414
- }
415
- }
416
-
417
- // Export singleton instance
418
- export const fileVaultProvider = new FileVaultProvider();
419
-
420
- // Default export
421
- export default fileVaultProvider;