ai-evaluate 2.1.7 → 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 (92) hide show
  1. package/README.md +61 -4
  2. package/dist/evaluate.d.ts.map +1 -1
  3. package/dist/evaluate.js +18 -16
  4. package/dist/evaluate.js.map +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/miniflare-pool.d.ts +109 -0
  10. package/dist/miniflare-pool.d.ts.map +1 -0
  11. package/dist/miniflare-pool.js +308 -0
  12. package/dist/miniflare-pool.js.map +1 -0
  13. package/dist/node.d.ts.map +1 -1
  14. package/dist/node.js +42 -10
  15. package/dist/node.js.map +1 -1
  16. package/dist/shared.d.ts +66 -0
  17. package/dist/shared.d.ts.map +1 -0
  18. package/dist/shared.js +169 -0
  19. package/dist/shared.js.map +1 -0
  20. package/dist/type-guards.d.ts +21 -0
  21. package/dist/type-guards.d.ts.map +1 -0
  22. package/dist/type-guards.js +216 -0
  23. package/dist/type-guards.js.map +1 -0
  24. package/dist/types.d.ts +17 -2
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/validation.d.ts +26 -0
  27. package/dist/validation.d.ts.map +1 -0
  28. package/dist/validation.js +104 -0
  29. package/dist/validation.js.map +1 -0
  30. package/dist/worker-template/code-transforms.d.ts +9 -0
  31. package/dist/worker-template/code-transforms.d.ts.map +1 -0
  32. package/dist/worker-template/code-transforms.js +28 -0
  33. package/dist/worker-template/code-transforms.js.map +1 -0
  34. package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
  35. package/dist/worker-template/core.d.ts.map +1 -0
  36. package/dist/worker-template/core.js +502 -0
  37. package/dist/worker-template/core.js.map +1 -0
  38. package/dist/worker-template/helpers.d.ts +14 -0
  39. package/dist/worker-template/helpers.d.ts.map +1 -0
  40. package/dist/worker-template/helpers.js +79 -0
  41. package/dist/worker-template/helpers.js.map +1 -0
  42. package/dist/worker-template/index.d.ts +14 -0
  43. package/dist/worker-template/index.d.ts.map +1 -0
  44. package/dist/worker-template/index.js +19 -0
  45. package/dist/worker-template/index.js.map +1 -0
  46. package/dist/worker-template/sdk-generator.d.ts +17 -0
  47. package/dist/worker-template/sdk-generator.d.ts.map +1 -0
  48. package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
  49. package/dist/worker-template/sdk-generator.js.map +1 -0
  50. package/dist/worker-template/test-generator.d.ts +16 -0
  51. package/dist/worker-template/test-generator.d.ts.map +1 -0
  52. package/dist/worker-template/test-generator.js +357 -0
  53. package/dist/worker-template/test-generator.js.map +1 -0
  54. package/dist/worker-template.d.ts +2 -2
  55. package/dist/worker-template.d.ts.map +1 -1
  56. package/dist/worker-template.js +22 -10
  57. package/dist/worker-template.js.map +1 -1
  58. package/example/src/index.ts +12 -5
  59. package/package.json +1 -3
  60. package/src/evaluate.ts +21 -17
  61. package/src/index.ts +3 -1
  62. package/src/miniflare-pool.ts +395 -0
  63. package/src/node.ts +56 -11
  64. package/src/shared.ts +186 -0
  65. package/src/type-guards.ts +323 -0
  66. package/src/types.ts +18 -2
  67. package/src/validation.ts +120 -0
  68. package/src/worker-template/code-transforms.ts +32 -0
  69. package/src/worker-template/core.ts +557 -0
  70. package/src/worker-template/helpers.ts +90 -0
  71. package/src/worker-template/index.ts +23 -0
  72. package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1613
  73. package/src/worker-template/test-generator.ts +358 -0
  74. package/test/miniflare-pool.test.ts +246 -0
  75. package/test/node.test.ts +467 -0
  76. package/test/security.test.ts +1009 -0
  77. package/test/shared.test.ts +105 -0
  78. package/test/type-guards.test.ts +303 -0
  79. package/test/validation.test.ts +240 -0
  80. package/test/worker-template.test.ts +21 -19
  81. package/src/evaluate.js +0 -187
  82. package/src/index.js +0 -10
  83. package/src/node.d.ts +0 -17
  84. package/src/node.d.ts.map +0 -1
  85. package/src/node.js +0 -168
  86. package/src/node.js.map +0 -1
  87. package/src/types.d.ts +0 -172
  88. package/src/types.d.ts.map +0 -1
  89. package/src/types.js +0 -4
  90. package/src/types.js.map +0 -1
  91. package/src/worker-template.d.ts.map +0 -1
  92. package/src/worker-template.js.map +0 -1
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 */
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Input validation for EvaluateOptions
3
+ *
4
+ * Validates options to prevent resource exhaustion and provide clear error messages.
5
+ */
6
+
7
+ import type { EvaluateOptions } from './types.js'
8
+
9
+ /**
10
+ * Validation limits for EvaluateOptions
11
+ */
12
+ export const MAX_SCRIPT_SIZE = 1024 * 1024 // 1MB
13
+ export const MAX_IMPORTS = 100
14
+ export const MAX_TIMEOUT = 60000 // 60 seconds
15
+ export const DEFAULT_TIMEOUT = 5000 // 5 seconds
16
+
17
+ /**
18
+ * Validation error thrown when options fail validation
19
+ */
20
+ export class ValidationError extends Error {
21
+ constructor(message: string) {
22
+ super(message)
23
+ this.name = 'ValidationError'
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Validate a URL string
29
+ */
30
+ function isValidUrl(urlString: string): boolean {
31
+ try {
32
+ const url = new URL(urlString)
33
+ return url.protocol === 'http:' || url.protocol === 'https:'
34
+ } catch {
35
+ return false
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Validate EvaluateOptions
41
+ *
42
+ * @throws ValidationError if any validation fails
43
+ */
44
+ export function validateOptions(options: EvaluateOptions): void {
45
+ // Validate timeout
46
+ if (options.timeout !== undefined) {
47
+ if (typeof options.timeout !== 'number') {
48
+ throw new ValidationError('timeout must be a number')
49
+ }
50
+ if (!Number.isFinite(options.timeout)) {
51
+ throw new ValidationError('timeout must be a finite number')
52
+ }
53
+ if (options.timeout <= 0) {
54
+ throw new ValidationError('timeout must be a positive number')
55
+ }
56
+ if (options.timeout > MAX_TIMEOUT) {
57
+ throw new ValidationError(`timeout exceeds maximum allowed value of ${MAX_TIMEOUT}ms`)
58
+ }
59
+ }
60
+
61
+ // Validate script length
62
+ if (options.script !== undefined && options.script !== null) {
63
+ if (typeof options.script !== 'string') {
64
+ throw new ValidationError('script must be a string')
65
+ }
66
+ const scriptBytes = new TextEncoder().encode(options.script).length
67
+ if (scriptBytes > MAX_SCRIPT_SIZE) {
68
+ throw new ValidationError(
69
+ `script size (${scriptBytes} bytes) exceeds maximum allowed size of ${MAX_SCRIPT_SIZE} bytes (1MB)`
70
+ )
71
+ }
72
+ }
73
+
74
+ // Validate module length
75
+ if (options.module !== undefined && options.module !== null) {
76
+ if (typeof options.module !== 'string') {
77
+ throw new ValidationError('module must be a string')
78
+ }
79
+ const moduleBytes = new TextEncoder().encode(options.module).length
80
+ if (moduleBytes > MAX_SCRIPT_SIZE) {
81
+ throw new ValidationError(
82
+ `module size (${moduleBytes} bytes) exceeds maximum allowed size of ${MAX_SCRIPT_SIZE} bytes (1MB)`
83
+ )
84
+ }
85
+ }
86
+
87
+ // Validate tests length
88
+ if (options.tests !== undefined && options.tests !== null) {
89
+ if (typeof options.tests !== 'string') {
90
+ throw new ValidationError('tests must be a string')
91
+ }
92
+ const testsBytes = new TextEncoder().encode(options.tests).length
93
+ if (testsBytes > MAX_SCRIPT_SIZE) {
94
+ throw new ValidationError(
95
+ `tests size (${testsBytes} bytes) exceeds maximum allowed size of ${MAX_SCRIPT_SIZE} bytes (1MB)`
96
+ )
97
+ }
98
+ }
99
+
100
+ // Validate imports
101
+ if (options.imports !== undefined && options.imports !== null) {
102
+ if (!Array.isArray(options.imports)) {
103
+ throw new ValidationError('imports must be an array')
104
+ }
105
+ if (options.imports.length > MAX_IMPORTS) {
106
+ throw new ValidationError(
107
+ `imports count (${options.imports.length}) exceeds maximum allowed count of ${MAX_IMPORTS}`
108
+ )
109
+ }
110
+ for (let i = 0; i < options.imports.length; i++) {
111
+ const importUrl = options.imports[i]
112
+ if (typeof importUrl !== 'string') {
113
+ throw new ValidationError(`imports[${i}] must be a string`)
114
+ }
115
+ if (!isValidUrl(importUrl)) {
116
+ throw new ValidationError(`imports[${i}] is not a valid URL: ${importUrl}`)
117
+ }
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Module transformation and export detection utilities
3
+ */
4
+
5
+ /**
6
+ * Transform module code to work in sandbox
7
+ * Converts ES module exports to CommonJS-style for the sandbox
8
+ */
9
+ export function transformModuleCode(moduleCode: string): string {
10
+ let code = moduleCode
11
+
12
+ // Transform: export const foo = ... -> const foo = ...; exports.foo = foo;
13
+ code = code.replace(/export\s+(const|let|var)\s+(\w+)\s*=/g, '$1 $2 = exports.$2 =')
14
+
15
+ // Transform: export function foo(...) -> function foo(...) exports.foo = foo;
16
+ // Also handles async generators: export async function* foo
17
+ code = code.replace(/export\s+(async\s+)?function(\*?)\s+(\w+)/g, '$1function$2 $3')
18
+ // Add exports for functions after their definition
19
+ const funcNames = [...moduleCode.matchAll(/export\s+(?:async\s+)?function\*?\s+(\w+)/g)]
20
+ for (const [, name] of funcNames) {
21
+ code += `\nexports.${name} = ${name};`
22
+ }
23
+
24
+ // Transform: export class Foo -> class Foo; exports.Foo = Foo;
25
+ code = code.replace(/export\s+class\s+(\w+)/g, 'class $1')
26
+ const classNames = [...moduleCode.matchAll(/export\s+class\s+(\w+)/g)]
27
+ for (const [, name] of classNames) {
28
+ code += `\nexports.${name} = ${name};`
29
+ }
30
+
31
+ return code
32
+ }