ai-evaluate 2.1.6 → 2.1.8

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 (100) hide show
  1. package/README.md +90 -3
  2. package/dist/capnweb-bundle.d.ts +10 -0
  3. package/dist/capnweb-bundle.d.ts.map +1 -0
  4. package/dist/capnweb-bundle.js +2596 -0
  5. package/dist/capnweb-bundle.js.map +1 -0
  6. package/dist/evaluate.d.ts +1 -1
  7. package/dist/evaluate.d.ts.map +1 -1
  8. package/dist/evaluate.js +186 -7
  9. package/dist/evaluate.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/miniflare-pool.d.ts +109 -0
  15. package/dist/miniflare-pool.d.ts.map +1 -0
  16. package/dist/miniflare-pool.js +308 -0
  17. package/dist/miniflare-pool.js.map +1 -0
  18. package/dist/node.d.ts.map +1 -1
  19. package/dist/node.js +42 -10
  20. package/dist/node.js.map +1 -1
  21. package/dist/shared.d.ts +66 -0
  22. package/dist/shared.d.ts.map +1 -0
  23. package/dist/shared.js +169 -0
  24. package/dist/shared.js.map +1 -0
  25. package/dist/type-guards.d.ts +21 -0
  26. package/dist/type-guards.d.ts.map +1 -0
  27. package/dist/type-guards.js +216 -0
  28. package/dist/type-guards.js.map +1 -0
  29. package/dist/types.d.ts +17 -2
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/validation.d.ts +26 -0
  32. package/dist/validation.d.ts.map +1 -0
  33. package/dist/validation.js +104 -0
  34. package/dist/validation.js.map +1 -0
  35. package/dist/worker-template/code-transforms.d.ts +9 -0
  36. package/dist/worker-template/code-transforms.d.ts.map +1 -0
  37. package/dist/worker-template/code-transforms.js +28 -0
  38. package/dist/worker-template/code-transforms.js.map +1 -0
  39. package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
  40. package/dist/worker-template/core.d.ts.map +1 -0
  41. package/dist/worker-template/core.js +502 -0
  42. package/dist/worker-template/core.js.map +1 -0
  43. package/dist/worker-template/helpers.d.ts +14 -0
  44. package/dist/worker-template/helpers.d.ts.map +1 -0
  45. package/dist/worker-template/helpers.js +79 -0
  46. package/dist/worker-template/helpers.js.map +1 -0
  47. package/dist/worker-template/index.d.ts +14 -0
  48. package/dist/worker-template/index.d.ts.map +1 -0
  49. package/dist/worker-template/index.js +19 -0
  50. package/dist/worker-template/index.js.map +1 -0
  51. package/dist/worker-template/sdk-generator.d.ts +17 -0
  52. package/dist/worker-template/sdk-generator.d.ts.map +1 -0
  53. package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
  54. package/dist/worker-template/sdk-generator.js.map +1 -0
  55. package/dist/worker-template/test-generator.d.ts +16 -0
  56. package/dist/worker-template/test-generator.d.ts.map +1 -0
  57. package/dist/worker-template/test-generator.js +357 -0
  58. package/dist/worker-template/test-generator.js.map +1 -0
  59. package/dist/worker-template.d.ts +2 -2
  60. package/dist/worker-template.d.ts.map +1 -1
  61. package/dist/worker-template.js +64 -31
  62. package/dist/worker-template.js.map +1 -1
  63. package/example/package.json +7 -3
  64. package/example/src/index.ts +194 -40
  65. package/example/wrangler.jsonc +18 -2
  66. package/package.json +1 -3
  67. package/src/capnweb-bundle.ts +2596 -0
  68. package/src/evaluate.ts +216 -7
  69. package/src/index.ts +3 -1
  70. package/src/miniflare-pool.ts +395 -0
  71. package/src/node.ts +56 -11
  72. package/src/shared.ts +186 -0
  73. package/src/type-guards.ts +323 -0
  74. package/src/types.ts +18 -2
  75. package/src/validation.ts +120 -0
  76. package/src/worker-template/code-transforms.ts +32 -0
  77. package/src/worker-template/core.ts +557 -0
  78. package/src/worker-template/helpers.ts +90 -0
  79. package/src/worker-template/index.ts +23 -0
  80. package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1566
  81. package/src/worker-template/test-generator.ts +358 -0
  82. package/test/miniflare-pool.test.ts +246 -0
  83. package/test/node.test.ts +467 -0
  84. package/test/security.test.ts +1009 -0
  85. package/test/shared.test.ts +105 -0
  86. package/test/type-guards.test.ts +303 -0
  87. package/test/validation.test.ts +240 -0
  88. package/test/worker-template.test.ts +21 -19
  89. package/src/evaluate.js +0 -187
  90. package/src/index.js +0 -10
  91. package/src/node.d.ts +0 -17
  92. package/src/node.d.ts.map +0 -1
  93. package/src/node.js +0 -168
  94. package/src/node.js.map +0 -1
  95. package/src/types.d.ts +0 -172
  96. package/src/types.d.ts.map +0 -1
  97. package/src/types.js +0 -4
  98. package/src/types.js.map +0 -1
  99. package/src/worker-template.d.ts.map +0 -1
  100. package/src/worker-template.js.map +0 -1
