@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,564 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { getDefaultArtifactsDir } = require('@pulse-compute/wasm-build-support/artifacts-dir');
8
+ const { spawnSync } = require('node:child_process');
9
+ const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = loadContractsDiagnostics();
10
+
11
+
12
+ function loadContractsDiagnostics() {
13
+ try {
14
+ return require('@pulse-compute/wasm-contracts/diagnostics');
15
+ } catch (error) {
16
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
17
+ return require('../../../../wasm/packages/contracts/src/diagnostics.js');
18
+ }
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ const PHASE = '13C';
24
+ const FASTLY_ADAPTER_VERSION = 'pulsewasm.fastly-adapter.v1';
25
+ const FASTLY_STREAM_HEADER_ADAPTER_VERSION = 'pulsewasm.fastly-stream-header-adapter.v1';
26
+ const FASTLY_LOCAL_SMOKE_VERSION = 'pulsewasm.fastly-local-smoke.v1';
27
+ const FASTLY_PACKAGING_SMOKE_VERSION = 'pulsewasm.fastly-packaging-smoke.v1';
28
+ const FASTLY_DEPLOYMENT_READINESS_VERSION = 'pulsewasm.fastly-deployment-readiness.v1';
29
+
30
+ function sha256Text(text) { return crypto.createHash('sha256').update(String(text || ''), 'utf8').digest('hex'); }
31
+ function sha256Buffer(buffer) { return crypto.createHash('sha256').update(buffer).digest('hex'); }
32
+ function makeFile(file, text) { return { file, text, bytes: Buffer.byteLength(text, 'utf8'), sha256: sha256Text(text) }; }
33
+ function normalizeSlash(value) { return String(value || '').replace(/\\/g, '/'); }
34
+ function diag(code, message, hint, details) {
35
+ return normalizeDiagnostic({ phase: 'fastly-adapter', severity: 'error', code, message, hint, details, loc: { file: '<fastly-adapter>' } });
36
+ }
37
+ function warn(code, message, hint, details) {
38
+ return normalizeDiagnostic({ phase: 'fastly-adapter', severity: 'warning', code, message, hint, details, loc: { file: '<fastly-adapter>' } });
39
+ }
40
+ function fileRecord(filePath, cwd, kind) {
41
+ const buffer = fs.readFileSync(filePath);
42
+ return { file: normalizeSlash(path.relative(cwd, filePath)), kind, bytes: buffer.length, sha256: sha256Buffer(buffer) };
43
+ }
44
+ function findFile(files, fileName) {
45
+ return (files || []).find((file) => file.file === fileName || file.file.endsWith(`/${fileName}`));
46
+ }
47
+ function findOutputFile(outputFiles, suffix) {
48
+ return (outputFiles || []).find((file) => file.file && file.file.endsWith(suffix));
49
+ }
50
+
51
+ function buildFastlyAdapterSource() {
52
+ return `/* Generated by PulseWasm.
53
+ * Fastly stream/header adapter proof.
54
+ * This is a platform-shape adapter over the compiled-Wasm host runtime.
55
+ * It is not an official Fastly SDK and does not redefine PulseWasm semantics.
56
+ */
57
+ 'use strict';
58
+
59
+ function normalizeRawHeaders(rawHeaders) {
60
+ if (!rawHeaders) return [];
61
+ const headers = [];
62
+ if (Array.isArray(rawHeaders)) {
63
+ if (rawHeaders.length > 0 && Array.isArray(rawHeaders[0])) {
64
+ for (const pair of rawHeaders) if (pair && pair.length >= 2) headers.push([String(pair[0]), String(pair[1])]);
65
+ return headers;
66
+ }
67
+ for (let i = 0; i < rawHeaders.length; i += 2) {
68
+ if (rawHeaders[i] !== undefined && rawHeaders[i + 1] !== undefined) headers.push([String(rawHeaders[i]), String(rawHeaders[i + 1])]);
69
+ }
70
+ }
71
+ return headers;
72
+ }
73
+
74
+ function normalizeHeaders(headers) {
75
+ if (!headers) return [];
76
+ if (Array.isArray(headers)) return normalizeRawHeaders(headers);
77
+ if (headers && typeof headers[Symbol.iterator] === 'function') return Array.from(headers, ([key, value]) => [String(key), String(value)]);
78
+ return Object.entries(headers).flatMap(([key, value]) => Array.isArray(value) ? value.map((item) => [String(key), String(item)]) : [[String(key), String(value)]]);
79
+ }
80
+
81
+ function normalizeFastlyPath(request) {
82
+ if (!request) return '/';
83
+ if (request.path) return String(request.path || '/');
84
+ if (request.url) {
85
+ try { return new URL(String(request.url), 'http://fastly.local').pathname || '/'; } catch (_) { return String(request.url || '/'); }
86
+ }
87
+ return '/';
88
+ }
89
+
90
+ function fastlyRequestToHostRequest(request) {
91
+ return {
92
+ method: String((request && request.method) || 'GET').toUpperCase(),
93
+ path: normalizeFastlyPath(request),
94
+ headers: normalizeRawHeaders(request && request.rawHeaders).concat(normalizeHeaders(request && request.headers)),
95
+ bodyText: request && request.bodyText !== undefined ? String(request.bodyText) : undefined
96
+ };
97
+ }
98
+
99
+ function groupHeaderValues(headers, name) {
100
+ const lower = String(name).toLowerCase();
101
+ return (headers || []).filter(([key]) => String(key).toLowerCase() === lower).map(([, value]) => String(value));
102
+ }
103
+
104
+ function fastlyBodyFromHostResult(result) {
105
+ if (!result) return '';
106
+ if (result.kind === 'stream') {
107
+ if (result.bodyStream && Array.isArray(result.bodyStream.chunks)) return result.bodyStream.chunks.join('');
108
+ if (result.response && result.response.bodyStream && Array.isArray(result.response.bodyStream.chunks)) return result.response.bodyStream.chunks.join('');
109
+ if (result.stream && Array.isArray(result.stream.chunks)) return result.stream.chunks.join('');
110
+ return '';
111
+ }
112
+ if (result.body === undefined || result.body === null) return '';
113
+ return Buffer.isBuffer(result.body) ? result.body.toString('utf8') : String(result.body);
114
+ }
115
+
116
+ function hostResultToFastlyResponse(result) {
117
+ const headers = (result && Array.isArray(result.headers)) ? result.headers.map(([key, value]) => [String(key), String(value)]) : [];
118
+ return {
119
+ status: Number(result && result.status) || 500,
120
+ kind: result && result.kind || 'unknown',
121
+ headers,
122
+ body: fastlyBodyFromHostResult(result),
123
+ responseRef: result && result.responseRef || 0,
124
+ streamRef: result && result.streamRef || 0,
125
+ bodyStream: result && (result.bodyStream || (result.response && result.response.bodyStream) || result.stream),
126
+ diagnostics: result && result.diagnostics || []
127
+ };
128
+ }
129
+
130
+ function createFastlyStreamHeaderAdapter(options) {
131
+ const runtime = options && options.runtime;
132
+ if (!runtime || typeof runtime.executeRequest !== 'function') {
133
+ const error = new Error('Fastly adapter requires a PulseWasm compiled host runtime.');
134
+ error.code = 'PULSEWASM_FASTLY_RUNTIME_REQUIRED';
135
+ throw error;
136
+ }
137
+ const observed = { requests: [], responses: [] };
138
+ return {
139
+ observed,
140
+ executeRequest(request) {
141
+ const hostRequest = fastlyRequestToHostRequest(request);
142
+ observed.requests.push(hostRequest);
143
+ const hostResult = runtime.executeRequest(hostRequest);
144
+ const fastlyResponse = hostResultToFastlyResponse(hostResult);
145
+ observed.responses.push(fastlyResponse);
146
+ return fastlyResponse;
147
+ },
148
+ handleRequest(request) { return this.executeRequest(request); },
149
+ executeConnect(input) { return runtime.executeConnect ? runtime.executeConnect(input || {}) : { status: 204, headers: [], kind: 'empty' }; },
150
+ executeDisconnect(input) { return runtime.executeDisconnect ? runtime.executeDisconnect(input || {}) : { status: 204, headers: [], kind: 'empty' }; }
151
+ };
152
+ }
153
+
154
+ module.exports = {
155
+ createFastlyStreamHeaderAdapter,
156
+ fastlyRequestToHostRequest,
157
+ hostResultToFastlyResponse,
158
+ normalizeRawHeaders,
159
+ normalizeHeaders,
160
+ normalizeFastlyPath,
161
+ groupHeaderValues
162
+ };
163
+ `;
164
+ }
165
+
166
+ function buildFastlyToml() {
167
+ return `manifest_version = 3
168
+ name = "pulsewasm-fastly-proof"
169
+ description = "PulseWasm Fastly stream/header adapter proof scaffold."
170
+ language = "other"
171
+
172
+ [scripts]
173
+ build = "echo 'PulseWasm custom build: use fastly compute pack --wasm-binary artifacts/generated/fastly/pulsewasm-compute.wasm'"
174
+ `;
175
+ }
176
+
177
+ function buildFastlyReadme() {
178
+ return `# PulseWasm Fastly Scaffold
179
+
180
+ This scaffold is generated for the Fastly stream/header adapter proof.
181
+
182
+ It keeps the SDK posture locked:
183
+
184
+ - Fastly SDKs are references, not dependencies.
185
+ - The old AssemblyScript SDK is not revived.
186
+ - The custom hostcall path is vendor-unsupported and recorded as risk.
187
+ - This scaffold is intended for local packaging/testing once the Fastly CLI can run its local Compute lifecycle.
188
+
189
+ ## Candidate pack command
190
+
191
+ \`\`\`bash
192
+ fastly compute pack --wasm-binary artifacts/generated/fastly/pulsewasm-compute.wasm
193
+ \`\`\`
194
+
195
+ ## Candidate local serve command
196
+
197
+ \`\`\`bash
198
+ fastly compute serve
199
+ \`\`\`
200
+
201
+ This package runs a mock Fastly hostcall adapter smoke in Node. It does not claim a real \`fastly compute serve\` run unless the external reality task executes through the Fastly CLI.
202
+ `;
203
+ }
204
+
205
+ function buildStreamHeaderAdapterArtifact({ generatedBy, smoke, packagingSmoke, compiledWasmRuntime, fastlyHostcallBinding }) {
206
+ const checks = smoke ? smoke.summary.checks : 0;
207
+ const failed = smoke ? smoke.summary.failedChecks : 0;
208
+ return {
209
+ version: FASTLY_STREAM_HEADER_ADAPTER_VERSION,
210
+ generatedBy,
211
+ phase: PHASE,
212
+ status: failed === 0 ? 'ok' : 'error',
213
+ posture: {
214
+ adapterProof: true,
215
+ realFastlyHostcalls: false,
216
+ mockHostcallSurface: true,
217
+ sdkDependency: false,
218
+ sdkReferenceOnly: true,
219
+ deploymentPackageGenerated: Boolean(packagingSmoke && packagingSmoke.summary && packagingSmoke.summary.packageGenerated),
220
+ fastlyCliRequiredForPackServe: true,
221
+ fastlyCliAvailable: Boolean(packagingSmoke && packagingSmoke.fastlyCli && packagingSmoke.fastlyCli.available),
222
+ localFastlyServeExecuted: Boolean(packagingSmoke && packagingSmoke.summary && packagingSmoke.summary.serveExecuted)
223
+ },
224
+ maps: {
225
+ incomingRequest: 'Fastly-like request handle -> PulseWasm HostRequest',
226
+ headers: 'ordered repeated headers -> Pulse header bag -> ordered repeated response headers',
227
+ resultStream: 'Pulse RESULT_STREAM / responseRef -> Fastly-like response/body stream',
228
+ body: 'host-owned stream bytes; no copy into Wasm',
229
+ runtime: 'compiled-wasm host runtime; no pulsewasm_handlers JS imports'
230
+ },
231
+ linkedInputs: {
232
+ compiledWasmRuntime: compiledWasmRuntime?.artifact?.version,
233
+ fastlyHostcallBinding: fastlyHostcallBinding?.artifact?.version
234
+ },
235
+ summary: {
236
+ checks,
237
+ failedChecks: failed,
238
+ mockFastlyAdapter: true,
239
+ streamHeaderProof: failed === 0,
240
+ fastlyCliAvailable: Boolean(packagingSmoke && packagingSmoke.fastlyCli && packagingSmoke.fastlyCli.available),
241
+ packCommandReady: true,
242
+ serveCommandReady: true
243
+ }
244
+ };
245
+ }
246
+
247
+ function buildPackagingSmoke({ cwd, outDir, wasmSource, generatedBy }) {
248
+ const fastlyVersion = spawnSync('fastly', ['--version'], { cwd, encoding: 'utf8', timeout: 10_000 });
249
+ const fastlyAvailable = !fastlyVersion.error && fastlyVersion.status === 0;
250
+ const targetDir = path.join(outDir, 'generated', 'fastly');
251
+ fs.mkdirSync(targetDir, { recursive: true });
252
+ const wasmTarget = path.join(targetDir, 'pulsewasm-compute.wasm');
253
+ let copied = false;
254
+ let copiedRecord;
255
+ if (wasmSource && fs.existsSync(wasmSource)) {
256
+ fs.copyFileSync(wasmSource, wasmTarget);
257
+ copied = true;
258
+ copiedRecord = fileRecord(wasmTarget, cwd, 'fastly-compute-wasm-candidate');
259
+ }
260
+ const packCommand = 'fastly compute pack --wasm-binary artifacts/generated/fastly/pulsewasm-compute.wasm';
261
+ return {
262
+ version: FASTLY_PACKAGING_SMOKE_VERSION,
263
+ generatedBy,
264
+ phase: PHASE,
265
+ status: 'ok',
266
+ fastlyCli: {
267
+ available: fastlyAvailable,
268
+ versionOutput: fastlyAvailable ? String(fastlyVersion.stdout || fastlyVersion.stderr || '').trim() : undefined,
269
+ error: fastlyAvailable ? undefined : (fastlyVersion.error ? fastlyVersion.error.message : `exit ${fastlyVersion.status}`)
270
+ },
271
+ commands: {
272
+ pack: packCommand,
273
+ serve: 'fastly compute serve',
274
+ deploy: 'fastly compute deploy'
275
+ },
276
+ packageCandidate: {
277
+ wasmCopied: copied,
278
+ wasm: copiedRecord,
279
+ fastlyToml: 'generated/fastly/fastly.toml',
280
+ readme: 'generated/fastly/README.md'
281
+ },
282
+ execution: {
283
+ packExecuted: false,
284
+ serveExecuted: false,
285
+ deployExecuted: false,
286
+ reason: fastlyAvailable
287
+ ? 'Fastly CLI is available, but this artifact does not execute pack/serve automatically; use the external reality task.'
288
+ : 'The Fastly CLI local Compute lifecycle was not available in this environment, so the generator emitted a pack-ready scaffold and ran the mock adapter smoke only.'
289
+ },
290
+ summary: {
291
+ fastlyCliAvailable: fastlyAvailable,
292
+ wasmCopied: copied,
293
+ packageGenerated: false,
294
+ packExecuted: false,
295
+ serveExecuted: false,
296
+ deployExecuted: false
297
+ }
298
+ };
299
+ }
300
+
301
+ function loadModuleFromText(text, filePath) {
302
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
303
+ fs.writeFileSync(filePath, text, 'utf8');
304
+ delete require.cache[filePath];
305
+ return require(filePath);
306
+ }
307
+
308
+ function runFastlyLocalSmoke({ compiledWasmRuntime, adapterSource, wasmFile, cwd }) {
309
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsewasm-fastly-adapter-'));
310
+ const hostFile = findFile(compiledWasmRuntime?.files, 'generated/host/compiled-wasm-host-runtime.cjs');
311
+ if (!hostFile) throw new Error('compiled-wasm-host-runtime.cjs was not available from the compiled-wasm runtime output.');
312
+ const hostModule = loadModuleFromText(hostFile.text, path.join(tempDir, 'compiled-wasm-host-runtime.cjs'));
313
+ const adapterModule = loadModuleFromText(adapterSource, path.join(tempDir, 'fastly-adapter.cjs'));
314
+
315
+ const runtime = hostModule.createPulseWasmCompiledHostRuntime({
316
+ wasmPath: wasmFile,
317
+ deploymentPosture: { engine: 'wasm', wasmOptimized: true },
318
+ hostResponses: {
319
+ 77: {
320
+ status: 206,
321
+ headers: [
322
+ ['Content-Type', 'application/octet-stream'],
323
+ ['Cache-Control', 'public, max-age=60'],
324
+ ['Grip-Channel', 'backend:stream'],
325
+ ['Server', 'origin']
326
+ ],
327
+ bodyStream: { kind: 'mock-fastly-body', chunks: ['asset:', '42'] }
328
+ }
329
+ }
330
+ });
331
+
332
+ const adapter = adapterModule.createFastlyStreamHeaderAdapter({ runtime });
333
+ const streamResponse = adapter.handleRequest({
334
+ method: 'GET',
335
+ path: '/stream',
336
+ rawHeaders: ['X-Trace', 'abc', 'Grip-Channel', 'client:a', 'Grip-Channel', 'client:b']
337
+ });
338
+ const schemaResponse = adapter.handleRequest({
339
+ method: 'POST',
340
+ path: '/users',
341
+ rawHeaders: ['Content-Type', 'application/json'],
342
+ bodyText: '{"name":"Ada","age":42,"active":true}'
343
+ });
344
+ const missingResponse = adapter.handleRequest({ method: 'GET', path: '/missing' });
345
+
346
+ function values(headers, name) {
347
+ return (headers || []).filter(([key]) => String(key).toLowerCase() === String(name).toLowerCase()).map(([, value]) => value);
348
+ }
349
+ const firstRequest = adapter.observed.requests[0];
350
+ const checks = [];
351
+ function check(name, expected, actual) {
352
+ checks.push({ name, expected, actual, status: Object.is(expected, actual) ? 'ok' : 'error', message: Object.is(expected, actual) ? undefined : `Expected ${expected}, got ${actual}.` });
353
+ }
354
+ check('fastly_request_path_mapped', '/stream', firstRequest.path);
355
+ check('fastly_request_method_mapped', 'GET', firstRequest.method);
356
+ check('fastly_repeated_request_headers_preserved', 2, values(firstRequest.headers, 'Grip-Channel').length);
357
+ check('stream_status', 206, streamResponse.status);
358
+ check('stream_kind', 'stream', streamResponse.kind);
359
+ check('stream_body_forwarded', 'asset:42', streamResponse.body);
360
+ check('stream_response_ref_visible', 77, streamResponse.responseRef);
361
+ check('backend_content_type_preserved', 'application/octet-stream', values(streamResponse.headers, 'Content-Type')[0]);
362
+ check('cache_control_overridden', 'public, max-age=120', values(streamResponse.headers, 'Cache-Control')[0]);
363
+ check('backend_server_header_preserved_without_delete', 1, values(streamResponse.headers, 'Server').length);
364
+ check('grip_channel_repeated_preserved', 2, values(streamResponse.headers, 'Grip-Channel').length);
365
+ check('schema_status', 201, schemaResponse.status);
366
+ check('schema_kind', 'json', schemaResponse.kind);
367
+ check('schema_body', '{"name":"Ada","age":42,"active":true}', schemaResponse.body);
368
+ check('missing_status', 404, missingResponse.status);
369
+ check('no_js_handler_imports', 0, runtime.imports.filter((item) => item.module === 'pulsewasm_handlers').length);
370
+ return {
371
+ version: FASTLY_LOCAL_SMOKE_VERSION,
372
+ generatedBy: PACKAGE_VERSION,
373
+ phase: PHASE,
374
+ status: checks.every((item) => item.status === 'ok') ? 'ok' : 'error',
375
+ checks,
376
+ failedChecks: checks.filter((item) => item.status !== 'ok'),
377
+ observed: {
378
+ requests: adapter.observed.requests,
379
+ streamResponse,
380
+ schemaResponse,
381
+ missingResponse
382
+ },
383
+ summary: {
384
+ executed: true,
385
+ checks: checks.length,
386
+ failedChecks: checks.filter((item) => item.status !== 'ok').length,
387
+ streamHeaderProof: true,
388
+ compiledWasmRuntimeDriven: true,
389
+ nodeAdapterUsed: false,
390
+ fastlyCliRequired: false,
391
+ realFastlyHostcalls: false,
392
+ mockHostcallSurface: true
393
+ }
394
+ };
395
+ }
396
+
397
+ function buildDeploymentReadiness({ generatedBy, localSmoke, packagingSmoke }) {
398
+ const localOk = Boolean(localSmoke && localSmoke.summary && localSmoke.summary.failedChecks === 0);
399
+ return {
400
+ version: FASTLY_DEPLOYMENT_READINESS_VERSION,
401
+ generatedBy,
402
+ phase: PHASE,
403
+ readiness: {
404
+ mockAdapterSmokeReady: localOk,
405
+ packageScaffoldReady: Boolean(packagingSmoke && packagingSmoke.summary && packagingSmoke.summary.wasmCopied),
406
+ localFastlyServeReady: Boolean(packagingSmoke && packagingSmoke.fastlyCli && packagingSmoke.fastlyCli.available),
407
+ remoteDeployReady: false,
408
+ deployRequiresAccountServiceToken: true
409
+ },
410
+ nextManualSteps: [
411
+ 'Install the Fastly CLI and ensure `fastly compute serve` can run its managed local Compute engine.',
412
+ 'Run `fastly compute pack --wasm-binary artifacts/generated/fastly/pulsewasm-compute.wasm` from the PulseWasm root.',
413
+ 'Run `fastly compute serve` for local Fastly tooling validation.',
414
+ 'Use `fastly compute deploy` only after service/account/token setup.'
415
+ ],
416
+ verdict: localOk
417
+ ? 'The stream/header adapter shape is validated through a mock Fastly hostcall surface and a pack-ready scaffold. Real Fastly serve/deploy remains an explicit environment step.'
418
+ : 'The local adapter smoke failed; inspect fastly-local-smoke.json.',
419
+ summary: {
420
+ readyForManualPack: Boolean(packagingSmoke && packagingSmoke.summary && packagingSmoke.summary.wasmCopied),
421
+ readyForManualServeIfCliInstalled: localOk,
422
+ readyForRemoteDeploy: false
423
+ }
424
+ };
425
+ }
426
+
427
+ function buildMarkdown(artifact) {
428
+ const lines = [
429
+ '# PulseWasm Fastly Stream/Header Adapter Proof',
430
+ '',
431
+ '## Status',
432
+ artifact.status === 'ok' ? 'Implemented.' : 'Generated with errors.',
433
+ '',
434
+ '## Boundary',
435
+ '',
436
+ '- SDKs are references, not dependencies.',
437
+ '- This proof uses a mock Fastly hostcall surface in Node.',
438
+ '- Fastly CLI pack/serve is recorded as a scaffold step, not silently claimed.',
439
+ '- No backend fetch effect, Fanout/GRIP, or real asset adapter is implemented.',
440
+ '',
441
+ '## Summary',
442
+ '',
443
+ `- Local smoke checks: ${artifact.summary.checks}`,
444
+ `- Failed checks: ${artifact.summary.failedChecks}`,
445
+ `- Fastly CLI available: ${artifact.summary.fastlyCliAvailable}`,
446
+ `- Pack-ready scaffold emitted: ${artifact.summary.packReady}`,
447
+ ''
448
+ ];
449
+ return `${lines.join('\n')}\n`;
450
+ }
451
+
452
+ function buildFastlyStreamHeaderAdapter({ compiledWasmRuntime, fastlyHostcallBinding, fastlyReadiness, streamingPassthrough, wasmHostAbi } = {}, options = {}) {
453
+ const cwd = options.cwd || process.cwd();
454
+ const outDir = options.outDir || getDefaultArtifactsDir(cwd);
455
+ const generatedBy = options.generatedBy || PACKAGE_VERSION;
456
+ const diagnostics = [];
457
+ if (!compiledWasmRuntime || !compiledWasmRuntime.artifact) diagnostics.push(diag('PULSEWASM_FASTLY_COMPILED_RUNTIME_REQUIRED', 'The Fastly adapter requires compiled-wasm-runtime.json.', 'Enable --emit-compiled-wasm-runtime.'));
458
+ if (!fastlyHostcallBinding || !fastlyHostcallBinding.artifact) diagnostics.push(diag('PULSEWASM_FASTLY_HOSTCALL_CONTRACT_REQUIRED', 'The Fastly adapter requires the Fastly hostcall binding contract.', 'Enable --emit-fastly-hostcall-binding.'));
459
+ const wasmOutput = findOutputFile(compiledWasmRuntime?.outputFiles, 'pulsewasm-compiled-runtime.wasm');
460
+ const wasmFile = wasmOutput ? path.resolve(cwd, wasmOutput.file) : path.join(outDir, 'generated/compiled-wasm-runtime/pulsewasm-compiled-runtime.wasm');
461
+ if (!fs.existsSync(wasmFile)) diagnostics.push(diag('PULSEWASM_FASTLY_COMPILED_WASM_MISSING', 'The Fastly adapter expected the compiled runtime Wasm to exist.', 'Run --emit-compiled-wasm-runtime before Fastly adapter proof.', { wasmFile }));
462
+
463
+ const adapterSource = buildFastlyAdapterSource();
464
+ const files = [
465
+ makeFile('generated/fastly/fastly-adapter.cjs', adapterSource),
466
+ makeFile('generated/fastly/fastly.toml', buildFastlyToml()),
467
+ makeFile('generated/fastly/README.md', buildFastlyReadme())
468
+ ];
469
+ let packagingSmoke = buildPackagingSmoke({ cwd, outDir, wasmSource: fs.existsSync(wasmFile) ? wasmFile : undefined, generatedBy });
470
+ const outputFiles = [];
471
+ if (packagingSmoke.packageCandidate.wasm && packagingSmoke.packageCandidate.wasm.file) {
472
+ outputFiles.push(packagingSmoke.packageCandidate.wasm);
473
+ }
474
+ let localSmoke;
475
+ if (diagnostics.length === 0) {
476
+ try {
477
+ localSmoke = runFastlyLocalSmoke({ compiledWasmRuntime, adapterSource, wasmFile, cwd });
478
+ if (localSmoke.failedChecks.length > 0) diagnostics.push(diag('PULSEWASM_FASTLY_LOCAL_SMOKE_FAILED', 'Fastly stream/header adapter smoke failed.', 'Inspect fastly-local-smoke.json.', { failedChecks: localSmoke.failedChecks }));
479
+ } catch (error) {
480
+ diagnostics.push(diag('PULSEWASM_FASTLY_LOCAL_SMOKE_EXECUTION_FAILED', 'Fastly stream/header adapter smoke could not execute.', 'Inspect the generated adapter and compiled runtime artifacts.', { message: error && error.message ? error.message : String(error), stack: error && error.stack ? error.stack : undefined }));
481
+ }
482
+ }
483
+ if (!localSmoke) {
484
+ localSmoke = {
485
+ version: FASTLY_LOCAL_SMOKE_VERSION,
486
+ generatedBy,
487
+ phase: PHASE,
488
+ status: 'not-run',
489
+ checks: [],
490
+ failedChecks: [],
491
+ summary: { executed: false, checks: 0, failedChecks: 0, streamHeaderProof: false }
492
+ };
493
+ }
494
+ const streamHeaderAdapter = buildStreamHeaderAdapterArtifact({ generatedBy, smoke: localSmoke, packagingSmoke, compiledWasmRuntime, fastlyHostcallBinding });
495
+ const deploymentReadiness = buildDeploymentReadiness({ generatedBy, localSmoke, packagingSmoke });
496
+ const artifact = normalizeArtifact({
497
+ version: FASTLY_ADAPTER_VERSION,
498
+ generatedBy,
499
+ phase: PHASE,
500
+ status: diagnostics.filter((item) => item.severity !== 'warning').length === 0 ? 'ok' : 'error',
501
+ posture: {
502
+ adapterImplemented: true,
503
+ streamHeaderProof: true,
504
+ realFastlyHostcalls: false,
505
+ mockHostcallSurface: true,
506
+ sdkDependency: false,
507
+ officialSdkWrapper: false,
508
+ customHostcallPath: true,
509
+ packagingScaffoldGenerated: true,
510
+ fastlyPackExecuted: false,
511
+ fastlyServeExecuted: false,
512
+ remoteDeployExecuted: false
513
+ },
514
+ scope: [
515
+ 'Fastly-like request method/path/header normalization',
516
+ 'compiled-wasm runtime execution',
517
+ 'RESULT_STREAM / responseRef translation',
518
+ 'ordered repeated response header preservation',
519
+ 'pack-ready scaffold generation'
520
+ ],
521
+ reserved: [
522
+ 'real Fastly hostcall implementation',
523
+ 'fastly compute serve execution',
524
+ 'remote deploy',
525
+ 'backend fetch effect runtime',
526
+ 'Fanout / GRIP',
527
+ 'real pulse.assets',
528
+ 'WIT generation'
529
+ ],
530
+ linkedArtifacts: {
531
+ compiledWasmRuntime: compiledWasmRuntime?.artifact?.version,
532
+ fastlyHostcallBinding: fastlyHostcallBinding?.artifact?.version,
533
+ fastlyReadiness: fastlyReadiness?.artifact?.version,
534
+ wasmHostAbi: wasmHostAbi?.artifact?.version,
535
+ streamingPassthrough: streamingPassthrough?.artifact?.version
536
+ },
537
+ summary: {
538
+ checks: localSmoke.summary.checks,
539
+ failedChecks: localSmoke.summary.failedChecks,
540
+ adapterImplemented: true,
541
+ localMockSmokeExecuted: Boolean(localSmoke.summary.executed),
542
+ streamHeaderProof: localSmoke.summary.failedChecks === 0 && Boolean(localSmoke.summary.executed),
543
+ fastlyCliAvailable: Boolean(packagingSmoke.fastlyCli.available),
544
+ packReady: Boolean(packagingSmoke.summary.wasmCopied),
545
+ packExecuted: false,
546
+ serveExecuted: false,
547
+ deployExecuted: false,
548
+ diagnostics: diagnostics.length
549
+ }
550
+ }, cwd);
551
+ files.push(makeFile('generated/host/fastly-stream-header-adapter.md', buildMarkdown(artifact)));
552
+ return {
553
+ artifact,
554
+ streamHeaderAdapter: normalizeArtifact(streamHeaderAdapter, cwd),
555
+ localSmoke: normalizeArtifact(localSmoke, cwd),
556
+ packagingSmoke: normalizeArtifact(packagingSmoke, cwd),
557
+ deploymentReadiness: normalizeArtifact(deploymentReadiness, cwd),
558
+ files,
559
+ outputFiles,
560
+ diagnostics
561
+ };
562
+ }
563
+
564
+ module.exports = { buildFastlyStreamHeaderAdapter };