@enspirit/emb 0.15.0 → 0.17.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 +218 -43
- package/bin/release +122 -0
- package/dist/src/cli/abstract/BaseCommand.d.ts +1 -0
- package/dist/src/cli/abstract/BaseCommand.js +23 -4
- package/dist/src/cli/abstract/FlavouredCommand.d.ts +1 -0
- package/dist/src/cli/abstract/KubernetesCommand.d.ts +1 -0
- package/dist/src/cli/commands/components/logs.d.ts +2 -1
- package/dist/src/cli/commands/components/logs.js +21 -24
- package/dist/src/cli/commands/secrets/index.d.ts +14 -0
- package/dist/src/cli/commands/secrets/index.js +71 -0
- package/dist/src/cli/commands/secrets/providers.d.ts +12 -0
- package/dist/src/cli/commands/secrets/providers.js +50 -0
- package/dist/src/cli/commands/secrets/validate.d.ts +18 -0
- package/dist/src/cli/commands/secrets/validate.js +145 -0
- package/dist/src/cli/commands/tasks/run.js +6 -1
- package/dist/src/cli/hooks/init.js +7 -1
- package/dist/src/config/index.d.ts +10 -1
- package/dist/src/config/index.js +28 -3
- package/dist/src/config/schema.d.ts +7 -4
- package/dist/src/config/schema.json +173 -9
- package/dist/src/context.d.ts +9 -0
- package/dist/src/context.js +19 -0
- package/dist/src/docker/compose/operations/ComposeLogsOperation.d.ts +21 -0
- package/dist/src/docker/compose/operations/ComposeLogsOperation.js +85 -0
- package/dist/src/docker/compose/operations/index.d.ts +1 -0
- package/dist/src/docker/compose/operations/index.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/monorepo/monorepo.js +13 -5
- package/dist/src/monorepo/operations/shell/ExecuteLocalCommandOperation.js +40 -10
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.d.ts +1 -1
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +1 -1
- package/dist/src/monorepo/plugins/VaultPlugin.d.ts +46 -0
- package/dist/src/monorepo/plugins/VaultPlugin.js +91 -0
- package/dist/src/monorepo/plugins/index.d.ts +1 -0
- package/dist/src/monorepo/plugins/index.js +3 -0
- package/dist/src/secrets/SecretDiscovery.d.ts +46 -0
- package/dist/src/secrets/SecretDiscovery.js +82 -0
- package/dist/src/secrets/SecretManager.d.ts +52 -0
- package/dist/src/secrets/SecretManager.js +75 -0
- package/dist/src/secrets/SecretProvider.d.ts +45 -0
- package/dist/src/secrets/SecretProvider.js +38 -0
- package/dist/src/secrets/index.d.ts +3 -0
- package/dist/src/secrets/index.js +3 -0
- package/dist/src/secrets/providers/VaultOidcHelper.d.ts +39 -0
- package/dist/src/secrets/providers/VaultOidcHelper.js +226 -0
- package/dist/src/secrets/providers/VaultProvider.d.ts +74 -0
- package/dist/src/secrets/providers/VaultProvider.js +266 -0
- package/dist/src/secrets/providers/VaultTokenCache.d.ts +60 -0
- package/dist/src/secrets/providers/VaultTokenCache.js +188 -0
- package/dist/src/secrets/providers/index.d.ts +2 -0
- package/dist/src/secrets/providers/index.js +2 -0
- package/dist/src/types.d.ts +2 -0
- package/dist/src/utils/TemplateExpander.d.ts +13 -1
- package/dist/src/utils/TemplateExpander.js +68 -15
- package/oclif.manifest.json +578 -173
- package/package.json +12 -5
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/* eslint-disable n/no-unsupported-features/node-builtins -- fetch is stable in Node 20+ */
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { URL, URLSearchParams } from 'node:url';
|
|
5
|
+
import { VaultError } from './VaultProvider.js';
|
|
6
|
+
/**
|
|
7
|
+
* Perform an interactive OIDC login with Vault.
|
|
8
|
+
*
|
|
9
|
+
* This function:
|
|
10
|
+
* 1. Starts a local HTTP server to receive the callback
|
|
11
|
+
* 2. Requests an OIDC auth URL from Vault
|
|
12
|
+
* 3. Opens the user's browser to the auth URL
|
|
13
|
+
* 4. Waits for the callback with the Vault token
|
|
14
|
+
* 5. Returns the token and TTL
|
|
15
|
+
*
|
|
16
|
+
* @param options - OIDC login options
|
|
17
|
+
* @returns The Vault client token and TTL
|
|
18
|
+
* @throws VaultError if the login fails
|
|
19
|
+
*/
|
|
20
|
+
export async function performOidcLogin(options) {
|
|
21
|
+
const { vaultAddress, role, namespace, timeout = 120_000 } = options;
|
|
22
|
+
const port = options.port ?? 8250;
|
|
23
|
+
const callbackUrl = `http://localhost:${port}/oidc/callback`;
|
|
24
|
+
// Generate a random state and nonce for security
|
|
25
|
+
const state = randomBytes(16).toString('hex');
|
|
26
|
+
const nonce = randomBytes(16).toString('hex');
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let timeoutId;
|
|
29
|
+
let server;
|
|
30
|
+
const cleanup = () => {
|
|
31
|
+
if (timeoutId) {
|
|
32
|
+
clearTimeout(timeoutId);
|
|
33
|
+
timeoutId = undefined;
|
|
34
|
+
}
|
|
35
|
+
if (server) {
|
|
36
|
+
server.close();
|
|
37
|
+
server = undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
// Set up timeout
|
|
41
|
+
timeoutId = setTimeout(() => {
|
|
42
|
+
cleanup();
|
|
43
|
+
reject(new VaultError('OIDC login timed out. Please try again.', 'VAULT_AUTH_ERROR'));
|
|
44
|
+
}, timeout);
|
|
45
|
+
// Create the callback server
|
|
46
|
+
server = createServer(async (req, res) => {
|
|
47
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
48
|
+
if (url.pathname === '/oidc/callback') {
|
|
49
|
+
// Check for errors in callback
|
|
50
|
+
const error = url.searchParams.get('error');
|
|
51
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
52
|
+
if (error) {
|
|
53
|
+
sendHtmlResponse(res, 'Login Failed', `<p>Error: ${error}</p><p>${errorDescription || ''}</p>`);
|
|
54
|
+
cleanup();
|
|
55
|
+
reject(new VaultError(`OIDC login failed: ${error} - ${errorDescription || 'Unknown error'}`, 'VAULT_AUTH_ERROR'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Extract the code from the callback
|
|
59
|
+
const code = url.searchParams.get('code');
|
|
60
|
+
const returnedState = url.searchParams.get('state');
|
|
61
|
+
if (!code) {
|
|
62
|
+
sendHtmlResponse(res, 'Login Failed', '<p>No authorization code received.</p>');
|
|
63
|
+
cleanup();
|
|
64
|
+
reject(new VaultError('OIDC login failed: No authorization code received', 'VAULT_AUTH_ERROR'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Note: We don't validate state client-side because Vault generates
|
|
68
|
+
// its own state (prefixed with 'st_') regardless of what we send.
|
|
69
|
+
// Vault validates the state internally when we call the callback endpoint.
|
|
70
|
+
try {
|
|
71
|
+
// Exchange the code for a Vault token
|
|
72
|
+
// Pass the returned state/nonce from Keycloak (which are Vault's values)
|
|
73
|
+
const returnedNonce = url.searchParams.get('nonce') || nonce;
|
|
74
|
+
const result = await exchangeCodeForToken({
|
|
75
|
+
vaultAddress,
|
|
76
|
+
role,
|
|
77
|
+
namespace,
|
|
78
|
+
code,
|
|
79
|
+
state: returnedState || state,
|
|
80
|
+
nonce: returnedNonce,
|
|
81
|
+
});
|
|
82
|
+
sendHtmlResponse(res, 'Login Successful', '<p>You have been authenticated. You may close this window.</p>');
|
|
83
|
+
cleanup();
|
|
84
|
+
resolve(result);
|
|
85
|
+
}
|
|
86
|
+
catch (error_) {
|
|
87
|
+
const errorMessage = error_ instanceof Error ? error_.message : 'Unknown error';
|
|
88
|
+
sendHtmlResponse(res, 'Login Failed', `<p>Failed to complete authentication: ${errorMessage}</p>`);
|
|
89
|
+
cleanup();
|
|
90
|
+
reject(error_);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
res.writeHead(404);
|
|
95
|
+
res.end('Not Found');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
server.on('error', (err) => {
|
|
99
|
+
cleanup();
|
|
100
|
+
reject(new VaultError(`Failed to start callback server: ${err.message}`, 'VAULT_AUTH_ERROR'));
|
|
101
|
+
});
|
|
102
|
+
server.listen(port, 'localhost', async () => {
|
|
103
|
+
try {
|
|
104
|
+
// Get the OIDC auth URL from Vault
|
|
105
|
+
const authUrl = await getOidcAuthUrl({
|
|
106
|
+
vaultAddress,
|
|
107
|
+
role,
|
|
108
|
+
namespace,
|
|
109
|
+
redirectUri: callbackUrl,
|
|
110
|
+
state,
|
|
111
|
+
nonce,
|
|
112
|
+
});
|
|
113
|
+
// Open the browser
|
|
114
|
+
const open = (await import('open')).default;
|
|
115
|
+
await open(authUrl);
|
|
116
|
+
console.log('Opening browser for authentication...');
|
|
117
|
+
console.log(`If the browser doesn't open, navigate to:\n${authUrl}`);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
cleanup();
|
|
121
|
+
reject(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get the OIDC auth URL from Vault.
|
|
128
|
+
*/
|
|
129
|
+
async function getOidcAuthUrl(options) {
|
|
130
|
+
const { vaultAddress, role, namespace, redirectUri, state, nonce } = options;
|
|
131
|
+
const url = new URL('/v1/auth/oidc/oidc/auth_url', vaultAddress);
|
|
132
|
+
const headers = {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
};
|
|
135
|
+
if (namespace) {
|
|
136
|
+
headers['X-Vault-Namespace'] = namespace;
|
|
137
|
+
}
|
|
138
|
+
// Build the request body
|
|
139
|
+
// Vault API uses snake_case for these parameters
|
|
140
|
+
/* eslint-disable camelcase */
|
|
141
|
+
const body = {
|
|
142
|
+
redirect_uri: redirectUri,
|
|
143
|
+
state,
|
|
144
|
+
nonce,
|
|
145
|
+
};
|
|
146
|
+
if (role) {
|
|
147
|
+
body.role = role;
|
|
148
|
+
}
|
|
149
|
+
/* eslint-enable camelcase */
|
|
150
|
+
const response = await fetch(url.toString(), {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers,
|
|
153
|
+
body: JSON.stringify(body),
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const data = (await response.json().catch(() => ({})));
|
|
157
|
+
const errorMessage = data.errors?.join(', ') || `HTTP ${response.status}`;
|
|
158
|
+
throw new VaultError(`Failed to get OIDC auth URL: ${errorMessage}`, 'VAULT_AUTH_ERROR', response.status);
|
|
159
|
+
}
|
|
160
|
+
const data = (await response.json());
|
|
161
|
+
const authUrl = data.data?.auth_url;
|
|
162
|
+
if (!authUrl) {
|
|
163
|
+
throw new VaultError('Vault did not return an OIDC auth URL', 'VAULT_AUTH_ERROR');
|
|
164
|
+
}
|
|
165
|
+
return authUrl;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Exchange the authorization code for a Vault token.
|
|
169
|
+
*/
|
|
170
|
+
async function exchangeCodeForToken(options) {
|
|
171
|
+
const { vaultAddress, role, namespace, code, state, nonce } = options;
|
|
172
|
+
const url = new URL('/v1/auth/oidc/oidc/callback', vaultAddress);
|
|
173
|
+
const params = new URLSearchParams({
|
|
174
|
+
code,
|
|
175
|
+
state,
|
|
176
|
+
nonce,
|
|
177
|
+
});
|
|
178
|
+
if (role) {
|
|
179
|
+
params.set('role', role);
|
|
180
|
+
}
|
|
181
|
+
const headers = {
|
|
182
|
+
'Content-Type': 'application/json',
|
|
183
|
+
};
|
|
184
|
+
if (namespace) {
|
|
185
|
+
headers['X-Vault-Namespace'] = namespace;
|
|
186
|
+
}
|
|
187
|
+
const response = await fetch(`${url.toString()}?${params.toString()}`, {
|
|
188
|
+
method: 'GET',
|
|
189
|
+
headers,
|
|
190
|
+
});
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const data = (await response.json().catch(() => ({})));
|
|
193
|
+
const errorMessage = data.errors?.join(', ') || `HTTP ${response.status}`;
|
|
194
|
+
throw new VaultError(`Failed to exchange code for token: ${errorMessage}`, 'VAULT_AUTH_ERROR', response.status);
|
|
195
|
+
}
|
|
196
|
+
const data = (await response.json());
|
|
197
|
+
const token = data.auth?.client_token;
|
|
198
|
+
if (!token) {
|
|
199
|
+
throw new VaultError('Vault did not return a client token', 'VAULT_AUTH_ERROR');
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
token,
|
|
203
|
+
ttlSeconds: data.auth?.lease_duration || 3600,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Send an HTML response to the browser.
|
|
208
|
+
*/
|
|
209
|
+
function sendHtmlResponse(res, title, body) {
|
|
210
|
+
const html = `<!DOCTYPE html>
|
|
211
|
+
<html>
|
|
212
|
+
<head>
|
|
213
|
+
<title>${title}</title>
|
|
214
|
+
<style>
|
|
215
|
+
body { font-family: sans-serif; padding: 40px; text-align: center; }
|
|
216
|
+
h1 { color: #333; }
|
|
217
|
+
</style>
|
|
218
|
+
</head>
|
|
219
|
+
<body>
|
|
220
|
+
<h1>${title}</h1>
|
|
221
|
+
${body}
|
|
222
|
+
</body>
|
|
223
|
+
</html>`;
|
|
224
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
225
|
+
res.end(html);
|
|
226
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AbstractSecretProvider, SecretReference } from '../SecretProvider.js';
|
|
2
|
+
/**
|
|
3
|
+
* Authentication configuration for HashiCorp Vault.
|
|
4
|
+
*/
|
|
5
|
+
export type VaultAuthConfig = {
|
|
6
|
+
method: 'approle';
|
|
7
|
+
roleId: string;
|
|
8
|
+
secretId: string;
|
|
9
|
+
} | {
|
|
10
|
+
method: 'jwt';
|
|
11
|
+
role: string;
|
|
12
|
+
jwt: string;
|
|
13
|
+
} | {
|
|
14
|
+
method: 'kubernetes';
|
|
15
|
+
role: string;
|
|
16
|
+
} | {
|
|
17
|
+
method: 'oidc';
|
|
18
|
+
role?: string;
|
|
19
|
+
port?: number;
|
|
20
|
+
} | {
|
|
21
|
+
method: 'token';
|
|
22
|
+
token: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for the Vault provider.
|
|
26
|
+
*/
|
|
27
|
+
export interface VaultProviderConfig {
|
|
28
|
+
/** Vault server address (defaults to VAULT_ADDR env var) */
|
|
29
|
+
address: string;
|
|
30
|
+
/** Authentication configuration */
|
|
31
|
+
auth: VaultAuthConfig;
|
|
32
|
+
/** Vault namespace (optional, defaults to VAULT_NAMESPACE env var) */
|
|
33
|
+
namespace?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Error class for Vault-specific errors.
|
|
37
|
+
*/
|
|
38
|
+
export declare class VaultError extends Error {
|
|
39
|
+
code: string;
|
|
40
|
+
statusCode?: number | undefined;
|
|
41
|
+
constructor(message: string, code: string, statusCode?: number | undefined);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* HashiCorp Vault secret provider.
|
|
45
|
+
* Supports KV v2 secrets engine.
|
|
46
|
+
*/
|
|
47
|
+
export declare class VaultProvider extends AbstractSecretProvider<VaultProviderConfig> {
|
|
48
|
+
private token;
|
|
49
|
+
connect(): Promise<void>;
|
|
50
|
+
disconnect(): Promise<void>;
|
|
51
|
+
fetchSecret(ref: SecretReference): Promise<Record<string, unknown>>;
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a path for the appropriate secrets engine.
|
|
54
|
+
* - KV v2: Insert '/data/' after the mount point
|
|
55
|
+
* - 1Password Connect: Use path as-is (contains /vaults/ and /items/)
|
|
56
|
+
* - Other engines: Use path as-is
|
|
57
|
+
*/
|
|
58
|
+
private normalizeKvPath;
|
|
59
|
+
private buildHeaders;
|
|
60
|
+
private loginAppRole;
|
|
61
|
+
private loginKubernetes;
|
|
62
|
+
/**
|
|
63
|
+
* Authenticate using JWT (non-interactive).
|
|
64
|
+
* Suitable for CI/CD pipelines where a JWT is provided externally.
|
|
65
|
+
*/
|
|
66
|
+
private loginJwt;
|
|
67
|
+
/**
|
|
68
|
+
* Authenticate using OIDC (interactive browser flow).
|
|
69
|
+
* Opens a browser for the user to authenticate with Keycloak/OIDC provider.
|
|
70
|
+
*/
|
|
71
|
+
private loginOidc;
|
|
72
|
+
private verifyToken;
|
|
73
|
+
private parseErrorResponse;
|
|
74
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/* eslint-disable n/no-unsupported-features/node-builtins -- fetch is stable in Node 20+ */
|
|
2
|
+
import { AbstractSecretProvider } from '../SecretProvider.js';
|
|
3
|
+
import { cacheToken, clearCachedToken, getCachedToken, } from './VaultTokenCache.js';
|
|
4
|
+
/**
|
|
5
|
+
* Error class for Vault-specific errors.
|
|
6
|
+
*/
|
|
7
|
+
export class VaultError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
statusCode;
|
|
10
|
+
constructor(message, code, statusCode) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.name = 'VaultError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* HashiCorp Vault secret provider.
|
|
19
|
+
* Supports KV v2 secrets engine.
|
|
20
|
+
*/
|
|
21
|
+
export class VaultProvider extends AbstractSecretProvider {
|
|
22
|
+
token = null;
|
|
23
|
+
async connect() {
|
|
24
|
+
const { auth, address, namespace } = this.config;
|
|
25
|
+
// Try to use cached token first (for methods that benefit from caching)
|
|
26
|
+
if (auth.method === 'oidc') {
|
|
27
|
+
const cached = await getCachedToken(address, namespace);
|
|
28
|
+
if (cached) {
|
|
29
|
+
this.token = cached.token;
|
|
30
|
+
try {
|
|
31
|
+
await this.verifyToken();
|
|
32
|
+
// Cached token is still valid
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Cached token is invalid, clear it and proceed with fresh auth
|
|
37
|
+
await clearCachedToken(address, namespace);
|
|
38
|
+
this.token = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let authResult;
|
|
43
|
+
switch (auth.method) {
|
|
44
|
+
case 'approle': {
|
|
45
|
+
authResult = await this.loginAppRole(auth.roleId, auth.secretId);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'jwt': {
|
|
49
|
+
authResult = await this.loginJwt(auth.role, auth.jwt);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'kubernetes': {
|
|
53
|
+
authResult = await this.loginKubernetes(auth.role);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'oidc': {
|
|
57
|
+
authResult = await this.loginOidc(auth.role, auth.port);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case 'token': {
|
|
61
|
+
// For explicit tokens, we don't know the TTL - use a default
|
|
62
|
+
authResult = { token: auth.token, ttlSeconds: 3600 };
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
throw new VaultError(`Unsupported auth method: ${auth.method}`, 'VAULT_AUTH_ERROR');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.token = authResult.token;
|
|
70
|
+
// Verify the token works by looking it up
|
|
71
|
+
await this.verifyToken();
|
|
72
|
+
// Cache the token for methods that benefit from caching
|
|
73
|
+
if (auth.method === 'oidc' && authResult.ttlSeconds > 0) {
|
|
74
|
+
await cacheToken(address, authResult.token, authResult.ttlSeconds, namespace);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async disconnect() {
|
|
78
|
+
this.token = null;
|
|
79
|
+
this.clearCache();
|
|
80
|
+
}
|
|
81
|
+
async fetchSecret(ref) {
|
|
82
|
+
if (!this.token) {
|
|
83
|
+
throw new VaultError('Not connected to Vault', 'VAULT_NOT_CONNECTED');
|
|
84
|
+
}
|
|
85
|
+
// For KV v2, the path needs 'data' inserted after the mount point
|
|
86
|
+
// e.g., "secret/myapp" becomes "secret/data/myapp"
|
|
87
|
+
const path = this.normalizeKvPath(ref.path);
|
|
88
|
+
const url = new URL(`/v1/${path}`, this.config.address);
|
|
89
|
+
if (ref.version) {
|
|
90
|
+
url.searchParams.set('version', ref.version);
|
|
91
|
+
}
|
|
92
|
+
const response = await fetch(url.toString(), {
|
|
93
|
+
method: 'GET',
|
|
94
|
+
headers: this.buildHeaders(),
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await this.parseErrorResponse(response);
|
|
98
|
+
const namespace = this.config.namespace
|
|
99
|
+
? ` (namespace: ${this.config.namespace})`
|
|
100
|
+
: '';
|
|
101
|
+
throw new VaultError(`Failed to read secret at '${ref.path}'${namespace}: ${error.message}`, 'VAULT_READ_ERROR', response.status);
|
|
102
|
+
}
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
// KV v2 wraps the data in a 'data' field
|
|
105
|
+
return (data.data?.data ||
|
|
106
|
+
data.data ||
|
|
107
|
+
{});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalize a path for the appropriate secrets engine.
|
|
111
|
+
* - KV v2: Insert '/data/' after the mount point
|
|
112
|
+
* - 1Password Connect: Use path as-is (contains /vaults/ and /items/)
|
|
113
|
+
* - Other engines: Use path as-is
|
|
114
|
+
*/
|
|
115
|
+
normalizeKvPath(path) {
|
|
116
|
+
// If path already contains '/data/', assume it's correctly formatted for KV v2
|
|
117
|
+
if (path.includes('/data/')) {
|
|
118
|
+
return path;
|
|
119
|
+
}
|
|
120
|
+
// 1Password Connect paths contain /vaults/ and /items/ - don't modify
|
|
121
|
+
if (path.includes('/vaults/') || path.includes('/items/')) {
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
// For KV v2, insert /data/ after the mount point
|
|
125
|
+
// Split by first '/' to get mount and rest of path
|
|
126
|
+
const firstSlash = path.indexOf('/');
|
|
127
|
+
if (firstSlash === -1) {
|
|
128
|
+
// Just a mount, no sub-path
|
|
129
|
+
return `${path}/data`;
|
|
130
|
+
}
|
|
131
|
+
const mount = path.slice(0, Math.max(0, firstSlash));
|
|
132
|
+
const subPath = path.slice(Math.max(0, firstSlash + 1));
|
|
133
|
+
return `${mount}/data/${subPath}`;
|
|
134
|
+
}
|
|
135
|
+
buildHeaders() {
|
|
136
|
+
const headers = {
|
|
137
|
+
'X-Vault-Token': this.token,
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
};
|
|
140
|
+
if (this.config.namespace) {
|
|
141
|
+
headers['X-Vault-Namespace'] = this.config.namespace;
|
|
142
|
+
}
|
|
143
|
+
return headers;
|
|
144
|
+
}
|
|
145
|
+
async loginAppRole(roleId, secretId) {
|
|
146
|
+
const url = new URL('/v1/auth/approle/login', this.config.address);
|
|
147
|
+
const response = await fetch(url.toString(), {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
...(this.config.namespace && {
|
|
152
|
+
'X-Vault-Namespace': this.config.namespace,
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
// Vault API uses snake_case for these properties
|
|
156
|
+
// eslint-disable-next-line camelcase
|
|
157
|
+
body: JSON.stringify({ role_id: roleId, secret_id: secretId }),
|
|
158
|
+
});
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const error = await this.parseErrorResponse(response);
|
|
161
|
+
throw new VaultError(`AppRole login failed: ${error.message}`, 'VAULT_AUTH_ERROR', response.status);
|
|
162
|
+
}
|
|
163
|
+
const data = (await response.json());
|
|
164
|
+
return {
|
|
165
|
+
token: data.auth?.client_token || '',
|
|
166
|
+
ttlSeconds: data.auth?.lease_duration || 3600,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async loginKubernetes(role) {
|
|
170
|
+
// Read the service account token from the mounted file
|
|
171
|
+
const fs = await import('node:fs/promises');
|
|
172
|
+
const tokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token';
|
|
173
|
+
let jwt;
|
|
174
|
+
try {
|
|
175
|
+
jwt = await fs.readFile(tokenPath, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new VaultError(`Could not read Kubernetes service account token from ${tokenPath}`, 'VAULT_AUTH_ERROR');
|
|
179
|
+
}
|
|
180
|
+
const url = new URL('/v1/auth/kubernetes/login', this.config.address);
|
|
181
|
+
const response = await fetch(url.toString(), {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
...(this.config.namespace && {
|
|
186
|
+
'X-Vault-Namespace': this.config.namespace,
|
|
187
|
+
}),
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({ role, jwt }),
|
|
190
|
+
});
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const error = await this.parseErrorResponse(response);
|
|
193
|
+
throw new VaultError(`Kubernetes login failed: ${error.message}`, 'VAULT_AUTH_ERROR', response.status);
|
|
194
|
+
}
|
|
195
|
+
const data = (await response.json());
|
|
196
|
+
return {
|
|
197
|
+
token: data.auth?.client_token || '',
|
|
198
|
+
ttlSeconds: data.auth?.lease_duration || 3600,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Authenticate using JWT (non-interactive).
|
|
203
|
+
* Suitable for CI/CD pipelines where a JWT is provided externally.
|
|
204
|
+
*/
|
|
205
|
+
async loginJwt(role, jwt) {
|
|
206
|
+
const url = new URL('/v1/auth/jwt/login', this.config.address);
|
|
207
|
+
const response = await fetch(url.toString(), {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Content-Type': 'application/json',
|
|
211
|
+
...(this.config.namespace && {
|
|
212
|
+
'X-Vault-Namespace': this.config.namespace,
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
body: JSON.stringify({ role, jwt }),
|
|
216
|
+
});
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const error = await this.parseErrorResponse(response);
|
|
219
|
+
throw new VaultError(`JWT login failed: ${error.message}`, 'VAULT_AUTH_ERROR', response.status);
|
|
220
|
+
}
|
|
221
|
+
const data = (await response.json());
|
|
222
|
+
return {
|
|
223
|
+
token: data.auth?.client_token || '',
|
|
224
|
+
ttlSeconds: data.auth?.lease_duration || 3600,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Authenticate using OIDC (interactive browser flow).
|
|
229
|
+
* Opens a browser for the user to authenticate with Keycloak/OIDC provider.
|
|
230
|
+
*/
|
|
231
|
+
async loginOidc(role, port) {
|
|
232
|
+
const { performOidcLogin } = await import('./VaultOidcHelper.js');
|
|
233
|
+
const result = await performOidcLogin({
|
|
234
|
+
vaultAddress: this.config.address,
|
|
235
|
+
role,
|
|
236
|
+
port: port ?? 8250,
|
|
237
|
+
namespace: this.config.namespace,
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
token: result.token,
|
|
241
|
+
ttlSeconds: result.ttlSeconds,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async verifyToken() {
|
|
245
|
+
const url = new URL('/v1/auth/token/lookup-self', this.config.address);
|
|
246
|
+
const response = await fetch(url.toString(), {
|
|
247
|
+
method: 'GET',
|
|
248
|
+
headers: this.buildHeaders(),
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
const error = await this.parseErrorResponse(response);
|
|
252
|
+
throw new VaultError(`Token verification failed: ${error.message}`, 'VAULT_AUTH_ERROR', response.status);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async parseErrorResponse(response) {
|
|
256
|
+
try {
|
|
257
|
+
const data = (await response.json());
|
|
258
|
+
return {
|
|
259
|
+
message: data.errors?.join(', ') || `HTTP ${response.status}`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return { message: `HTTP ${response.status}: ${response.statusText}` };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cached token metadata stored on disk.
|
|
3
|
+
*/
|
|
4
|
+
export interface CachedToken {
|
|
5
|
+
/** Unix timestamp (ms) when the token was cached */
|
|
6
|
+
createdAt: number;
|
|
7
|
+
/** Unix timestamp (ms) when the token expires */
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
/** Vault namespace (if any) */
|
|
10
|
+
namespace?: string;
|
|
11
|
+
/** The Vault client token */
|
|
12
|
+
token: string;
|
|
13
|
+
/** Vault address this token is for */
|
|
14
|
+
vaultAddress: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Options for token caching.
|
|
18
|
+
*/
|
|
19
|
+
export interface TokenCacheOptions {
|
|
20
|
+
/** Custom cache directory (default: ~/.emb/vault-tokens) */
|
|
21
|
+
cacheDir?: string;
|
|
22
|
+
/** Buffer time in ms before expiry to consider token invalid (default: 5 minutes) */
|
|
23
|
+
expiryBuffer?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Retrieve a cached token if it exists and is still valid.
|
|
27
|
+
*
|
|
28
|
+
* @param vaultAddress - The Vault server address
|
|
29
|
+
* @param namespace - Optional Vault namespace
|
|
30
|
+
* @param options - Cache options
|
|
31
|
+
* @returns The cached token or null if not found/expired
|
|
32
|
+
*/
|
|
33
|
+
export declare function getCachedToken(vaultAddress: string, namespace?: string, options?: TokenCacheOptions): Promise<CachedToken | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Cache a Vault token to disk (encrypted).
|
|
36
|
+
*
|
|
37
|
+
* @param vaultAddress - The Vault server address
|
|
38
|
+
* @param token - The Vault client token
|
|
39
|
+
* @param ttlSeconds - Token TTL in seconds (from Vault's lease_duration)
|
|
40
|
+
* @param namespace - Optional Vault namespace
|
|
41
|
+
* @param options - Cache options
|
|
42
|
+
*/
|
|
43
|
+
export declare function cacheToken(vaultAddress: string, token: string, ttlSeconds: number, namespace?: string, options?: TokenCacheOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Clear a cached token.
|
|
46
|
+
*
|
|
47
|
+
* @param vaultAddress - The Vault server address
|
|
48
|
+
* @param namespace - Optional Vault namespace
|
|
49
|
+
* @param options - Cache options
|
|
50
|
+
*/
|
|
51
|
+
export declare function clearCachedToken(vaultAddress: string, namespace?: string, options?: TokenCacheOptions): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Check if a cached token exists and is valid (without returning the token).
|
|
54
|
+
*
|
|
55
|
+
* @param vaultAddress - The Vault server address
|
|
56
|
+
* @param namespace - Optional Vault namespace
|
|
57
|
+
* @param options - Cache options
|
|
58
|
+
* @returns True if a valid cached token exists
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasCachedToken(vaultAddress: string, namespace?: string, options?: TokenCacheOptions): Promise<boolean>;
|