@ricsam/isolate-runtime 0.0.1 → 0.1.1
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/CHANGELOG.md +15 -0
- package/package.json +33 -7
- package/src/index.test.ts +503 -0
- package/src/index.ts +164 -0
- package/tsconfig.json +8 -0
- package/README.md +0 -45
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @ricsam/isolate-runtime
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- initial release
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @ricsam/isolate-console@0.1.1
|
|
10
|
+
- @ricsam/isolate-core@0.1.1
|
|
11
|
+
- @ricsam/isolate-crypto@0.1.1
|
|
12
|
+
- @ricsam/isolate-encoding@0.1.1
|
|
13
|
+
- @ricsam/isolate-fetch@0.1.1
|
|
14
|
+
- @ricsam/isolate-fs@0.1.1
|
|
15
|
+
- @ricsam/isolate-timers@0.1.1
|
package/package.json
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/isolate-runtime",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.ts",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "node --test --experimental-strip-types 'src/**/*.test.ts'",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@ricsam/isolate-core": "*",
|
|
20
|
+
"@ricsam/isolate-console": "*",
|
|
21
|
+
"@ricsam/isolate-crypto": "*",
|
|
22
|
+
"@ricsam/isolate-encoding": "*",
|
|
23
|
+
"@ricsam/isolate-fetch": "*",
|
|
24
|
+
"@ricsam/isolate-fs": "*",
|
|
25
|
+
"@ricsam/isolate-timers": "*",
|
|
26
|
+
"isolated-vm": "^6"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@ricsam/isolate-test-utils": "*",
|
|
30
|
+
"@types/node": "^24",
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"isolated-vm": "^6"
|
|
35
|
+
}
|
|
10
36
|
}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { createRuntime, type RuntimeHandle } from "./index.ts";
|
|
4
|
+
|
|
5
|
+
describe("@ricsam/isolate-runtime", () => {
|
|
6
|
+
describe("createRuntime", () => {
|
|
7
|
+
test("creates runtime with default options", async () => {
|
|
8
|
+
const runtime = await createRuntime();
|
|
9
|
+
try {
|
|
10
|
+
assert(runtime.isolate, "isolate should be defined");
|
|
11
|
+
assert(runtime.context, "context should be defined");
|
|
12
|
+
assert(typeof runtime.tick === "function", "tick should be a function");
|
|
13
|
+
assert(
|
|
14
|
+
typeof runtime.dispose === "function",
|
|
15
|
+
"dispose should be a function"
|
|
16
|
+
);
|
|
17
|
+
} finally {
|
|
18
|
+
runtime.dispose();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("runtime has all globals defined", async () => {
|
|
23
|
+
const runtime = await createRuntime();
|
|
24
|
+
try {
|
|
25
|
+
const result = await runtime.context.eval(`
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
hasFetch: typeof fetch === 'function',
|
|
28
|
+
hasConsole: typeof console === 'object',
|
|
29
|
+
hasCrypto: typeof crypto === 'object',
|
|
30
|
+
hasSetTimeout: typeof setTimeout === 'function',
|
|
31
|
+
hasSetInterval: typeof setInterval === 'function',
|
|
32
|
+
hasClearTimeout: typeof clearTimeout === 'function',
|
|
33
|
+
hasClearInterval: typeof clearInterval === 'function',
|
|
34
|
+
hasPath: typeof path === 'object',
|
|
35
|
+
hasTextEncoder: typeof TextEncoder === 'function',
|
|
36
|
+
hasTextDecoder: typeof TextDecoder === 'function',
|
|
37
|
+
hasBlob: typeof Blob === 'function',
|
|
38
|
+
hasFile: typeof File === 'function',
|
|
39
|
+
hasURL: typeof URL === 'function',
|
|
40
|
+
hasURLSearchParams: typeof URLSearchParams === 'function',
|
|
41
|
+
hasHeaders: typeof Headers === 'function',
|
|
42
|
+
hasRequest: typeof Request === 'function',
|
|
43
|
+
hasResponse: typeof Response === 'function',
|
|
44
|
+
hasFormData: typeof FormData === 'function',
|
|
45
|
+
hasAbortController: typeof AbortController === 'function',
|
|
46
|
+
hasAbortSignal: typeof AbortSignal === 'function',
|
|
47
|
+
hasReadableStream: typeof ReadableStream === 'function',
|
|
48
|
+
hasBtoa: typeof btoa === 'function',
|
|
49
|
+
hasAtob: typeof atob === 'function',
|
|
50
|
+
})
|
|
51
|
+
`);
|
|
52
|
+
const globals = JSON.parse(result as string);
|
|
53
|
+
|
|
54
|
+
assert.strictEqual(globals.hasFetch, true, "fetch should be defined");
|
|
55
|
+
assert.strictEqual(
|
|
56
|
+
globals.hasConsole,
|
|
57
|
+
true,
|
|
58
|
+
"console should be defined"
|
|
59
|
+
);
|
|
60
|
+
assert.strictEqual(globals.hasCrypto, true, "crypto should be defined");
|
|
61
|
+
assert.strictEqual(
|
|
62
|
+
globals.hasSetTimeout,
|
|
63
|
+
true,
|
|
64
|
+
"setTimeout should be defined"
|
|
65
|
+
);
|
|
66
|
+
assert.strictEqual(
|
|
67
|
+
globals.hasSetInterval,
|
|
68
|
+
true,
|
|
69
|
+
"setInterval should be defined"
|
|
70
|
+
);
|
|
71
|
+
assert.strictEqual(
|
|
72
|
+
globals.hasClearTimeout,
|
|
73
|
+
true,
|
|
74
|
+
"clearTimeout should be defined"
|
|
75
|
+
);
|
|
76
|
+
assert.strictEqual(
|
|
77
|
+
globals.hasClearInterval,
|
|
78
|
+
true,
|
|
79
|
+
"clearInterval should be defined"
|
|
80
|
+
);
|
|
81
|
+
assert.strictEqual(globals.hasPath, true, "path should be defined");
|
|
82
|
+
assert.strictEqual(
|
|
83
|
+
globals.hasTextEncoder,
|
|
84
|
+
true,
|
|
85
|
+
"TextEncoder should be defined"
|
|
86
|
+
);
|
|
87
|
+
assert.strictEqual(
|
|
88
|
+
globals.hasTextDecoder,
|
|
89
|
+
true,
|
|
90
|
+
"TextDecoder should be defined"
|
|
91
|
+
);
|
|
92
|
+
assert.strictEqual(globals.hasBlob, true, "Blob should be defined");
|
|
93
|
+
assert.strictEqual(globals.hasFile, true, "File should be defined");
|
|
94
|
+
assert.strictEqual(globals.hasURL, true, "URL should be defined");
|
|
95
|
+
assert.strictEqual(
|
|
96
|
+
globals.hasURLSearchParams,
|
|
97
|
+
true,
|
|
98
|
+
"URLSearchParams should be defined"
|
|
99
|
+
);
|
|
100
|
+
assert.strictEqual(
|
|
101
|
+
globals.hasHeaders,
|
|
102
|
+
true,
|
|
103
|
+
"Headers should be defined"
|
|
104
|
+
);
|
|
105
|
+
assert.strictEqual(
|
|
106
|
+
globals.hasRequest,
|
|
107
|
+
true,
|
|
108
|
+
"Request should be defined"
|
|
109
|
+
);
|
|
110
|
+
assert.strictEqual(
|
|
111
|
+
globals.hasResponse,
|
|
112
|
+
true,
|
|
113
|
+
"Response should be defined"
|
|
114
|
+
);
|
|
115
|
+
assert.strictEqual(
|
|
116
|
+
globals.hasFormData,
|
|
117
|
+
true,
|
|
118
|
+
"FormData should be defined"
|
|
119
|
+
);
|
|
120
|
+
assert.strictEqual(
|
|
121
|
+
globals.hasAbortController,
|
|
122
|
+
true,
|
|
123
|
+
"AbortController should be defined"
|
|
124
|
+
);
|
|
125
|
+
assert.strictEqual(
|
|
126
|
+
globals.hasAbortSignal,
|
|
127
|
+
true,
|
|
128
|
+
"AbortSignal should be defined"
|
|
129
|
+
);
|
|
130
|
+
assert.strictEqual(
|
|
131
|
+
globals.hasReadableStream,
|
|
132
|
+
true,
|
|
133
|
+
"ReadableStream should be defined"
|
|
134
|
+
);
|
|
135
|
+
assert.strictEqual(globals.hasBtoa, true, "btoa should be defined");
|
|
136
|
+
assert.strictEqual(globals.hasAtob, true, "atob should be defined");
|
|
137
|
+
} finally {
|
|
138
|
+
runtime.dispose();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("dispose cleans up resources", async () => {
|
|
143
|
+
const runtime = await createRuntime();
|
|
144
|
+
runtime.dispose();
|
|
145
|
+
|
|
146
|
+
// After dispose, the isolate should be disposed
|
|
147
|
+
// Attempting to use it should throw
|
|
148
|
+
assert.throws(
|
|
149
|
+
() => {
|
|
150
|
+
runtime.isolate.createContextSync();
|
|
151
|
+
},
|
|
152
|
+
/disposed/i,
|
|
153
|
+
"isolate should be disposed"
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("accepts memory limit option", async () => {
|
|
158
|
+
const runtime = await createRuntime({
|
|
159
|
+
memoryLimit: 128,
|
|
160
|
+
});
|
|
161
|
+
try {
|
|
162
|
+
assert(runtime.isolate, "isolate should be created with memory limit");
|
|
163
|
+
} finally {
|
|
164
|
+
runtime.dispose();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("console integration", () => {
|
|
170
|
+
test("console.log is captured", async () => {
|
|
171
|
+
const logs: Array<{ level: string; args: unknown[] }> = [];
|
|
172
|
+
|
|
173
|
+
const runtime = await createRuntime({
|
|
174
|
+
console: {
|
|
175
|
+
onLog: (level, ...args) => {
|
|
176
|
+
logs.push({ level, args });
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await runtime.context.eval(`
|
|
183
|
+
console.log("hello", "world");
|
|
184
|
+
console.warn("warning message");
|
|
185
|
+
console.error("error message");
|
|
186
|
+
`);
|
|
187
|
+
|
|
188
|
+
assert.strictEqual(logs.length, 3, "should have captured 3 logs");
|
|
189
|
+
assert.strictEqual(logs[0].level, "log");
|
|
190
|
+
assert.deepStrictEqual(logs[0].args, ["hello", "world"]);
|
|
191
|
+
assert.strictEqual(logs[1].level, "warn");
|
|
192
|
+
assert.deepStrictEqual(logs[1].args, ["warning message"]);
|
|
193
|
+
assert.strictEqual(logs[2].level, "error");
|
|
194
|
+
assert.deepStrictEqual(logs[2].args, ["error message"]);
|
|
195
|
+
} finally {
|
|
196
|
+
runtime.dispose();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("fetch integration", () => {
|
|
202
|
+
test("fetch calls onFetch handler", async () => {
|
|
203
|
+
let capturedRequest: Request | null = null;
|
|
204
|
+
|
|
205
|
+
const runtime = await createRuntime({
|
|
206
|
+
fetch: {
|
|
207
|
+
onFetch: async (request) => {
|
|
208
|
+
capturedRequest = request;
|
|
209
|
+
return new Response(JSON.stringify({ message: "mocked" }), {
|
|
210
|
+
status: 200,
|
|
211
|
+
headers: { "Content-Type": "application/json" },
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const result = await runtime.context.eval(
|
|
219
|
+
`
|
|
220
|
+
(async () => {
|
|
221
|
+
const response = await fetch("https://example.com/api", {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: { "X-Custom": "header" },
|
|
224
|
+
body: "test body"
|
|
225
|
+
});
|
|
226
|
+
return JSON.stringify({
|
|
227
|
+
status: response.status,
|
|
228
|
+
body: await response.json()
|
|
229
|
+
});
|
|
230
|
+
})()
|
|
231
|
+
`,
|
|
232
|
+
{ promise: true }
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const data = JSON.parse(result as string);
|
|
236
|
+
assert.strictEqual(data.status, 200);
|
|
237
|
+
assert.deepStrictEqual(data.body, { message: "mocked" });
|
|
238
|
+
|
|
239
|
+
// Verify the request was captured correctly
|
|
240
|
+
assert(capturedRequest, "request should be captured");
|
|
241
|
+
assert.strictEqual(capturedRequest!.url, "https://example.com/api");
|
|
242
|
+
assert.strictEqual(capturedRequest!.method, "POST");
|
|
243
|
+
assert.strictEqual(capturedRequest!.headers.get("X-Custom"), "header");
|
|
244
|
+
} finally {
|
|
245
|
+
runtime.dispose();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("timers integration", () => {
|
|
251
|
+
test("setTimeout works with tick()", async () => {
|
|
252
|
+
const runtime = await createRuntime();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Set up a timeout that modifies a global variable
|
|
256
|
+
await runtime.context.eval(`
|
|
257
|
+
globalThis.timerFired = false;
|
|
258
|
+
globalThis.timerValue = 0;
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
globalThis.timerFired = true;
|
|
261
|
+
globalThis.timerValue = 42;
|
|
262
|
+
}, 100);
|
|
263
|
+
`);
|
|
264
|
+
|
|
265
|
+
// Before tick, timer should not have fired
|
|
266
|
+
let result = await runtime.context.eval(`globalThis.timerFired`);
|
|
267
|
+
assert.strictEqual(result, false, "timer should not fire before tick");
|
|
268
|
+
|
|
269
|
+
// Tick forward 50ms - still not enough
|
|
270
|
+
await runtime.tick(50);
|
|
271
|
+
result = await runtime.context.eval(`globalThis.timerFired`);
|
|
272
|
+
assert.strictEqual(result, false, "timer should not fire at 50ms");
|
|
273
|
+
|
|
274
|
+
// Tick forward another 50ms (total 100ms) - now it should fire
|
|
275
|
+
await runtime.tick(50);
|
|
276
|
+
result = await runtime.context.eval(`globalThis.timerFired`);
|
|
277
|
+
assert.strictEqual(result, true, "timer should fire at 100ms");
|
|
278
|
+
|
|
279
|
+
result = await runtime.context.eval(`globalThis.timerValue`);
|
|
280
|
+
assert.strictEqual(result, 42, "timer should have set value");
|
|
281
|
+
} finally {
|
|
282
|
+
runtime.dispose();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("setInterval works with tick()", async () => {
|
|
287
|
+
const runtime = await createRuntime();
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await runtime.context.eval(`
|
|
291
|
+
globalThis.intervalCount = 0;
|
|
292
|
+
setInterval(() => {
|
|
293
|
+
globalThis.intervalCount++;
|
|
294
|
+
}, 100);
|
|
295
|
+
`);
|
|
296
|
+
|
|
297
|
+
// Tick incrementally - interval fires at each 100ms boundary
|
|
298
|
+
await runtime.tick(100); // t=100ms, first fire
|
|
299
|
+
let count = await runtime.context.eval(`globalThis.intervalCount`);
|
|
300
|
+
assert.strictEqual(count, 1, "interval should fire once at 100ms");
|
|
301
|
+
|
|
302
|
+
await runtime.tick(100); // t=200ms, second fire
|
|
303
|
+
count = await runtime.context.eval(`globalThis.intervalCount`);
|
|
304
|
+
assert.strictEqual(count, 2, "interval should fire twice at 200ms");
|
|
305
|
+
} finally {
|
|
306
|
+
runtime.dispose();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("crypto integration", () => {
|
|
312
|
+
test("crypto.randomUUID generates valid UUIDs", async () => {
|
|
313
|
+
const runtime = await createRuntime();
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const uuid = (await runtime.context.eval(
|
|
317
|
+
`crypto.randomUUID()`
|
|
318
|
+
)) as string;
|
|
319
|
+
assert.match(
|
|
320
|
+
uuid,
|
|
321
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
322
|
+
"should generate valid UUID v4"
|
|
323
|
+
);
|
|
324
|
+
} finally {
|
|
325
|
+
runtime.dispose();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("path integration", () => {
|
|
331
|
+
test("path.join works correctly", async () => {
|
|
332
|
+
const runtime = await createRuntime();
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const result = await runtime.context.eval(`path.join('a', 'b', 'c')`);
|
|
336
|
+
assert.strictEqual(result, "a/b/c");
|
|
337
|
+
} finally {
|
|
338
|
+
runtime.dispose();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("encoding integration", () => {
|
|
344
|
+
test("btoa and atob work correctly", async () => {
|
|
345
|
+
const runtime = await createRuntime();
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const encoded = await runtime.context.eval(`btoa('hello')`);
|
|
349
|
+
assert.strictEqual(encoded, "aGVsbG8=");
|
|
350
|
+
|
|
351
|
+
const decoded = await runtime.context.eval(`atob('aGVsbG8=')`);
|
|
352
|
+
assert.strictEqual(decoded, "hello");
|
|
353
|
+
} finally {
|
|
354
|
+
runtime.dispose();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("GC disposal", () => {
|
|
360
|
+
test("resources are cleaned up on dispose", async () => {
|
|
361
|
+
const runtime = await createRuntime();
|
|
362
|
+
|
|
363
|
+
// Create some resources
|
|
364
|
+
await runtime.context.eval(`
|
|
365
|
+
const blob = new Blob(["test"]);
|
|
366
|
+
const url = new URL("https://example.com");
|
|
367
|
+
setTimeout(() => {}, 1000);
|
|
368
|
+
`);
|
|
369
|
+
|
|
370
|
+
// Dispose should not throw
|
|
371
|
+
assert.doesNotThrow(() => {
|
|
372
|
+
runtime.dispose();
|
|
373
|
+
}, "dispose should not throw");
|
|
374
|
+
|
|
375
|
+
// After dispose, attempting to use the context should fail
|
|
376
|
+
await assert.rejects(
|
|
377
|
+
async () => {
|
|
378
|
+
await runtime.context.eval(`1 + 1`);
|
|
379
|
+
},
|
|
380
|
+
/released|disposed/i,
|
|
381
|
+
"context should be released after dispose"
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("fs integration", () => {
|
|
387
|
+
test("getDirectory works when handler provided", async () => {
|
|
388
|
+
const files = new Map<
|
|
389
|
+
string,
|
|
390
|
+
{ data: Uint8Array; lastModified: number; type: string }
|
|
391
|
+
>();
|
|
392
|
+
const directories = new Set<string>(["/"]); // Root directory exists
|
|
393
|
+
|
|
394
|
+
const createHandler = () => ({
|
|
395
|
+
async getFileHandle(path: string, options?: { create?: boolean }) {
|
|
396
|
+
if (!files.has(path) && !options?.create) {
|
|
397
|
+
throw new Error("[NotFoundError]File not found");
|
|
398
|
+
}
|
|
399
|
+
if (!files.has(path) && options?.create) {
|
|
400
|
+
files.set(path, {
|
|
401
|
+
data: new Uint8Array(0),
|
|
402
|
+
lastModified: Date.now(),
|
|
403
|
+
type: "",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async getDirectoryHandle(path: string, options?: { create?: boolean }) {
|
|
408
|
+
if (!directories.has(path) && !options?.create) {
|
|
409
|
+
throw new Error("[NotFoundError]Directory not found");
|
|
410
|
+
}
|
|
411
|
+
if (options?.create) {
|
|
412
|
+
directories.add(path);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
async removeEntry(path: string) {
|
|
416
|
+
files.delete(path);
|
|
417
|
+
directories.delete(path);
|
|
418
|
+
},
|
|
419
|
+
async readDirectory(path: string) {
|
|
420
|
+
const entries: Array<{ name: string; kind: "file" | "directory" }> =
|
|
421
|
+
[];
|
|
422
|
+
for (const filePath of files.keys()) {
|
|
423
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/")) || "/";
|
|
424
|
+
if (dir === path) {
|
|
425
|
+
entries.push({
|
|
426
|
+
name: filePath.substring(filePath.lastIndexOf("/") + 1),
|
|
427
|
+
kind: "file",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const dirPath of directories) {
|
|
432
|
+
if (dirPath !== path && dirPath.startsWith(path)) {
|
|
433
|
+
const relativePath = dirPath.substring(path.length);
|
|
434
|
+
const parts = relativePath.split("/").filter(Boolean);
|
|
435
|
+
if (parts.length === 1) {
|
|
436
|
+
entries.push({ name: parts[0], kind: "directory" });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return entries;
|
|
441
|
+
},
|
|
442
|
+
async readFile(path: string) {
|
|
443
|
+
const file = files.get(path);
|
|
444
|
+
if (!file) {
|
|
445
|
+
throw new Error("[NotFoundError]File not found");
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
data: file.data,
|
|
449
|
+
size: file.data.length,
|
|
450
|
+
lastModified: file.lastModified,
|
|
451
|
+
type: file.type,
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
async writeFile(path: string, data: Uint8Array) {
|
|
455
|
+
const existing = files.get(path);
|
|
456
|
+
files.set(path, {
|
|
457
|
+
data,
|
|
458
|
+
lastModified: Date.now(),
|
|
459
|
+
type: existing?.type ?? "",
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
async truncateFile(path: string, size: number) {
|
|
463
|
+
const file = files.get(path);
|
|
464
|
+
if (file) {
|
|
465
|
+
file.data = file.data.slice(0, size);
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
async getFileMetadata(path: string) {
|
|
469
|
+
const file = files.get(path);
|
|
470
|
+
if (!file) {
|
|
471
|
+
throw new Error("[NotFoundError]File not found");
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
size: file.data.length,
|
|
475
|
+
lastModified: file.lastModified,
|
|
476
|
+
type: file.type,
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const runtime = await createRuntime({
|
|
482
|
+
fs: {
|
|
483
|
+
getDirectory: async () => createHandler(),
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const result = await runtime.context.eval(
|
|
489
|
+
`
|
|
490
|
+
(async () => {
|
|
491
|
+
const root = await getDirectory("/");
|
|
492
|
+
return root.kind;
|
|
493
|
+
})()
|
|
494
|
+
`,
|
|
495
|
+
{ promise: true }
|
|
496
|
+
);
|
|
497
|
+
assert.strictEqual(result, "directory");
|
|
498
|
+
} finally {
|
|
499
|
+
runtime.dispose();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import ivm from "isolated-vm";
|
|
2
|
+
import { setupCore } from "@ricsam/isolate-core";
|
|
3
|
+
import { setupConsole } from "@ricsam/isolate-console";
|
|
4
|
+
import { setupEncoding } from "@ricsam/isolate-encoding";
|
|
5
|
+
import { setupTimers } from "@ricsam/isolate-timers";
|
|
6
|
+
import { setupPath } from "@ricsam/isolate-path";
|
|
7
|
+
import { setupCrypto } from "@ricsam/isolate-crypto";
|
|
8
|
+
import { setupFetch } from "@ricsam/isolate-fetch";
|
|
9
|
+
import { setupFs } from "@ricsam/isolate-fs";
|
|
10
|
+
|
|
11
|
+
import type { ConsoleOptions, ConsoleHandle } from "@ricsam/isolate-console";
|
|
12
|
+
import type { FetchOptions, FetchHandle } from "@ricsam/isolate-fetch";
|
|
13
|
+
import type { FsOptions, FsHandle } from "@ricsam/isolate-fs";
|
|
14
|
+
import type { CoreHandle } from "@ricsam/isolate-core";
|
|
15
|
+
import type { EncodingHandle } from "@ricsam/isolate-encoding";
|
|
16
|
+
import type { TimersHandle } from "@ricsam/isolate-timers";
|
|
17
|
+
import type { PathHandle } from "@ricsam/isolate-path";
|
|
18
|
+
import type { CryptoHandle } from "@ricsam/isolate-crypto";
|
|
19
|
+
|
|
20
|
+
export interface RuntimeOptions {
|
|
21
|
+
/** Isolate memory limit in MB */
|
|
22
|
+
memoryLimit?: number;
|
|
23
|
+
/** Console options */
|
|
24
|
+
console?: ConsoleOptions;
|
|
25
|
+
/** Fetch options */
|
|
26
|
+
fetch?: FetchOptions;
|
|
27
|
+
/** File system options (optional - fs only set up if provided) */
|
|
28
|
+
fs?: FsOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RuntimeHandle {
|
|
32
|
+
/** The isolate instance */
|
|
33
|
+
readonly isolate: ivm.Isolate;
|
|
34
|
+
/** The context instance */
|
|
35
|
+
readonly context: ivm.Context;
|
|
36
|
+
/** The fetch handle for serve() and WebSocket dispatching */
|
|
37
|
+
readonly fetch: FetchHandle;
|
|
38
|
+
/** Process pending timers */
|
|
39
|
+
tick(ms?: number): Promise<void>;
|
|
40
|
+
/** Dispose all resources */
|
|
41
|
+
dispose(): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a fully configured isolated-vm runtime
|
|
46
|
+
*
|
|
47
|
+
* Sets up all WHATWG APIs: fetch, fs, console, crypto, encoding, timers
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const runtime = await createRuntime({
|
|
51
|
+
* console: {
|
|
52
|
+
* onLog: (level, ...args) => console.log(`[${level}]`, ...args)
|
|
53
|
+
* },
|
|
54
|
+
* fetch: {
|
|
55
|
+
* onFetch: async (request) => fetch(request)
|
|
56
|
+
* }
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* await runtime.context.eval(`
|
|
60
|
+
* console.log("Hello from sandbox!");
|
|
61
|
+
* const response = await fetch("https://example.com");
|
|
62
|
+
* `);
|
|
63
|
+
*
|
|
64
|
+
* runtime.dispose();
|
|
65
|
+
*/
|
|
66
|
+
export async function createRuntime(
|
|
67
|
+
options?: RuntimeOptions
|
|
68
|
+
): Promise<RuntimeHandle> {
|
|
69
|
+
const opts = options ?? {};
|
|
70
|
+
|
|
71
|
+
// Create isolate with optional memory limit
|
|
72
|
+
const isolate = new ivm.Isolate({
|
|
73
|
+
memoryLimit: opts.memoryLimit,
|
|
74
|
+
});
|
|
75
|
+
const context = await isolate.createContext();
|
|
76
|
+
|
|
77
|
+
// Store all handles for disposal
|
|
78
|
+
const handles: {
|
|
79
|
+
core?: CoreHandle;
|
|
80
|
+
console?: ConsoleHandle;
|
|
81
|
+
encoding?: EncodingHandle;
|
|
82
|
+
timers?: TimersHandle;
|
|
83
|
+
path?: PathHandle;
|
|
84
|
+
crypto?: CryptoHandle;
|
|
85
|
+
fetch?: FetchHandle;
|
|
86
|
+
fs?: FsHandle;
|
|
87
|
+
} = {};
|
|
88
|
+
|
|
89
|
+
// Setup all APIs in order
|
|
90
|
+
// Core must be first as it provides Blob, File, streams, URL, etc.
|
|
91
|
+
handles.core = await setupCore(context);
|
|
92
|
+
|
|
93
|
+
// Console
|
|
94
|
+
handles.console = await setupConsole(context, opts.console);
|
|
95
|
+
|
|
96
|
+
// Encoding (btoa/atob)
|
|
97
|
+
handles.encoding = await setupEncoding(context);
|
|
98
|
+
|
|
99
|
+
// Timers (setTimeout, setInterval)
|
|
100
|
+
handles.timers = await setupTimers(context);
|
|
101
|
+
|
|
102
|
+
// Path module
|
|
103
|
+
handles.path = await setupPath(context);
|
|
104
|
+
|
|
105
|
+
// Crypto (randomUUID, getRandomValues)
|
|
106
|
+
handles.crypto = await setupCrypto(context);
|
|
107
|
+
|
|
108
|
+
// Fetch API
|
|
109
|
+
handles.fetch = await setupFetch(context, opts.fetch);
|
|
110
|
+
|
|
111
|
+
// File system (only if handler provided)
|
|
112
|
+
if (opts.fs) {
|
|
113
|
+
handles.fs = await setupFs(context, opts.fs);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
isolate,
|
|
118
|
+
context,
|
|
119
|
+
fetch: handles.fetch!,
|
|
120
|
+
async tick(ms?: number) {
|
|
121
|
+
await handles.timers!.tick(ms);
|
|
122
|
+
},
|
|
123
|
+
dispose() {
|
|
124
|
+
// Dispose all handles
|
|
125
|
+
handles.fs?.dispose();
|
|
126
|
+
handles.fetch?.dispose();
|
|
127
|
+
handles.crypto?.dispose();
|
|
128
|
+
handles.path?.dispose();
|
|
129
|
+
handles.timers?.dispose();
|
|
130
|
+
handles.encoding?.dispose();
|
|
131
|
+
handles.console?.dispose();
|
|
132
|
+
handles.core?.dispose();
|
|
133
|
+
|
|
134
|
+
// Release context and dispose isolate
|
|
135
|
+
context.release();
|
|
136
|
+
isolate.dispose();
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Re-export all package types and functions
|
|
142
|
+
export { setupCore } from "@ricsam/isolate-core";
|
|
143
|
+
export type { CoreHandle, SetupCoreOptions } from "@ricsam/isolate-core";
|
|
144
|
+
|
|
145
|
+
export { setupConsole } from "@ricsam/isolate-console";
|
|
146
|
+
export type { ConsoleHandle, ConsoleOptions } from "@ricsam/isolate-console";
|
|
147
|
+
|
|
148
|
+
export { setupCrypto } from "@ricsam/isolate-crypto";
|
|
149
|
+
export type { CryptoHandle } from "@ricsam/isolate-crypto";
|
|
150
|
+
|
|
151
|
+
export { setupEncoding } from "@ricsam/isolate-encoding";
|
|
152
|
+
export type { EncodingHandle } from "@ricsam/isolate-encoding";
|
|
153
|
+
|
|
154
|
+
export { setupFetch } from "@ricsam/isolate-fetch";
|
|
155
|
+
export type { FetchHandle, FetchOptions, WebSocketCommand, UpgradeRequest } from "@ricsam/isolate-fetch";
|
|
156
|
+
|
|
157
|
+
export { setupFs, createNodeFileSystemHandler } from "@ricsam/isolate-fs";
|
|
158
|
+
export type { FsHandle, FsOptions, FileSystemHandler, NodeFileSystemHandlerOptions } from "@ricsam/isolate-fs";
|
|
159
|
+
|
|
160
|
+
export { setupPath } from "@ricsam/isolate-path";
|
|
161
|
+
export type { PathHandle } from "@ricsam/isolate-path";
|
|
162
|
+
|
|
163
|
+
export { setupTimers } from "@ricsam/isolate-timers";
|
|
164
|
+
export type { TimersHandle } from "@ricsam/isolate-timers";
|
package/tsconfig.json
ADDED
package/README.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# @ricsam/isolate-runtime
|
|
2
|
-
|
|
3
|
-
## ⚠️ IMPORTANT NOTICE ⚠️
|
|
4
|
-
|
|
5
|
-
**This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
|
|
6
|
-
|
|
7
|
-
This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
This package exists to:
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@ricsam/isolate-runtime`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
15
|
-
|
|
16
|
-
## What is OIDC Trusted Publishing?
|
|
17
|
-
|
|
18
|
-
OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
|
|
19
|
-
|
|
20
|
-
## Setup Instructions
|
|
21
|
-
|
|
22
|
-
To properly configure OIDC trusted publishing for this package:
|
|
23
|
-
|
|
24
|
-
1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
28
|
-
|
|
29
|
-
## DO NOT USE THIS PACKAGE
|
|
30
|
-
|
|
31
|
-
This package is a placeholder for OIDC configuration only. It:
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
36
|
-
|
|
37
|
-
## More Information
|
|
38
|
-
|
|
39
|
-
For more details about npm's trusted publishing feature, see:
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
**Maintained for OIDC setup purposes only**
|