@kustodian/plugin-doppler 1.0.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/package.json +45 -0
- package/src/executor.ts +228 -0
- package/src/index.ts +25 -0
- package/src/plugin.ts +141 -0
- package/src/resolver.ts +87 -0
- package/src/types.ts +37 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kustodian/plugin-doppler",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Doppler secret provider plugin for Kustodian",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"test:watch": "bun test --watch",
|
|
20
|
+
"typecheck": "bun run tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"kustodian",
|
|
24
|
+
"plugin",
|
|
25
|
+
"doppler",
|
|
26
|
+
"secrets",
|
|
27
|
+
"kubernetes"
|
|
28
|
+
],
|
|
29
|
+
"author": "Luca Silverentand <luca@onezero.company>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/lucasilverentand/kustodian.git",
|
|
34
|
+
"directory": "plugins/doppler"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://npm.pkg.github.com"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@kustodian/core": "workspace:*",
|
|
41
|
+
"@kustodian/plugins": "workspace:*",
|
|
42
|
+
"@kustodian/schema": "workspace:*"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {}
|
|
45
|
+
}
|
package/src/executor.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { Errors, type ResultType, failure, success } from '@kustodian/core';
|
|
5
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_TIMEOUT, type DopplerPluginOptionsType } from './types.js';
|
|
8
|
+
|
|
9
|
+
const exec_file_async = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Result of a command execution.
|
|
13
|
+
*/
|
|
14
|
+
export interface CommandResultType {
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
exit_code: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for command execution.
|
|
22
|
+
*/
|
|
23
|
+
export interface ExecOptionsType {
|
|
24
|
+
cwd?: string | undefined;
|
|
25
|
+
timeout?: number | undefined;
|
|
26
|
+
env?: Record<string, string> | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Executes a command and returns the result.
|
|
31
|
+
* Uses execFile for security (no shell invocation).
|
|
32
|
+
*/
|
|
33
|
+
export async function exec_command(
|
|
34
|
+
command: string,
|
|
35
|
+
args: string[] = [],
|
|
36
|
+
options: ExecOptionsType = {},
|
|
37
|
+
): Promise<ResultType<CommandResultType, KustodianErrorType>> {
|
|
38
|
+
try {
|
|
39
|
+
const { stdout, stderr } = await exec_file_async(command, args, {
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
timeout: options.timeout,
|
|
42
|
+
env: { ...process.env, ...options.env },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return success({
|
|
46
|
+
stdout,
|
|
47
|
+
stderr,
|
|
48
|
+
exit_code: 0,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (is_exec_error(error)) {
|
|
52
|
+
return success({
|
|
53
|
+
stdout: error.stdout ?? '',
|
|
54
|
+
stderr: error.stderr ?? '',
|
|
55
|
+
exit_code: error.code ?? 1,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return failure(Errors.unknown(`Failed to execute command: ${command}`, error));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Type guard for exec errors.
|
|
65
|
+
*/
|
|
66
|
+
function is_exec_error(
|
|
67
|
+
error: unknown,
|
|
68
|
+
): error is { stdout?: string; stderr?: string; code?: number } {
|
|
69
|
+
return (
|
|
70
|
+
typeof error === 'object' &&
|
|
71
|
+
error !== null &&
|
|
72
|
+
('stdout' in error || 'stderr' in error || 'code' in error)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets environment variables for Doppler CLI authentication.
|
|
78
|
+
*/
|
|
79
|
+
function get_doppler_auth_env(options: DopplerPluginOptionsType): Record<string, string> {
|
|
80
|
+
const env: Record<string, string> = {};
|
|
81
|
+
|
|
82
|
+
const token = options.token ?? process.env['DOPPLER_TOKEN'];
|
|
83
|
+
if (token) {
|
|
84
|
+
env['DOPPLER_TOKEN'] = token;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return env;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Checks if doppler CLI is available in the system PATH.
|
|
92
|
+
*/
|
|
93
|
+
export async function check_doppler_available(): Promise<ResultType<string, KustodianErrorType>> {
|
|
94
|
+
const result = await exec_command('doppler', ['--version']);
|
|
95
|
+
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (result.value.exit_code !== 0) {
|
|
101
|
+
return failure(Errors.secret_cli_not_found('Doppler', 'doppler'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Parse version from output (e.g., "v3.68.0")
|
|
105
|
+
const version = result.value.stdout.trim();
|
|
106
|
+
return success(version);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fetches all secrets for a project/config combination.
|
|
111
|
+
* Returns a JSON object of all secrets.
|
|
112
|
+
*/
|
|
113
|
+
export async function doppler_secrets_download(
|
|
114
|
+
project: string,
|
|
115
|
+
config: string,
|
|
116
|
+
options: DopplerPluginOptionsType = {},
|
|
117
|
+
): Promise<ResultType<Record<string, string>, KustodianErrorType>> {
|
|
118
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
119
|
+
const env = get_doppler_auth_env(options);
|
|
120
|
+
|
|
121
|
+
const result = await exec_command(
|
|
122
|
+
'doppler',
|
|
123
|
+
['secrets', 'download', '--project', project, '--config', config, '--format', 'json', '--no-file'],
|
|
124
|
+
{
|
|
125
|
+
timeout,
|
|
126
|
+
env,
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { stdout, stderr, exit_code } = result.value;
|
|
135
|
+
|
|
136
|
+
if (exit_code !== 0) {
|
|
137
|
+
// Parse specific error types from stderr
|
|
138
|
+
const stderr_lower = stderr.toLowerCase();
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
stderr_lower.includes('not found') ||
|
|
142
|
+
stderr_lower.includes("doesn't exist") ||
|
|
143
|
+
stderr_lower.includes('no project') ||
|
|
144
|
+
stderr_lower.includes('no config')
|
|
145
|
+
) {
|
|
146
|
+
return failure(Errors.secret_not_found('Doppler', `${project}/${config}`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
stderr_lower.includes('unauthorized') ||
|
|
151
|
+
stderr_lower.includes('authentication') ||
|
|
152
|
+
stderr_lower.includes('invalid token') ||
|
|
153
|
+
stderr_lower.includes('access denied')
|
|
154
|
+
) {
|
|
155
|
+
return failure(Errors.secret_auth_error('Doppler', stderr));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (stderr_lower.includes('timeout')) {
|
|
159
|
+
return failure(Errors.secret_timeout('Doppler', timeout));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return failure(Errors.unknown(`Doppler error: ${stderr}`));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const secrets = JSON.parse(stdout) as Record<string, string>;
|
|
167
|
+
return success(secrets);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return failure(Errors.unknown('Failed to parse Doppler secrets output', error));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fetches a single secret value from Doppler.
|
|
175
|
+
*/
|
|
176
|
+
export async function doppler_secret_get(
|
|
177
|
+
project: string,
|
|
178
|
+
config: string,
|
|
179
|
+
secret_name: string,
|
|
180
|
+
options: DopplerPluginOptionsType = {},
|
|
181
|
+
): Promise<ResultType<string, KustodianErrorType>> {
|
|
182
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
183
|
+
const env = get_doppler_auth_env(options);
|
|
184
|
+
|
|
185
|
+
const result = await exec_command(
|
|
186
|
+
'doppler',
|
|
187
|
+
['secrets', 'get', secret_name, '--project', project, '--config', config, '--plain'],
|
|
188
|
+
{
|
|
189
|
+
timeout,
|
|
190
|
+
env,
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (!result.success) {
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { stdout, stderr, exit_code } = result.value;
|
|
199
|
+
|
|
200
|
+
if (exit_code !== 0) {
|
|
201
|
+
// Parse specific error types from stderr
|
|
202
|
+
const stderr_lower = stderr.toLowerCase();
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
stderr_lower.includes('not found') ||
|
|
206
|
+
stderr_lower.includes("doesn't exist") ||
|
|
207
|
+
stderr_lower.includes('no secret')
|
|
208
|
+
) {
|
|
209
|
+
return failure(Errors.secret_not_found('Doppler', `${project}/${config}/${secret_name}`));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
stderr_lower.includes('unauthorized') ||
|
|
214
|
+
stderr_lower.includes('authentication') ||
|
|
215
|
+
stderr_lower.includes('invalid token')
|
|
216
|
+
) {
|
|
217
|
+
return failure(Errors.secret_auth_error('Doppler', stderr));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (stderr_lower.includes('timeout')) {
|
|
221
|
+
return failure(Errors.secret_timeout('Doppler', timeout));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return failure(Errors.unknown(`Doppler error: ${stderr}`));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return success(stdout.trim());
|
|
228
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Plugin exports
|
|
2
|
+
export { create_doppler_plugin, plugin } from './plugin.js';
|
|
3
|
+
export { plugin as default } from './plugin.js';
|
|
4
|
+
|
|
5
|
+
// Executor exports
|
|
6
|
+
export {
|
|
7
|
+
check_doppler_available,
|
|
8
|
+
exec_command,
|
|
9
|
+
doppler_secret_get,
|
|
10
|
+
doppler_secrets_download,
|
|
11
|
+
type CommandResultType,
|
|
12
|
+
type ExecOptionsType,
|
|
13
|
+
} from './executor.js';
|
|
14
|
+
|
|
15
|
+
// Resolver exports
|
|
16
|
+
export { resolve_doppler_substitutions } from './resolver.js';
|
|
17
|
+
|
|
18
|
+
// Types
|
|
19
|
+
export {
|
|
20
|
+
create_cache_key,
|
|
21
|
+
DEFAULT_TIMEOUT,
|
|
22
|
+
type DopplerCacheKeyType,
|
|
23
|
+
type DopplerPluginOptionsType,
|
|
24
|
+
type DopplerRefType,
|
|
25
|
+
} from './types.js';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { success } from '@kustodian/core';
|
|
2
|
+
import type {
|
|
3
|
+
CommandType,
|
|
4
|
+
HookContextType,
|
|
5
|
+
HookEventType,
|
|
6
|
+
KustodianPluginType,
|
|
7
|
+
PluginCommandContributionType,
|
|
8
|
+
PluginHookContributionType,
|
|
9
|
+
PluginManifestType,
|
|
10
|
+
} from '@kustodian/plugins';
|
|
11
|
+
|
|
12
|
+
import { check_doppler_available, doppler_secret_get, doppler_secrets_download } from './executor.js';
|
|
13
|
+
import type { DopplerPluginOptionsType } from './types.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Doppler plugin manifest.
|
|
17
|
+
*/
|
|
18
|
+
const manifest: PluginManifestType = {
|
|
19
|
+
name: '@kustodian/plugin-doppler',
|
|
20
|
+
version: '0.1.0',
|
|
21
|
+
description: 'Doppler secret provider for Kustodian',
|
|
22
|
+
capabilities: ['commands', 'hooks'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates the Doppler plugin.
|
|
27
|
+
*/
|
|
28
|
+
export function create_doppler_plugin(options: DopplerPluginOptionsType = {}): KustodianPluginType {
|
|
29
|
+
return {
|
|
30
|
+
manifest,
|
|
31
|
+
|
|
32
|
+
async activate() {
|
|
33
|
+
// Verify CLI availability on activation (warning only)
|
|
34
|
+
const check_result = await check_doppler_available();
|
|
35
|
+
if (!check_result.success) {
|
|
36
|
+
console.warn('Doppler CLI not found - secret resolution may fail');
|
|
37
|
+
}
|
|
38
|
+
return success(undefined);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async deactivate() {
|
|
42
|
+
return success(undefined);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
get_commands(): PluginCommandContributionType[] {
|
|
46
|
+
const doppler_command: CommandType = {
|
|
47
|
+
name: 'doppler',
|
|
48
|
+
description: 'Doppler secret management commands',
|
|
49
|
+
subcommands: [
|
|
50
|
+
{
|
|
51
|
+
name: 'check',
|
|
52
|
+
description: 'Check Doppler CLI availability and authentication',
|
|
53
|
+
handler: async () => {
|
|
54
|
+
const result = await check_doppler_available();
|
|
55
|
+
if (result.success) {
|
|
56
|
+
console.log(`Doppler CLI version: ${result.value}`);
|
|
57
|
+
return success(undefined);
|
|
58
|
+
}
|
|
59
|
+
console.error('Doppler CLI not available');
|
|
60
|
+
return result;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'test',
|
|
65
|
+
description: 'Test reading a secret from Doppler',
|
|
66
|
+
arguments: [
|
|
67
|
+
{ name: 'project', description: 'Doppler project', required: true },
|
|
68
|
+
{ name: 'config', description: 'Doppler config', required: true },
|
|
69
|
+
{ name: 'secret', description: 'Secret name', required: true },
|
|
70
|
+
],
|
|
71
|
+
handler: async (ctx) => {
|
|
72
|
+
const [project, config, secret] = ctx.args;
|
|
73
|
+
if (!project || !config || !secret) {
|
|
74
|
+
console.error('Missing required arguments: project, config, secret');
|
|
75
|
+
return success(undefined);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await doppler_secret_get(project, config, secret, options);
|
|
79
|
+
if (result.success) {
|
|
80
|
+
console.log('Secret retrieved successfully (value hidden)');
|
|
81
|
+
console.log(`Length: ${result.value.length} characters`);
|
|
82
|
+
return success(undefined);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.error(`Failed to read secret: ${result.error.message}`);
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'list-secrets',
|
|
91
|
+
description: 'List available secrets in a Doppler config',
|
|
92
|
+
arguments: [
|
|
93
|
+
{ name: 'project', description: 'Doppler project', required: true },
|
|
94
|
+
{ name: 'config', description: 'Doppler config', required: true },
|
|
95
|
+
],
|
|
96
|
+
handler: async (ctx) => {
|
|
97
|
+
const [project, config] = ctx.args;
|
|
98
|
+
if (!project || !config) {
|
|
99
|
+
console.error('Missing required arguments: project, config');
|
|
100
|
+
return success(undefined);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = await doppler_secrets_download(project, config, options);
|
|
104
|
+
if (result.success) {
|
|
105
|
+
console.log('Available secrets:');
|
|
106
|
+
for (const key of Object.keys(result.value)) {
|
|
107
|
+
console.log(` - ${key}`);
|
|
108
|
+
}
|
|
109
|
+
return success(undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.error(`Failed to list secrets: ${result.error.message}`);
|
|
113
|
+
return result;
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
return [{ command: doppler_command }];
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
get_hooks(): PluginHookContributionType[] {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
event: 'generator:after_resolve',
|
|
125
|
+
priority: 50, // Run before default (100) to inject secrets early
|
|
126
|
+
handler: async (_event: HookEventType, ctx: HookContextType) => {
|
|
127
|
+
// Hook for secret injection will be implemented in generator integration
|
|
128
|
+
return success(ctx);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Default plugin export.
|
|
138
|
+
*/
|
|
139
|
+
export const plugin = create_doppler_plugin();
|
|
140
|
+
|
|
141
|
+
export default plugin;
|
package/src/resolver.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type ResultType, failure, success } from '@kustodian/core';
|
|
2
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
3
|
+
import type { DopplerSubstitutionType } from '@kustodian/schema';
|
|
4
|
+
|
|
5
|
+
import { doppler_secrets_download } from './executor.js';
|
|
6
|
+
import { create_cache_key, type DopplerCacheKeyType, type DopplerPluginOptionsType } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves Doppler substitutions to actual secret values.
|
|
10
|
+
* Groups by project/config to minimize API calls.
|
|
11
|
+
* Returns a map from substitution name to resolved value.
|
|
12
|
+
*/
|
|
13
|
+
export async function resolve_doppler_substitutions(
|
|
14
|
+
substitutions: DopplerSubstitutionType[],
|
|
15
|
+
options: DopplerPluginOptionsType = {},
|
|
16
|
+
): Promise<ResultType<Record<string, string>, KustodianErrorType>> {
|
|
17
|
+
if (substitutions.length === 0) {
|
|
18
|
+
return success({});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Group substitutions by project/config to minimize API calls
|
|
22
|
+
const groups = new Map<DopplerCacheKeyType, DopplerSubstitutionType[]>();
|
|
23
|
+
|
|
24
|
+
for (const sub of substitutions) {
|
|
25
|
+
const key = create_cache_key(sub.project, sub.config);
|
|
26
|
+
const group = groups.get(key) ?? [];
|
|
27
|
+
group.push(sub);
|
|
28
|
+
groups.set(key, group);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Cache for downloaded secrets per project/config
|
|
32
|
+
const secrets_cache = new Map<DopplerCacheKeyType, Record<string, string>>();
|
|
33
|
+
|
|
34
|
+
const results: Record<string, string> = {};
|
|
35
|
+
|
|
36
|
+
// Fetch secrets for each group
|
|
37
|
+
for (const [key, subs] of groups) {
|
|
38
|
+
// Get project and config from the first substitution in the group
|
|
39
|
+
const first = subs[0]!;
|
|
40
|
+
|
|
41
|
+
// Check if we already have the secrets cached
|
|
42
|
+
let secrets = secrets_cache.get(key);
|
|
43
|
+
|
|
44
|
+
if (!secrets) {
|
|
45
|
+
const download_result = await doppler_secrets_download(first.project, first.config, options);
|
|
46
|
+
|
|
47
|
+
if (!download_result.success) {
|
|
48
|
+
// If we can't download secrets, try to use defaults
|
|
49
|
+
let all_have_defaults = true;
|
|
50
|
+
for (const sub of subs) {
|
|
51
|
+
if (sub.default !== undefined) {
|
|
52
|
+
results[sub.name] = sub.default;
|
|
53
|
+
} else if (options.fail_on_missing !== false) {
|
|
54
|
+
all_have_defaults = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!all_have_defaults) {
|
|
59
|
+
return failure(download_result.error);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
secrets = download_result.value;
|
|
66
|
+
secrets_cache.set(key, secrets);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract the requested secrets
|
|
70
|
+
for (const sub of subs) {
|
|
71
|
+
const value = secrets[sub.secret];
|
|
72
|
+
|
|
73
|
+
if (value !== undefined) {
|
|
74
|
+
results[sub.name] = value;
|
|
75
|
+
} else if (sub.default !== undefined) {
|
|
76
|
+
results[sub.name] = sub.default;
|
|
77
|
+
} else if (options.fail_on_missing !== false) {
|
|
78
|
+
return failure({
|
|
79
|
+
code: 'SECRET_NOT_FOUND',
|
|
80
|
+
message: `Secret not found in Doppler: ${sub.project}/${sub.config}/${sub.secret}`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return success(results);
|
|
87
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the Doppler plugin.
|
|
3
|
+
*/
|
|
4
|
+
export interface DopplerPluginOptionsType {
|
|
5
|
+
/** Doppler service token (can also be set via DOPPLER_TOKEN env var) */
|
|
6
|
+
token?: string | undefined;
|
|
7
|
+
/** Timeout for CLI operations in milliseconds (default: 30000) */
|
|
8
|
+
timeout?: number | undefined;
|
|
9
|
+
/** Whether to fail on missing secrets (default: true) */
|
|
10
|
+
fail_on_missing?: boolean | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parsed Doppler secret reference.
|
|
15
|
+
*/
|
|
16
|
+
export interface DopplerRefType {
|
|
17
|
+
project: string;
|
|
18
|
+
config: string;
|
|
19
|
+
secret: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cache key for Doppler project/config combination.
|
|
24
|
+
*/
|
|
25
|
+
export type DopplerCacheKeyType = `${string}/${string}`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default timeout for Doppler CLI operations.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_TIMEOUT = 30000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a cache key for a Doppler project/config combination.
|
|
34
|
+
*/
|
|
35
|
+
export function create_cache_key(project: string, config: string): DopplerCacheKeyType {
|
|
36
|
+
return `${project}/${config}`;
|
|
37
|
+
}
|