@griffin-app/griffin-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 (153) 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/adapters/in-memory.d.ts +52 -0
  15. package/dist/events/adapters/in-memory.d.ts.map +1 -0
  16. package/dist/events/adapters/in-memory.js +70 -0
  17. package/dist/events/adapters/in-memory.js.map +1 -0
  18. package/dist/events/adapters/in-memory.test.d.ts +2 -0
  19. package/dist/events/adapters/in-memory.test.d.ts.map +1 -0
  20. package/dist/events/adapters/in-memory.test.js +109 -0
  21. package/dist/events/adapters/in-memory.test.js.map +1 -0
  22. package/dist/events/adapters/index.d.ts +9 -0
  23. package/dist/events/adapters/index.d.ts.map +1 -0
  24. package/dist/events/adapters/index.js +9 -0
  25. package/dist/events/adapters/index.js.map +1 -0
  26. package/dist/events/adapters/kinesis.d.ts +91 -0
  27. package/dist/events/adapters/kinesis.d.ts.map +1 -0
  28. package/dist/events/adapters/kinesis.js +136 -0
  29. package/dist/events/adapters/kinesis.js.map +1 -0
  30. package/dist/events/adapters/kinesis.test.d.ts +2 -0
  31. package/dist/events/adapters/kinesis.test.d.ts.map +1 -0
  32. package/dist/events/adapters/kinesis.test.js +249 -0
  33. package/dist/events/adapters/kinesis.test.js.map +1 -0
  34. package/dist/events/emitter.d.ts +68 -0
  35. package/dist/events/emitter.d.ts.map +1 -0
  36. package/dist/events/emitter.js +83 -0
  37. package/dist/events/emitter.js.map +1 -0
  38. package/dist/events/emitter.test.d.ts +2 -0
  39. package/dist/events/emitter.test.d.ts.map +1 -0
  40. package/dist/events/emitter.test.js +262 -0
  41. package/dist/events/emitter.test.js.map +1 -0
  42. package/dist/events/index.d.ts +4 -0
  43. package/dist/events/index.d.ts.map +1 -0
  44. package/dist/events/index.js +4 -0
  45. package/dist/events/index.js.map +1 -0
  46. package/dist/events/types.d.ts +112 -0
  47. package/dist/events/types.d.ts.map +1 -0
  48. package/dist/events/types.js +9 -0
  49. package/dist/events/types.js.map +1 -0
  50. package/dist/executor.d.ts +4 -0
  51. package/dist/executor.d.ts.map +1 -0
  52. package/dist/executor.js +799 -0
  53. package/dist/executor.js.map +1 -0
  54. package/dist/executor.test.d.ts +2 -0
  55. package/dist/executor.test.d.ts.map +1 -0
  56. package/dist/executor.test.js +1584 -0
  57. package/dist/executor.test.js.map +1 -0
  58. package/dist/index.d.ts +9 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +15 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/secrets/factory.d.ts +121 -0
  63. package/dist/secrets/factory.d.ts.map +1 -0
  64. package/dist/secrets/factory.js +137 -0
  65. package/dist/secrets/factory.js.map +1 -0
  66. package/dist/secrets/index.d.ts +14 -0
  67. package/dist/secrets/index.d.ts.map +1 -0
  68. package/dist/secrets/index.js +18 -0
  69. package/dist/secrets/index.js.map +1 -0
  70. package/dist/secrets/providers/aws.d.ts +63 -0
  71. package/dist/secrets/providers/aws.d.ts.map +1 -0
  72. package/dist/secrets/providers/aws.js +110 -0
  73. package/dist/secrets/providers/aws.js.map +1 -0
  74. package/dist/secrets/providers/env.d.ts +36 -0
  75. package/dist/secrets/providers/env.d.ts.map +1 -0
  76. package/dist/secrets/providers/env.js +37 -0
  77. package/dist/secrets/providers/env.js.map +1 -0
  78. package/dist/secrets/providers/index.d.ts +7 -0
  79. package/dist/secrets/providers/index.d.ts.map +1 -0
  80. package/dist/secrets/providers/index.js +7 -0
  81. package/dist/secrets/providers/index.js.map +1 -0
  82. package/dist/secrets/providers/vault.d.ts +75 -0
  83. package/dist/secrets/providers/vault.d.ts.map +1 -0
  84. package/dist/secrets/providers/vault.js +143 -0
  85. package/dist/secrets/providers/vault.js.map +1 -0
  86. package/dist/secrets/registry.d.ts +39 -0
  87. package/dist/secrets/registry.d.ts.map +1 -0
  88. package/dist/secrets/registry.js +134 -0
  89. package/dist/secrets/registry.js.map +1 -0
  90. package/dist/secrets/resolver.d.ts +45 -0
  91. package/dist/secrets/resolver.d.ts.map +1 -0
  92. package/dist/secrets/resolver.js +188 -0
  93. package/dist/secrets/resolver.js.map +1 -0
  94. package/dist/secrets/secrets.test.d.ts +2 -0
  95. package/dist/secrets/secrets.test.d.ts.map +1 -0
  96. package/dist/secrets/secrets.test.js +317 -0
  97. package/dist/secrets/secrets.test.js.map +1 -0
  98. package/dist/secrets/types.d.ts +70 -0
  99. package/dist/secrets/types.d.ts.map +1 -0
  100. package/dist/secrets/types.js +42 -0
  101. package/dist/secrets/types.js.map +1 -0
  102. package/dist/shared.d.ts +8 -0
  103. package/dist/shared.d.ts.map +1 -0
  104. package/dist/shared.js +30 -0
  105. package/dist/shared.js.map +1 -0
  106. package/dist/test-monitor-types.d.ts +43 -0
  107. package/dist/test-monitor-types.d.ts.map +1 -0
  108. package/dist/test-monitor-types.js +2 -0
  109. package/dist/test-monitor-types.js.map +1 -0
  110. package/dist/test-plan-types.d.ts +43 -0
  111. package/dist/test-plan-types.d.ts.map +1 -0
  112. package/dist/test-plan-types.js +2 -0
  113. package/dist/test-plan-types.js.map +1 -0
  114. package/dist/types.d.ts +93 -0
  115. package/dist/types.d.ts.map +1 -0
  116. package/dist/types.js +3 -0
  117. package/dist/types.js.map +1 -0
  118. package/dist/utils/dates.d.ts +11 -0
  119. package/dist/utils/dates.d.ts.map +1 -0
  120. package/dist/utils/dates.js +13 -0
  121. package/dist/utils/dates.js.map +1 -0
  122. package/package.json +39 -0
  123. package/src/adapters/axios.ts +39 -0
  124. package/src/adapters/index.ts +2 -0
  125. package/src/adapters/stub.ts +47 -0
  126. package/src/events/adapters/README.md +144 -0
  127. package/src/events/adapters/in-memory.test.ts +146 -0
  128. package/src/events/adapters/in-memory.ts +93 -0
  129. package/src/events/adapters/index.ts +9 -0
  130. package/src/events/adapters/kinesis.test.ts +323 -0
  131. package/src/events/adapters/kinesis.ts +211 -0
  132. package/src/events/emitter.test.ts +327 -0
  133. package/src/events/emitter.ts +133 -0
  134. package/src/events/index.ts +3 -0
  135. package/src/events/types.ts +136 -0
  136. package/src/executor.test.ts +1732 -0
  137. package/src/executor.ts +1075 -0
  138. package/src/index.ts +81 -0
  139. package/src/secrets/factory.ts +248 -0
  140. package/src/secrets/index.ts +48 -0
  141. package/src/secrets/providers/aws.ts +178 -0
  142. package/src/secrets/providers/env.ts +66 -0
  143. package/src/secrets/providers/index.ts +15 -0
  144. package/src/secrets/providers/vault.ts +257 -0
  145. package/src/secrets/resolver.ts +269 -0
  146. package/src/secrets/secrets.test.ts +402 -0
  147. package/src/secrets/types.ts +106 -0
  148. package/src/shared.ts +46 -0
  149. package/src/test-monitor-types.ts +49 -0
  150. package/src/types.ts +114 -0
  151. package/src/utils/dates.ts +13 -0
  152. package/tsconfig.json +20 -0
  153. package/vitest.config.ts +14 -0
