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.
- package/README.md +90 -3
- package/dist/capnweb-bundle.d.ts +10 -0
- package/dist/capnweb-bundle.d.ts.map +1 -0
- package/dist/capnweb-bundle.js +2596 -0
- package/dist/capnweb-bundle.js.map +1 -0
- package/dist/evaluate.d.ts +1 -1
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +186 -7
- package/dist/evaluate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/miniflare-pool.d.ts +109 -0
- package/dist/miniflare-pool.d.ts.map +1 -0
- package/dist/miniflare-pool.js +308 -0
- package/dist/miniflare-pool.js.map +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +42 -10
- package/dist/node.js.map +1 -1
- package/dist/shared.d.ts +66 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +169 -0
- package/dist/shared.js.map +1 -0
- package/dist/type-guards.d.ts +21 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +216 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +26 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +104 -0
- package/dist/validation.js.map +1 -0
- package/dist/worker-template/code-transforms.d.ts +9 -0
- package/dist/worker-template/code-transforms.d.ts.map +1 -0
- package/dist/worker-template/code-transforms.js +28 -0
- package/dist/worker-template/code-transforms.js.map +1 -0
- package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
- package/dist/worker-template/core.d.ts.map +1 -0
- package/dist/worker-template/core.js +502 -0
- package/dist/worker-template/core.js.map +1 -0
- package/dist/worker-template/helpers.d.ts +14 -0
- package/dist/worker-template/helpers.d.ts.map +1 -0
- package/dist/worker-template/helpers.js +79 -0
- package/dist/worker-template/helpers.js.map +1 -0
- package/dist/worker-template/index.d.ts +14 -0
- package/dist/worker-template/index.d.ts.map +1 -0
- package/dist/worker-template/index.js +19 -0
- package/dist/worker-template/index.js.map +1 -0
- package/dist/worker-template/sdk-generator.d.ts +17 -0
- package/dist/worker-template/sdk-generator.d.ts.map +1 -0
- package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
- package/dist/worker-template/sdk-generator.js.map +1 -0
- package/dist/worker-template/test-generator.d.ts +16 -0
- package/dist/worker-template/test-generator.d.ts.map +1 -0
- package/dist/worker-template/test-generator.js +357 -0
- package/dist/worker-template/test-generator.js.map +1 -0
- package/dist/worker-template.d.ts +2 -2
- package/dist/worker-template.d.ts.map +1 -1
- package/dist/worker-template.js +64 -31
- package/dist/worker-template.js.map +1 -1
- package/example/package.json +7 -3
- package/example/src/index.ts +194 -40
- package/example/wrangler.jsonc +18 -2
- package/package.json +1 -3
- package/src/capnweb-bundle.ts +2596 -0
- package/src/evaluate.ts +216 -7
- package/src/index.ts +3 -1
- package/src/miniflare-pool.ts +395 -0
- package/src/node.ts +56 -11
- package/src/shared.ts +186 -0
- package/src/type-guards.ts +323 -0
- package/src/types.ts +18 -2
- package/src/validation.ts +120 -0
- package/src/worker-template/code-transforms.ts +32 -0
- package/src/worker-template/core.ts +557 -0
- package/src/worker-template/helpers.ts +90 -0
- package/src/worker-template/index.ts +23 -0
- package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1566
- package/src/worker-template/test-generator.ts +358 -0
- package/test/miniflare-pool.test.ts +246 -0
- package/test/node.test.ts +467 -0
- package/test/security.test.ts +1009 -0
- package/test/shared.test.ts +105 -0
- package/test/type-guards.test.ts +303 -0
- package/test/validation.test.ts +240 -0
- package/test/worker-template.test.ts +21 -19
- package/src/evaluate.js +0 -187
- package/src/index.js +0 -10
- package/src/node.d.ts +0 -17
- package/src/node.d.ts.map +0 -1
- package/src/node.js +0 -168
- package/src/node.js.map +0 -1
- package/src/types.d.ts +0 -172
- package/src/types.d.ts.map +0 -1
- package/src/types.js +0 -4
- package/src/types.js.map +0 -1
- package/src/worker-template.d.ts.map +0 -1
- 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 {
|
|
9
|
-
|
|
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
|
-
//
|
|
144
|
-
|
|
145
|
-
const
|
|
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
|
-
//
|
|
152
|
-
...(
|
|
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
|
-
/**
|
|
42
|
-
|
|
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 */
|