@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.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.
Files changed (63) hide show
  1. package/README.md +52 -1
  2. package/dist/config-schema.json +73 -0
  3. package/dist/index.d.ts +61 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +59 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +87 -5
  8. package/src/build/canonical-target.js +436 -0
  9. package/src/build/entities-native.js +564 -0
  10. package/src/build/fixed-memory-runtime.as.ts +148 -0
  11. package/src/build/native-http-effects.js +871 -0
  12. package/src/build/native-http-shell.js +703 -0
  13. package/src/build/native-platform-capabilities.js +2109 -0
  14. package/src/compiler/assets-package-out-parity-proof.js +338 -0
  15. package/src/compiler/assets-provider-proof.js +499 -0
  16. package/src/compiler/config-provider-resolution.js +573 -0
  17. package/src/compiler/fastly-adapter.js +564 -0
  18. package/src/compiler/fastly-command-entry.js +390 -0
  19. package/src/compiler/fastly-config-secret-serve.js +323 -0
  20. package/src/compiler/fastly-hostcall-binding.js +598 -0
  21. package/src/compiler/fastly-readiness.js +477 -0
  22. package/src/compiler/index.js +40 -0
  23. package/src/compiler/lifecycle-parity-contract.js +70 -0
  24. package/src/compiler/lifecycle-parity-proof.js +260 -0
  25. package/src/compiler/route-handler-effect-proof.js +255 -0
  26. package/src/compiler/runtime-provider-kv.js +237 -0
  27. package/src/compiler/runtime-provider-shape.js +109 -0
  28. package/src/config-api.js +63 -0
  29. package/src/config-schema.d.ts +13 -0
  30. package/src/config-schema.js +27 -0
  31. package/src/config-schema.json +73 -0
  32. package/src/index.js +3 -0
  33. package/src/index.ts +118 -0
  34. package/src/javascript/assets-adapter.js +122 -0
  35. package/src/javascript/bindings-adapter.js +237 -0
  36. package/src/javascript/errors.js +26 -0
  37. package/src/javascript/fetch-adapter.js +93 -0
  38. package/src/javascript/grip-broadcast.js +261 -0
  39. package/src/javascript/jwt-verifier.js +137 -0
  40. package/src/javascript/lifecycle.js +55 -0
  41. package/src/javascript/local-loader.js +138 -0
  42. package/src/javascript/logging.js +26 -0
  43. package/src/javascript/package-effects.js +68 -0
  44. package/src/javascript/runtime-host.js +108 -0
  45. package/src/javascript/sha256.js +128 -0
  46. package/src/javascript/source-package.js +776 -0
  47. package/src/javascript/support.js +142 -0
  48. package/src/javascript/target-support-policy.js +260 -0
  49. package/src/javascript/target.js +73 -0
  50. package/src/javascript/test-runtime.js +407 -0
  51. package/src/provider-contract.js +72 -0
  52. package/src/runtime/assets-provider.js +464 -0
  53. package/src/runtime/canonical-api-runtime.js +597 -0
  54. package/src/runtime/index.js +28 -0
  55. package/src/runtime/provider-primitives-contract.js +272 -0
  56. package/src/runtime/provider-primitives.js +82 -0
  57. package/src/runtime/route-handler-effects.js +507 -0
  58. package/src/testing/fastly-cli.js +538 -0
  59. package/src/testing/native-http-effects-host.js +390 -0
  60. package/src/testing/native-http-shell-host.js +190 -0
  61. package/src/testing/native-platform-capabilities-host.js +599 -0
  62. package/src/toolchain/config.js +128 -0
  63. package/src/toolchain/index.js +343 -0