@@ -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
+ { ref },
120
+ );
121
+ }
122
+
123
+ if (response.status === 403) {
124
+ throw new SecretResolutionError(
125
+ `Access denied to secret "${secretPath}". Check Vault policies.`,
126
+ { ref },
127
+ );
128
+ }
129
+
130
+ if (response.status !== 200) {
131
+ throw new SecretResolutionError(
132
+ `Vault returned status ${response.status} for secret "${secretPath}"`,
133
+ { 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
+ { 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
+ { 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
+ { 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
+ { 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
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Secret resolution utilities for test monitors.
3
+ */
4
+ import { type MonitorV1 } from "@griffin-app/griffin-hub-sdk";
5
+ import type { SecretProvider } from "./types.js";
6
+ import type { SecretRef, SecretRefData } from "./types.js";
7
+ import { isSecretRef, isStringLiteral, SecretResolutionError } from "./types.js";
8
+
9
+ /**
10
+ * Collected secret references and literals from a monitor.
11
+ */
12
+ interface CollectedSecrets {
13
+ /** All unique secret references found */
14
+ refs: SecretRefData[];
15
+ /** Paths where secrets were found (for substitution) */
16
+ paths: Array<{
17
+ path: (string | number)[];
18
+ secretRef: SecretRefData;
19
+ }>;
20
+ /** Paths where string literals were found (for unwrapping) */
21
+ literalPaths: Array<{
22
+ path: (string | number)[];
23
+ value: string;
24
+ }>;
25
+ }
26
+
27
+ /**
28
+ * Recursively collect all secret references and string literals from a value.
29
+ * @param value - The value to scan
30
+ * @param currentPath - Current path in the object tree
31
+ * @param collected - Accumulator for found secrets and literals
32
+ */
33
+ function collectSecretsFromValue(
34
+ value: unknown,
35
+ currentPath: (string | number)[],
36
+ collected: CollectedSecrets,
37
+ ): void {
38
+ if (value === null || value === undefined) {
39
+ return;
40
+ }
41
+
42
+ if (isSecretRef(value)) {
43
+ collected.refs.push(value.$secret);
44
+ collected.paths.push({
45
+ path: [...currentPath],
46
+ secretRef: value.$secret,
47
+ });
48
+ return;
49
+ }
50
+
51
+ if (isStringLiteral(value)) {
52
+ collected.literalPaths.push({
53
+ path: [...currentPath],
54
+ value: value.$literal,
55
+ });
56
+ return;
57
+ }
58
+
59
+ if (Array.isArray(value)) {
60
+ for (let i = 0; i < value.length; i++) {
61
+ collectSecretsFromValue(value[i], [...currentPath, i], collected);
62
+ }
63
+ return;
64
+ }
65
+
66
+ if (typeof value === "object") {
67
+ for (const [key, val] of Object.entries(value)) {
68
+ collectSecretsFromValue(val, [...currentPath, key], collected);
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Collect all secret references and string literals from a test monitor.
75
+ * Scans endpoint headers and bodies for $secret markers and $literal wrappers.
76
+ */
77
+ export function collectSecretsFromMonitor(
78
+ monitor: MonitorV1,
79
+ ): CollectedSecrets {
80
+ const collected: CollectedSecrets = {
81
+ refs: [],
82
+ paths: [],
83
+ literalPaths: [],
84
+ };
85
+
86
+ for (let nodeIndex = 0; nodeIndex < monitor.nodes.length; nodeIndex++) {
87
+ const node = monitor.nodes[nodeIndex];
88
+
89
+ // Only endpoints can have secrets (in headers and body)
90
+ if (node.type !== "HTTP_REQUEST") {
91
+ continue;
92
+ }
93
+
94
+ //const endpoint = node;
95
+
96
+ // Scan headers
97
+ if (node.headers) {
98
+ for (const [headerKey, headerValue] of Object.entries(node.headers)) {
99
+ collectSecretsFromValue(
100
+ headerValue,
101
+ ["nodes", nodeIndex, "headers", headerKey],
102
+ collected,
103
+ );
104
+ }
105
+ }
106
+
107
+ // Scan body
108
+ if (node.body !== undefined) {
109
+ collectSecretsFromValue(
110
+ node.body,
111
+ ["nodes", nodeIndex, "body"],
112
+ collected,
113
+ );
114
+ }
115
+ }
116
+
117
+ // Deduplicate refs by creating a unique key
118
+ const seen = new Set<string>();
119
+ const uniqueRefs: SecretRefData[] = [];
120
+
121
+ for (const ref of collected.refs) {
122
+ const key = `${ref.ref}:${ref.version || ""}:${ref.field || ""}`;
123
+ if (!seen.has(key)) {
124
+ seen.add(key);
125
+ uniqueRefs.push(ref);
126
+ }
127
+ }
128
+
129
+ collected.refs = uniqueRefs;
130
+ return collected;
131
+ }
132
+
133
+ /**
134
+ * Set a value at a path in an object.
135
+ * Creates intermediate objects/arrays as needed.
136
+ */
137
+ function setAtPath(
138
+ obj: unknown,
139
+ path: (string | number)[],
140
+ value: unknown,
141
+ ): void {
142
+ if (path.length === 0) {
143
+ return;
144
+ }
145
+
146
+ let current: any = obj;
147
+ for (let i = 0; i < path.length - 1; i++) {
148
+ const key = path[i];
149
+ if (current[key] === undefined) {
150
+ // Create intermediate object or array based on next key type
151
+ current[key] = typeof path[i + 1] === "number" ? [] : {};
152
+ }
153
+ current = current[key];
154
+ }
155
+
156
+ current[path[path.length - 1]] = value;
157
+ }
158
+
159
+ /**
160
+ * Deep clone a value.
161
+ */
162
+ function deepClone<T>(value: T): T {
163
+ if (value === null || value === undefined) {
164
+ return value;
165
+ }
166
+ return JSON.parse(JSON.stringify(value));
167
+ }
168
+
169
+ /**
170
+ * Resolve all secrets and unwrap string literals in a monitor and return a new monitor with substituted values.
171
+ * The original monitor is not modified.
172
+ *
173
+ * @param monitor - The test monitor containing secret references and string literals
174
+ * @param provider - The secret provider (required when monitor contains secret refs)
175
+ * @returns A new monitor with all secrets resolved to their values and literals unwrapped
176
+ * @throws SecretResolutionError if any secret cannot be resolved (fail-fast)
177
+ */
178
+ export async function resolveSecretsInMonitor(
179
+ monitor: MonitorV1,
180
+ provider: SecretProvider | null,
181
+ ): Promise<MonitorV1> {
182
+ // Collect all secret references and string literals
183
+ const collected = collectSecretsFromMonitor(monitor);
184
+
185
+ if (collected.refs.length === 0 && collected.literalPaths.length === 0) {
186
+ // No secrets or literals to resolve
187
+ return monitor;
188
+ }
189
+
190
+ // Resolve secrets when we have refs (provider required)
191
+ if (collected.refs.length > 0 && !provider) {
192
+ throw new SecretResolutionError(
193
+ "Monitor contains secret references but no secret provider was provided",
194
+ { ref: "unknown" },
195
+ );
196
+ }
197
+
198
+ // Clone the monitor for modification
199
+ const resolvedMonitor = deepClone(monitor);
200
+
201
+ // Substitute resolved secret values at each path
202
+ if (provider) {
203
+ for (const { path, secretRef } of collected.paths) {
204
+ const value = await provider.resolve(secretRef.ref, {
205
+ version: secretRef.version,
206
+ field: secretRef.field,
207
+ });
208
+ setAtPath(resolvedMonitor, path, value);
209
+ }
210
+ }
211
+
212
+ // Unwrap string literals at each path
213
+ for (const { path, value } of collected.literalPaths) {
214
+ setAtPath(resolvedMonitor, path, value);
215
+ }
216
+
217
+ return resolvedMonitor;
218
+ }
219
+
220
+ /**
221
+ * Check if a monitor contains any secret references or string literals that need resolution.
222
+ * Useful for short-circuiting resolution when no secrets or literals are present.
223
+ */
224
+ export function planHasSecrets(monitor: MonitorV1): boolean {
225
+ for (const node of monitor.nodes) {
226
+ if (node.type !== "HTTP_REQUEST") {
227
+ continue;
228
+ }
229
+
230
+ // Check headers
231
+ if (node.headers) {
232
+ for (const headerValue of Object.values(node.headers)) {
233
+ if (isSecretRef(headerValue) || isStringLiteral(headerValue)) {
234
+ return true;
235
+ }
236
+ }
237
+ }
238
+
239
+ // Check body (recursive check)
240
+ if (node.body !== undefined && containsSecretOrLiteral(node.body)) {
241
+ return true;
242
+ }
243
+ }
244
+
245
+ return false;
246
+ }
247
+
248
+ /**
249
+ * Recursively check if a value contains any secret references or string literals.
250
+ */
251
+ function containsSecretOrLiteral(value: unknown): boolean {
252
+ if (value === null || value === undefined) {
253
+ return false;
254
+ }
255
+
256
+ if (isSecretRef(value) || isStringLiteral(value)) {
257
+ return true;
258
+ }
259
+
260
+ if (Array.isArray(value)) {
261
+ return value.some(containsSecretOrLiteral);
262
+ }
263
+
264
+ if (typeof value === "object") {
265
+ return Object.values(value).some(containsSecretOrLiteral);
266
+ }
267
+
268
+ return false;
269
+ }