package/src/node.ts CHANGED
@@ -5,8 +5,15 @@
5
5
  * For Workers-only builds, import from 'ai-evaluate' instead.
6
6
  */
7
7
 
8
- import type { EvaluateOptions, EvaluateResult, WorkerLoader, SandboxEnv } from './types.js'
9
- import { generateWorkerCode, generateDevWorkerCode } from './worker-template.js'
8
+ import type {
9
+ EvaluateOptions,
10
+ EvaluateResult,
11
+ WorkerLoader,
12
+ SandboxEnv,
13
+ FetchConfig,
14
+ } from './types.js'
15
+ import { generateWorkerCode, generateDevWorkerCode } from './worker-template/index.js'
16
+ import { isDomainAllowed, normalizeImports } from './shared.js'
10
17
 
11
18
  /**
12
19
  * Check if code contains JSX syntax that needs transformation
@@ -60,6 +67,7 @@ export async function evaluate(
60
67
  module: transformedModule,
61
68
  tests: transformedTests,
62
69
  script: transformedScript,
70
+ imports: normalizeImports(options.imports),
63
71
  }
64
72
 
65
73
  // Use worker_loaders if available (Cloudflare Workers)
@@ -122,6 +130,25 @@ async function evaluateWithWorkerLoader(
122
130
  }
123
131
  }
124
132
 
133
+ /**
134
+ * Determine if network access should be blocked based on fetch options
135
+ * fetch: false | null -> block
136
+ */
137
+ function shouldBlockNetwork(options: EvaluateOptions): boolean {
138
+ return options.fetch === false || options.fetch === null
139
+ }
140
+
141
+ /**
142
+ * Get allowlist domains if fetch is an array
143
+ * fetch: string[] -> allowlist
144
+ */
145
+ function getAllowlistDomains(options: EvaluateOptions): string[] | null {
146
+ if (Array.isArray(options.fetch)) {
147
+ return options.fetch
148
+ }
149
+ return null
150
+ }
151
+
125
152
  /**
126
153
  * Evaluate using Miniflare (for Node.js/development)
127
154
  */
@@ -140,20 +167,38 @@ async function evaluateWithMiniflare(
140
167
  fetch: options.fetch, // Pass fetch option to worker template
141
168
  })
142
169
 
143
- // Block outbound network requests at Miniflare level when fetch: null
144
- // This complements the globalThis.fetch override in the worker template
145
- const blockNetwork = options.fetch === null
170
+ // Determine outbound service configuration based on fetch option
171
+ const blockNetwork = shouldBlockNetwork(options)
172
+ const allowlistDomains = getAllowlistDomains(options)
173
+
174
+ // Build outboundService based on mode:
175
+ // - block: throw error for all requests
176
+ // - allowlist: check domain against allowlist
177
+ // - allow (default): no outboundService (allow all)
178
+ type OutboundServiceFn = (() => never) | ((request: Request) => Response | Promise<Response>)
179
+ let outboundService: OutboundServiceFn | undefined
180
+ if (blockNetwork) {
181
+ outboundService = () => {
182
+ throw new Error('Network access blocked: fetch is disabled in this sandbox')
183
+ }
184
+ } else if (allowlistDomains) {
185
+ outboundService = (request: Request) => {
186
+ const url = request.url
187
+ if (!isDomainAllowed(url, allowlistDomains)) {
188
+ const hostname = new URL(url).hostname
189
+ throw new Error(`Network access blocked: domain not in allowlist. Attempted: ${hostname}`)
190
+ }
191
+ // Allow the request by returning a fetched response
192
+ return fetch(request)
193
+ }
194
+ }
146
195
 
