@utdk/isolate 0.1.0-dev.646adf4

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.
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ resolveAuthProvider,
4
+ resolveOperation,
5
+ extractAuthConfigs,
6
+ loadProviderClient,
7
+ createBridge,
8
+ } from "../src/bridge.js";
9
+ import type { UtdkAuthConfig } from "../src/types.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // resolveAuthProvider
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe("resolveAuthProvider", () => {
16
+ it("returns undefined when credentials are empty", () => {
17
+ const auth = resolveAuthProvider({});
18
+ expect(auth).toBeUndefined();
19
+ });
20
+
21
+ it("resolves BearerToken from api_key config with Bearer pattern", async () => {
22
+ const configs: UtdkAuthConfig[] = [
23
+ {
24
+ auth_type: "api_key",
25
+ api_key: "Bearer ${GITHUB_TOKEN}",
26
+ var_name: "Authorization",
27
+ location: "header",
28
+ } as unknown as UtdkAuthConfig,
29
+ ];
30
+ const auth = resolveAuthProvider({ GITHUB_TOKEN: "ghs_test123" }, configs);
31
+ expect(auth).toBeDefined();
32
+
33
+ const headers: Record<string, string> = {};
34
+ await auth!.authenticate(headers);
35
+ expect(headers["Authorization"]).toBe("Bearer ghs_test123");
36
+ });
37
+
38
+ it("resolves ApiKey from raw ${VAR_NAME} api_key pattern", async () => {
39
+ const configs: UtdkAuthConfig[] = [
40
+ {
41
+ auth_type: "api_key",
42
+ api_key: "${DD_API_KEY}",
43
+ var_name: "DD-API-Key",
44
+ location: "header",
45
+ } as unknown as UtdkAuthConfig,
46
+ ];
47
+ const auth = resolveAuthProvider({ DD_API_KEY: "dd_secret" }, configs);
48
+ expect(auth).toBeDefined();
49
+
50
+ const headers: Record<string, string> = {};
51
+ await auth!.authenticate(headers);
52
+ expect(headers["DD-API-Key"]).toBe("dd_secret");
53
+ });
54
+
55
+ it("resolves BearerToken from oauth2 ACCESS_TOKEN credential", async () => {
56
+ const configs: UtdkAuthConfig[] = [
57
+ { auth_type: "oauth2", flow: "authorization_code" },
58
+ ];
59
+ const auth = resolveAuthProvider(
60
+ { SPOTIFY_ACCESS_TOKEN: "eyJaccess" },
61
+ configs,
62
+ );
63
+ expect(auth).toBeDefined();
64
+
65
+ const headers: Record<string, string> = {};
66
+ await auth!.authenticate(headers);
67
+ expect(headers["Authorization"]).toBe("Bearer eyJaccess");
68
+ });
69
+
70
+ it("falls back to BearerToken with first credential when auth config is empty", async () => {
71
+ const auth = resolveAuthProvider({ SOME_TOKEN: "fallback-token" });
72
+ expect(auth).toBeDefined();
73
+
74
+ const headers: Record<string, string> = {};
75
+ await auth!.authenticate(headers);
76
+ expect(headers["Authorization"]).toBe("Bearer fallback-token");
77
+ });
78
+
79
+ it("credential value is never written to process.env", () => {
80
+ const originalEnv = { ...process.env };
81
+ resolveAuthProvider({ SECRET_KEY: "my-secret" });
82
+ // Verify process.env was not modified
83
+ expect(process.env).toEqual(originalEnv);
84
+ });
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // resolveOperation
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe("resolveOperation", () => {
92
+ it("resolves a simple top-level function", () => {
93
+ const fn = vi.fn();
94
+ const client = { getUser: fn };
95
+ const op = resolveOperation(client, "getUser");
96
+ expect(op).toBe(fn);
97
+ });
98
+
99
+ it("resolves a nested dot-notation path", () => {
100
+ const fn = vi.fn();
101
+ const client = { users: { getByUsername: fn } };
102
+ const op = resolveOperation(client, "users.getByUsername");
103
+ expect(op).toBe(fn);
104
+ });
105
+
106
+ it("resolves a deeply nested path", () => {
107
+ const fn = vi.fn();
108
+ const client = { a: { b: { c: fn } } };
109
+ const op = resolveOperation(client, "a.b.c");
110
+ expect(op).toBe(fn);
111
+ });
112
+
113
+ it("throws TypeError when intermediate segment is missing", () => {
114
+ const client = { users: {} };
115
+ expect(() => resolveOperation(client, "users.missing.fn")).toThrow(TypeError);
116
+ });
117
+
118
+ it("throws TypeError when operation is not a function", () => {
119
+ const client = { count: 42 };
120
+ expect(() => resolveOperation(client, "count")).toThrow(TypeError);
121
+ });
122
+ });
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // extractAuthConfigs
126
+ // ---------------------------------------------------------------------------
127
+
128
+ describe("extractAuthConfigs", () => {
129
+ it("returns empty array when module has no packageJson export", () => {
130
+ const mod: Record<string, unknown> = {};
131
+ expect(extractAuthConfigs(mod)).toEqual([]);
132
+ });
133
+
134
+ it("returns empty array when packageJson has no utdk field", () => {
135
+ const mod = { packageJson: { name: "@utdk/test" } };
136
+ expect(extractAuthConfigs(mod)).toEqual([]);
137
+ });
138
+
139
+ it("extracts auth array from packageJson.utdk.auth", () => {
140
+ const authConfig: UtdkAuthConfig[] = [
141
+ { auth_type: "api_key", api_key: "Bearer ${TOKEN}", var_name: "Authorization" },
142
+ ];
143
+ const mod = {
144
+ packageJson: {
145
+ name: "@utdk/test",
146
+ utdk: { auth: authConfig },
147
+ },
148
+ };
149
+ expect(extractAuthConfigs(mod)).toEqual(authConfig);
150
+ });
151
+ });
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // loadProviderClient
155
+ // ---------------------------------------------------------------------------
156
+
157
+ describe("loadProviderClient", () => {
158
+ it("calls the create*Client factory with auth option", async () => {
159
+ const mockClient = { users: { getByUsername: vi.fn() } };
160
+ const createMockClient = vi.fn().mockResolvedValue(mockClient);
161
+ const mod = { createMockClient };
162
+
163
+ const auth = { authenticate: vi.fn() };
164
+ const result = await loadProviderClient(mod, auth);
165
+
166
+ expect(createMockClient).toHaveBeenCalledWith({ auth });
167
+ expect(result).toBe(mockClient);
168
+ });
169
+
170
+ it("calls the factory with empty options when no auth is provided", async () => {
171
+ const mockClient = {};
172
+ const createNoAuthClient = vi.fn().mockResolvedValue(mockClient);
173
+ const mod = { createNoAuthClient };
174
+
175
+ await loadProviderClient(mod, undefined);
176
+ expect(createNoAuthClient).toHaveBeenCalledWith({});
177
+ });
178
+
179
+ it("throws when no create*Client export is found", async () => {
180
+ const mod = { someOtherExport: "value" };
181
+ await expect(loadProviderClient(mod, undefined)).rejects.toThrow(
182
+ /does not export a create\*Client function/,
183
+ );
184
+ });
185
+ });
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // createBridge
189
+ // ---------------------------------------------------------------------------
190
+
191
+ describe("createBridge", () => {
192
+ it("returns an object with a call method", () => {
193
+ const op = vi.fn().mockResolvedValue({ id: 1 });
194
+ const bridge = createBridge({ operation: op });
195
+ expect(typeof bridge.call).toBe("function");
196
+ });
197
+
198
+ it("delegates call to the underlying operation", async () => {
199
+ const op = vi.fn().mockResolvedValue({ login: "octocat" });
200
+ const bridge = createBridge({ operation: op });
201
+
202
+ const result = await bridge.call({ username: "octocat" });
203
+
204
+ expect(op).toHaveBeenCalledWith({ username: "octocat" });
205
+ expect(result).toEqual({ login: "octocat" });
206
+ });
207
+
208
+ it("propagates errors from the operation", async () => {
209
+ const op = vi.fn().mockRejectedValue(new Error("API error"));
210
+ const bridge = createBridge({ operation: op });
211
+
212
+ await expect(bridge.call({})).rejects.toThrow("API error");
213
+ });
214
+ });
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Integration tests for the Isolate runtime.
3
+ *
4
+ * These tests exercise the full execute() path using mock provider modules.
5
+ * Real network calls are avoided — all provider operations are vi.fn() mocks.
6
+ *
7
+ * Provider simulation:
8
+ * - `@utdk/github` → Bearer token auth, users.getByUsername operation
9
+ * - `@utdk/spotify` → OAuth2 pre-resolved token, tracks.search operation
10
+ */
11
+
12
+ import vm from "node:vm";
13
+ import { afterEach, describe, expect, it, vi } from "vitest";
14
+ import { createBridge, resolveAuthProvider } from "../src/bridge.js";
15
+ import { IsolateTimeoutError } from "../src/index.js";
16
+ import { createSandboxContext } from "../src/sandbox.js";
17
+ import type { UtdkAuthConfig } from "../src/types.js";
18
+
19
+ // Reusable sandbox script (same one the Isolate uses internally)
20
+ const SANDBOX_SCRIPT = new vm.Script("__bridge__.call(__args__)");
21
+
22
+ afterEach(() => {
23
+ vi.restoreAllMocks();
24
+ });
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Sandbox execution via bridge
28
+ // ---------------------------------------------------------------------------
29
+
30
+ describe("sandbox script invokes bridge.call", () => {
31
+ it("passes args to the operation and returns the result", async () => {
32
+ const operationFn = vi.fn().mockResolvedValue({ id: "track-123", name: "Bohemian Rhapsody" });
33
+ const bridge = createBridge({ operation: operationFn });
34
+ const args = { q: "Bohemian Rhapsody", limit: 1 };
35
+
36
+ const context = createSandboxContext({
37
+ extraGlobals: { __bridge__: bridge, __args__: args },
38
+ });
39
+
40
+ const result = await (SANDBOX_SCRIPT.runInContext(context) as Promise<unknown>);
41
+
42
+ expect(operationFn).toHaveBeenCalledWith(args);
43
+ expect(result).toEqual({ id: "track-123", name: "Bohemian Rhapsody" });
44
+ });
45
+
46
+ it("propagates errors thrown by the bridge", async () => {
47
+ const operationFn = vi.fn().mockRejectedValue(new Error("API error 429"));
48
+ const bridge = createBridge({ operation: operationFn });
49
+
50
+ const context = createSandboxContext({
51
+ extraGlobals: { __bridge__: bridge, __args__: {} },
52
+ });
53
+
54
+ await expect(SANDBOX_SCRIPT.runInContext(context) as Promise<unknown>).rejects.toThrow("API error 429");
55
+ });
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Sandbox isolation
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe("sandbox isolation", () => {
63
+ it("process is not accessible inside the sandbox", () => {
64
+ const ctx = createSandboxContext();
65
+ const result = vm.runInContext("typeof process", ctx);
66
+ expect(result).toBe("undefined");
67
+ });
68
+
69
+ it("process.env is not accessible inside the sandbox", () => {
70
+ const ctx = createSandboxContext();
71
+ const result = vm.runInContext(
72
+ "try { typeof process.env } catch (e) { 'threw' }",
73
+ ctx,
74
+ );
75
+ expect(["threw", "undefined"]).toContain(result);
76
+ });
77
+
78
+ it("require is not accessible inside the sandbox", () => {
79
+ const ctx = createSandboxContext();
80
+ expect(vm.runInContext("typeof require", ctx)).toBe("undefined");
81
+ });
82
+
83
+ it("global is not accessible inside the sandbox", () => {
84
+ const ctx = createSandboxContext();
85
+ expect(vm.runInContext("typeof global", ctx)).toBe("undefined");
86
+ });
87
+
88
+ it("credentials are NOT injected as standalone sandbox globals", () => {
89
+ // Credentials must only flow through the bridge, never as top-level globals.
90
+ const ctx = createSandboxContext({
91
+ extraGlobals: {
92
+ __bridge__: { call: vi.fn().mockResolvedValue({}) },
93
+ __args__: {},
94
+ // GITHUB_TOKEN intentionally NOT added
95
+ },
96
+ });
97
+ expect(vm.runInContext("typeof GITHUB_TOKEN", ctx)).toBe("undefined");
98
+ expect(vm.runInContext("typeof SPOTIFY_ACCESS_TOKEN", ctx)).toBe("undefined");
99
+ });
100
+
101
+ it("state from one execution context does not leak to the next", () => {
102
+ const ctx1 = createSandboxContext();
103
+ const ctx2 = createSandboxContext();
104
+
105
+ vm.runInContext("var leakyVar = 'should-not-escape'", ctx1);
106
+
107
+ expect(vm.runInContext("typeof leakyVar", ctx2)).toBe("undefined");
108
+ });
109
+ });
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Timeout enforcement
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe("timeout enforcement", () => {
116
+ it("rejects with TimeoutError when execution stalls", async () => {
117
+ const neverResolves = new Promise<never>(() => { /* stalled */ });
118
+ const bridge = { call: vi.fn().mockReturnValue(neverResolves) };
119
+
120
+ const ctx = createSandboxContext({
121
+ extraGlobals: { __bridge__: bridge, __args__: {} },
122
+ });
123
+
124
+ const resultPromise = SANDBOX_SCRIPT.runInContext(ctx) as Promise<unknown>;
125
+ const timeoutMs = 50;
126
+
127
+ const timeoutPromise = new Promise<never>((_, reject) => {
128
+ setTimeout(() => reject(new IsolateTimeoutError(timeoutMs)), timeoutMs);
129
+ });
130
+
131
+ await expect(Promise.race([resultPromise, timeoutPromise])).rejects.toMatchObject({
132
+ name: "TimeoutError",
133
+ });
134
+ }, 1_000);
135
+
136
+ it("TimeoutError message includes the configured timeout value", () => {
137
+ const err = new IsolateTimeoutError(5_000);
138
+ expect(err.message).toContain("5000ms");
139
+ expect(err.name).toBe("TimeoutError");
140
+ });
141
+ });
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Credential injection — github simulation
145
+ // ---------------------------------------------------------------------------
146
+
147
+ describe("credential injection — github provider simulation", () => {
148
+ it("bearer token is injected via auth provider, NOT process.env", async () => {
149
+ const authConfigs: UtdkAuthConfig[] = [
150
+ {
151
+ auth_type: "api_key",
152
+ api_key: "Bearer ${GITHUB_TOKEN}",
153
+ var_name: "Authorization",
154
+ },
155
+ ];
156
+ const credentials = { GITHUB_TOKEN: "ghs_injected_by_gateway" };
157
+ const auth = resolveAuthProvider(credentials, authConfigs);
158
+
159
+ const headers: Record<string, string> = {};
160
+ await auth!.authenticate(headers);
161
+
162
+ expect(headers["Authorization"]).toBe("Bearer ghs_injected_by_gateway");
163
+ // process.env must NOT have been touched
164
+ expect(process.env["GITHUB_TOKEN"]).toBeUndefined();
165
+ });
166
+
167
+ it("executes github-like operation via sandbox bridge", async () => {
168
+ const mockUser = { login: "octocat", id: 1, type: "User" };
169
+ const getUserFn = vi.fn().mockResolvedValue(mockUser);
170
+
171
+ // Simulate how the client would be structured after createGithubClient()
172
+ const bridge = createBridge({
173
+ operation: (args) => getUserFn(args),
174
+ });
175
+
176
+ const context = createSandboxContext({
177
+ extraGlobals: { __bridge__: bridge, __args__: { username: "octocat" } },
178
+ });
179
+
180
+ const result = await (SANDBOX_SCRIPT.runInContext(context) as Promise<unknown>);
181
+
182
+ expect(getUserFn).toHaveBeenCalledWith({ username: "octocat" });
183
+ expect(result).toEqual(mockUser);
184
+ });
185
+ });
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Credential injection — spotify simulation
189
+ // ---------------------------------------------------------------------------
190
+
191
+ describe("credential injection — spotify provider simulation", () => {
192
+ it("pre-resolved OAuth2 token is injected as BearerToken, NOT via process.env", async () => {
193
+ const authConfigs: UtdkAuthConfig[] = [
194
+ { auth_type: "oauth2", flow: "authorization_code" },
195
+ ];
196
+ // Gateway pre-resolves the OAuth2 exchange and provides the access token directly
197
+ const credentials = { SPOTIFY_ACCESS_TOKEN: "eyJspotify_access_token" };
198
+ const auth = resolveAuthProvider(credentials, authConfigs);
199
+
200
+ const headers: Record<string, string> = {};
201
+ await auth!.authenticate(headers);
202
+
203
+ expect(headers["Authorization"]).toBe("Bearer eyJspotify_access_token");
204
+ expect(process.env["SPOTIFY_ACCESS_TOKEN"]).toBeUndefined();
205
+ expect(process.env["SPOTIFY_CLIENT_SECRET"]).toBeUndefined();
206
+ });
207
+
208
+ it("executes spotify-like search operation via sandbox bridge", async () => {
209
+ const mockTracks = {
210
+ tracks: { items: [{ name: "Bohemian Rhapsody", id: "abc" }] },
211
+ };
212
+ const searchFn = vi.fn().mockResolvedValue(mockTracks);
213
+ const bridge = createBridge({ operation: searchFn });
214
+
215
+ const context = createSandboxContext({
216
+ extraGlobals: {
217
+ __bridge__: bridge,
218
+ __args__: { q: "Bohemian Rhapsody", type: "track", limit: 1 },
219
+ },
220
+ });
221
+
222
+ const result = await (SANDBOX_SCRIPT.runInContext(context) as Promise<unknown>);
223
+
224
+ expect(searchFn).toHaveBeenCalledWith({
225
+ q: "Bohemian Rhapsody",
226
+ type: "track",
227
+ limit: 1,
228
+ });
229
+ expect(result).toEqual(mockTracks);
230
+ });
231
+ });
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Performance overhead measurement
235
+ // ---------------------------------------------------------------------------
236
+
237
+ describe("performance overhead", () => {
238
+ it("sandbox overhead is within acceptable bounds (<200ms for trivial calls)", async () => {
239
+ const operationFn = vi.fn().mockResolvedValue({ ok: true });
240
+ const bridge = createBridge({ operation: operationFn });
241
+ const args = { test: true };
242
+
243
+ const ITERATIONS = 5;
244
+ const sandboxDurations: number[] = [];
245
+ const directDurations: number[] = [];
246
+
247
+ for (let i = 0; i < ITERATIONS; i++) {
248
+ // Sandbox path
249
+ const ctx = createSandboxContext({
250
+ extraGlobals: { __bridge__: bridge, __args__: args },
251
+ });
252
+ const sandboxStart = performance.now();
253
+ await (SANDBOX_SCRIPT.runInContext(ctx) as Promise<unknown>);
254
+ sandboxDurations.push(performance.now() - sandboxStart);
255
+
256
+ // Direct call baseline
257
+ const directStart = performance.now();
258
+ await operationFn(args);
259
+ directDurations.push(performance.now() - directStart);
260
+ }
261
+
262
+ const avgSandbox = sandboxDurations.reduce((a, b) => a + b, 0) / ITERATIONS;
263
+ const avgDirect = directDurations.reduce((a, b) => a + b, 0) / ITERATIONS;
264
+ const overhead = avgSandbox - avgDirect;
265
+
266
+ console.log(
267
+ `[perf] avg sandbox: ${avgSandbox.toFixed(2)}ms, ` +
268
+ `avg direct: ${avgDirect.toFixed(2)}ms, ` +
269
+ `overhead: ${overhead.toFixed(2)}ms`,
270
+ );
271
+
272
+ // Context creation + vm.Script execution overhead should be well under 200ms
273
+ expect(avgSandbox).toBeLessThan(200);
274
+ });
275
+ });
@@ -0,0 +1,126 @@
1
+ import vm from "node:vm";
2
+ import { describe, it, expect } from "vitest";
3
+ import { createSandboxContext } from "../src/sandbox.js";
4
+
5
+ describe("createSandboxContext", () => {
6
+ it("creates a valid vm context", () => {
7
+ const ctx = createSandboxContext();
8
+ expect(vm.isContext(ctx)).toBe(true);
9
+ });
10
+
11
+ describe("isolation — blocked host APIs", () => {
12
+ it("does not expose process", () => {
13
+ const ctx = createSandboxContext();
14
+ const result = vm.runInContext("typeof process", ctx);
15
+ expect(result).toBe("undefined");
16
+ });
17
+
18
+ it("does not expose process.env (guard against prototype chain leakage)", () => {
19
+ const ctx = createSandboxContext();
20
+ // If `process` is undefined, accessing .env should throw or be undefined
21
+ const result = vm.runInContext(
22
+ "try { typeof process.env } catch (e) { 'threw' }",
23
+ ctx,
24
+ );
25
+ // Either 'threw' (TypeError: Cannot read properties of undefined) or 'undefined'
26
+ expect(["threw", "undefined"]).toContain(result);
27
+ });
28
+
29
+ it("does not expose require", () => {
30
+ const ctx = createSandboxContext();
31
+ expect(vm.runInContext("typeof require", ctx)).toBe("undefined");
32
+ });
33
+
34
+ it("does not expose global", () => {
35
+ const ctx = createSandboxContext();
36
+ // `global` is Node.js-specific; not in context
37
+ expect(vm.runInContext("typeof global", ctx)).toBe("undefined");
38
+ });
39
+
40
+ it("does not expose Buffer", () => {
41
+ const ctx = createSandboxContext();
42
+ expect(vm.runInContext("typeof Buffer", ctx)).toBe("undefined");
43
+ });
44
+
45
+ it("does not expose __dirname", () => {
46
+ const ctx = createSandboxContext();
47
+ expect(vm.runInContext("typeof __dirname", ctx)).toBe("undefined");
48
+ });
49
+
50
+ it("does not expose __filename", () => {
51
+ const ctx = createSandboxContext();
52
+ expect(vm.runInContext("typeof __filename", ctx)).toBe("undefined");
53
+ });
54
+ });
55
+
56
+ describe("safe globals — accessible inside sandbox", () => {
57
+ it("exposes JSON", () => {
58
+ const ctx = createSandboxContext();
59
+ expect(vm.runInContext("JSON.stringify({ a: 1 })", ctx)).toBe('{"a":1}');
60
+ });
61
+
62
+ it("exposes Math", () => {
63
+ const ctx = createSandboxContext();
64
+ expect(vm.runInContext("Math.max(2, 3)", ctx)).toBe(3);
65
+ });
66
+
67
+ it("exposes Date", () => {
68
+ const ctx = createSandboxContext();
69
+ expect(vm.runInContext("typeof new Date()", ctx)).toBe("object");
70
+ });
71
+
72
+ it("exposes URL", () => {
73
+ const ctx = createSandboxContext();
74
+ const href = vm.runInContext('new URL("https://example.com").href', ctx);
75
+ expect(href).toBe("https://example.com/");
76
+ });
77
+
78
+ it("exposes Promise", () => {
79
+ const ctx = createSandboxContext();
80
+ expect(vm.runInContext("typeof Promise.resolve", ctx)).toBe("function");
81
+ });
82
+
83
+ it("exposes setTimeout", () => {
84
+ const ctx = createSandboxContext();
85
+ expect(vm.runInContext("typeof setTimeout", ctx)).toBe("function");
86
+ });
87
+
88
+ it("exposes TextEncoder / TextDecoder", () => {
89
+ const ctx = createSandboxContext();
90
+ expect(vm.runInContext("typeof TextEncoder", ctx)).toBe("function");
91
+ expect(vm.runInContext("typeof TextDecoder", ctx)).toBe("function");
92
+ });
93
+ });
94
+
95
+ describe("extraGlobals", () => {
96
+ it("injects arbitrary globals into the context", () => {
97
+ const ctx = createSandboxContext({
98
+ extraGlobals: { __answer__: 42 },
99
+ });
100
+ expect(vm.runInContext("__answer__", ctx)).toBe(42);
101
+ });
102
+
103
+ it("injected bridge object is callable from the sandbox", () => {
104
+ const bridge = { call: (args: unknown) => ({ echo: args }) };
105
+ const ctx = createSandboxContext({
106
+ extraGlobals: {
107
+ __bridge__: bridge,
108
+ __args__: { x: 1 },
109
+ },
110
+ });
111
+ const result = vm.runInContext("__bridge__.call(__args__)", ctx) as { echo: unknown };
112
+ expect(result.echo).toEqual({ x: 1 });
113
+ });
114
+ });
115
+
116
+ describe("context isolation between calls", () => {
117
+ it("state written in one context does not leak to another", () => {
118
+ const ctx1 = createSandboxContext();
119
+ const ctx2 = createSandboxContext();
120
+
121
+ vm.runInContext("var secret = 'ctx1-secret'", ctx1);
122
+
123
+ expect(vm.runInContext("typeof secret", ctx2)).toBe("undefined");
124
+ });
125
+ });
126
+ });
@@ -0,0 +1 @@
1
+ export {};