@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 +140 -140
- package/package.json +56 -54
- package/rollup.config.js +31 -31
- package/tsconfig.json +21 -21
- package/tsconfig.test.json +5 -5
- package/vitest.config.ts +12 -12
- package/src/index.ts +0 -421
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
|
-
"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
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"@
|
|
45
|
-
"@rollup/plugin-
|
|
46
|
-
"@
|
|
47
|
-
"rollup": "^
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
package/tsconfig.test.json
CHANGED
|
@@ -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;
|