147
196
  const mf = new Miniflare({
148
197
  modules: true,
149
198
  script: workerCode,
150
199
  compatibilityDate: '2026-01-01',
151
- // Block all outbound fetch/connect when network is disabled
152
- ...(blockNetwork && {
153
- outboundService: () => {
154
- throw new Error('Network access blocked: fetch is disabled in this sandbox')
155
- },
156
- }),
200
+ // Configure outbound service based on fetch mode
201
+ ...(outboundService && { outboundService }),
157
202
  })
158
203
 
159
204
  try {
package/src/shared.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Shared utilities for ai-evaluate
3
+ *
4
+ * Contains constants and helper functions used by both
5
+ * evaluate.ts (Workers) and node.ts (Node.js/Miniflare)
6
+ */
7
+
8
+ import type { EvaluateResult } from './types.js'
9
+
10
+ /**
11
+ * Compatibility date for dynamic workers (2026)
12
+ */
13
+ export const COMPATIBILITY_DATE = '2026-01-01'
14
+
15
+ /**
16
+ * Normalize an import specifier to a full URL
17
+ *
18
+ * Supports:
19
+ * - Full URLs: https://esm.sh/lodash@4.17.21 (unchanged)
20
+ * - Bare package names: lodash -> https://esm.sh/lodash
21
+ * - Package with version: lodash@4.17.21 -> https://esm.sh/lodash@4.17.21
22
+ * - Scoped packages: @scope/pkg -> https://esm.sh/@scope/pkg
23
+ */
24
+ export function normalizeImport(specifier: string): string {
25
+ // Already a URL - return as-is
26
+ if (specifier.includes('://')) {
27
+ return specifier
28
+ }
29
+
30
+ // Bare package name or scoped package - prepend esm.sh
31
+ return `https://esm.sh/${specifier}`
32
+ }
33
+
34
+ /**
35
+ * Normalize an array of import specifiers
36
+ */
37
+ export function normalizeImports(imports: string[] | undefined): string[] | undefined {
38
+ if (!imports || imports.length === 0) return imports
39
+ return imports.map(normalizeImport)
40
+ }
41
+
42
+ /**
43
+ * Extract package name from import specifier for variable naming
44
+ * Supports: lodash, lodash@4.17.21, @scope/pkg, https://esm.sh/lodash
45
+ */
46
+ export function extractPackageName(specifier: string, index: number): string {
47
+ let pkgName: string
48
+ if (specifier.includes('://')) {
49
+ // Full URL - extract from path
50
+ const match = specifier.match(/esm\.sh\/(@?[^@/]+)/)
51
+ pkgName = match ? match[1].replace(/^@/, '').replace(/-/g, '_') : `pkg${index}`
52
+ } else {
53
+ // Bare package name - extract before @ version
54
+ pkgName = specifier.split('@')[0].replace(/^@/, '').replace(/-/g, '_').replace(/\//g, '_')
55
+ }
56
+ return pkgName
57
+ }
58
+
59
+ /**
60
+ * Default sandbox URL for worker fetch requests
61
+ */
62
+ export const SANDBOX_URL = 'http://sandbox/execute'
63
+
64
+ /**
65
+ * Generate a unique sandbox worker ID
66
+ */
67
+ export const generateSandboxId = (): string =>
68
+ `sandbox-${Date.now()}-${Math.random().toString(36).slice(2)}`
69
+
70
+ /**
71
+ * Create an error result with consistent structure
72
+ */
73
+ export function createErrorResult(error: unknown, start: number): EvaluateResult {
74
+ return {
75
+ success: false,
76
+ logs: [],
77
+ error: error instanceof Error ? error.message : String(error),
78
+ duration: Date.now() - start,
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Process a result from worker execution, adding duration
84
+ */
85
+ export function processResult(result: EvaluateResult, start: number): EvaluateResult {
86
+ return {
87
+ ...result,
88
+ duration: Date.now() - start,
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Check if a domain matches a pattern (supports wildcards)
94
+ * @param domain - The domain to check (e.g., 'api.example.com')
95
+ * @param pattern - The pattern to match against (e.g., '*.example.com' or 'api.example.com')
96
+ * @returns true if the domain matches the pattern
97
+ */
98
+ export function matchesDomainPattern(domain: string, pattern: string): boolean {
99
+ // Normalize both to lowercase
100
+ const normalizedDomain = domain.toLowerCase()
101
+ const normalizedPattern = pattern.toLowerCase()
102
+
103
+ // Exact match
104
+ if (normalizedDomain === normalizedPattern) {
105
+ return true
106
+ }
107
+
108
+ // Wildcard pattern: *.example.com
109
+ if (normalizedPattern.startsWith('*.')) {
110
+ const suffix = normalizedPattern.slice(2) // Remove '*.'
111
+ // Domain must end with the suffix and have at least one character before it
112
+ // e.g., 'api.example.com' matches '*.example.com'
113
+ // but 'example.com' does not match '*.example.com'
114
+ return normalizedDomain.endsWith('.' + suffix) || normalizedDomain === suffix
115
+ }
116
+
117
+ return false
118
+ }
119
+
120
+ /**
121
+ * Check if a URL's domain is in the allowed list
122
+ * @param url - The URL to check
123
+ * @param allowedDomains - List of allowed domains (supports wildcards like '*.example.com')
124
+ * @returns true if the URL's domain is allowed
125
+ */
126
+ export function isDomainAllowed(url: string, allowedDomains: string[]): boolean {
127
+ try {
128
+ const parsedUrl = new URL(url)
129
+ const hostname = parsedUrl.hostname
130
+
131
+ for (const pattern of allowedDomains) {
132
+ if (matchesDomainPattern(hostname, pattern)) {
133
+ return true
134
+ }
135
+ }
136
+
137
+ return false
138
+ } catch {
139
+ // Invalid URL - not allowed
140
+ return false
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Generate JavaScript code for domain checking in workers
146
+ * This is embedded into the worker source code
147
+ */
148
+ export function generateDomainCheckCode(allowedDomains: string[]): string {
149
+ const domainsJson = JSON.stringify(allowedDomains)
150
+
151
+ return `
152
+ // Domain allowlist checking
153
+ const __allowedDomains__ = ${domainsJson};
154
+
155
+ const __matchesDomainPattern__ = (domain, pattern) => {
156
+ const normalizedDomain = domain.toLowerCase();
157
+ const normalizedPattern = pattern.toLowerCase();
158
+ if (normalizedDomain === normalizedPattern) return true;
159
+ if (normalizedPattern.startsWith('*.')) {
160
+ const suffix = normalizedPattern.slice(2);
161
+ return normalizedDomain.endsWith('.' + suffix) || normalizedDomain === suffix;
162
+ }
163
+ return false;
164
+ };
165
+
166
+ const __isDomainAllowed__ = (url) => {
167
+ try {
168
+ const parsedUrl = new URL(url);
169
+ const hostname = parsedUrl.hostname;
170
+ for (const pattern of __allowedDomains__) {
171
+ if (__matchesDomainPattern__(hostname, pattern)) return true;
172
+ }
173
+ return false;
174
+ } catch { return false; }
175
+ };
176
+
177
+ const __originalFetch__ = globalThis.fetch;
178
+ globalThis.fetch = async (input, init) => {
179
+ const url = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);
180
+ if (!__isDomainAllowed__(url)) {
181
+ throw new Error(\`Network access blocked: domain not in allowlist. Attempted: \${new URL(url).hostname}\`);
182
+ }
183
+ return __originalFetch__(input, init);
184
+ };
185
+ `
186
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Runtime type guards for JSON response validation
3
+ */
4
+
5
+ import type { EvaluateResult, LogEntry, TestResults, TestResult } from './types.js'
6
+
7
+ /**
8
+ * Check if a value is a valid LogEntry
9
+ */
10
+ function isLogEntry(value: unknown): value is LogEntry {
11
+ if (typeof value !== 'object' || value === null) {
12
+ return false
13
+ }
14
+
15
+ const obj = value as Record<string, unknown>
16
+
17
+ // Check level is one of the allowed values
18
+ const validLevels = ['log', 'warn', 'error', 'info', 'debug']
19
+ if (typeof obj.level !== 'string' || !validLevels.includes(obj.level)) {
20
+ return false
21
+ }
22
+
23
+ // Check message is a string
24
+ if (typeof obj.message !== 'string') {
25
+ return false
26
+ }
27
+
28
+ // Check timestamp is a number
29
+ if (typeof obj.timestamp !== 'number') {
30
+ return false
31
+ }
32
+
33
+ return true
34
+ }
35
+
36
+ /**
37
+ * Check if a value is a valid TestResult
38
+ */
39
+ function isTestResult(value: unknown): value is TestResult {
40
+ if (typeof value !== 'object' || value === null) {
41
+ return false
42
+ }
43
+
44
+ const obj = value as Record<string, unknown>
45
+
46
+ // Check required fields
47
+ if (typeof obj.name !== 'string') {
48
+ return false
49
+ }
50
+
51
+ if (typeof obj.passed !== 'boolean') {
52
+ return false
53
+ }
54
+
55
+ if (typeof obj.duration !== 'number') {
56
+ return false
57
+ }
58
+
59
+ // Check optional error field
60
+ if (obj.error !== undefined && typeof obj.error !== 'string') {
61
+ return false
62
+ }
63
+
64
+ return true
65
+ }
66
+
67
+ /**
68
+ * Check if a value is a valid TestResults
69
+ */
70
+ function isTestResults(value: unknown): value is TestResults {
71
+ if (typeof value !== 'object' || value === null) {
72
+ return false
73
+ }
74
+
75
+ const obj = value as Record<string, unknown>
76
+
77
+ // Check required numeric fields
78
+ if (typeof obj.total !== 'number') {
79
+ return false
80
+ }
81
+
82
+ if (typeof obj.passed !== 'number') {
83
+ return false
84
+ }
85
+
86
+ if (typeof obj.failed !== 'number') {
87
+ return false
88
+ }
89
+
90
+ if (typeof obj.skipped !== 'number') {
91
+ return false
92
+ }
93
+
94
+ if (typeof obj.duration !== 'number') {
95
+ return false
96
+ }
97
+
98
+ // Check tests array
99
+ if (!Array.isArray(obj.tests)) {
100
+ return false
101
+ }
102
+
103
+ for (const test of obj.tests) {
104
+ if (!isTestResult(test)) {
105
+ return false
106
+ }
107
+ }
108
+
109
+ return true
110
+ }
111
+
112
+ /**
113
+ * Type guard to check if a value is a valid EvaluateResult
114
+ *
115
+ * Validates all required fields: success, duration, logs
116
+ *
117
+ * @param value - The value to check
118
+ * @returns True if the value is a valid EvaluateResult
119
+ */
120
+ export function isEvaluateResult(value: unknown): value is EvaluateResult {
121
+ if (typeof value !== 'object' || value === null) {
122
+ return false
123
+ }
124
+
125
+ const obj = value as Record<string, unknown>
126
+
127
+ // Check required fields
128
+ if (typeof obj.success !== 'boolean') {
129
+ return false
130
+ }
131
+
132
+ if (typeof obj.duration !== 'number') {
133
+ return false
134
+ }
135
+
136
+ // Check logs is an array of valid LogEntry objects
137
+ if (!Array.isArray(obj.logs)) {
138
+ return false
139
+ }
140
+
141
+ for (const log of obj.logs) {
142
+ if (!isLogEntry(log)) {
143
+ return false
144
+ }
145
+ }
146
+
147
+ // Check optional fields have correct types if present
148
+ if (obj.error !== undefined && typeof obj.error !== 'string') {
149
+ return false
150
+ }
151
+
152
+ if (obj.testResults !== undefined && !isTestResults(obj.testResults)) {
153
+ return false
154
+ }
155
+
156
+ // value can be any type, so no validation needed for it
157
+
158
+ return true
159
+ }
160
+
161
+ /**
162
+ * Assertion function that throws a descriptive error if the value is not a valid EvaluateResult
163
+ *
164
+ * @param value - The value to validate
165
+ * @throws Error with descriptive message if validation fails
166
+ */
167
+ export function assertEvaluateResult(value: unknown): asserts value is EvaluateResult {
168
+ if (typeof value !== 'object' || value === null) {
169
+ throw new Error(
170
+ `Invalid EvaluateResult: expected object, got ${value === null ? 'null' : typeof value}`
171
+ )
172
+ }
173
+
174
+ const obj = value as Record<string, unknown>
175
+
176
+ // Validate required field: success
177
+ if (typeof obj.success !== 'boolean') {
178
+ throw new Error(
179
+ `Invalid EvaluateResult: 'success' must be a boolean, got ${typeof obj.success}`
180
+ )
181
+ }
182
+
183
+ // Validate required field: duration
184
+ if (typeof obj.duration !== 'number') {
185
+ throw new Error(
186
+ `Invalid EvaluateResult: 'duration' must be a number, got ${typeof obj.duration}`
187
+ )
188
+ }
189
+
190
+ // Validate required field: logs
191
+ if (!Array.isArray(obj.logs)) {
192
+ throw new Error(`Invalid EvaluateResult: 'logs' must be an array, got ${typeof obj.logs}`)
193
+ }
194
+
195
+ // Validate each log entry
196
+ for (let i = 0; i < obj.logs.length; i++) {
197
+ const log = obj.logs[i]
198
+ if (typeof log !== 'object' || log === null) {
199
+ throw new Error(
200
+ `Invalid EvaluateResult: logs[${i}] must be an object, got ${
201
+ log === null ? 'null' : typeof log
202
+ }`
203
+ )
204
+ }
205
+
206
+ const logObj = log as Record<string, unknown>
207
+ const validLevels = ['log', 'warn', 'error', 'info', 'debug']
208
+
209
+ if (typeof logObj.level !== 'string' || !validLevels.includes(logObj.level)) {
210
+ throw new Error(
211
+ `Invalid EvaluateResult: logs[${i}].level must be one of ${validLevels.join(', ')}, got '${
212
+ logObj.level
213
+ }'`
214
+ )
215
+ }
216
+
217
+ if (typeof logObj.message !== 'string') {
218
+ throw new Error(
219
+ `Invalid EvaluateResult: logs[${i}].message must be a string, got ${typeof logObj.message}`
220
+ )
221
+ }
222
+
223
+ if (typeof logObj.timestamp !== 'number') {
224
+ throw new Error(
225
+ `Invalid EvaluateResult: logs[${i}].timestamp must be a number, got ${typeof logObj.timestamp}`
226
+ )
227
+ }
228
+ }
229
+
230
+ // Validate optional field: error
231
+ if (obj.error !== undefined && typeof obj.error !== 'string') {
232
+ throw new Error(
233
+ `Invalid EvaluateResult: 'error' must be a string if present, got ${typeof obj.error}`
234
+ )
235
+ }
236
+
237
+ // Validate optional field: testResults
238
+ if (obj.testResults !== undefined) {
239
+ if (typeof obj.testResults !== 'object' || obj.testResults === null) {
240
+ throw new Error(
241
+ `Invalid EvaluateResult: 'testResults' must be an object if present, got ${
242
+ obj.testResults === null ? 'null' : typeof obj.testResults
243
+ }`
244
+ )
245
+ }
246
+
247
+ const testResults = obj.testResults as Record<string, unknown>
248
+
249
+ if (typeof testResults.total !== 'number') {
250
+ throw new Error(
251
+ `Invalid EvaluateResult: testResults.total must be a number, got ${typeof testResults.total}`
252
+ )
253
+ }
254
+
255
+ if (typeof testResults.passed !== 'number') {
256
+ throw new Error(
257
+ `Invalid EvaluateResult: testResults.passed must be a number, got ${typeof testResults.passed}`
258
+ )
259
+ }
260
+
261
+ if (typeof testResults.failed !== 'number') {
262
+ throw new Error(
263
+ `Invalid EvaluateResult: testResults.failed must be a number, got ${typeof testResults.failed}`
264
+ )
265
+ }
266
+
267
+ if (typeof testResults.skipped !== 'number') {
268
+ throw new Error(
269
+ `Invalid EvaluateResult: testResults.skipped must be a number, got ${typeof testResults.skipped}`
270
+ )
271
+ }
272
+
273
+ if (typeof testResults.duration !== 'number') {
274
+ throw new Error(
275
+ `Invalid EvaluateResult: testResults.duration must be a number, got ${typeof testResults.duration}`
276
+ )
277
+ }
278
+
279
+ if (!Array.isArray(testResults.tests)) {
280
+ throw new Error(
281
+ `Invalid EvaluateResult: testResults.tests must be an array, got ${typeof testResults.tests}`
282
+ )
283
+ }
284
+
285
+ // Validate each test result
286
+ for (let i = 0; i < testResults.tests.length; i++) {
287
+ const test = testResults.tests[i]
288
+ if (typeof test !== 'object' || test === null) {
289
+ throw new Error(
290
+ `Invalid EvaluateResult: testResults.tests[${i}] must be an object, got ${
291
+ test === null ? 'null' : typeof test
292
+ }`
293
+ )
294
+ }
295
+
296
+ const testObj = test as Record<string, unknown>
297
+
298
+ if (typeof testObj.name !== 'string') {
299
+ throw new Error(
300
+ `Invalid EvaluateResult: testResults.tests[${i}].name must be a string, got ${typeof testObj.name}`
301
+ )
302
+ }
303
+
304
+ if (typeof testObj.passed !== 'boolean') {
305
+ throw new Error(
306
+ `Invalid EvaluateResult: testResults.tests[${i}].passed must be a boolean, got ${typeof testObj.passed}`
307
+ )
308
+ }
309
+
310
+ if (typeof testObj.duration !== 'number') {
311
+ throw new Error(
312
+ `Invalid EvaluateResult: testResults.tests[${i}].duration must be a number, got ${typeof testObj.duration}`
313
+ )
314
+ }
315
+
316
+ if (testObj.error !== undefined && typeof testObj.error !== 'string') {
317
+ throw new Error(
318
+ `Invalid EvaluateResult: testResults.tests[${i}].error must be a string if present, got ${typeof testObj.error}`
319
+ )
320
+ }
321
+ }
322
+ }
323
+ }
package/src/types.ts CHANGED
@@ -24,6 +24,17 @@ export interface SDKConfig {
24
24
  aiGatewayToken?: string
25
25
  }
26
26
 
27
+ /**
28
+ * Network access configuration
29
+ *
30
+ * @example
31
+ * fetch: true // allow all (default)
32
+ * fetch: false // block all
33
+ * fetch: null // block all (backwards compat)
34
+ * fetch: ['api.example.com', '*.trusted.com'] // allowlist with wildcards
35
+ */
36
+ export type FetchConfig = boolean | null | string[]
37
+
27
38
  /**
28
39
  * Options for evaluate()
29
40
  */
@@ -38,8 +49,13 @@ export interface EvaluateOptions {
38
49
  timeout?: number | undefined
39
50
  /** Environment variables to pass to the sandbox */
40
51
  env?: Record<string, string> | undefined
41
- /** Fetch configuration. Set to null to block network access. Default: allowed */
42
- fetch?: null | undefined
52
+ /**
53
+ * Network access control
54
+ * - true: allow all (default)
55
+ * - false/null: block all
56
+ * - string[]: allowlist of domains (wildcards: '*.example.com')
57
+ */
58
+ fetch?: FetchConfig
43
59
  /** RPC services to expose via capnweb (URL -> handler) */
44
60
  rpc?: Record<string, unknown> | undefined
45
61
  /** Outbound RPC interceptor - intercepts fetch calls to RPC URLs */