@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.
Files changed (57) hide show
  1. package/README.md +218 -43
  2. package/bin/release +122 -0
  3. package/dist/src/cli/abstract/BaseCommand.d.ts +1 -0
  4. package/dist/src/cli/abstract/BaseCommand.js +23 -4
  5. package/dist/src/cli/abstract/FlavouredCommand.d.ts +1 -0
  6. package/dist/src/cli/abstract/KubernetesCommand.d.ts +1 -0
  7. package/dist/src/cli/commands/components/logs.d.ts +2 -1
  8. package/dist/src/cli/commands/components/logs.js +21 -24
  9. package/dist/src/cli/commands/secrets/index.d.ts +14 -0
  10. package/dist/src/cli/commands/secrets/index.js +71 -0
  11. package/dist/src/cli/commands/secrets/providers.d.ts +12 -0
  12. package/dist/src/cli/commands/secrets/providers.js +50 -0
  13. package/dist/src/cli/commands/secrets/validate.d.ts +18 -0
  14. package/dist/src/cli/commands/secrets/validate.js +145 -0
  15. package/dist/src/cli/commands/tasks/run.js +6 -1
  16. package/dist/src/cli/hooks/init.js +7 -1
  17. package/dist/src/config/index.d.ts +10 -1
  18. package/dist/src/config/index.js +28 -3
  19. package/dist/src/config/schema.d.ts +7 -4
  20. package/dist/src/config/schema.json +173 -9
  21. package/dist/src/context.d.ts +9 -0
  22. package/dist/src/context.js +19 -0
  23. package/dist/src/docker/compose/operations/ComposeLogsOperation.d.ts +21 -0
  24. package/dist/src/docker/compose/operations/ComposeLogsOperation.js +85 -0
  25. package/dist/src/docker/compose/operations/index.d.ts +1 -0
  26. package/dist/src/docker/compose/operations/index.js +1 -0
  27. package/dist/src/index.d.ts +1 -0
  28. package/dist/src/index.js +1 -0
  29. package/dist/src/monorepo/monorepo.js +13 -5
  30. package/dist/src/monorepo/operations/shell/ExecuteLocalCommandOperation.js +40 -10
  31. package/dist/src/monorepo/operations/tasks/RunTasksOperation.d.ts +1 -1
  32. package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +1 -1
  33. package/dist/src/monorepo/plugins/VaultPlugin.d.ts +46 -0
  34. package/dist/src/monorepo/plugins/VaultPlugin.js +91 -0
  35. package/dist/src/monorepo/plugins/index.d.ts +1 -0
  36. package/dist/src/monorepo/plugins/index.js +3 -0
  37. package/dist/src/secrets/SecretDiscovery.d.ts +46 -0
  38. package/dist/src/secrets/SecretDiscovery.js +82 -0
  39. package/dist/src/secrets/SecretManager.d.ts +52 -0
  40. package/dist/src/secrets/SecretManager.js +75 -0
  41. package/dist/src/secrets/SecretProvider.d.ts +45 -0
  42. package/dist/src/secrets/SecretProvider.js +38 -0
  43. package/dist/src/secrets/index.d.ts +3 -0
  44. package/dist/src/secrets/index.js +3 -0
  45. package/dist/src/secrets/providers/VaultOidcHelper.d.ts +39 -0
  46. package/dist/src/secrets/providers/VaultOidcHelper.js +226 -0
  47. package/dist/src/secrets/providers/VaultProvider.d.ts +74 -0
  48. package/dist/src/secrets/providers/VaultProvider.js +266 -0
  49. package/dist/src/secrets/providers/VaultTokenCache.d.ts +60 -0
  50. package/dist/src/secrets/providers/VaultTokenCache.js +188 -0
  51. package/dist/src/secrets/providers/index.d.ts +2 -0
  52. package/dist/src/secrets/providers/index.js +2 -0
  53. package/dist/src/types.d.ts +2 -0
  54. package/dist/src/utils/TemplateExpander.d.ts +13 -1
  55. package/dist/src/utils/TemplateExpander.js +68 -15
  56. package/oclif.manifest.json +578 -173
  57. package/package.json +12 -5
