@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.2

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 (69) hide show
  1. package/README.md +53 -1
  2. package/dist/config-schema.json +77 -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 +88 -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/kv-native.as.ts +229 -0
  12. package/src/build/kv-native.js +20 -0
  13. package/src/build/native-http-effects.js +871 -0
  14. package/src/build/native-http-shell.js +703 -0
  15. package/src/build/native-platform-capabilities.js +2196 -0
  16. package/src/build/s3-native.as.ts +215 -0
  17. package/src/build/s3-native.js +30 -0
  18. package/src/compiler/assets-package-out-parity-proof.js +338 -0
  19. package/src/compiler/assets-provider-proof.js +499 -0
  20. package/src/compiler/config-provider-resolution.js +573 -0
  21. package/src/compiler/fastly-adapter.js +564 -0
  22. package/src/compiler/fastly-command-entry.js +390 -0
  23. package/src/compiler/fastly-config-secret-serve.js +323 -0
  24. package/src/compiler/fastly-hostcall-binding.js +598 -0
  25. package/src/compiler/fastly-readiness.js +477 -0
  26. package/src/compiler/index.js +40 -0
  27. package/src/compiler/lifecycle-parity-contract.js +70 -0
  28. package/src/compiler/lifecycle-parity-proof.js +260 -0
  29. package/src/compiler/route-handler-effect-proof.js +255 -0
  30. package/src/compiler/runtime-provider-kv.js +237 -0
  31. package/src/compiler/runtime-provider-shape.js +109 -0
  32. package/src/config-api.js +63 -0
  33. package/src/config-schema.d.ts +13 -0
  34. package/src/config-schema.js +27 -0
  35. package/src/config-schema.json +77 -0
  36. package/src/index.js +3 -0
  37. package/src/index.ts +118 -0
  38. package/src/javascript/assets-adapter.js +122 -0
  39. package/src/javascript/bindings-adapter.js +237 -0
  40. package/src/javascript/errors.js +26 -0
  41. package/src/javascript/fetch-adapter.js +93 -0
  42. package/src/javascript/grip-broadcast.js +261 -0
  43. package/src/javascript/jwt-verifier.js +137 -0
  44. package/src/javascript/lifecycle.js +55 -0
  45. package/src/javascript/local-loader.js +138 -0
  46. package/src/javascript/logging.js +26 -0
  47. package/src/javascript/package-effects.js +68 -0
  48. package/src/javascript/runtime-host.js +108 -0
  49. package/src/javascript/sha256.js +128 -0
  50. package/src/javascript/source-package.js +776 -0
  51. package/src/javascript/support.js +142 -0
  52. package/src/javascript/target-support-policy.js +264 -0
  53. package/src/javascript/target.js +73 -0
  54. package/src/javascript/test-runtime.js +407 -0
  55. package/src/provider-contract.js +79 -0
  56. package/src/runtime/assets-provider.js +464 -0
  57. package/src/runtime/canonical-api-runtime.js +601 -0
  58. package/src/runtime/index.js +28 -0
  59. package/src/runtime/provider-primitives-contract.js +272 -0
  60. package/src/runtime/provider-primitives.js +82 -0
  61. package/src/runtime/route-handler-effects.js +507 -0
  62. package/src/testing/conditional-kv-host.js +109 -0
  63. package/src/testing/fastly-cli.js +538 -0
  64. package/src/testing/native-http-effects-host.js +390 -0
  65. package/src/testing/native-http-shell-host.js +190 -0
  66. package/src/testing/native-platform-capabilities-host.js +676 -0
  67. package/src/toolchain/config.js +131 -0
  68. package/src/toolchain/index.js +345 -0
  69. package/src/toolchain/s3.js +26 -0
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ function loadDiagnostics() {
4
+ try { return require('@pulse-compute/wasm-contracts/diagnostics'); }
5
+ catch (error) { if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) return require('../../../../wasm/packages/contracts/src/diagnostics.js'); throw error; }
6
+ }
7
+ function loadParityContract() {
8
+ return require('./lifecycle-parity-contract.js');
9
+ }
10
+ function loadBackendBodyContract() {
11
+ try { return require('@pulse-compute/wasm-contracts/handler/backend-json-request-body'); }
12
+ catch (error) { if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) return require('../../../../wasm/packages/contracts/src/handler/backend-json-request-body.js'); throw error; }
13
+ }
14
+
15
+ const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = loadDiagnostics();
16
+ const parity = loadParityContract();
17
+ const backendBody = loadBackendBodyContract();
18
+ const {
19
+ createFastlyRouteHandlerEffectProvider,
20
+ buildFastlyRouteEffectProviderConfig,
21
+ requestFromEffect,
22
+ headerValue
23
+ } = require('../runtime/route-handler-effects.js');
24
+
25
+ function clone(value) { return value == null ? value : JSON.parse(JSON.stringify(value)); }
26
+ function normalize(input) { return input && input.artifact ? input.artifact : input; }
27
+ function makeDiagnostic(code, message, severity = 'error', details) { return normalizeDiagnostic({ phase: 'fastly-lifecycle-parity-proof', code, severity, message, details, loc: { file: '<fastly-lifecycle-parity-proof>' } }); }
28
+ function normalizedMethod(value) { return String(value || 'GET').toUpperCase(); }
29
+ function pathParamNames(routePath) { return Array.from(String(routePath || '').matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g), (match) => match[1]); }
30
+ function paramsForRoute(route, params = {}) {
31
+ const out = {};
32
+ for (const name of pathParamNames(route && route.path)) out[name] = name === 'id' ? 'abc' : 'value';
33
+ return { ...out, ...params };
34
+ }
35
+ function routeHasResolve(route) { return Boolean(route && Array.isArray(route.resolves) && route.resolves.some((resolve) => Array.isArray(resolve.effects) && resolve.effects.length > 0)); }
36
+ function firstEffect(route, predicate = () => true) {
37
+ for (const resolve of route && route.resolves || []) for (const effect of resolve.effects || []) if (effect && predicate(effect, resolve)) return { resolve, effect };
38
+ return undefined;
39
+ }
40
+ function findRoute(plan, predicate) { return (plan && plan.routes || []).find(predicate); }
41
+ function findGetUserRoute(plan) { return findRoute(plan, (route) => normalizedMethod(route.method) === 'GET' && String(route.path) === '/users/:id' && routeHasResolve(route)); }
42
+ function findDashboardRoute(plan) { return findRoute(plan, (route) => normalizedMethod(route.method) === 'GET' && String(route.path) === '/dashboard/:id' && routeHasResolve(route)); }
43
+ function findPostJsonRoute(plan) {
44
+ return findRoute(plan, (route) => normalizedMethod(route.method) === 'POST' && (route.resolves || []).some((resolve) => (resolve.effects || []).some((effect) => effect && effect.request && effect.request.bodyMode === 'json')));
45
+ }
46
+ function responseHeaders(effect, request, extra = []) {
47
+ return [
48
+ ['content-type', request.method === 'HEAD' ? 'application/json; charset=utf-8' : 'application/json; charset=utf-8'],
49
+ ['x-pulsewasm-fastly-backend', effect.backend],
50
+ ['x-pulsewasm-fastly-effect', effect.name || '$default'],
51
+ ...extra
52
+ ];
53
+ }
54
+ function fixtureForEffect(effect, route, params) {
55
+ const request = requestFromEffect(effect, { route, params: paramsForRoute(route, params) });
56
+ if (request.method === 'HEAD') {
57
+ return { request, response: { status: 200, kind: 'empty', headers: responseHeaders(effect, request, [['x-count', '2'], ['x-user-id', paramsForRoute(route, params).id || 'abc']]), body: '' } };
58
+ }
59
+ if (/\/users\/[^/]+\/posts$/.test(request.path)) {
60
+ const id = request.path.split('/')[2] || 'abc';
61
+ return { request, response: { status: 200, kind: 'json-text', headers: responseHeaders(effect, request, [['x-count', '2']]), body: JSON.stringify([{ id: 'post-1', userId: id, title: 'Reference Post 1' }, { id: 'post-2', userId: id, title: 'Reference Post 2' }]) } };
62
+ }
63
+ if (/\/users\/[^/]+$/.test(request.path)) {
64
+ const id = request.path.split('/')[2] || 'abc';
65
+ return { request, response: { status: 200, kind: 'json-text', headers: responseHeaders(effect, request, [['x-user-id', id]]), body: JSON.stringify({ id, name: 'Fastly Reference User', active: true }) } };
66
+ }
67
+ return { request, response: { status: 200, kind: 'json-text', headers: responseHeaders(effect, request), body: JSON.stringify({ ok: true, path: request.path }) } };
68
+ }
69
+ function createReferenceBackends(plan, params) {
70
+ const backends = {};
71
+ for (const route of plan && plan.routes || []) {
72
+ for (const resolve of route.resolves || []) {
73
+ for (const effect of resolve.effects || []) {
74
+ if (!effect || effect.kind !== 'backend-fetch') continue;
75
+ const method = normalizedMethod(effect.request && effect.request.method);
76
+ if (method === 'POST' && effect.request && effect.request.bodyMode === 'json') continue;
77
+ const { request, response } = fixtureForEffect(effect, route, params);
78
+ if (!backends[effect.backend]) backends[effect.backend] = {};
79
+ backends[effect.backend][`${request.method} ${request.path}`] = response;
80
+ if (request.method === 'GET') backends[effect.backend][request.path] = response;
81
+ }
82
+ }
83
+ }
84
+ return backends;
85
+ }
86
+ function collectBodyModes(handlerIoPlan) {
87
+ const modes = new Set();
88
+ for (const route of handlerIoPlan && handlerIoPlan.routes || []) {
89
+ for (const fetch of route.effects && route.effects.backendFetches || []) if (fetch.requestBody && fetch.requestBody.mode) modes.add(fetch.requestBody.mode);
90
+ }
91
+ return Array.from(modes).sort();
92
+ }
93
+ function routeReadiness(route, handlerIoPlan, postJsonRoute) {
94
+ const io = (handlerIoPlan && handlerIoPlan.routes || []).find((entry) => entry.routeId === route.routeId || (entry.method === route.method && entry.path === route.path));
95
+ const hasJsonBody = Boolean(io && (io.effects && io.effects.backendFetches || []).some((fetch) => fetch.requestBody && fetch.requestBody.mode === 'json'));
96
+ const hasEffects = routeHasResolve(route);
97
+ const readiness = hasJsonBody || (postJsonRoute && postJsonRoute.routeId === route.routeId) ? 'plan-only' : (hasEffects ? 'implemented-fixture-proof' : 'not-required');
98
+ return {
99
+ routeId: route.routeId,
100
+ method: route.method,
101
+ path: route.path,
102
+ handlerName: route.handlerName,
103
+ provider: 'fastly',
104
+ symbolicBackendConfig: hasEffects,
105
+ originMapping: readiness,
106
+ providerReadiness: readiness,
107
+ compiledWasmReadiness: 'metadata-only',
108
+ requestBodyModes: io && io.requestBody ? clone(io.requestBody.modes || []) : [],
109
+ backendRequestBodyModes: io ? (io.effects.backendFetches || []).map((fetch) => fetch.requestBody && fetch.requestBody.mode).filter(Boolean) : [],
110
+ responseBodyModes: io && io.originResponse ? clone(io.originResponse.modes || []) : [],
111
+ diagnostics: hasJsonBody ? [parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.postJsonBodyPlanOnly] : []
112
+ };
113
+ }
114
+ function check(name, condition, details, failures, diagnostics) {
115
+ const out = { name, status: condition ? 'ok' : 'error', details: details || {} };
116
+ if (!condition) {
117
+ failures.push(out);
118
+ diagnostics.push(makeDiagnostic(parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.smokeFailed, `Fastly lifecycle parity check failed: ${name}.`, 'error', { check: name, details: out.details }));
119
+ }
120
+ return out;
121
+ }
122
+ function serializeExecution(execution) {
123
+ if (!execution) return undefined;
124
+ return {
125
+ status: execution.status,
126
+ provider: execution.provider,
127
+ routeId: execution.routeId,
128
+ route: clone(execution.route),
129
+ groupShape: execution.groupShape,
130
+ continuation: execution.continuation,
131
+ effectCount: execution.effectCount,
132
+ effects: clone(execution.effects || []),
133
+ resolved: clone(execution.resolved || {}),
134
+ finalResult: clone(execution.finalResult),
135
+ lifecycle: clone(execution.lifecycle),
136
+ responseHandleSurface: clone(execution.responseHandleSurface || []),
137
+ continuationResultAccesses: clone(execution.continuationResultAccesses || []),
138
+ continuationResponseMethodCalls: clone(execution.continuationResponseMethodCalls || []),
139
+ diagnostics: clone(execution.diagnostics || [])
140
+ };
141
+ }
142
+
143
+ function buildFastlyLifecycleParityProof(options = {}) {
144
+ const cwd = options.cwd || process.cwd();
145
+ const generatedBy = options.generatedBy || PACKAGE_VERSION;
146
+ const plan = normalize(options.routeHandlerEffectPlan || options.effectPlan || options.plan);
147
+ const handlerIoPlan = normalize(options.handlerIoLifecyclePlan || options.handlerIoPlan);
148
+ const backendJsonPlan = normalize(options.backendJsonRequestBodyPlan || options.backendJsonPlan);
149
+ const diagnostics = [];
150
+ const failures = [];
151
+ const checks = [];
152
+ if (!plan) diagnostics.push(makeDiagnostic(parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.planRequired, 'Fastly lifecycle parity proof requires route-handler-effect-plan.json.'));
153
+
154
+ const getUserRoute = findGetUserRoute(plan);
155
+ const dashboardRoute = findDashboardRoute(plan);
156
+ const postJsonRoute = findPostJsonRoute(plan);
157
+ if (plan && !getUserRoute) diagnostics.push(makeDiagnostic(parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.getRouteMissing, 'Fastly lifecycle parity proof expected reference GET /users/:id route.'));
158
+ if (plan && !dashboardRoute) diagnostics.push(makeDiagnostic(parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.headRouteMissing, 'Fastly lifecycle parity proof expected reference dashboard route with a HEAD backend effect.'));
159
+
160
+ const providerConfig = options.providerConfig || buildFastlyRouteEffectProviderConfig({ plan, resolvedConfig: options.resolvedConfig, mode: options.mode || 'fastly-lifecycle-parity-fixture' });
161
+ const backends = options.backends || createReferenceBackends(plan || {}, options.params || { id: 'abc' });
162
+ const provider = createFastlyRouteHandlerEffectProvider({ plan, backends, providerConfig, continuations: options.continuations, resolvedConfig: options.resolvedConfig, mode: options.mode || 'fastly-lifecycle-parity-fixture' });
163
+
164
+ checks.push(check('symbolic backend config maps reference backends to Fastly refs', providerConfig && providerConfig.provider === 'fastly' && providerConfig.symbolicRefsOnly === true && (providerConfig.backends || []).some((entry) => entry.backend === 'users' && String(entry.baseUrlRef || '').startsWith('$config:')), { providerConfig }, failures, diagnostics));
165
+
166
+ let getExecution;
167
+ let dashboardExecution;
168
+ if (getUserRoute) {
169
+ getExecution = provider.executeRoute({ path: '/users/:id', method: 'GET', params: { id: 'abc' } });
170
+ const resolved = getExecution.resolved && (getExecution.resolved.$default || getExecution.resolved[Object.keys(getExecution.resolved || {})[0]]);
171
+ checks.push(check('GET /users/:id maps to a symbolic Fastly backend and resolves status/header/body', getExecution.status === 'resolved' && resolved && resolved.request && resolved.request.method === 'GET' && resolved.request.path === '/users/abc' && resolved.status === 200 && headerValue(resolved.headers, 'x-pulsewasm-fastly-backend') === 'users' && String(resolved.body || '').includes('abc'), { execution: serializeExecution(getExecution) }, failures, diagnostics));
172
+ checks.push(check('GET continuation lifecycle metadata is present', getExecution.lifecycle && getExecution.lifecycle.handlerReturnedResolveRequest === true && getExecution.lifecycle.continuationReentered === true && getExecution.lifecycle.symbolicBackendRefs === true && getExecution.lifecycle.actualNetworkFetch === false, { lifecycle: getExecution.lifecycle }, failures, diagnostics));
173
+ }
174
+ if (dashboardRoute) {
175
+ dashboardExecution = provider.executeRoute({ path: '/dashboard/:id', method: 'GET', params: { id: 'abc' } });
176
+ const head = Object.values(dashboardExecution.resolved || {}).find((entry) => entry.request && entry.request.method === 'HEAD');
177
+ checks.push(check('HEAD backend effect maps to symbolic Fastly backend with status/headers and no body', dashboardExecution.status === 'resolved' && head && head.request.path === '/users/abc/posts' && head.status === 200 && head.body === '' && headerValue(head.headers, 'x-count') === '2', { head, execution: serializeExecution(dashboardExecution) }, failures, diagnostics));
178
+ }
179
+
180
+ const responseModes = collectBodyModes(handlerIoPlan);
181
+ const hasJsonTextPolicy = Boolean(handlerIoPlan && (handlerIoPlan.originResponseBodyModes || handlerIoPlan.finalResponseModes));
182
+ checks.push(check('JSON/text body mode policy is explicit in handler lifecycle metadata', hasJsonTextPolicy && (handlerIoPlan.originResponseBodyModes.json || handlerIoPlan.finalResponseModes.jsonText), { originResponseBodyModes: handlerIoPlan && handlerIoPlan.originResponseBodyModes, finalResponseModes: handlerIoPlan && handlerIoPlan.finalResponseModes, backendBodyModes: responseModes }, failures, diagnostics));
183
+
184
+ const postJsonReadiness = postJsonRoute ? 'plan-only' : 'not-present';
185
+ if (postJsonRoute) diagnostics.push(makeDiagnostic(parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.postJsonBodyPlanOnly, 'Fastly POST JSON backend request bodies are explicitly classified as plan-only until Fastly body send support is wired.', 'warning', { routeId: postJsonRoute.routeId, method: postJsonRoute.method, path: postJsonRoute.path }));
186
+ const backendSummary = backendJsonPlan && backendJsonPlan.summary || {};
187
+ const unsupportedModesDiagnosed = postJsonReadiness === 'plan-only' && backendSummary.binaryBodyReserved === true && backendSummary.streamBodyReserved === true && backendSummary.multipartBodyReserved === true;
188
+
189
+ const routes = (plan && plan.routes || []).map((route) => routeReadiness(route, handlerIoPlan, postJsonRoute));
190
+ const errorDiagnostics = diagnostics.filter((entry) => (entry.severity || 'error') === 'error');
191
+ const artifact = normalizeArtifact({
192
+ version: parity.FASTLY_LIFECYCLE_PARITY_PROOF_VERSION,
193
+ generatedBy,
194
+ phase: parity.FASTLY_LIFECYCLE_PARITY_PROOF_PHASE,
195
+ artifact: parity.FASTLY_LIFECYCLE_PARITY_PROOF_ARTIFACT,
196
+ contractId: parity.FASTLY_LIFECYCLE_PARITY_CONTRACT_ID,
197
+ status: errorDiagnostics.length === 0 ? 'ok' : 'error',
198
+ provider: 'fastly',
199
+ routeHandlerEffectPlanConnected: Boolean(plan),
200
+ handlerIoLifecyclePlanConnected: Boolean(handlerIoPlan),
201
+ backendJsonRequestBodyPlanConnected: Boolean(backendJsonPlan),
202
+ sourcePlanVersion: plan && plan.version,
203
+ sourcePlanArtifact: plan && plan.artifact,
204
+ scope: clone(parity.FASTLY_LIFECYCLE_PARITY_SCOPE),
205
+ policy: clone(parity.FASTLY_LIFECYCLE_PARITY_POLICY),
206
+ backendJsonPolicy: clone(backendBody.BACKEND_JSON_REQUEST_BODY_POLICY),
207
+ proof: {
208
+ package: '@pulse-compute/provider-fastly/compiler/lifecycle-parity-proof',
209
+ mode: options.mode || 'fastly-lifecycle-parity-fixture',
210
+ symbolicBackendRefs: true,
211
+ actualNetworkFetch: false,
212
+ fixtureBackends: true,
213
+ noExternalFastlyServiceRequired: true,
214
+ compiledWasmSuspendResumeBridge: false,
215
+ postJsonRequestBodyReadiness: postJsonReadiness
216
+ },
217
+ providerConfig: clone(providerConfig),
218
+ routes,
219
+ readiness: {
220
+ getHeadOriginMapping: 'implemented-fixture-proof',
221
+ statusHeaderBodyPassthrough: 'implemented-fixture-proof',
222
+ resolveContinuationLifecycle: 'implemented-fixture-proof',
223
+ postJsonRequestBody: postJsonReadiness,
224
+ compiledWasmFastlyBridge: 'metadata-only'
225
+ },
226
+ smoke: {
227
+ checks,
228
+ failedChecks: failures,
229
+ results: {
230
+ getUser: serializeExecution(getExecution),
231
+ dashboard: serializeExecution(dashboardExecution),
232
+ postJson: postJsonRoute ? { routeId: postJsonRoute.routeId, method: postJsonRoute.method, path: postJsonRoute.path, readiness: postJsonReadiness, diagnostic: parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.postJsonBodyPlanOnly } : undefined
233
+ }
234
+ },
235
+ diagnostics,
236
+ summary: {
237
+ symbolicBackendConfigValidated: checks.some((entry) => entry.name.startsWith('symbolic backend config') && entry.status === 'ok'),
238
+ getOriginMappingValidated: checks.some((entry) => entry.name.startsWith('GET /users') && entry.status === 'ok'),
239
+ headOriginMappingValidated: checks.some((entry) => entry.name.startsWith('HEAD backend') && entry.status === 'ok'),
240
+ statusPassthroughValidated: Boolean(getExecution && getExecution.status === 'resolved' && getExecution.finalResult && getExecution.finalResult.status === 200),
241
+ headerPassthroughValidated: Boolean(getExecution && (headerValue(getExecution.finalResult && getExecution.finalResult.headers, 'x-reference-continuation') || headerValue((getExecution.resolved && getExecution.resolved.$default && getExecution.resolved.$default.headers) || [], 'x-pulsewasm-fastly-backend'))),
242
+ bodyModePolicyValidated: checks.some((entry) => entry.name.startsWith('JSON/text body mode policy') && entry.status === 'ok'),
243
+ resolveContinuationLifecycleValidated: checks.some((entry) => entry.name.startsWith('GET continuation lifecycle') && entry.status === 'ok'),
244
+ postJsonRequestBodyReadiness: postJsonReadiness,
245
+ postJsonRequestBodyDiagnostic: postJsonRoute ? parity.FASTLY_LIFECYCLE_PARITY_DIAGNOSTICS.postJsonBodyPlanOnly : undefined,
246
+ unsupportedModesDiagnosed,
247
+ noExternalFastlyServiceRequired: true,
248
+ actualNetworkFetch: false,
249
+ binaryBodyReserved: backendSummary.binaryBodyReserved === true,
250
+ streamBodyReserved: backendSummary.streamBodyReserved === true,
251
+ multipartBodyReserved: backendSummary.multipartBodyReserved === true,
252
+ diagnostics: diagnostics.length,
253
+ warnings: diagnostics.filter((entry) => (entry.severity || 'error') === 'warning').length,
254
+ errors: errorDiagnostics.length
255
+ }
256
+ }, cwd);
257
+ return { artifact, diagnostics, provider, providerConfig, backends, failures };
258
+ }
259
+
260
+ module.exports = { buildFastlyLifecycleParityProof, createReferenceBackends };
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ function loadHandlerEffectContracts() {
4
+ try {
5
+ return require('@pulse-compute/wasm-contracts/handler/effects');
6
+ } catch (error) {
7
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
8
+ return require('../../../../wasm/packages/contracts/src/handler/effects.js');
9
+ }
10
+ throw error;
11
+ }
12
+ }
13
+
14
+ function loadDiagnostics() {
15
+ try {
16
+ return require('@pulse-compute/wasm-contracts/diagnostics');
17
+ } catch (error) {
18
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
19
+ return require('../../../../wasm/packages/contracts/src/diagnostics.js');
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ const handlerEffects = loadHandlerEffectContracts();
26
+ const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = loadDiagnostics();
27
+ const {
28
+ createFastlyRouteHandlerEffectProvider,
29
+ createDefaultBackendsFromPlan,
30
+ buildFastlyRouteEffectProviderConfig,
31
+ headerValue
32
+ } = require('../runtime/route-handler-effects.js');
33
+
34
+ const phaseName = 'fastly-route-handler-effect-proof';
35
+
36
+ function clone(value) {
37
+ if (Array.isArray(value)) return value.map(clone);
38
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clone(entry)]));
39
+ return value;
40
+ }
41
+
42
+ function normalizePlan(input) {
43
+ const plan = input && input.artifact && typeof input.artifact === 'object' ? input.artifact : input;
44
+ if (!plan || typeof plan !== 'object') return undefined;
45
+ return {
46
+ ...plan,
47
+ routes: Array.isArray(plan.routes) ? plan.routes.map(clone) : [],
48
+ continuations: Array.isArray(plan.continuations) ? plan.continuations.map(clone) : []
49
+ };
50
+ }
51
+
52
+ function plannedResolveEntries(plan) {
53
+ const entries = [];
54
+ for (const route of plan && plan.routes || []) {
55
+ for (const resolve of route.resolves || []) {
56
+ if (resolve && Array.isArray(resolve.effects) && resolve.effects.length > 0) entries.push({ route, resolve });
57
+ }
58
+ }
59
+ return entries;
60
+ }
61
+
62
+ function firstEntryByShape(entries, shape) {
63
+ return entries.find((entry) => entry.resolve && entry.resolve.groupShape === shape);
64
+ }
65
+
66
+ function makeDiagnostic(code, message, severity, details) {
67
+ return normalizeDiagnostic({
68
+ phase: phaseName,
69
+ severity: severity || 'error',
70
+ code,
71
+ message,
72
+ hint: details && details.hint,
73
+ details,
74
+ loc: { file: '<fastly-route-handler-effect-proof>' }
75
+ });
76
+ }
77
+
78
+ function check(name, condition, details, failures, diagnostics) {
79
+ const entry = { name, status: condition ? 'ok' : 'error', details: details || {} };
80
+ if (!condition) {
81
+ failures.push(entry);
82
+ diagnostics.push(makeDiagnostic(handlerEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectSmokeFailed, `Fastly route-handler effect proof check failed: ${name}.`, 'error', { check: name, details }));
83
+ }
84
+ return entry;
85
+ }
86
+
87
+ function cloneEffectWithMethod(effect, method) {
88
+ const next = clone(effect);
89
+ next.request = { ...(next.request || {}), method };
90
+ return next;
91
+ }
92
+
93
+ function serializeExecution(execution) {
94
+ if (!execution) return undefined;
95
+ return {
96
+ status: execution.status,
97
+ routeId: execution.routeId,
98
+ route: clone(execution.route),
99
+ groupShape: execution.groupShape,
100
+ continuation: execution.continuation,
101
+ continuationPlanned: execution.continuationPlanned,
102
+ effectCount: execution.effectCount,
103
+ effects: clone(execution.effects || []),
104
+ resolved: clone(execution.resolved || {}),
105
+ finalResult: clone(execution.finalResult),
106
+ responseHandleSurface: clone(execution.responseHandleSurface),
107
+ continuationResultAccesses: clone(execution.continuationResultAccesses),
108
+ continuationResponseMethodCalls: clone(execution.continuationResponseMethodCalls),
109
+ providerConfig: clone(execution.providerConfig),
110
+ lifecycle: clone(execution.lifecycle),
111
+ diagnostics: clone(execution.diagnostics || [])
112
+ };
113
+ }
114
+
115
+ function buildFastlyRouteHandlerEffectProof(inputs = {}) {
116
+ const cwd = inputs.cwd || process.cwd();
117
+ const generatedBy = inputs.generatedBy || PACKAGE_VERSION;
118
+ const plan = normalizePlan(inputs.routeHandlerEffectPlan || inputs.plan);
119
+ const diagnostics = [];
120
+ const failures = [];
121
+ const checks = [];
122
+
123
+ if (!plan) {
124
+ diagnostics.push(makeDiagnostic(handlerEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectPlanRequired, 'Fastly route-handler effect proof requires a route-handler-effect-plan artifact.', 'error', { hint: 'Emit --route-handler-effects before requesting --fastly-route-handler-effect-proof.' }));
125
+ }
126
+
127
+ const entries = plannedResolveEntries(plan);
128
+ if (plan && entries.length === 0) {
129
+ diagnostics.push(makeDiagnostic(handlerEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectNoPlannedEffects, 'Fastly route-handler effect proof requires at least one ctx.resolve(ctx.fetch(...), continuation) boundary.', 'error', { routes: Array.isArray(plan.routes) ? plan.routes.length : 0, hint: 'Add a lowerable ctx.resolve(ctx.fetch("backend", requestSpec), afterFetch) route handler.' }));
130
+ }
131
+
132
+ const backends = inputs.backends || createDefaultBackendsFromPlan(plan || {}, inputs.defaultParams || {});
133
+ const providerConfig = inputs.providerConfig || buildFastlyRouteEffectProviderConfig({ plan, resolvedConfig: inputs.resolvedConfig, mode: inputs.mode || 'fastly-backend-fetch-fixture' });
134
+ const continuations = inputs.continuations || {
135
+ renderUser(ctx) {
136
+ const user = ctx.resolved();
137
+ if (!user.ok()) return ctx.result.text(user.status(), user.text());
138
+ return ctx.result.text(200, user.text(), { headers: { 'X-PulseWasm-Continuation': 'renderUser' } });
139
+ },
140
+ renderDashboard(ctx) {
141
+ const user = ctx.resolved('user');
142
+ const posts = ctx.resolved('posts');
143
+ return ctx.result.text(200, user.text() + posts.header('x-count'), { headers: { 'X-PulseWasm-Continuation': 'renderDashboard' } });
144
+ }
145
+ };
146
+ const provider = createFastlyRouteHandlerEffectProvider({ plan, backends, continuations, providerConfig, resolvedConfig: inputs.resolvedConfig, mode: inputs.mode || 'fastly-backend-fetch-fixture' });
147
+ diagnostics.push(...(provider.diagnostics || []));
148
+
149
+ checks.push(check(
150
+ 'Fastly route-effect provider config maps planned backends to symbolic refs',
151
+ providerConfig && providerConfig.provider === 'fastly' && providerConfig.symbolicRefsOnly === true && Array.isArray(providerConfig.backends) && providerConfig.backends.length > 0 && providerConfig.backends.every((backend) => typeof backend.baseUrlRef === 'string' && backend.baseUrlRef.startsWith('$config:')),
152
+ { provider: providerConfig && providerConfig.provider, backends: providerConfig && providerConfig.backends ? providerConfig.backends.length : 0, symbolicRefsOnly: providerConfig && providerConfig.symbolicRefsOnly, refs: providerConfig && providerConfig.backends ? providerConfig.backends.map((backend) => backend.baseUrlRef) : [] },
153
+ failures,
154
+ diagnostics
155
+ ));
156
+
157
+ let singleExecution;
158
+ let namedExecution;
159
+ let unsupportedResult;
160
+ let missingBackendResult;
161
+
162
+ if (entries.length > 0) {
163
+ const single = firstEntryByShape(entries, 'single') || entries[0];
164
+ singleExecution = provider.executeResolve(single.route, single.resolve, inputs.params || { id: 'abc' });
165
+ const singleResolved = singleExecution.resolved && (singleExecution.resolved.$default || singleExecution.resolved[Object.keys(singleExecution.resolved || {})[0]]);
166
+ checks.push(check('single ctx.resolve(ctx.fetch(...), continuation) resolves one Fastly backend fetch handle', singleExecution.status === 'resolved' && Boolean(singleResolved) && singleResolved.status >= 200 && singleResolved.status < 300, { status: singleExecution.status, resolvedNames: Object.keys(singleExecution.resolved || {}) }, failures, diagnostics));
167
+ checks.push(check('continuation reads Fastly single fetch through ctx.resolved()', singleExecution.continuationPlanned === true && singleExecution.continuationResultAccesses.some((access) => access.name === '$default'), { continuation: singleExecution.continuation, resultAccesses: singleExecution.continuationResultAccesses }, failures, diagnostics));
168
+ checks.push(check('resolved Fastly fetch response exposes status ok header text jsonText json surface', singleExecution.responseHandleSurface.includes('status') && singleExecution.responseHandleSurface.includes('ok') && singleExecution.responseHandleSurface.includes('header') && singleExecution.responseHandleSurface.includes('text') && singleExecution.responseHandleSurface.includes('jsonText') && singleExecution.responseHandleSurface.includes('json'), { responseHandleSurface: singleExecution.responseHandleSurface }, failures, diagnostics));
169
+
170
+ const named = firstEntryByShape(entries, 'named-object');
171
+ if (named) {
172
+ namedExecution = provider.executeResolve(named.route, named.resolve, inputs.params || { id: 'abc' });
173
+ const names = Object.keys(namedExecution.resolved || {}).sort();
174
+ checks.push(check('named ctx.resolve({ name: ctx.fetch(...) }, continuation) resolves all named Fastly fetch handles', namedExecution.status === 'resolved' && names.includes('user') && names.includes('posts'), { status: namedExecution.status, names }, failures, diagnostics));
175
+ const head = Object.values(namedExecution.resolved || {}).find((entry) => entry.request && entry.request.method === 'HEAD');
176
+ checks.push(check('HEAD Fastly backend fetch resolves headers/status with empty body text', Boolean(head) && head.status >= 200 && head.status < 300 && head.body === '', { head }, failures, diagnostics));
177
+ } else {
178
+ checks.push(check('named object effect group is present in Fastly proof fixture', false, { entries: entries.map((entry) => ({ routeId: entry.route.routeId, groupShape: entry.resolve.groupShape })) }, failures, diagnostics));
179
+ }
180
+
181
+ const firstEffect = single.resolve.effects[0];
182
+ unsupportedResult = provider.executeFetch(cloneEffectWithMethod(firstEffect, 'PUT'), { route: single.route, params: { id: 'abc' } }).toJSON();
183
+ checks.push(check('unsupported Fastly backend fetch methods are diagnosed', unsupportedResult.status === 405 && unsupportedResult.diagnostics.some((entry) => entry.code === handlerEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectUnsupportedMethod), { status: unsupportedResult.status, diagnostics: unsupportedResult.diagnostics.map((entry) => entry.code) }, failures, diagnostics));
184
+
185
+ const missingEffect = clone(firstEffect);
186
+ missingEffect.backend = 'missingBackend';
187
+ missingBackendResult = provider.executeFetch(missingEffect, { route: single.route, params: { id: 'abc' } }).toJSON();
188
+ checks.push(check('missing Fastly backend fixtures are diagnosed', missingBackendResult.status === 502 && missingBackendResult.diagnostics.some((entry) => entry.code === handlerEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectBackendMissing), { status: missingBackendResult.status, diagnostics: missingBackendResult.diagnostics.map((entry) => entry.code) }, failures, diagnostics));
189
+
190
+ checks.push(check('resolved Fastly header passthrough is available to continuations', singleResolved && headerValue(singleResolved.headers, 'x-pulsewasm-fastly-backend') === firstEffect.backend, { headers: singleResolved && singleResolved.headers }, failures, diagnostics));
191
+ }
192
+
193
+ const errorDiagnostics = diagnostics.filter((entry) => (entry.severity || 'error') === 'error');
194
+ const policy = handlerEffects.defaultRouteHandlerFastlyEffectProofPolicy();
195
+ const effects = entries.flatMap((entry) => entry.resolve.effects || []);
196
+ const artifact = normalizeArtifact({
197
+ version: handlerEffects.ROUTE_HANDLER_FASTLY_EFFECT_PROOF_VERSION,
198
+ generatedBy,
199
+ phase: handlerEffects.ROUTE_HANDLER_FASTLY_EFFECT_PROOF_PHASE,
200
+ artifact: handlerEffects.ROUTE_HANDLER_FASTLY_EFFECT_PROOF_ARTIFACT,
201
+ contractId: handlerEffects.ROUTE_HANDLER_EFFECT_CONTRACT_ID,
202
+ provider: 'fastly',
203
+ status: errorDiagnostics.length === 0 ? 'ok' : 'error',
204
+ routeEffectPlanConnected: Boolean(plan),
205
+ loweringPlanConnected: Boolean(plan),
206
+ sourcePlanVersion: plan && plan.version,
207
+ sourcePlanArtifact: plan && plan.artifact,
208
+ providerBehaviorImplemented: true,
209
+ fastlyProviderEffectExecutionImplemented: true,
210
+ compiledWasmEffectExecutionImplemented: false,
211
+ compiledWasmSuspendResumeBridgeImplemented: false,
212
+ scope: policy,
213
+ runtime: { version: handlerEffects.ROUTE_HANDLER_FASTLY_EFFECT_RUNTIME_VERSION, package: '@pulse-compute/provider-fastly/runtime/route-handler-effects' },
214
+ proof: { package: '@pulse-compute/provider-fastly/compiler/route-handler-effect-proof', mode: inputs.mode || 'fastly-backend-fetch-fixture', singleEffect: true, namedObjectEffectGroup: true, arrayEffectGroup: false, fixtureBackends: true, symbolicBackendRefs: true, actualNetworkFetch: false },
215
+ lifecycle: { ...handlerEffects.ROUTE_HANDLER_EFFECT_LIFECYCLE, fastlyProofHostEffectExecution: entries.length > 0, fastlyProofContinuationReentryPlanned: entries.length > 0, languageLevelAsync: false, promises: false, asyncAwait: false, asyncify: false },
216
+ methods: [...handlerEffects.ROUTE_HANDLER_EFFECT_ALLOWED_METHODS],
217
+ groupShapes: clone(handlerEffects.ROUTE_HANDLER_EFFECT_GROUP_SHAPES),
218
+ responseHandleSurface: [...handlerEffects.ROUTE_HANDLER_CONTEXT_SURFACE.resolvedFetchResponse],
219
+ reservedResponseHandleSurface: [...handlerEffects.ROUTE_HANDLER_CONTEXT_SURFACE.reservedResolvedFetchResponse],
220
+ policy: { compilerOwnsProviderBehavior: false, compilerOrchestrationOnly: true, providerOwner: '@pulse-compute/provider-fastly', directProviderSmoke: true, localFixtureFetch: true, symbolicBackendRefs: true, actualNetworkFetch: false, languageLevelAsync: false, promises: false, asyncAwait: false, asyncify: false, compiledWasmSuspendResumeBridge: false },
221
+ providerConfig: clone(providerConfig),
222
+ entriesConsumed: entries.map(({ route, resolve }) => ({ routeId: route.routeId, method: route.method, path: route.path, handlerName: route.handlerName, groupShape: resolve.groupShape, continuation: resolve.continuation && resolve.continuation.name, effects: (resolve.effects || []).map((effect) => ({ name: effect.name || '$default', kind: effect.kind, backend: effect.backend, method: effect.request && effect.request.method })) })),
223
+ backendFixtures: Object.keys(backends || {}).sort().map((backend) => ({ backend, responses: Object.keys(backends[backend] || {}).sort() })),
224
+ smoke: { checks, failedChecks: failures, results: { single: serializeExecution(singleExecution), namedObject: serializeExecution(namedExecution), unsupportedMethod: unsupportedResult, missingBackend: missingBackendResult } },
225
+ summary: {
226
+ routesWithEffects: entries.length,
227
+ effects: effects.length,
228
+ backendFetchEffects: effects.filter((effect) => effect.kind === 'backend-fetch').length,
229
+ checks: checks.length,
230
+ failedChecks: failures.length,
231
+ singleResolveValidated: checks.some((entry) => entry.name.startsWith('single ctx.resolve') && entry.status === 'ok'),
232
+ singleValidated: checks.some((entry) => entry.name.startsWith('single ctx.resolve') && entry.status === 'ok'),
233
+ namedObjectValidated: checks.some((entry) => entry.name.startsWith('named ctx.resolve') && entry.status === 'ok'),
234
+ headValidated: checks.some((entry) => entry.name.startsWith('HEAD Fastly backend fetch') && entry.status === 'ok'),
235
+ continuationReentryValidated: checks.some((entry) => entry.name.includes('continuation reads Fastly') && entry.status === 'ok'),
236
+ unsupportedMethodValidated: checks.some((entry) => entry.name.startsWith('unsupported Fastly backend fetch methods') && entry.status === 'ok'),
237
+ unsupportedMethodDiagnosticValidated: checks.some((entry) => entry.name.startsWith('unsupported Fastly backend fetch methods') && entry.status === 'ok'),
238
+ missingBackendValidated: checks.some((entry) => entry.name.startsWith('missing Fastly backend fixtures') && entry.status === 'ok'),
239
+ missingBackendDiagnosticValidated: checks.some((entry) => entry.name.startsWith('missing Fastly backend fixtures') && entry.status === 'ok'),
240
+ responseHandleSurfaceValidated: checks.some((entry) => entry.name.startsWith('resolved Fastly fetch response exposes') && entry.status === 'ok'),
241
+ providerConfigValidated: checks.some((entry) => entry.name.startsWith('Fastly route-effect provider config') && entry.status === 'ok'),
242
+ providerConfigMapped: checks.some((entry) => entry.name.startsWith('Fastly route-effect provider config') && entry.status === 'ok'),
243
+ symbolicBackendRefs: Boolean(providerConfig && providerConfig.symbolicRefsOnly),
244
+ providerBehaviorImplemented: true,
245
+ compiledWasmEffectExecutionImplemented: false,
246
+ diagnostics: diagnostics.length,
247
+ errorDiagnostics: errorDiagnostics.length,
248
+ errors: errorDiagnostics.length
249
+ },
250
+ diagnostics
251
+ }, cwd);
252
+ return { artifact, diagnostics, failures, provider, providerConfig };
253
+ }
254
+
255
+ module.exports = { buildFastlyRouteHandlerEffectProof, phaseName };