@@ -0,0 +1,538 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const http = require('node:http');
5
+ const net = require('node:net');
6
+ const path = require('node:path');
7
+ const { spawn, spawnSync } = require('node:child_process');
8
+
9
+ const fastlyComputeRealityVersion = 'pulse.fastly-compute-reality.v3';
10
+ const maxCapturedLogBytes = 2 * 1024 * 1024;
11
+
12
+ class FastlyComputeRealityError extends Error {
13
+ constructor(code, message, detail = {}) {
14
+ super(message);
15
+ this.name = 'FastlyComputeRealityError';
16
+ this.code = code;
17
+ this.detail = Object.freeze({ ...detail });
18
+ }
19
+ }
20
+
21
+ function executable(file) {
22
+ if (!file) return false;
23
+ try {
24
+ fs.accessSync(file, fs.constants.X_OK);
25
+ return fs.statSync(file).isFile();
26
+ } catch (_) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ function pathCandidates(binaryName = 'fastly', env = process.env) {
32
+ const delimiter = process.platform === 'win32' ? ';' : ':';
33
+ const extension = process.platform === 'win32' ? '.exe' : '';
34
+ return String(env.PATH || '')
35
+ .split(delimiter)
36
+ .filter(Boolean)
37
+ .map((entry) => path.join(entry, `${binaryName}${extension}`));
38
+ }
39
+
40
+ function resolveFastlyCliBinary(options = {}) {
41
+ const env = options.env || process.env;
42
+ const candidates = [
43
+ options.binary,
44
+ env.PULSE_FASTLY_BIN,
45
+ ...pathCandidates('fastly', env)
46
+ ].filter(Boolean).map((entry) => path.resolve(String(entry)));
47
+ const found = candidates.find(executable);
48
+ if (!found) {
49
+ throw new FastlyComputeRealityError(
50
+ 'PULSE_FASTLY_CLI_UNAVAILABLE',
51
+ 'The Fastly CLI is required for the Fastly Compute reality gate.',
52
+ {
53
+ searched: Object.freeze([...new Set(candidates)]),
54
+ hint: 'Install the Fastly CLI with Homebrew, a supported package manager, or an official Fastly CLI release; then ensure `fastly` is on PATH. CI may set PULSE_FASTLY_BIN explicitly.'
55
+ }
56
+ );
57
+ }
58
+ return found;
59
+ }
60
+
61
+ function resolveViceroyBinary(options = {}) {
62
+ const env = options.env || process.env;
63
+ const candidates = [
64
+ options.viceroyBinary,
65
+ env.PULSE_VICEROY_BIN,
66
+ ...pathCandidates('viceroy', env)
67
+ ].filter(Boolean).map((entry) => path.resolve(String(entry)));
68
+ const found = candidates.find(executable);
69
+ if (!found) {
70
+ throw new FastlyComputeRealityError(
71
+ 'PULSE_VICEROY_UNAVAILABLE',
72
+ 'The selected Viceroy override is unavailable.',
73
+ {
74
+ searched: Object.freeze([...new Set(candidates)]),
75
+ hint: 'Point PULSE_VICEROY_BIN at an executable Viceroy binary, or unset it and provide the Fastly CLI through PULSE_FASTLY_BIN or PATH.'
76
+ }
77
+ );
78
+ }
79
+ return found;
80
+ }
81
+
82
+ function inspectViceroy(options = {}) {
83
+ const binary = resolveViceroyBinary(options);
84
+ const attempts = [['--version'], ['version']];
85
+ const records = [];
86
+ let selected;
87
+ for (const args of attempts) {
88
+ const result = spawnSync(binary, args, {
89
+ cwd: options.cwd || process.cwd(),
90
+ env: { ...process.env, ...(options.env || {}) },
91
+ encoding: 'utf8',
92
+ timeout: Number(options.timeoutMs || 10000),
93
+ windowsHide: true
94
+ });
95
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
96
+ records.push(Object.freeze({ args: Object.freeze([...args]), status: result.status, output, message: result.error && result.error.message }));
97
+ if (!result.error && result.status === 0) {
98
+ selected = { args, output };
99
+ break;
100
+ }
101
+ }
102
+ if (!selected) {
103
+ throw new FastlyComputeRealityError(
104
+ 'PULSE_VICEROY_INSPECTION_FAILED',
105
+ `Unable to inspect Viceroy at ${binary}.`,
106
+ { binary, attempts: Object.freeze(records) }
107
+ );
108
+ }
109
+ const match = selected.output.match(/(?:viceroy(?:\s+version)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/i);
110
+ const env = options.env || process.env;
111
+ return Object.freeze({
112
+ binary,
113
+ version: match ? match[1] : undefined,
114
+ output: selected.output,
115
+ args: Object.freeze([...selected.args]),
116
+ source: options.viceroyBinary ? 'explicit' : env.PULSE_VICEROY_BIN ? 'environment' : 'path'
117
+ });
118
+ }
119
+
120
+ function inspectFastlyCli(options = {}) {
121
+ const binary = resolveFastlyCliBinary(options);
122
+ const attempts = [['version'], ['--version']];
123
+ const records = [];
124
+ let selected;
125
+ for (const args of attempts) {
126
+ const result = spawnSync(binary, args, {
127
+ cwd: options.cwd || process.cwd(),
128
+ env: { ...process.env, ...(options.env || {}) },
129
+ encoding: 'utf8',
130
+ timeout: Number(options.timeoutMs || 10000),
131
+ windowsHide: true
132
+ });
133
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
134
+ const match = output.match(/(?:fastly(?:\s+cli)?(?:\s+version)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/i);
135
+ const timedOutAfterCompleteOutput = result.error && result.error.code === 'ETIMEDOUT' && Boolean(match);
136
+ records.push(Object.freeze({
137
+ args: Object.freeze([...args]),
138
+ status: result.status,
139
+ output,
140
+ message: result.error && result.error.message,
141
+ completeVersionOutput: Boolean(match)
142
+ }));
143
+ if ((!result.error && result.status === 0) || timedOutAfterCompleteOutput) {
144
+ selected = {
145
+ args,
146
+ output,
147
+ version: match && match[1],
148
+ termination: timedOutAfterCompleteOutput ? 'timeout-after-complete-version-output' : 'clean-exit'
149
+ };
150
+ break;
151
+ }
152
+ }
153
+ if (!selected) {
154
+ throw new FastlyComputeRealityError(
155
+ 'PULSE_FASTLY_CLI_INSPECTION_FAILED',
156
+ `Unable to inspect the Fastly CLI at ${binary}.`,
157
+ { binary, attempts: Object.freeze(records) }
158
+ );
159
+ }
160
+ return Object.freeze({
161
+ binary,
162
+ version: selected.version,
163
+ output: selected.output,
164
+ args: Object.freeze([...selected.args]),
165
+ termination: selected.termination,
166
+ source: options.binary ? 'explicit' : (options.env || process.env).PULSE_FASTLY_BIN ? 'environment' : 'path'
167
+ });
168
+ }
169
+
170
+ function inspectFastlyComputeLauncher(options = {}) {
171
+ const env = options.env || process.env;
172
+ const requestedKind = options.launcherKind;
173
+ if (
174
+ requestedKind !== undefined
175
+ && requestedKind !== 'fastly-cli'
176
+ && requestedKind !== 'viceroy-direct'
177
+ ) {
178
+ throw new FastlyComputeRealityError(
179
+ 'PULSE_FASTLY_LAUNCHER_INVALID',
180
+ `Unknown Fastly Compute reality launcher: ${String(requestedKind)}.`,
181
+ { launcherKind: requestedKind }
182
+ );
183
+ }
184
+
185
+ const explicitFastly = Boolean(
186
+ options.inspection
187
+ || options.binary
188
+ || env.PULSE_FASTLY_BIN
189
+ );
190
+ const explicitViceroy = Boolean(
191
+ options.viceroyInspection
192
+ || options.viceroyBinary
193
+ || env.PULSE_VICEROY_BIN
194
+ );
195
+ const useDirectViceroy = requestedKind === 'viceroy-direct'
196
+ || (!requestedKind && !explicitFastly && explicitViceroy);
197
+
198
+ if (useDirectViceroy) {
199
+ const inspection = options.viceroyInspection || inspectViceroy(options);
200
+ return Object.freeze({
201
+ kind: 'viceroy-direct',
202
+ owner: 'pulse-test-harness',
203
+ inspection,
204
+ fastlyCliInspection: undefined,
205
+ viceroyInspection: inspection
206
+ });
207
+ }
208
+
209
+ if (requestedKind === 'fastly-cli' || explicitFastly) {
210
+ const inspection = options.inspection || inspectFastlyCli(options);
211
+ const viceroyRequested = Boolean(
212
+ options.viceroyInspection
213
+ || options.viceroyBinary
214
+ || env.PULSE_VICEROY_BIN
215
+ || options.requireViceroy
216
+ );
217
+ const viceroyInspection = viceroyRequested
218
+ ? (options.viceroyInspection || inspectViceroy(options))
219
+ : undefined;
220
+ return Object.freeze({
221
+ kind: 'fastly-cli',
222
+ owner: 'fastly-cli',
223
+ inspection,
224
+ fastlyCliInspection: inspection,
225
+ viceroyInspection
226
+ });
227
+ }
228
+
229
+ try {
230
+ const inspection = inspectFastlyCli(options);
231
+ return Object.freeze({
232
+ kind: 'fastly-cli',
233
+ owner: 'fastly-cli',
234
+ inspection,
235
+ fastlyCliInspection: inspection,
236
+ viceroyInspection: undefined
237
+ });
238
+ } catch (fastlyError) {
239
+ if (fastlyError.code !== 'PULSE_FASTLY_CLI_UNAVAILABLE') throw fastlyError;
240
+ try {
241
+ const inspection = inspectViceroy(options);
242
+ return Object.freeze({
243
+ kind: 'viceroy-direct',
244
+ owner: 'pulse-test-harness',
245
+ inspection,
246
+ fastlyCliInspection: undefined,
247
+ viceroyInspection: inspection
248
+ });
249
+ } catch (viceroyError) {
250
+ if (viceroyError.code !== 'PULSE_VICEROY_UNAVAILABLE') {
251
+ throw viceroyError;
252
+ }
253
+ throw new FastlyComputeRealityError(
254
+ 'PULSE_FASTLY_REALITY_ENGINE_UNAVAILABLE',
255
+ 'The Fastly Compute reality gate requires either the Fastly CLI or Viceroy.',
256
+ {
257
+ fastlyCli: fastlyError.detail,
258
+ viceroy: viceroyError.detail,
259
+ hint: 'Set PULSE_VICEROY_BIN to an executable Viceroy release, set PULSE_FASTLY_BIN to the Fastly CLI, or place either tool on PATH.'
260
+ }
261
+ );
262
+ }
263
+ }
264
+ }
265
+
266
+ function tomlString(value) {
267
+ return JSON.stringify(String(value));
268
+ }
269
+
270
+ function tomlKey(value) {
271
+ return JSON.stringify(String(value));
272
+ }
273
+
274
+ function kvData(value) {
275
+ if (typeof value === 'string') return value;
276
+ return JSON.stringify({ __pulseKv: 1, value });
277
+ }
278
+
279
+ function renderFastlyLocalConfig(input = {}) {
280
+ const lines = [
281
+ 'manifest_version = 3',
282
+ `name = ${tomlString(input.name || 'pulse-fastly-compute-reality')}`,
283
+ `description = ${tomlString(input.description || 'Pulse Fastly Compute reality fixture')}`,
284
+ 'authors = []',
285
+ 'language = "other"',
286
+ ''
287
+ ];
288
+
289
+ for (const [name, backend] of Object.entries(input.backends || {})) {
290
+ const value = typeof backend === 'string' ? { url: backend } : backend || {};
291
+ lines.push(`[local_server.backends.${tomlKey(name)}]`);
292
+ lines.push(`url = ${tomlString(value.url)}`);
293
+ if (value.overrideHost !== undefined) lines.push(`override_host = ${tomlString(value.overrideHost)}`);
294
+ if (value.useSni !== undefined) lines.push(`use_sni = ${value.useSni ? 'true' : 'false'}`);
295
+ lines.push('');
296
+ }
297
+
298
+ for (const [name, contents] of Object.entries(input.configStores || {})) {
299
+ lines.push(`[local_server.config_stores.${tomlKey(name)}]`);
300
+ lines.push('format = "inline-toml"');
301
+ lines.push(`[local_server.config_stores.${tomlKey(name)}.contents]`);
302
+ for (const [key, value] of Object.entries(contents || {})) lines.push(`${tomlKey(key)} = ${tomlString(value)}`);
303
+ lines.push('');
304
+ }
305
+
306
+ const secretStores = Object.entries(input.secretStores || {});
307
+ if (secretStores.length > 0) {
308
+ lines.push('[local_server.secret_stores]');
309
+ for (const [name, contents] of secretStores) {
310
+ const entries = Object.entries(contents || {}).map(([key, value]) => `{ key = ${tomlString(key)}, data = ${tomlString(value)} }`);
311
+ lines.push(`${tomlKey(name)} = [${entries.join(', ')}]`);
312
+ }
313
+ lines.push('');
314
+ }
315
+
316
+ const kvStores = Object.entries(input.kvStores || {});
317
+ if (kvStores.length > 0) {
318
+ lines.push('[local_server.kv_stores]');
319
+ for (const [name, contents] of kvStores) {
320
+ const entries = Object.entries(contents || {}).map(([key, value]) => `{ key = ${tomlString(key)}, data = ${tomlString(kvData(value))} }`);
321
+ lines.push(`${tomlKey(name)} = [${entries.join(', ')}]`);
322
+ }
323
+ lines.push('');
324
+ }
325
+
326
+ return `${lines.join('\n').trim()}\n`;
327
+ }
328
+
329
+ function reservePort(host = '127.0.0.1') {
330
+ return new Promise((resolve, reject) => {
331
+ const server = net.createServer();
332
+ server.unref();
333
+ server.once('error', reject);
334
+ server.listen(0, host, () => {
335
+ const address = server.address();
336
+ const port = address && typeof address === 'object' ? address.port : undefined;
337
+ server.close((error) => error ? reject(error) : resolve(port));
338
+ });
339
+ });
340
+ }
341
+
342
+ function waitForPort(host, port, child, logs, timeoutMs) {
343
+ const started = Date.now();
344
+ return new Promise((resolve, reject) => {
345
+ function attempt() {
346
+ if (child.exitCode !== null) {
347
+ reject(new FastlyComputeRealityError(
348
+ 'PULSE_FASTLY_SERVE_START_FAILED',
349
+ `Fastly Compute serve exited before listening on ${host}:${port}.`,
350
+ { exitCode: child.exitCode, signal: child.signalCode, stdout: logs.stdout, stderr: logs.stderr }
351
+ ));
352
+ return;
353
+ }
354
+ if (Date.now() - started >= timeoutMs) {
355
+ reject(new FastlyComputeRealityError(
356
+ 'PULSE_FASTLY_SERVE_START_TIMEOUT',
357
+ `Fastly Compute serve did not listen on ${host}:${port} within ${timeoutMs}ms.`,
358
+ { stdout: logs.stdout, stderr: logs.stderr }
359
+ ));
360
+ return;
361
+ }
362
+ const socket = net.connect({ host, port });
363
+ socket.once('connect', () => {
364
+ socket.destroy();
365
+ resolve();
366
+ });
367
+ socket.once('error', () => {
368
+ socket.destroy();
369
+ setTimeout(attempt, 75);
370
+ });
371
+ }
372
+ attempt();
373
+ });
374
+ }
375
+
376
+ function signalProcessTree(child, signal) {
377
+ if (!child || child.exitCode !== null || !child.pid) return;
378
+ if (process.platform === 'win32') {
379
+ const args = ['/PID', String(child.pid), '/T'];
380
+ if (signal === 'SIGKILL') args.push('/F');
381
+ spawnSync('taskkill', args, { stdio: 'ignore', windowsHide: true });
382
+ return;
383
+ }
384
+ try {
385
+ process.kill(-child.pid, signal);
386
+ } catch (_) {
387
+ try { child.kill(signal); } catch (_) {}
388
+ }
389
+ }
390
+
391
+ function terminate(child, timeoutMs = 3000) {
392
+ if (!child || child.exitCode !== null) return Promise.resolve();
393
+ return new Promise((resolve) => {
394
+ let timer;
395
+ let fallback;
396
+ const done = () => {
397
+ if (timer) clearTimeout(timer);
398
+ if (fallback) clearTimeout(fallback);
399
+ resolve();
400
+ };
401
+ child.once('exit', done);
402
+ signalProcessTree(child, 'SIGTERM');
403
+ timer = setTimeout(() => {
404
+ if (child.exitCode === null) signalProcessTree(child, 'SIGKILL');
405
+ fallback = setTimeout(done, 150);
406
+ }, timeoutMs);
407
+ });
408
+ }
409
+
410
+ function appendLog(logs, key, chunk) {
411
+ const value = `${logs[key]}${chunk}`;
412
+ logs[key] = value.length <= maxCapturedLogBytes ? value : value.slice(value.length - maxCapturedLogBytes);
413
+ }
414
+
415
+ async function startFastlyComputeServe(options = {}) {
416
+ const launcher = options.launcher || inspectFastlyComputeLauncher(options);
417
+ const inspection = launcher.inspection;
418
+ const fastlyCliInspection = launcher.fastlyCliInspection;
419
+ const viceroyInspection = launcher.viceroyInspection;
420
+ const packageRoot = path.resolve(options.packageRoot || options.cwd || process.cwd());
421
+ const wasmFile = path.resolve(options.wasmFile || path.join(packageRoot, 'bin', 'main.wasm'));
422
+ const manifestFile = path.resolve(options.manifestFile || path.join(packageRoot, 'fastly.toml'));
423
+ if (!fs.existsSync(wasmFile)) throw new FastlyComputeRealityError('PULSE_FASTLY_WASM_MISSING', `The native Fastly provider module is missing: ${wasmFile}`, { wasmFile });
424
+ if (!fs.existsSync(manifestFile)) throw new FastlyComputeRealityError('PULSE_FASTLY_MANIFEST_MISSING', `Fastly manifest is missing: ${manifestFile}`, { manifestFile });
425
+ if (path.dirname(manifestFile) !== packageRoot || path.basename(manifestFile) !== 'fastly.toml') {
426
+ throw new FastlyComputeRealityError(
427
+ 'PULSE_FASTLY_MANIFEST_LOCATION_INVALID',
428
+ 'Fastly Compute serve requires fastly.toml at the generated package root.',
429
+ { packageRoot, manifestFile }
430
+ );
431
+ }
432
+ const host = String(options.host || '127.0.0.1');
433
+ const port = Number(options.port || await reservePort(host));
434
+ const extraArgs = options.extraArgs === undefined ? [] : options.extraArgs;
435
+ if (!Array.isArray(extraArgs) || extraArgs.some((value) => typeof value !== 'string')) {
436
+ throw new FastlyComputeRealityError('PULSE_FASTLY_SERVE_ARGS_INVALID', 'Fastly Compute serve extraArgs must be an array of strings.', { extraArgs });
437
+ }
438
+ const args = launcher.kind === 'viceroy-direct'
439
+ ? [
440
+ 'serve',
441
+ '--addr', `${host}:${port}`,
442
+ '--config', manifestFile,
443
+ ...extraArgs,
444
+ wasmFile
445
+ ]
446
+ : [
447
+ 'compute', 'serve',
448
+ '--dir', packageRoot,
449
+ '--file', wasmFile,
450
+ '--addr', `${host}:${port}`,
451
+ ...(viceroyInspection ? ['--viceroy-path', viceroyInspection.binary] : []),
452
+ ...extraArgs
453
+ ];
454
+ const logs = { stdout: '', stderr: '' };
455
+ const child = spawn(inspection.binary, args, {
456
+ cwd: packageRoot,
457
+ env: { ...process.env, ...(options.env || {}) },
458
+ stdio: ['ignore', 'pipe', 'pipe'],
459
+ windowsHide: true,
460
+ detached: process.platform !== 'win32'
461
+ });
462
+ child.stdout.setEncoding('utf8');
463
+ child.stderr.setEncoding('utf8');
464
+ child.stdout.on('data', (chunk) => appendLog(logs, 'stdout', chunk));
465
+ child.stderr.on('data', (chunk) => appendLog(logs, 'stderr', chunk));
466
+ try {
467
+ await waitForPort(host, port, child, logs, Number(options.startTimeoutMs || 45000));
468
+ } catch (error) {
469
+ await terminate(child, Number(options.stopTimeoutMs || 3000));
470
+ throw error;
471
+ }
472
+ return Object.freeze({
473
+ version: fastlyComputeRealityVersion,
474
+ launcher,
475
+ inspection,
476
+ fastlyCliInspection,
477
+ viceroyInspection,
478
+ child,
479
+ host,
480
+ port,
481
+ baseUrl: `http://${host}:${port}`,
482
+ packageRoot,
483
+ wasmFile,
484
+ manifestFile,
485
+ args: Object.freeze([...args]),
486
+ logs,
487
+ stop: () => terminate(child, Number(options.stopTimeoutMs || 3000))
488
+ });
489
+ }
490
+
491
+ function rawHeaders(pairs) {
492
+ const out = [];
493
+ for (let index = 0; index < pairs.length; index += 2) out.push([String(pairs[index]), String(pairs[index + 1])]);
494
+ return out;
495
+ }
496
+
497
+ function requestFastlyCompute(server, input = {}) {
498
+ const body = input.body === undefined || input.body === null
499
+ ? undefined
500
+ : Buffer.isBuffer(input.body) ? input.body : Buffer.from(String(input.body));
501
+ return new Promise((resolve, reject) => {
502
+ const request = http.request({
503
+ hostname: server.host,
504
+ port: server.port,
505
+ method: String(input.method || 'GET').toUpperCase(),
506
+ path: String(input.path || '/'),
507
+ headers: { ...(input.headers || {}), ...(body ? { 'content-length': String(body.length) } : {}) }
508
+ }, (response) => {
509
+ const chunks = [];
510
+ response.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
511
+ response.on('error', reject);
512
+ response.on('end', () => resolve(Object.freeze({
513
+ status: Number(response.statusCode || 0),
514
+ headers: Object.freeze(rawHeaders(response.rawHeaders || [])),
515
+ body: Buffer.concat(chunks)
516
+ })));
517
+ });
518
+ request.setTimeout(Number(input.timeoutMs || 30000), () => request.destroy(new FastlyComputeRealityError('PULSE_FASTLY_REQUEST_TIMEOUT', 'Fastly Compute request timed out.', { path: input.path })));
519
+ request.once('error', reject);
520
+ if (body) request.write(body);
521
+ request.end();
522
+ });
523
+ }
524
+
525
+ module.exports = Object.freeze({
526
+ fastlyComputeRealityVersion,
527
+ FastlyComputeRealityError,
528
+ resolveFastlyCliBinary,
529
+ inspectFastlyCli,
530
+ inspectFastlyComputeLauncher,
531
+ resolveViceroyBinary,
532
+ inspectViceroy,
533
+ renderFastlyLocalConfig,
534
+ reservePort,
535
+ startFastlyComputeServe,
536
+ requestFastlyCompute,
537
+ terminate
538
+ });