@@ -0,0 +1,91 @@
1
+ import { getContext } from '../../context.js';
2
+ import { VaultProvider, } from '../../secrets/providers/VaultProvider.js';
3
+ import { AbstractPlugin } from './plugin.js';
4
+ /**
5
+ * Plugin that integrates HashiCorp Vault with EMB.
6
+ *
7
+ * Usage in .emb.yml:
8
+ * ```yaml
9
+ * plugins:
10
+ * - name: vault
11
+ * config:
12
+ * address: ${env:VAULT_ADDR:-http://localhost:8200}
13
+ * auth:
14
+ * method: token
15
+ * token: ${env:VAULT_TOKEN}
16
+ * ```
17
+ *
18
+ * Then use secrets in templates:
19
+ * ```yaml
20
+ * env:
21
+ * DB_PASSWORD: ${vault:secret/myapp/db#password}
22
+ * ```
23
+ */
24
+ export class VaultPlugin extends AbstractPlugin {
25
+ static name = 'vault';
26
+ provider = null;
27
+ async init() {
28
+ const resolvedConfig = this.resolveConfig();
29
+ this.provider = new VaultProvider(resolvedConfig);
30
+ await this.provider.connect();
31
+ // Register the provider with the global SecretManager
32
+ const context = getContext();
33
+ if (context?.secrets) {
34
+ context.secrets.register('vault', this.provider);
35
+ }
36
+ }
37
+ /**
38
+ * Resolve the plugin configuration, filling in defaults from env vars.
39
+ */
40
+ resolveConfig() {
41
+ const address = this.config.address || process.env.VAULT_ADDR;
42
+ if (!address) {
43
+ throw new Error('Vault address not configured. Set VAULT_ADDR environment variable or configure address in plugin config.');
44
+ }
45
+ const auth = this.resolveAuth();
46
+ return {
47
+ address,
48
+ namespace: this.config.namespace || process.env.VAULT_NAMESPACE,
49
+ auth,
50
+ };
51
+ }
52
+ /**
53
+ * Resolve authentication configuration.
54
+ */
55
+ resolveAuth() {
56
+ // If explicit auth config is provided, use it
57
+ if (this.config.auth) {
58
+ return this.config.auth;
59
+ }
60
+ // Try to infer from environment
61
+ const token = process.env.VAULT_TOKEN;
62
+ if (token) {
63
+ return { method: 'token', token };
64
+ }
65
+ const roleId = process.env.VAULT_ROLE_ID;
66
+ const secretId = process.env.VAULT_SECRET_ID;
67
+ if (roleId && secretId) {
68
+ return { method: 'approle', roleId, secretId };
69
+ }
70
+ const k8sRole = process.env.VAULT_K8S_ROLE;
71
+ if (k8sRole) {
72
+ return { method: 'kubernetes', role: k8sRole };
73
+ }
74
+ // JWT auth (non-interactive, for CI/CD)
75
+ const jwt = process.env.VAULT_JWT;
76
+ const jwtRole = process.env.VAULT_JWT_ROLE;
77
+ if (jwt && jwtRole) {
78
+ return { method: 'jwt', role: jwtRole, jwt };
79
+ }
80
+ // OIDC auth (interactive browser flow)
81
+ const oidcRole = process.env.VAULT_OIDC_ROLE;
82
+ if (oidcRole !== undefined) {
83
+ return { method: 'oidc', role: oidcRole || undefined };
84
+ }
85
+ throw new Error('Vault authentication not configured. ' +
86
+ 'Set VAULT_TOKEN, or VAULT_ROLE_ID + VAULT_SECRET_ID, ' +
87
+ 'or VAULT_K8S_ROLE, or VAULT_JWT + VAULT_JWT_ROLE, ' +
88
+ 'or VAULT_OIDC_ROLE environment variable, ' +
89
+ 'or configure auth in plugin config.');
90
+ }
91
+ }
@@ -2,6 +2,7 @@ import { AbstractPlugin } from './plugin.js';
2
2
  export * from './AutoDockerPlugin.js';
