@griffin-app/griffin-plan-executor 0.1.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.
Files changed (114) hide show
  1. package/README.md +152 -0
  2. package/dist/adapters/axios.d.ts +5 -0
  3. package/dist/adapters/axios.d.ts.map +1 -0
  4. package/dist/adapters/axios.js +36 -0
  5. package/dist/adapters/axios.js.map +1 -0
  6. package/dist/adapters/index.d.ts +3 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +3 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/stub.d.ts +22 -0
  11. package/dist/adapters/stub.d.ts.map +1 -0
  12. package/dist/adapters/stub.js +36 -0
  13. package/dist/adapters/stub.js.map +1 -0
  14. package/dist/events/emitter.d.ts +68 -0
  15. package/dist/events/emitter.d.ts.map +1 -0
  16. package/dist/events/emitter.js +83 -0
  17. package/dist/events/emitter.js.map +1 -0
  18. package/dist/events/emitter.test.d.ts +2 -0
  19. package/dist/events/emitter.test.d.ts.map +1 -0
  20. package/dist/events/emitter.test.js +251 -0
  21. package/dist/events/emitter.test.js.map +1 -0
  22. package/dist/events/index.d.ts +3 -0
  23. package/dist/events/index.d.ts.map +1 -0
  24. package/dist/events/index.js +3 -0
  25. package/dist/events/index.js.map +1 -0
  26. package/dist/events/types.d.ts +109 -0
  27. package/dist/events/types.d.ts.map +1 -0
  28. package/dist/events/types.js +9 -0
  29. package/dist/events/types.js.map +1 -0
  30. package/dist/executor.d.ts +4 -0
  31. package/dist/executor.d.ts.map +1 -0
  32. package/dist/executor.js +732 -0
  33. package/dist/executor.js.map +1 -0
  34. package/dist/executor.test.d.ts +2 -0
  35. package/dist/executor.test.d.ts.map +1 -0
  36. package/dist/executor.test.js +1524 -0
  37. package/dist/executor.test.js.map +1 -0
  38. package/dist/index.d.ts +8 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +12 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/secrets/index.d.ts +14 -0
  43. package/dist/secrets/index.d.ts.map +1 -0
  44. package/dist/secrets/index.js +18 -0
  45. package/dist/secrets/index.js.map +1 -0
  46. package/dist/secrets/providers/aws.d.ts +63 -0
  47. package/dist/secrets/providers/aws.d.ts.map +1 -0
  48. package/dist/secrets/providers/aws.js +111 -0
  49. package/dist/secrets/providers/aws.js.map +1 -0
  50. package/dist/secrets/providers/env.d.ts +36 -0
  51. package/dist/secrets/providers/env.d.ts.map +1 -0
  52. package/dist/secrets/providers/env.js +37 -0
  53. package/dist/secrets/providers/env.js.map +1 -0
  54. package/dist/secrets/providers/index.d.ts +7 -0
  55. package/dist/secrets/providers/index.d.ts.map +1 -0
  56. package/dist/secrets/providers/index.js +7 -0
  57. package/dist/secrets/providers/index.js.map +1 -0
  58. package/dist/secrets/providers/vault.d.ts +75 -0
  59. package/dist/secrets/providers/vault.d.ts.map +1 -0
  60. package/dist/secrets/providers/vault.js +143 -0
  61. package/dist/secrets/providers/vault.js.map +1 -0
  62. package/dist/secrets/registry.d.ts +61 -0
  63. package/dist/secrets/registry.d.ts.map +1 -0
  64. package/dist/secrets/registry.js +182 -0
  65. package/dist/secrets/registry.js.map +1 -0
  66. package/dist/secrets/resolver.d.ts +40 -0
  67. package/dist/secrets/resolver.d.ts.map +1 -0
  68. package/dist/secrets/resolver.js +178 -0
  69. package/dist/secrets/resolver.js.map +1 -0
  70. package/dist/secrets/secrets.test.d.ts +2 -0
  71. package/dist/secrets/secrets.test.d.ts.map +1 -0
  72. package/dist/secrets/secrets.test.js +243 -0
  73. package/dist/secrets/secrets.test.js.map +1 -0
  74. package/dist/secrets/types.d.ts +71 -0
  75. package/dist/secrets/types.d.ts.map +1 -0
  76. package/dist/secrets/types.js +38 -0
  77. package/dist/secrets/types.js.map +1 -0
  78. package/dist/shared.d.ts +8 -0
  79. package/dist/shared.d.ts.map +1 -0
  80. package/dist/shared.js +30 -0
  81. package/dist/shared.js.map +1 -0
  82. package/dist/test-plan-types.d.ts +43 -0
  83. package/dist/test-plan-types.d.ts.map +1 -0
  84. package/dist/test-plan-types.js +2 -0
  85. package/dist/test-plan-types.js.map +1 -0
  86. package/dist/types.d.ts +77 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +3 -0
  89. package/dist/types.js.map +1 -0
  90. package/package.json +35 -0
  91. package/src/adapters/axios.ts +41 -0
  92. package/src/adapters/index.ts +2 -0
  93. package/src/adapters/stub.ts +47 -0
  94. package/src/events/emitter.test.ts +316 -0
  95. package/src/events/emitter.ts +133 -0
  96. package/src/events/index.ts +2 -0
  97. package/src/events/types.ts +132 -0
  98. package/src/executor.test.ts +1674 -0
  99. package/src/executor.ts +986 -0
  100. package/src/index.ts +69 -0
  101. package/src/secrets/index.ts +41 -0
  102. package/src/secrets/providers/aws.ts +179 -0
  103. package/src/secrets/providers/env.ts +66 -0
  104. package/src/secrets/providers/index.ts +15 -0
  105. package/src/secrets/providers/vault.ts +257 -0
  106. package/src/secrets/registry.ts +234 -0
  107. package/src/secrets/resolver.ts +249 -0
  108. package/src/secrets/secrets.test.ts +318 -0
  109. package/src/secrets/types.ts +105 -0
  110. package/src/shared.ts +46 -0
  111. package/src/test-plan-types.ts +49 -0
  112. package/src/types.ts +95 -0
  113. package/tsconfig.json +20 -0
  114. package/vitest.config.ts +14 -0
