@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
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Secret provider registry for managing multiple secret providers.
3
+ */
4
+
5
+ import type {
6
+ SecretProvider,
7
+ SecretRefData,
8
+ SecretResolveOptions,
9
+ } from "./types.js";
10
+ import { SecretResolutionError } from "./types.js";
11
+
12
+ /**
13
+ * Registry for managing and accessing secret providers.
14
+ * Supports multiple providers simultaneously (e.g., env + aws + vault).
15
+ */
16
+ export class SecretProviderRegistry {
17
+ private providers = new Map<string, SecretProvider>();
18
+
19
+ /**
20
+ * Register a secret provider.
21
+ * @param provider - The provider to register
22
+ * @throws Error if a provider with the same name is already registered
23
+ */
24
+ register(provider: SecretProvider): void {
25
+ if (this.providers.has(provider.name)) {
26
+ throw new Error(
27
+ `Secret provider "${provider.name}" is already registered`,
28
+ );
29
+ }
30
+ this.providers.set(provider.name, provider);
31
+ }
32
+
33
+ /**
34
+ * Unregister a secret provider by name.
35
+ * @param name - The provider name to remove
36
+ * @returns true if the provider was removed, false if it wasn't registered
37
+ */
38
+ unregister(name: string): boolean {
39
+ return this.providers.delete(name);
40
+ }
41
+
42
+ /**
43
+ * Get a registered provider by name.
44
+ * @param name - The provider name
45
+ * @throws Error if the provider is not registered
46
+ */
47
+ get(name: string): SecretProvider {
48
+ const provider = this.providers.get(name);
49
+ if (!provider) {
50
+ const available = [...this.providers.keys()];
51
+ throw new SecretResolutionError(
52
+ `Secret provider "${name}" is not configured. Available providers: ${
53
+ available.length > 0 ? available.join(", ") : "(none)"
54
+ }`,
55
+ { provider: name, ref: "" },
56
+ );
57
+ }
58
+ return provider;
59
+ }
60
+
61
+ /**
62
+ * Check if a provider is registered.
63
+ */
64
+ has(name: string): boolean {
65
+ return this.providers.has(name);
66
+ }
67
+
68
+ /**
69
+ * Get all registered provider names.
70
+ */
71
+ getProviderNames(): string[] {
72
+ return [...this.providers.keys()];
73
+ }
74
+
75
+ /**
76
+ * Resolve a secret reference using the appropriate provider.
77
+ * @param secretRef - The secret reference data
78
+ * @returns The resolved secret value
79
+ * @throws SecretResolutionError if resolution fails
80
+ */
81
+ async resolve(secretRef: SecretRefData): Promise<string> {
82
+ const provider = this.get(secretRef.provider);
83
+
84
+ try {
85
+ return await provider.resolve(secretRef.ref, {
86
+ version: secretRef.version,
87
+ field: secretRef.field,
88
+ });
89
+ } catch (error) {
90
+ if (error instanceof SecretResolutionError) {
91
+ throw error;
92
+ }
93
+ throw new SecretResolutionError(
94
+ `Failed to resolve secret "${secretRef.provider}:${secretRef.ref}": ${
95
+ error instanceof Error ? error.message : String(error)
96
+ }`,
97
+ {
98
+ provider: secretRef.provider,
99
+ ref: secretRef.ref,
100
+ cause: error,
101
+ },
102
+ );
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Resolve multiple secrets, grouped by provider for efficiency.
108
+ * @param refs - Array of secret reference data
109
+ * @returns Map of "provider:ref" to resolved value
110
+ * @throws SecretResolutionError if any resolution fails (fail-fast)
111
+ */
112
+ async resolveMany(refs: SecretRefData[]): Promise<Map<string, string>> {
113
+ if (refs.length === 0) {
114
+ return new Map();
115
+ }
116
+
117
+ // Group refs by provider
118
+ const byProvider = new Map<
119
+ string,
120
+ Array<{ ref: string; options?: SecretResolveOptions; key: string }>
121
+ >();
122
+
123
+ for (const secretRef of refs) {
124
+ const key = this.makeKey(secretRef);
125
+ const group = byProvider.get(secretRef.provider) || [];
126
+ group.push({
127
+ ref: secretRef.ref,
128
+ options: {
129
+ version: secretRef.version,
130
+ field: secretRef.field,
131
+ },
132
+ key,
133
+ });
134
+ byProvider.set(secretRef.provider, group);
135
+ }
136
+
137
+ // Resolve each provider's secrets
138
+ const results = new Map<string, string>();
139
+
140
+ for (const [providerName, providerRefs] of byProvider) {
141
+ const provider = this.get(providerName);
142
+
143
+ // Use batch resolution if available, otherwise resolve individually
144
+ if (provider.resolveMany) {
145
+ const batchRefs = providerRefs.map((r) => ({
146
+ ref: r.ref,
147
+ options: r.options,
148
+ }));
149
+
150
+ try {
151
+ const batchResults = await provider.resolveMany(batchRefs);
152
+
153
+ for (const providerRef of providerRefs) {
154
+ const value = batchResults.get(providerRef.ref);
155
+ if (value === undefined) {
156
+ throw new SecretResolutionError(
157
+ `Secret "${providerName}:${providerRef.ref}" not found in batch results`,
158
+ { provider: providerName, ref: providerRef.ref },
159
+ );
160
+ }
161
+ results.set(providerRef.key, value);
162
+ }
163
+ } catch (error) {
164
+ if (error instanceof SecretResolutionError) {
165
+ throw error;
166
+ }
167
+ throw new SecretResolutionError(
168
+ `Batch resolution failed for provider "${providerName}": ${
169
+ error instanceof Error ? error.message : String(error)
170
+ }`,
171
+ {
172
+ provider: providerName,
173
+ ref: providerRefs[0]?.ref || "",
174
+ cause: error,
175
+ },
176
+ );
177
+ }
178
+ } else {
179
+ // Resolve individually (fail-fast on first error)
180
+ for (const providerRef of providerRefs) {
181
+ try {
182
+ const value = await provider.resolve(
183
+ providerRef.ref,
184
+ providerRef.options,
185
+ );
186
+ results.set(providerRef.key, value);
187
+ } catch (error) {
188
+ if (error instanceof SecretResolutionError) {
189
+ throw error;
190
+ }
191
+ throw new SecretResolutionError(
192
+ `Failed to resolve secret "${providerName}:${providerRef.ref}": ${
193
+ error instanceof Error ? error.message : String(error)
194
+ }`,
195
+ { provider: providerName, ref: providerRef.ref, cause: error },
196
+ );
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ return results;
203
+ }
204
+
205
+ /**
206
+ * Validate all registered providers.
207
+ * @throws Error if any provider validation fails
208
+ */
209
+ async validateAll(): Promise<void> {
210
+ for (const [name, provider] of this.providers) {
211
+ if (provider.validate) {
212
+ try {
213
+ await provider.validate();
214
+ } catch (error) {
215
+ throw new Error(
216
+ `Provider "${name}" validation failed: ${
217
+ error instanceof Error ? error.message : String(error)
218
+ }`,
219
+ );
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Create a unique key for a secret reference (for caching/deduplication).
227
+ */
228
+ makeKey(secretRef: SecretRefData): string {
229
+ const parts = [secretRef.provider, secretRef.ref];
230
+ if (secretRef.version) parts.push(`v:${secretRef.version}`);
231
+ if (secretRef.field) parts.push(`f:${secretRef.field}`);
232
+ return parts.join(":");
233
+ }
234
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Secret resolution utilities for test plans.
3
+ */
4
+
5
+ import { TestPlanV1, Node } from "griffin/types";
6
+ import { NodeType } from "griffin/schema";
7
+ import type { SecretProviderRegistry } from "./registry.js";
8
+ import type { SecretRef, SecretRefData } from "./types.js";
9
+ import { isSecretRef } from "./types.js";
10
+
11
+ /**
12
+ * Collected secret references from a plan.
13
+ */
14
+ interface CollectedSecrets {
15
+ /** All unique secret references found */
16
+ refs: SecretRefData[];
17
+ /** Paths where secrets were found (for substitution) */
18
+ paths: Array<{
19
+ path: (string | number)[];
20
+ secretRef: SecretRefData;
21
+ }>;
22
+ }
23
+
24
+ /**
25
+ * Recursively collect all secret references from a value.
26
+ * @param value - The value to scan
27
+ * @param currentPath - Current path in the object tree
28
+ * @param collected - Accumulator for found secrets
29
+ */
30
+ function collectSecretsFromValue(
31
+ value: unknown,
32
+ currentPath: (string | number)[],
33
+ collected: CollectedSecrets,
34
+ ): void {
35
+ if (value === null || value === undefined) {
36
+ return;
37
+ }
38
+
39
+ if (isSecretRef(value)) {
40
+ collected.refs.push(value.$secret);
41
+ collected.paths.push({
42
+ path: [...currentPath],
43
+ secretRef: value.$secret,
44
+ });
45
+ return;
46
+ }
47
+
48
+ if (Array.isArray(value)) {
49
+ for (let i = 0; i < value.length; i++) {
50
+ collectSecretsFromValue(value[i], [...currentPath, i], collected);
51
+ }
52
+ return;
53
+ }
54
+
55
+ if (typeof value === "object") {
56
+ for (const [key, val] of Object.entries(value)) {
57
+ collectSecretsFromValue(val, [...currentPath, key], collected);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Collect all secret references from a test plan.
64
+ * Scans endpoint headers and bodies for $secret markers.
65
+ */
66
+ export function collectSecretsFromPlan(plan: TestPlanV1): CollectedSecrets {
67
+ const collected: CollectedSecrets = {
68
+ refs: [],
69
+ paths: [],
70
+ };
71
+
72
+ for (let nodeIndex = 0; nodeIndex < plan.nodes.length; nodeIndex++) {
73
+ const node = plan.nodes[nodeIndex];
74
+
75
+ // Only endpoints can have secrets (in headers and body)
76
+ if (node.type !== NodeType.ENDPOINT) {
77
+ continue;
78
+ }
79
+
80
+ //const endpoint = node;
81
+
82
+ // Scan headers
83
+ if (node.headers) {
84
+ for (const [headerKey, headerValue] of Object.entries(node.headers)) {
85
+ collectSecretsFromValue(
86
+ headerValue,
87
+ ["nodes", nodeIndex, "headers", headerKey],
88
+ collected,
89
+ );
90
+ }
91
+ }
92
+
93
+ // Scan body
94
+ if (node.body !== undefined) {
95
+ collectSecretsFromValue(
96
+ node.body,
97
+ ["nodes", nodeIndex, "body"],
98
+ collected,
99
+ );
100
+ }
101
+ }
102
+
103
+ // Deduplicate refs by creating a unique key
104
+ const seen = new Set<string>();
105
+ const uniqueRefs: SecretRefData[] = [];
106
+
107
+ for (const ref of collected.refs) {
108
+ const key = `${ref.provider}:${ref.ref}:${ref.version || ""}:${ref.field || ""}`;
109
+ if (!seen.has(key)) {
110
+ seen.add(key);
111
+ uniqueRefs.push(ref);
112
+ }
113
+ }
114
+
115
+ collected.refs = uniqueRefs;
116
+ return collected;
117
+ }
118
+
119
+ /**
120
+ * Set a value at a path in an object.
121
+ * Creates intermediate objects/arrays as needed.
122
+ */
123
+ function setAtPath(
124
+ obj: unknown,
125
+ path: (string | number)[],
126
+ value: unknown,
127
+ ): void {
128
+ if (path.length === 0) {
129
+ return;
130
+ }
131
+
132
+ let current: any = obj;
133
+ for (let i = 0; i < path.length - 1; i++) {
134
+ const key = path[i];
135
+ if (current[key] === undefined) {
136
+ // Create intermediate object or array based on next key type
137
+ current[key] = typeof path[i + 1] === "number" ? [] : {};
138
+ }
139
+ current = current[key];
140
+ }
141
+
142
+ current[path[path.length - 1]] = value;
143
+ }
144
+
145
+ /**
146
+ * Deep clone a value.
147
+ */
148
+ function deepClone<T>(value: T): T {
149
+ if (value === null || value === undefined) {
150
+ return value;
151
+ }
152
+ return JSON.parse(JSON.stringify(value));
153
+ }
154
+
155
+ /**
156
+ * Resolve all secrets in a plan and return a new plan with substituted values.
157
+ * The original plan is not modified.
158
+ *
159
+ * @param plan - The test plan containing secret references
160
+ * @param registry - The secret provider registry
161
+ * @returns A new plan with all secrets resolved to their values
162
+ * @throws SecretResolutionError if any secret cannot be resolved (fail-fast)
163
+ */
164
+ export async function resolveSecretsInPlan(
165
+ plan: TestPlanV1,
166
+ registry: SecretProviderRegistry,
167
+ ): Promise<TestPlanV1> {
168
+ // Collect all secret references
169
+ const collected = collectSecretsFromPlan(plan);
170
+
171
+ if (collected.refs.length === 0) {
172
+ // No secrets to resolve
173
+ return plan;
174
+ }
175
+
176
+ // Resolve all secrets (fail-fast on any error)
177
+ const resolved = await registry.resolveMany(collected.refs);
178
+
179
+ // Clone the plan for modification
180
+ const resolvedPlan = deepClone(plan);
181
+
182
+ // Substitute resolved values at each path
183
+ for (const { path, secretRef } of collected.paths) {
184
+ const key = registry.makeKey(secretRef);
185
+ const value = resolved.get(key);
186
+
187
+ if (value === undefined) {
188
+ // This shouldn't happen if resolveMany worked correctly
189
+ throw new Error(
190
+ `Internal error: resolved value not found for secret "${secretRef.provider}:${secretRef.ref}"`,
191
+ );
192
+ }
193
+
194
+ setAtPath(resolvedPlan, path, value);
195
+ }
196
+
197
+ return resolvedPlan;
198
+ }
199
+
200
+ /**
201
+ * Check if a plan contains any secret references.
202
+ * Useful for short-circuiting resolution when no secrets are present.
203
+ */
204
+ export function planHasSecrets(plan: TestPlanV1): boolean {
205
+ for (const node of plan.nodes) {
206
+ if (node.type !== NodeType.ENDPOINT) {
207
+ continue;
208
+ }
209
+
210
+ // Check headers
211
+ if (node.headers) {
212
+ for (const headerValue of Object.values(node.headers)) {
213
+ if (isSecretRef(headerValue)) {
214
+ return true;
215
+ }
216
+ }
217
+ }
218
+
219
+ // Check body (recursive check)
220
+ if (node.body !== undefined && containsSecretRef(node.body)) {
221
+ return true;
222
+ }
223
+ }
224
+
225
+ return false;
226
+ }
227
+
228
+ /**
229
+ * Recursively check if a value contains any secret references.
230
+ */
231
+ function containsSecretRef(value: unknown): boolean {
232
+ if (value === null || value === undefined) {
233
+ return false;
234
+ }
235
+
236
+ if (isSecretRef(value)) {
237
+ return true;
238
+ }
239
+
240
+ if (Array.isArray(value)) {
241
+ return value.some(containsSecretRef);
242
+ }
243
+
244
+ if (typeof value === "object") {
245
+ return Object.values(value).some(containsSecretRef);
246
+ }
247
+
248
+ return false;
249
+ }