3
3
  export * from './DotEnvPlugin.js';
4
4
  export * from './EmbfileLoaderPlugin.js';
5
+ export * from './VaultPlugin.js';
5
6
  import { Monorepo } from '../index.js';
6
7
  export type AbstractPluginConstructor = new <C, P extends AbstractPlugin<C>>(config: C, monorepo: Monorepo) => P;
7
8
  export declare const registerPlugin: (plugin: AbstractPluginConstructor) => void;
@@ -1,9 +1,11 @@
1
1
  export * from './AutoDockerPlugin.js';
2
2
  export * from './DotEnvPlugin.js';
3
3
  export * from './EmbfileLoaderPlugin.js';
4
+ export * from './VaultPlugin.js';
4
5
  import { AutoDockerPlugin } from './AutoDockerPlugin.js';
5
6
  import { DotEnvPlugin } from './DotEnvPlugin.js';
6
7
  import { EmbfileLoaderPlugin } from './EmbfileLoaderPlugin.js';
8
+ import { VaultPlugin } from './VaultPlugin.js';
7
9
  const PluginRegistry = new Map();
8
10
  export const registerPlugin = (plugin) => {
9
11
  if (PluginRegistry.has(plugin.name)) {
@@ -21,3 +23,4 @@ export const getPlugin = (name) => {
21
23
  registerPlugin(AutoDockerPlugin);
22
24
  registerPlugin(DotEnvPlugin);
23
25
  registerPlugin(EmbfileLoaderPlugin);
26
+ registerPlugin(VaultPlugin);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Location where a secret reference was found.
3
+ */
4
+ export interface SecretLocation {
5
+ /** Component name (if applicable) */
6
+ component?: string;
7
+ /** Field path within the config (e.g., "env.DB_PASSWORD") */
8
+ field: string;
9
+ /** File path where the reference was found */
10
+ file?: string;
11
+ }
12
+ /**
13
+ * A discovered secret reference in the configuration.
14
+ */
15
+ export interface DiscoveredSecret {
16
+ /** Key within the secret (e.g., "password") */
17
+ key?: string;
18
+ /** Where this reference was found */
19
+ location: SecretLocation;
20
+ /** Original template string (e.g., "${vault:secret/myapp#password}") */
21
+ original: string;
22
+ /** Path to the secret (e.g., "secret/myapp/db") */
23
+ path: string;
24
+ /** Provider name (e.g., "vault") */
25
+ provider: string;
26
+ }
27
+ /**
28
+ * Discover all secret references in a configuration object.
29
+ *
30
+ * @param config - The configuration object to scan
31
+ * @param location - Base location information
32
+ * @param secretProviders - Set of registered secret provider names to look for
33
+ * @returns Array of discovered secret references
34
+ */
35
+ export declare function discoverSecrets(config: Record<string, unknown>, location?: Omit<SecretLocation, 'field'>, secretProviders?: Set<string>): DiscoveredSecret[];
36
+ /**
37
+ * Deduplicate secret references by provider+path+key.
38
+ * Keeps track of all locations where each secret is used.
39
+ */
40
+ export interface AggregatedSecret {
41
+ key?: string;
42
+ locations: SecretLocation[];
43
+ path: string;
44
+ provider: string;
45
+ }
46
+ export declare function aggregateSecrets(secrets: DiscoveredSecret[]): AggregatedSecret[];
@@ -0,0 +1,82 @@
1
+ // Matches ${provider:path#key} patterns for secret providers
2
+ // We're specifically looking for non-env providers (vault, aws, azure, etc.)
3
+ const SECRET_REGEX = /\${(\w+):([\w/.]+(?:-[\w/.]+)*)(?:#([\w-]+))?(?::-[^}]*)?}/g;
4
+ /**
5
+ * Recursively find all secret references in an object.
6
+ */
7
+ // eslint-disable-next-line max-params
8
+ function findSecretsInValue(value, fieldPath, location, secretProviders, results) {
9
+ if (typeof value === 'string') {
10
+ // Find all secret references in the string
11
+ let match;
12
+ SECRET_REGEX.lastIndex = 0; // Reset regex state
13
+ while ((match = SECRET_REGEX.exec(value)) !== null) {
14
+ const [original, provider, pathWithKey, explicitKey] = match;
15
+ // Only include registered secret providers
16
+ if (!secretProviders.has(provider)) {
17
+ continue;
18
+ }
19
+ // Parse path and key - key can be after # or part of path
20
+ let path = pathWithKey;
21
+ let key = explicitKey;
22
+ // If no explicit key via #, check if path contains #
23
+ if (!key && path.includes('#')) {
24
+ const hashIndex = path.indexOf('#');
25
+ key = path.slice(hashIndex + 1);
26
+ path = path.slice(0, hashIndex);
27
+ }
28
+ results.push({
29
+ provider,
30
+ path,
31
+ key,
32
+ original,
33
+ location: {
34
+ ...location,
35
+ field: fieldPath,
36
+ },
37
+ });
38
+ }
39
+ }
40
+ else if (Array.isArray(value)) {
41
+ value.forEach((item, index) => {
42
+ findSecretsInValue(item, `${fieldPath}[${index}]`, location, secretProviders, results);
43
+ });
44
+ }
45
+ else if (value !== null && typeof value === 'object') {
46
+ for (const [key, val] of Object.entries(value)) {
47
+ const newPath = fieldPath ? `${fieldPath}.${key}` : key;
48
+ findSecretsInValue(val, newPath, location, secretProviders, results);
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Discover all secret references in a configuration object.
54
+ *
55
+ * @param config - The configuration object to scan
56
+ * @param location - Base location information
57
+ * @param secretProviders - Set of registered secret provider names to look for
58
+ * @returns Array of discovered secret references
59
+ */
60
+ export function discoverSecrets(config, location = {}, secretProviders = new Set()) {
61
+ const results = [];
62
+ findSecretsInValue(config, '', location, secretProviders, results);
63
+ return results;
64
+ }
65
+ export function aggregateSecrets(secrets) {
66
+ const map = new Map();
67
+ for (const secret of secrets) {
68
+ const id = `${secret.provider}:${secret.path}#${secret.key || ''}`;
69
+ if (map.has(id)) {
70
+ map.get(id).locations.push(secret.location);
71
+ }
72
+ else {
73
+ map.set(id, {
74
+ provider: secret.provider,
75
+ path: secret.path,
76
+ key: secret.key,
77
+ locations: [secret.location],
78
+ });
79
+ }
80
+ }
81
+ return [...map.values()];
82
+ }
@@ -0,0 +1,52 @@
1
+ import { AbstractSecretProvider, SecretReference } from './SecretProvider.js';
2
+ /**
3
+ * Type for async source functions compatible with TemplateExpander.
4
+ */
5
+ export type AsyncSecretSource = (key: string) => Promise<unknown>;
6
+ /**
7
+ * Manages secret providers and creates template sources for them.
8
+ */
9
+ export declare class SecretManager {
10
+ private providers;
11
+ /**
12
+ * Register a secret provider.
13
+ * @param name Provider name (e.g., 'vault', 'op')
14
+ * @param provider The provider instance
15
+ */
16
+ register(name: string, provider: AbstractSecretProvider): void;
17
+ /**
18
+ * Get a registered provider by name.
19
+ * @param name Provider name
20
+ * @returns The provider instance or undefined if not found
21
+ */
22
+ get(name: string): AbstractSecretProvider | undefined;
23
+ /**
24
+ * Check if a provider is registered.
25
+ * @param name Provider name
26
+ */
27
+ has(name: string): boolean;
28
+ /**
29
+ * Get all registered provider names.
30
+ */
31
+ getProviderNames(): string[];
32
+ /**
33
+ * Connect all registered providers.
34
+ */
35
+ connectAll(): Promise<void>;
36
+ /**
37
+ * Disconnect all registered providers.
38
+ */
39
+ disconnectAll(): Promise<void>;
40
+ /**
41
+ * Parse a secret reference string into a SecretReference object.
42
+ * Format: "path/to/secret#key" or "path/to/secret"
43
+ * @param refString The reference string to parse
44
+ */
45
+ parseReference(refString: string): SecretReference;
46
+ /**
47
+ * Create an async source function for use with TemplateExpander.
48
+ * @param providerName The name of the provider to use
49
+ * @returns An async function that resolves secrets
50
+ */
51
+ createSource(providerName: string): AsyncSecretSource;
52
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Manages secret providers and creates template sources for them.
3
+ */
4
+ export class SecretManager {
5
+ providers = new Map();
6
+ /**
7
+ * Register a secret provider.
8
+ * @param name Provider name (e.g., 'vault', 'op')
9
+ * @param provider The provider instance
10
+ */
11
+ register(name, provider) {
12
+ if (this.providers.has(name)) {
13
+ throw new Error(`Secret provider '${name}' is already registered`);
14
+ }
15
+ this.providers.set(name, provider);
16
+ }
17
+ /**
18
+ * Get a registered provider by name.
19
+ * @param name Provider name
20
+ * @returns The provider instance or undefined if not found
21
+ */
22
+ get(name) {
23
+ return this.providers.get(name);
24
+ }
25
+ /**
26
+ * Check if a provider is registered.
27
+ * @param name Provider name
28
+ */
29
+ has(name) {
30
+ return this.providers.has(name);
31
+ }
32
+ /**
33
+ * Get all registered provider names.
34
+ */
35
+ getProviderNames() {
36
+ return [...this.providers.keys()];
37
+ }
38
+ /**
39
+ * Connect all registered providers.
40
+ */
41
+ async connectAll() {
42
+ await Promise.all([...this.providers.values()].map((p) => p.connect()));
43
+ }
44
+ /**
45
+ * Disconnect all registered providers.
46
+ */
47
+ async disconnectAll() {
48
+ await Promise.all([...this.providers.values()].map((p) => p.disconnect()));
49
+ }
50
+ /**
51
+ * Parse a secret reference string into a SecretReference object.
52
+ * Format: "path/to/secret#key" or "path/to/secret"
53
+ * @param refString The reference string to parse
54
+ */
55
+ parseReference(refString) {
56
+ const [path, key] = refString.split('#');
57
+ return { path, key };
58
+ }
59
+ /**
60
+ * Create an async source function for use with TemplateExpander.
61
+ * @param providerName The name of the provider to use
62
+ * @returns An async function that resolves secrets
63
+ */
64
+ createSource(providerName) {
65
+ return async (key) => {
66
+ const provider = this.get(providerName);
67
+ if (!provider) {
68
+ throw new Error(`Secret provider '${providerName}' not found. ` +
69
+ `Available providers: ${this.getProviderNames().join(', ') || 'none'}`);
70
+ }
71
+ const ref = this.parseReference(key);
72
+ return provider.get(ref);
73
+ };
74
+ }
75
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Reference to a secret in a provider.
3
+ */
4
+ export interface SecretReference {
5
+ /** Optional field within the secret */
6
+ key?: string;
7
+ /** Path to the secret, e.g., "secret/data/myapp/db" */
8
+ path: string;
9
+ /** Optional version of the secret */
10
+ version?: string;
11
+ }
12
+ /**
13
+ * Abstract base class for secret providers.
14
+ * Implementations should handle specific secret management systems
15
+ * (e.g., HashiCorp Vault, 1Password).
16
+ */
17
+ export declare abstract class AbstractSecretProvider<C = unknown> {
18
+ protected config: C;
19
+ protected cache: Map<string, Record<string, unknown>>;
20
+ constructor(config: C);
21
+ /**
22
+ * Connect to the secret provider and authenticate.
23
+ */
24
+ abstract connect(): Promise<void>;
25
+ /**
26
+ * Disconnect from the secret provider and clean up resources.
27
+ */
28
+ abstract disconnect(): Promise<void>;
29
+ /**
30
+ * Fetch a secret from the provider.
31
+ * @param ref Reference to the secret
32
+ * @returns The secret data as a key-value record
33
+ */
34
+ abstract fetchSecret(ref: SecretReference): Promise<Record<string, unknown>>;
35
+ /**
36
+ * Get a secret value, using cache if available.
37
+ * @param ref Reference to the secret
38
+ * @returns The secret value (entire record if no key specified, or specific field value)
39
+ */
40
+ get(ref: SecretReference): Promise<unknown>;
41
+ /**
42
+ * Clear the cache.
43
+ */
44
+ clearCache(): void;
45
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Abstract base class for secret providers.
3
+ * Implementations should handle specific secret management systems
4
+ * (e.g., HashiCorp Vault, 1Password).
5
+ */
6
+ export class AbstractSecretProvider {
7
+ config;
8
+ cache = new Map();
9
+ constructor(config) {
10
+ this.config = config;
11
+ }
12
+ /**
13
+ * Get a secret value, using cache if available.
14
+ * @param ref Reference to the secret
15
+ * @returns The secret value (entire record if no key specified, or specific field value)
16
+ */
17
+ async get(ref) {
18
+ const cacheKey = `${ref.path}:${ref.version || 'latest'}`;
19
+ if (!this.cache.has(cacheKey)) {
20
+ this.cache.set(cacheKey, await this.fetchSecret(ref));
21
+ }
22
+ const cached = this.cache.get(cacheKey);
23
+ if (ref.key) {
24
+ if (!(ref.key in cached)) {
25
+ const availableKeys = Object.keys(cached).join(', ') || 'none';
26
+ throw new Error(`Key '${ref.key}' not found in secret '${ref.path}'. Available keys: ${availableKeys}`);
27
+ }
28
+ return cached[ref.key];
29
+ }
30
+ return cached;
31
+ }
32
+ /**
33
+ * Clear the cache.
34
+ */
35
+ clearCache() {
36
+ this.cache.clear();
37
+ }
38
+ }
@@ -0,0 +1,3 @@
1
+ export * from './SecretDiscovery.js';
2
+ export * from './SecretManager.js';
3
+ export * from './SecretProvider.js';
@@ -0,0 +1,3 @@
1
+ export * from './SecretDiscovery.js';
2
+ export * from './SecretManager.js';
3
+ export * from './SecretProvider.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Options for the OIDC login flow.
3
+ */
4
+ export interface OidcLoginOptions {
5
+ /** Vault namespace (optional) */
6
+ namespace?: string;
7
+ /** Local port for the callback server (default: 8250) */
8
+ port?: number;
9
+ /** OIDC role to authenticate as (optional, uses default role if omitted) */
10
+ role?: string;
11
+ /** Timeout in milliseconds for the login flow (default: 120000 = 2 minutes) */
12
+ timeout?: number;
13
+ /** Vault server address */
14
+ vaultAddress: string;
15
+ }
16
+ /**
17
+ * Result of an OIDC login.
18
+ */
19
+ export interface OidcLoginResult {
20
+ /** The Vault client token */
21
+ token: string;
22
+ /** Token TTL in seconds */
23
+ ttlSeconds: number;
24
+ }
25
+ /**
26
+ * Perform an interactive OIDC login with Vault.
27
+ *
28
+ * This function:
29
+ * 1. Starts a local HTTP server to receive the callback
30
+ * 2. Requests an OIDC auth URL from Vault
31
+ * 3. Opens the user's browser to the auth URL
32
+ * 4. Waits for the callback with the Vault token
33
+ * 5. Returns the token and TTL
34
+ *
35
+ * @param options - OIDC login options
36
+ * @returns The Vault client token and TTL
37
+ * @throws VaultError if the login fails
38
+ */
39
+ export declare function performOidcLogin(options: OidcLoginOptions): Promise<OidcLoginResult>;