package/src/index.ts ADDED
@@ -0,0 +1,69 @@
1
+ export { executePlanV1 } from "./executor.js";
2
+ export type {
3
+ ExecutionOptions,
4
+ ExecutionResult,
5
+ NodeResult,
6
+ HttpClientAdapter,
7
+ HttpRequest,
8
+ HttpResponse,
9
+ } from "./types.js";
10
+ export type {
11
+ TestPlan,
12
+ Endpoint,
13
+ WaitNode,
14
+ AssertionNode,
15
+ Edge,
16
+ } from "./test-plan-types.js";
17
+ export {
18
+ AxiosAdapter,
19
+ StubAdapter,
20
+ type StubResponse,
21
+ } from "./adapters/index.js";
22
+
23
+ export {
24
+ LocalEventEmitter,
25
+ DurableEventEmitter,
26
+ type ExecutionEventEmitter,
27
+ type DurableEventBusAdapter,
28
+ } from "./events/emitter.js";
29
+ export type {
30
+ ExecutionEvent,
31
+ BaseEvent,
32
+ PlanStartEvent,
33
+ PlanEndEvent,
34
+ NodeStartEvent,
35
+ NodeEndEvent,
36
+ HttpRequestEvent,
37
+ HttpResponseEvent,
38
+ HttpRetryEvent,
39
+ AssertionResultEvent,
40
+ WaitStartEvent,
41
+ NodeStreamEvent,
42
+ ErrorEvent,
43
+ } from "./events/types.js";
44
+
45
+ // Export secrets system
46
+ export {
47
+ // Core types and utilities
48
+ type SecretProvider,
49
+ type SecretRef,
50
+ type SecretRefData,
51
+ type SecretResolveOptions,
52
+ SecretResolutionError,
53
+ isSecretRef,
54
+ // Registry
55
+ SecretProviderRegistry,
56
+ // Resolution utilities
57
+ resolveSecretsInPlan,
58
+ collectSecretsFromPlan,
59
+ planHasSecrets,
60
+ // Providers
61
+ EnvSecretProvider,
62
+ type EnvSecretProviderOptions,
63
+ AwsSecretsManagerProvider,
64
+ type AwsSecretsManagerProviderOptions,
65
+ type AwsSecretsManagerClient,
66
+ VaultProvider,
67
+ type VaultProviderOptions,
68
+ type VaultHttpClient,
69
+ } from "./secrets/index.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Secret management for griffin plan executor.
3
+ *
4
+ * This module provides:
5
+ * - SecretProvider interface for implementing custom providers
6
+ * - SecretProviderRegistry for managing multiple providers
7
+ * - Secret resolution utilities for test plans
8
+ * - Built-in providers: env, aws, vault
9
+ */
10
+
11
+ // Core types
12
+ export {
13
+ type SecretProvider,
14
+ type SecretRef,
15
+ type SecretRefData,
16
+ type SecretResolveOptions,
17
+ SecretResolutionError,
18
+ isSecretRef,
19
+ } from "./types.js";
20
+
21
+ // Registry
22
+ export { SecretProviderRegistry } from "./registry.js";
23
+
24
+ // Resolution utilities
25
+ export {
26
+ resolveSecretsInPlan,
27
+ collectSecretsFromPlan,
28
+ planHasSecrets,
29
+ } from "./resolver.js";
30
+
31
+ // Providers
32
+ export {
33
+ EnvSecretProvider,
34
+ type EnvSecretProviderOptions,
35
+ AwsSecretsManagerProvider,
36
+ type AwsSecretsManagerProviderOptions,
37
+ type AwsSecretsManagerClient,
38
+ VaultProvider,
39
+ type VaultProviderOptions,
40
+ type VaultHttpClient,
41
+ } from "./providers/index.js";
@@ -0,0 +1,179 @@
1
+ /**
2
+ * AWS Secrets Manager secret provider.
3
+ *
4
+ * Reads secrets from AWS Secrets Manager. Supports JSON secrets
5
+ * with field extraction and version staging.
6
+ *
7
+ * Usage in DSL:
8
+ * secret("aws:my-secret")
9
+ * secret("aws:prod/api-keys", { field: "stripe" })
10
+ * secret("aws:my-secret", { version: "AWSPREVIOUS" })
11
+ */
12
+
13
+ import type { SecretProvider, SecretResolveOptions } from "../types.js";
14
+ import { SecretResolutionError } from "../types.js";
15
+
16
+ /**
17
+ * Interface for AWS Secrets Manager client.
18
+ * This allows dependency injection of the actual AWS SDK client.
19
+ */
20
+ export interface AwsSecretsManagerClient {
21
+ getSecretValue(params: { SecretId: string; VersionStage?: string }): Promise<{
22
+ SecretString?: string;
23
+ SecretBinary?: Uint8Array;
24
+ }>;
25
+ }
26
+
27
+ export interface AwsSecretsManagerProviderOptions {
28
+ /**
29
+ * AWS Secrets Manager client instance.
30
+ * Should be pre-configured with region and credentials.
31
+ */
32
+ client: AwsSecretsManagerClient;
33
+
34
+ /**
35
+ * Optional prefix for secret names.
36
+ * For example, if prefix is "myapp/", then secret("aws:api-key")
37
+ * will look for "myapp/api-key" in Secrets Manager.
38
+ */
39
+ prefix?: string;
40
+
41
+ /**
42
+ * Default version stage to use if not specified.
43
+ * Defaults to "AWSCURRENT".
44
+ */
45
+ defaultVersionStage?: string;
46
+ }
47
+
48
+ export class AwsSecretsManagerProvider implements SecretProvider {
49
+ readonly name = "aws";
50
+ private readonly client: AwsSecretsManagerClient;
51
+ private readonly prefix: string;
52
+ private readonly defaultVersionStage: string;
53
+
54
+ // Simple in-memory cache with TTL
55
+ private cache = new Map<string, { value: string; expires: number }>();
56
+ private readonly cacheTtlMs = 5 * 60 * 1000; // 5 minutes
57
+
58
+ constructor(options: AwsSecretsManagerProviderOptions) {
59
+ this.client = options.client;
60
+ this.prefix = options.prefix ?? "";
61
+ this.defaultVersionStage = options.defaultVersionStage ?? "AWSCURRENT";
62
+ }
63
+
64
+ async resolve(ref: string, options?: SecretResolveOptions): Promise<string> {
65
+ const secretId = this.prefix + ref;
66
+ const versionStage = options?.version ?? this.defaultVersionStage;
67
+ const cacheKey = `${secretId}:${versionStage}`;
68
+
69
+ // Check cache
70
+ const cached = this.cache.get(cacheKey);
71
+ if (cached && cached.expires > Date.now()) {
72
+ return this.extractField(cached.value, options?.field, ref);
73
+ }
74
+
75
+ try {
76
+ const response = await this.client.getSecretValue({
77
+ SecretId: secretId,
78
+ VersionStage: versionStage,
79
+ });
80
+
81
+ if (!response.SecretString) {
82
+ throw new SecretResolutionError(
83
+ `Secret "${secretId}" does not contain a string value (binary secrets are not supported)`,
84
+ { provider: this.name, ref },
85
+ );
86
+ }
87
+
88
+ // Cache the raw value
89
+ this.cache.set(cacheKey, {
90
+ value: response.SecretString,
91
+ expires: Date.now() + this.cacheTtlMs,
92
+ });
93
+
94
+ return this.extractField(response.SecretString, options?.field, ref);
95
+ } catch (error) {
96
+ if (error instanceof SecretResolutionError) {
97
+ throw error;
98
+ }
99
+
100
+ // Handle common AWS errors
101
+ const awsError = error as { name?: string; message?: string };
102
+ let message = `Failed to retrieve secret "${secretId}"`;
103
+
104
+ if (awsError.name === "ResourceNotFoundException") {
105
+ message = `Secret "${secretId}" not found in AWS Secrets Manager`;
106
+ } else if (awsError.name === "AccessDeniedException") {
107
+ message = `Access denied to secret "${secretId}". Check IAM permissions.`;
108
+ } else if (awsError.message) {
109
+ message = `${message}: ${awsError.message}`;
110
+ }
111
+
112
+ throw new SecretResolutionError(message, {
113
+ provider: this.name,
114
+ ref,
115
+ cause: error,
116
+ });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Extract a field from a JSON secret string.
122
+ */
123
+ private extractField(
124
+ secretValue: string,
125
+ field: string | undefined,
126
+ ref: string,
127
+ ): string {
128
+ if (!field) {
129
+ return secretValue;
130
+ }
131
+
132
+ try {
133
+ const parsed = JSON.parse(secretValue);
134
+
135
+ if (typeof parsed !== "object" || parsed === null) {
136
+ throw new SecretResolutionError(
137
+ `Secret "${ref}" is not a JSON object, cannot extract field "${field}"`,
138
+ { provider: this.name, ref },
139
+ );
140
+ }
141
+
142
+ const value = parsed[field];
143
+
144
+ if (value === undefined) {
145
+ throw new SecretResolutionError(
146
+ `Field "${field}" not found in secret "${ref}"`,
147
+ { provider: this.name, ref },
148
+ );
149
+ }
150
+
151
+ // Convert to string if not already
152
+ return typeof value === "string" ? value : JSON.stringify(value);
153
+ } catch (error) {
154
+ if (error instanceof SecretResolutionError) {
155
+ throw error;
156
+ }
157
+
158
+ throw new SecretResolutionError(
159
+ `Failed to parse secret "${ref}" as JSON for field extraction: ${
160
+ error instanceof Error ? error.message : String(error)
161
+ }`,
162
+ { provider: this.name, ref, cause: error },
163
+ );
164
+ }
165
+ }
166
+
167
+ async validate(): Promise<void> {
168
+ // Try a simple operation to verify credentials
169
+ // This is a no-op if the client is properly configured
170
+ // The actual validation happens on first secret access
171
+ }
172
+
173
+ /**
174
+ * Clear the cache. Useful for testing or forced refresh.
175
+ */
176
+ clearCache(): void {
177
+ this.cache.clear();
178
+ }
179
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Environment variable secret provider.
3
+ *
4
+ * Reads secrets from process.env. Useful for local development
5
+ * and simple deployments where secrets are injected as environment variables.
6
+ *
7
+ * Usage in DSL:
8
+ * secret("env:MY_API_KEY")
9
+ * secret("env:DATABASE_URL")
10
+ */
11
+
12
+ import type { SecretProvider, SecretResolveOptions } from "../types.js";
13
+ import { SecretResolutionError } from "../types.js";
14
+
15
+ export interface EnvSecretProviderOptions {
16
+ /**
17
+ * Custom environment object to read from.
18
+ * Defaults to process.env.
19
+ */
20
+ env?: Record<string, string | undefined>;
21
+
22
+ /**
23
+ * Prefix to strip from secret refs.
24
+ * For example, if prefix is "APP_", then secret("env:API_KEY")
25
+ * will look for "APP_API_KEY" in the environment.
26
+ */
27
+ prefix?: string;
28
+ }
29
+
30
+ export class EnvSecretProvider implements SecretProvider {
31
+ readonly name = "env";
32
+ private readonly env: Record<string, string | undefined>;
33
+ private readonly prefix: string;
34
+
35
+ constructor(options: EnvSecretProviderOptions = {}) {
36
+ this.env = options.env ?? process.env;
37
+ this.prefix = options.prefix ?? "";
38
+ }
39
+
40
+ async resolve(ref: string, _options?: SecretResolveOptions): Promise<string> {
41
+ const envKey = this.prefix + ref;
42
+ const value = this.env[envKey];
43
+
44
+ if (value === undefined) {
45
+ throw new SecretResolutionError(
46
+ `Environment variable "${envKey}" is not set`,
47
+ { provider: this.name, ref },
48
+ );
49
+ }
50
+
51
+ return value;
52
+ }
53
+
54
+ async resolveMany(
55
+ refs: Array<{ ref: string; options?: SecretResolveOptions }>,
56
+ ): Promise<Map<string, string>> {
57
+ const results = new Map<string, string>();
58
+
59
+ for (const { ref } of refs) {
60
+ const value = await this.resolve(ref);
61
+ results.set(ref, value);
62
+ }
63
+
64
+ return results;
65
+ }
66
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Secret provider implementations.
3
+ */
4
+
5
+ export { EnvSecretProvider, type EnvSecretProviderOptions } from "./env.js";
6
+ export {
7
+ AwsSecretsManagerProvider,
8
+ type AwsSecretsManagerProviderOptions,
9
+ type AwsSecretsManagerClient,
10
+ } from "./aws.js";
11
+ export {
12
+ VaultProvider,
13
+ type VaultProviderOptions,
14
+ type VaultHttpClient,
15
+ } from "./vault.js";
@@ -0,0 +1,257 @@
1
+ /**
2
+ * HashiCorp Vault secret provider.
3
+ *
4
+ * Reads secrets from HashiCorp Vault KV secrets engine (v1 and v2).
5
+ * Supports field extraction from JSON secrets.
6
+ *
7
+ * Usage in DSL:
8
+ * secret("vault:secret/data/myapp/config")
9
+ * secret("vault:secret/data/myapp/config", { field: "api_key" })
10
+ * secret("vault:secret/data/myapp/config", { version: "2" })
11
+ */
12
+
13
+ import type { SecretProvider, SecretResolveOptions } from "../types.js";
14
+ import { SecretResolutionError } from "../types.js";
15
+
16
+ /**
17
+ * HTTP client interface for Vault API calls.
18
+ * This allows dependency injection without requiring a specific HTTP library.
19
+ */
20
+ export interface VaultHttpClient {
21
+ get(
22
+ url: string,
23
+ options: { headers: Record<string, string> },
24
+ ): Promise<{
25
+ status: number;
26
+ data: unknown;
27
+ }>;
28
+ }
29
+
30
+ export interface VaultProviderOptions {
31
+ /**
32
+ * Vault server address (e.g., "https://vault.example.com:8200").
33
+ */
34
+ address: string;
35
+
36
+ /**
37
+ * Authentication token.
38
+ */
39
+ token: string;
40
+
41
+ /**
42
+ * HTTP client for making requests to Vault.
43
+ */
44
+ httpClient: VaultHttpClient;
45
+
46
+ /**
47
+ * Optional namespace for Vault Enterprise.
48
+ */
49
+ namespace?: string;
50
+
51
+ /**
52
+ * KV secrets engine version (1 or 2).
53
+ * Defaults to 2.
54
+ */
55
+ kvVersion?: 1 | 2;
56
+
57
+ /**
58
+ * Optional prefix for secret paths.
59
+ */
60
+ prefix?: string;
61
+ }
62
+
63
+ export class VaultProvider implements SecretProvider {
64
+ readonly name = "vault";
65
+ private readonly address: string;
66
+ private readonly token: string;
67
+ private readonly httpClient: VaultHttpClient;
68
+ private readonly namespace?: string;
69
+ private readonly kvVersion: 1 | 2;
70
+ private readonly prefix: string;
71
+
72
+ // Simple in-memory cache with TTL
73
+ private cache = new Map<
74
+ string,
75
+ { value: Record<string, unknown>; expires: number }
76
+ >();
77
+ private readonly cacheTtlMs = 5 * 60 * 1000; // 5 minutes
78
+
79
+ constructor(options: VaultProviderOptions) {
80
+ this.address = options.address.replace(/\/$/, ""); // Remove trailing slash
81
+ this.token = options.token;
82
+ this.httpClient = options.httpClient;
83
+ this.namespace = options.namespace;
84
+ this.kvVersion = options.kvVersion ?? 2;
85
+ this.prefix = options.prefix ?? "";
86
+ }
87
+
88
+ async resolve(ref: string, options?: SecretResolveOptions): Promise<string> {
89
+ const secretPath = this.prefix + ref;
90
+ const version = options?.version;
91
+ const cacheKey = `${secretPath}:${version ?? "latest"}`;
92
+
93
+ // Check cache
94
+ const cached = this.cache.get(cacheKey);
95
+ if (cached && cached.expires > Date.now()) {
96
+ return this.extractField(cached.value, options?.field, ref);
97
+ }
98
+
99
+ try {
100
+ // Build request URL
101
+ let url = `${this.address}/v1/${secretPath}`;
102
+ if (this.kvVersion === 2 && version) {
103
+ url += `?version=${version}`;
104
+ }
105
+
106
+ // Build headers
107
+ const headers: Record<string, string> = {
108
+ "X-Vault-Token": this.token,
109
+ };
110
+ if (this.namespace) {
111
+ headers["X-Vault-Namespace"] = this.namespace;
112
+ }
113
+
114
+ const response = await this.httpClient.get(url, { headers });
115
+
116
+ if (response.status === 404) {
117
+ throw new SecretResolutionError(
118
+ `Secret "${secretPath}" not found in Vault`,
119
+ { provider: this.name, ref },
120
+ );
121
+ }
122
+
123
+ if (response.status === 403) {
124
+ throw new SecretResolutionError(
125
+ `Access denied to secret "${secretPath}". Check Vault policies.`,
126
+ { provider: this.name, ref },
127
+ );
128
+ }
129
+
130
+ if (response.status !== 200) {
131
+ throw new SecretResolutionError(
132
+ `Vault returned status ${response.status} for secret "${secretPath}"`,
133
+ { provider: this.name, ref },
134
+ );
135
+ }
136
+
137
+ // Parse response based on KV version
138
+ const data = response.data as {
139
+ data?: Record<string, unknown> | { data?: Record<string, unknown> };
140
+ };
141
+
142
+ let secretData: Record<string, unknown>;
143
+
144
+ if (this.kvVersion === 2) {
145
+ // KV v2 wraps data in an extra "data" object
146
+ const kvData = data?.data as
147
+ | { data?: Record<string, unknown> }
148
+ | undefined;
149
+ if (!kvData?.data) {
150
+ throw new SecretResolutionError(
151
+ `Invalid KV v2 response structure for secret "${secretPath}"`,
152
+ { provider: this.name, ref },
153
+ );
154
+ }
155
+ secretData = kvData.data;
156
+ } else {
157
+ // KV v1 has data directly
158
+ if (!data?.data || typeof data.data !== "object") {
159
+ throw new SecretResolutionError(
160
+ `Invalid KV v1 response structure for secret "${secretPath}"`,
161
+ { provider: this.name, ref },
162
+ );
163
+ }
164
+ secretData = data.data as Record<string, unknown>;
165
+ }
166
+
167
+ // Cache the secret data
168
+ this.cache.set(cacheKey, {
169
+ value: secretData,
170
+ expires: Date.now() + this.cacheTtlMs,
171
+ });
172
+
173
+ return this.extractField(secretData, options?.field, ref);
174
+ } catch (error) {
175
+ if (error instanceof SecretResolutionError) {
176
+ throw error;
177
+ }
178
+
179
+ throw new SecretResolutionError(
180
+ `Failed to retrieve secret "${secretPath}" from Vault: ${
181
+ error instanceof Error ? error.message : String(error)
182
+ }`,
183
+ { provider: this.name, ref, cause: error },
184
+ );
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Extract a field from the secret data.
190
+ * If no field is specified, returns the entire data as JSON string.
191
+ */
192
+ private extractField(
193
+ secretData: Record<string, unknown>,
194
+ field: string | undefined,
195
+ ref: string,
196
+ ): string {
197
+ if (!field) {
198
+ // Return entire secret as JSON if no field specified
199
+ return JSON.stringify(secretData);
200
+ }
201
+
202
+ const value = secretData[field];
203
+
204
+ if (value === undefined) {
205
+ throw new SecretResolutionError(
206
+ `Field "${field}" not found in secret "${ref}"`,
207
+ { provider: this.name, ref },
208
+ );
209
+ }
210
+
211
+ // Convert to string if not already
212
+ return typeof value === "string" ? value : JSON.stringify(value);
213
+ }
214
+
215
+ async validate(): Promise<void> {
216
+ // Verify we can authenticate with Vault
217
+ try {
218
+ const headers: Record<string, string> = {
219
+ "X-Vault-Token": this.token,
220
+ };
221
+ if (this.namespace) {
222
+ headers["X-Vault-Namespace"] = this.namespace;
223
+ }
224
+
225
+ const response = await this.httpClient.get(
226
+ `${this.address}/v1/auth/token/lookup-self`,
227
+ { headers },
228
+ );
229
+
230
+ if (response.status === 403) {
231
+ throw new Error("Invalid or expired Vault token");
232
+ }
233
+
234
+ if (response.status !== 200) {
235
+ throw new Error(
236
+ `Vault authentication check failed with status ${response.status}`,
237
+ );
238
+ }
239
+ } catch (error) {
240
+ if (error instanceof Error && error.message.includes("Vault")) {
241
+ throw error;
242
+ }
243
+ throw new Error(
244
+ `Failed to connect to Vault at ${this.address}: ${
245
+ error instanceof Error ? error.message : String(error)
246
+ }`,
247
+ );
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Clear the cache. Useful for testing or forced refresh.
253
+ */
254
+ clearCache(): void {
255
+ this.cache.clear();
256
+ }
257
+ }