@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.
- package/README.md +52 -1
- package/dist/config-schema.json +73 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -5
- package/src/build/canonical-target.js +436 -0
- package/src/build/entities-native.js +564 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/native-http-effects.js +871 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2109 -0
- package/src/compiler/assets-package-out-parity-proof.js +338 -0
- package/src/compiler/assets-provider-proof.js +499 -0
- package/src/compiler/config-provider-resolution.js +573 -0
- package/src/compiler/fastly-adapter.js +564 -0
- package/src/compiler/fastly-command-entry.js +390 -0
- package/src/compiler/fastly-config-secret-serve.js +323 -0
- package/src/compiler/fastly-hostcall-binding.js +598 -0
- package/src/compiler/fastly-readiness.js +477 -0
- package/src/compiler/index.js +40 -0
- package/src/compiler/lifecycle-parity-contract.js +70 -0
- package/src/compiler/lifecycle-parity-proof.js +260 -0
- package/src/compiler/route-handler-effect-proof.js +255 -0
- package/src/compiler/runtime-provider-kv.js +237 -0
- package/src/compiler/runtime-provider-shape.js +109 -0
- package/src/config-api.js +63 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +73 -0
- package/src/index.js +3 -0
- package/src/index.ts +118 -0
- package/src/javascript/assets-adapter.js +122 -0
- package/src/javascript/bindings-adapter.js +237 -0
- package/src/javascript/errors.js +26 -0
- package/src/javascript/fetch-adapter.js +93 -0
- package/src/javascript/grip-broadcast.js +261 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +26 -0
- package/src/javascript/package-effects.js +68 -0
- package/src/javascript/runtime-host.js +108 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +776 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +260 -0
- package/src/javascript/target.js +73 -0
- package/src/javascript/test-runtime.js +407 -0
- package/src/provider-contract.js +72 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +597 -0
- package/src/runtime/index.js +28 -0
- package/src/runtime/provider-primitives-contract.js +272 -0
- package/src/runtime/provider-primitives.js +82 -0
- package/src/runtime/route-handler-effects.js +507 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-http-effects-host.js +390 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +599 -0
- package/src/toolchain/config.js +128 -0
- package/src/toolchain/index.js +343 -0
|
@@ -0,0 +1,507 @@
|
|
|
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 routeEffects = loadHandlerEffectContracts();
|
|
26
|
+
const { normalizeDiagnostic } = loadDiagnostics();
|
|
27
|
+
|
|
28
|
+
function clone(value) {
|
|
29
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
30
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clone(entry)]));
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function plainObject(value) {
|
|
35
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePlan(input) {
|
|
39
|
+
const plan = input && input.artifact && typeof input.artifact === 'object' ? input.artifact : input;
|
|
40
|
+
if (!plan || typeof plan !== 'object') return undefined;
|
|
41
|
+
return {
|
|
42
|
+
...plan,
|
|
43
|
+
routes: Array.isArray(plan.routes) ? plan.routes.map(clone) : [],
|
|
44
|
+
continuations: Array.isArray(plan.continuations) ? plan.continuations.map(clone) : []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeResolvedConfig(input) {
|
|
49
|
+
const resolved = input && input.artifact ? input.artifact : input;
|
|
50
|
+
return resolved && typeof resolved === 'object' ? resolved : {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeDiagnostic(code, message, severity, details) {
|
|
54
|
+
return normalizeDiagnostic({
|
|
55
|
+
phase: 'fastly-route-handler-effects',
|
|
56
|
+
severity: severity || 'error',
|
|
57
|
+
code,
|
|
58
|
+
message,
|
|
59
|
+
hint: details && details.hint,
|
|
60
|
+
details,
|
|
61
|
+
loc: { file: '<fastly-route-handler-effects>' }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeHeaders(headers) {
|
|
66
|
+
if (!headers) return [];
|
|
67
|
+
if (Array.isArray(headers)) return headers.filter((entry) => entry && entry[0] !== undefined && entry[1] !== undefined).map(([name, value]) => [String(name), String(value)]);
|
|
68
|
+
if (typeof headers[Symbol.iterator] === 'function' && typeof headers !== 'string') return Array.from(headers, ([name, value]) => [String(name), String(value)]);
|
|
69
|
+
return Object.entries(headers).map(([name, value]) => [String(name), String(value)]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function headerValue(headers, name) {
|
|
73
|
+
const lower = String(name || '').toLowerCase();
|
|
74
|
+
const match = normalizeHeaders(headers).find(([key]) => String(key).toLowerCase() === lower);
|
|
75
|
+
return match ? match[1] : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pathParamNames(routePath) {
|
|
79
|
+
return Array.from(String(routePath || '').matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g), (match) => match[1]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function defaultParamsForRoute(route) {
|
|
83
|
+
const params = {};
|
|
84
|
+
for (const name of pathParamNames(route && route.path)) params[name] = 'abc';
|
|
85
|
+
return params;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function evaluateExpression(expression, context = {}) {
|
|
89
|
+
if (expression === undefined || expression === null) return '';
|
|
90
|
+
if (typeof expression === 'string' || typeof expression === 'number' || typeof expression === 'boolean') return expression;
|
|
91
|
+
if (typeof expression !== 'object') return String(expression);
|
|
92
|
+
if (expression.kind === 'literal') return String(expression.value ?? '');
|
|
93
|
+
if (expression.kind === 'number') return Number(expression.value || 0);
|
|
94
|
+
if (expression.kind === 'boolean') return Boolean(expression.value);
|
|
95
|
+
if (expression.kind === 'ctx.param') return String((context.params || {})[expression.name] ?? '');
|
|
96
|
+
if (expression.kind === 'ctx.paramI32') return Number((context.params || {})[expression.name] || 0);
|
|
97
|
+
if (expression.kind === 'local') return evaluateExpression(expression.value, context);
|
|
98
|
+
if (expression.kind === 'concat') return (expression.parts || []).map((part) => evaluateExpression(part, context)).join('');
|
|
99
|
+
return String(expression.value ?? '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function requestFromEffect(effect, context = {}) {
|
|
103
|
+
const request = effect && effect.request ? effect.request : {};
|
|
104
|
+
return {
|
|
105
|
+
method: String(request.method || 'GET').toUpperCase(),
|
|
106
|
+
path: String(evaluateExpression(request.path, context) || '/'),
|
|
107
|
+
headers: (request.headers || []).map((header) => [String(header.name), String(evaluateExpression(header.value, context))])
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function symbolicRefName(value, fallback) {
|
|
112
|
+
if (value && typeof value === 'object') {
|
|
113
|
+
if (typeof value.$config === 'string') return `$config:${value.$config}`;
|
|
114
|
+
if (typeof value.$secret === 'string') return `$secret:${value.$secret}`;
|
|
115
|
+
if (typeof value.$env === 'string') return `$config:${value.$env}`;
|
|
116
|
+
if (typeof value.fromEnv === 'string') return `$config:${value.fromEnv}`;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === 'string' && /^https?:\/\//.test(value)) return '$config:FASTLY_BACKEND_BASE_URL';
|
|
119
|
+
return fallback;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function backendConfigFromResolvedConfig(resolvedConfig, backendKey) {
|
|
123
|
+
const config = normalizeResolvedConfig(resolvedConfig);
|
|
124
|
+
const runtime = plainObject(config.runtime);
|
|
125
|
+
const capabilities = plainObject(runtime.capabilities);
|
|
126
|
+
const backends = plainObject(capabilities.backends || runtime.backends || config.backends);
|
|
127
|
+
return plainObject(backends[backendKey]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildFastlyRouteEffectProviderConfig(options = {}) {
|
|
131
|
+
const plan = normalizePlan(options.plan || options.routeHandlerEffectPlan || options.effectPlan);
|
|
132
|
+
const resolvedConfig = normalizeResolvedConfig(options.resolvedConfig || {});
|
|
133
|
+
const runtime = plainObject(resolvedConfig.runtime);
|
|
134
|
+
const platformFastly = plainObject(plainObject(runtime.platform).fastly);
|
|
135
|
+
const backendKeys = new Set();
|
|
136
|
+
for (const route of plan && plan.routes || []) {
|
|
137
|
+
for (const resolve of route.resolves || []) {
|
|
138
|
+
for (const effect of resolve.effects || []) if (effect && effect.kind === 'backend-fetch' && effect.backend) backendKeys.add(effect.backend);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const backends = Array.from(backendKeys).sort().map((backend) => {
|
|
142
|
+
const declared = backendConfigFromResolvedConfig(resolvedConfig, backend);
|
|
143
|
+
return {
|
|
144
|
+
backend,
|
|
145
|
+
logicalName: backend,
|
|
146
|
+
bindingName: `pulse_backend_${String(backend).replace(/[^A-Za-z0-9_]/g, '_').toLowerCase()}`,
|
|
147
|
+
mode: 'fastly-backend-fetch-fixture',
|
|
148
|
+
baseUrlRef: symbolicRefName(declared.baseUrl, `$config:${String(backend).replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_BASE_URL`),
|
|
149
|
+
serviceRef: symbolicRefName(declared.baseUrl, `$config:${String(backend).replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_BASE_URL`),
|
|
150
|
+
authTokenRef: symbolicRefName(declared.authToken || declared.token, `$secret:${String(backend).replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_TOKEN`),
|
|
151
|
+
allowedMethods: Array.isArray(declared.allowedMethods) ? declared.allowedMethods.map((method) => String(method).toUpperCase()) : [...routeEffects.ROUTE_HANDLER_EFFECT_ALLOWED_METHODS],
|
|
152
|
+
timeoutMs: Number(declared.timeoutMs || 1500),
|
|
153
|
+
headersSymbolicOnly: true,
|
|
154
|
+
actualNetworkFetch: false
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
version: routeEffects.ROUTE_HANDLER_FASTLY_EFFECT_PROVIDER_CONFIG_VERSION,
|
|
159
|
+
provider: 'fastly',
|
|
160
|
+
mode: options.mode || 'fastly-backend-fetch-fixture',
|
|
161
|
+
configStore: platformFastly.configStore || 'pulse_config',
|
|
162
|
+
secretStore: platformFastly.secretStore || 'pulse_secrets',
|
|
163
|
+
symbolicRefsOnly: true,
|
|
164
|
+
backends,
|
|
165
|
+
summary: {
|
|
166
|
+
backends: backends.length,
|
|
167
|
+
configStore: platformFastly.configStore || 'pulse_config',
|
|
168
|
+
secretStore: platformFastly.secretStore || 'pulse_secrets',
|
|
169
|
+
symbolicRefs: backends.reduce((count, backend) => count + [backend.baseUrlRef, backend.authTokenRef].filter(Boolean).length, 0),
|
|
170
|
+
actualNetworkFetch: false
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeResponse(value, request) {
|
|
176
|
+
const method = String(request && request.method || 'GET').toUpperCase();
|
|
177
|
+
if (value === undefined || value === null) return undefined;
|
|
178
|
+
if (typeof value === 'string' || Buffer.isBuffer(value)) {
|
|
179
|
+
return { status: 200, kind: method === 'HEAD' ? 'empty' : 'text', headers: [['Content-Type', 'text/plain; charset=utf-8']], body: method === 'HEAD' ? '' : String(value) };
|
|
180
|
+
}
|
|
181
|
+
if (typeof value === 'object') {
|
|
182
|
+
const body = value.body !== undefined ? value.body : value.text !== undefined ? value.text : value.content;
|
|
183
|
+
return {
|
|
184
|
+
status: Number(value.status || 200),
|
|
185
|
+
kind: method === 'HEAD' ? 'empty' : (value.kind || 'text'),
|
|
186
|
+
headers: normalizeHeaders(value.headers),
|
|
187
|
+
body: method === 'HEAD' ? '' : (Buffer.isBuffer(body) ? body.toString('utf8') : body === undefined ? '' : String(body))
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return { status: 200, kind: method === 'HEAD' ? 'empty' : 'text', headers: [['Content-Type', 'text/plain; charset=utf-8']], body: method === 'HEAD' ? '' : String(value) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createDefaultBackendsFromPlan(planInput, defaultParams = {}) {
|
|
194
|
+
const plan = normalizePlan(planInput) || { routes: [] };
|
|
195
|
+
const backends = {};
|
|
196
|
+
for (const route of plan.routes || []) {
|
|
197
|
+
const params = { ...defaultParamsForRoute(route), ...defaultParams };
|
|
198
|
+
for (const resolve of route.resolves || []) {
|
|
199
|
+
for (const effect of resolve.effects || []) {
|
|
200
|
+
if (!effect || effect.kind !== 'backend-fetch') continue;
|
|
201
|
+
const request = requestFromEffect(effect, { route, params });
|
|
202
|
+
if (!backends[effect.backend]) backends[effect.backend] = {};
|
|
203
|
+
const response = {
|
|
204
|
+
status: 200,
|
|
205
|
+
kind: request.method === 'HEAD' ? 'empty' : 'text',
|
|
206
|
+
headers: [
|
|
207
|
+
['Content-Type', 'text/plain; charset=utf-8'],
|
|
208
|
+
['X-PulseWasm-Fastly-Backend', effect.backend],
|
|
209
|
+
['X-PulseWasm-Fastly-Effect', effect.name || '$default'],
|
|
210
|
+
['X-Count', effect.name === 'posts' || request.method === 'HEAD' ? '2' : '1']
|
|
211
|
+
],
|
|
212
|
+
body: request.method === 'HEAD' ? '' : `fastly-route-effect:${resolve.groupShape === 'single' ? '$default' : (effect.name || '$default')}:${effect.backend}:${request.method}:${request.path}`
|
|
213
|
+
};
|
|
214
|
+
backends[effect.backend][request.path] = response;
|
|
215
|
+
backends[effect.backend][`${request.method} ${request.path}`] = response;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return backends;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function responseHandle(response, metadata = {}) {
|
|
223
|
+
const normalized = response || { status: 500, kind: 'text', headers: [], body: '', diagnostics: [] };
|
|
224
|
+
function bodyText() { return normalized.body === undefined || normalized.body === null ? '' : String(normalized.body); }
|
|
225
|
+
return {
|
|
226
|
+
status() { return Number(normalized.status || 0); },
|
|
227
|
+
ok() { return this.status() >= 200 && this.status() < 300; },
|
|
228
|
+
header(name) { return headerValue(normalized.headers, name) || ''; },
|
|
229
|
+
text() { return bodyText(); },
|
|
230
|
+
jsonText() { return bodyText(); },
|
|
231
|
+
json(schema) {
|
|
232
|
+
const text = bodyText();
|
|
233
|
+
let parsed;
|
|
234
|
+
let parseError;
|
|
235
|
+
try { parsed = text ? JSON.parse(text) : null; }
|
|
236
|
+
catch (error) { parseError = error; }
|
|
237
|
+
return {
|
|
238
|
+
ok() { return !parseError; },
|
|
239
|
+
errorText() { return parseError ? JSON.stringify({ error: 'schema_decode_failed', schema: String(schema || ''), message: parseError.message }) : ''; },
|
|
240
|
+
jsonText() { return text; },
|
|
241
|
+
getString(path) { const value = parsed && parsed[String(path)]; return value === undefined || value === null ? '' : String(value); },
|
|
242
|
+
getI32(path) { return Number.parseInt(this.getString(path), 10) || 0; },
|
|
243
|
+
getU32(path) { const value = Number.parseInt(this.getString(path), 10) || 0; return value < 0 ? 0 : value; },
|
|
244
|
+
getF64(path) { return Number.parseFloat(this.getString(path)) || 0; },
|
|
245
|
+
getBool(path) { return Boolean(parsed && parsed[String(path)]); },
|
|
246
|
+
has(path) { return Boolean(parsed && Object.prototype.hasOwnProperty.call(parsed, String(path))); },
|
|
247
|
+
schema: String(schema || ''),
|
|
248
|
+
response: this
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
get diagnostics() { return clone(normalized.diagnostics || []); },
|
|
252
|
+
toJSON() {
|
|
253
|
+
return {
|
|
254
|
+
status: this.status(),
|
|
255
|
+
ok: this.ok(),
|
|
256
|
+
kind: normalized.kind || 'text',
|
|
257
|
+
request: metadata.request ? clone(metadata.request) : undefined,
|
|
258
|
+
backend: metadata.backend,
|
|
259
|
+
bindingName: metadata.bindingName,
|
|
260
|
+
baseUrlRef: metadata.baseUrlRef,
|
|
261
|
+
serviceRef: metadata.serviceRef,
|
|
262
|
+
authTokenRef: metadata.authTokenRef,
|
|
263
|
+
headers: normalizeHeaders(normalized.headers),
|
|
264
|
+
body: normalized.body === undefined ? '' : String(normalized.body),
|
|
265
|
+
text: this.text(),
|
|
266
|
+
diagnostics: clone(normalized.diagnostics || [])
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createResultFactory(responseHeaders) {
|
|
273
|
+
function normalizeOptions(options) {
|
|
274
|
+
const headers = [];
|
|
275
|
+
if (options && options.headers) {
|
|
276
|
+
for (const [name, value] of Object.entries(options.headers)) headers.push([String(name), String(value)]);
|
|
277
|
+
}
|
|
278
|
+
return headers;
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
text(status, body, options) { return { status: Number(status), kind: 'text', headers: [...responseHeaders, ...normalizeOptions(options)], body: String(body ?? '') }; },
|
|
282
|
+
jsonText(status, body, options) { return { status: Number(status), kind: 'json-text', headers: [...responseHeaders, ...normalizeOptions(options)], body: String(body ?? '') }; },
|
|
283
|
+
json(schema, value, options) {
|
|
284
|
+
const body = value && typeof value.jsonText === 'function' ? value.jsonText() : (typeof value === 'string' ? value : JSON.stringify(value ?? null));
|
|
285
|
+
return { status: Number(options && options.status || 200), kind: 'json-text', schema: String(schema || ''), headers: [...responseHeaders, ...normalizeOptions(options)], body };
|
|
286
|
+
},
|
|
287
|
+
empty(status, options) { return { status: Number(status), kind: 'empty', headers: [...responseHeaders, ...normalizeOptions(options)], body: '' }; }
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function createFastlyRouteEffectContinuationContext(resolved) {
|
|
292
|
+
const responseHeaders = [];
|
|
293
|
+
return {
|
|
294
|
+
resolved(name) {
|
|
295
|
+
const key = name === undefined ? '$default' : String(name);
|
|
296
|
+
const entry = resolved[key];
|
|
297
|
+
if (!entry) {
|
|
298
|
+
const diagnostic = makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectResolvedNameMissing, `Fastly route-effect continuation tried to read missing resolved effect ${JSON.stringify(key)}.`, 'error', { name: key });
|
|
299
|
+
return responseHandle({ status: 500, kind: 'text', headers: [], body: '', diagnostics: [diagnostic] }, { name: key });
|
|
300
|
+
}
|
|
301
|
+
return responseHandle(entry.response, { backend: entry.effect.backend, request: entry.request, bindingName: entry.bindingName, baseUrlRef: entry.baseUrlRef, serviceRef: entry.serviceRef, authTokenRef: entry.authTokenRef });
|
|
302
|
+
},
|
|
303
|
+
result: createResultFactory(responseHeaders),
|
|
304
|
+
response: {
|
|
305
|
+
header: {
|
|
306
|
+
set(name, value) {
|
|
307
|
+
const lower = String(name).toLowerCase();
|
|
308
|
+
for (let index = responseHeaders.length - 1; index >= 0; index -= 1) if (String(responseHeaders[index][0]).toLowerCase() === lower) responseHeaders.splice(index, 1);
|
|
309
|
+
responseHeaders.push([String(name), String(value)]);
|
|
310
|
+
},
|
|
311
|
+
append(name, value) { responseHeaders.push([String(name), String(value)]); },
|
|
312
|
+
delete(name) {
|
|
313
|
+
const lower = String(name).toLowerCase();
|
|
314
|
+
for (let index = responseHeaders.length - 1; index >= 0; index -= 1) if (String(responseHeaders[index][0]).toLowerCase() === lower) responseHeaders.splice(index, 1);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function routeMatches(route, selector) {
|
|
322
|
+
if (!route) return false;
|
|
323
|
+
if (!selector || Object.keys(selector).length === 0) return true;
|
|
324
|
+
if (selector.routeId && route.routeId === selector.routeId) return true;
|
|
325
|
+
if (selector.handlerName && route.handlerName === selector.handlerName) return true;
|
|
326
|
+
if (selector.path && route.path === selector.path) return true;
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function createErrorResponse(status, diagnostic) {
|
|
331
|
+
return responseHandle({ status, kind: 'text', headers: [['Content-Type', 'text/plain; charset=utf-8']], body: '', diagnostics: [diagnostic] });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function createFastlyRouteHandlerEffectProvider(options = {}) {
|
|
335
|
+
const plan = normalizePlan(options.plan || options.routeHandlerEffectPlan || options.effectPlan);
|
|
336
|
+
const diagnostics = [];
|
|
337
|
+
if (!plan) {
|
|
338
|
+
diagnostics.push(makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectPlanRequired, 'Fastly route-effect provider requires route-handler-effect-plan.json.', 'error', { hint: 'Emit route-handler-effect-plan.json before invoking the Fastly provider proof.' }));
|
|
339
|
+
}
|
|
340
|
+
const defaultParams = options.defaultParams || {};
|
|
341
|
+
const backends = options.backends || createDefaultBackendsFromPlan(plan, defaultParams);
|
|
342
|
+
const continuations = options.continuations || {};
|
|
343
|
+
const providerConfig = options.providerConfig || buildFastlyRouteEffectProviderConfig({ plan, resolvedConfig: options.resolvedConfig, mode: options.mode });
|
|
344
|
+
|
|
345
|
+
function executeFetch(effect, context = {}) {
|
|
346
|
+
const route = context.route;
|
|
347
|
+
const params = { ...defaultParams, ...(context.params || {}) };
|
|
348
|
+
const request = requestFromEffect(effect, { route, params });
|
|
349
|
+
if (!routeEffects.ROUTE_HANDLER_EFFECT_ALLOWED_METHODS.includes(request.method)) {
|
|
350
|
+
return createErrorResponse(405, makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectUnsupportedMethod, `Fastly route-effect provider proof only supports ${routeEffects.ROUTE_HANDLER_EFFECT_ALLOWED_METHODS.join(' / ')} backend fetch methods.`, 'error', { method: request.method, allowedMethods: routeEffects.ROUTE_HANDLER_EFFECT_ALLOWED_METHODS }));
|
|
351
|
+
}
|
|
352
|
+
const backend = backends[effect.backend];
|
|
353
|
+
if (!backend) {
|
|
354
|
+
return createErrorResponse(502, makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectBackendMissing, `Fastly route-effect provider proof has no fixture backend named ${JSON.stringify(effect.backend)}.`, 'error', { backend: effect.backend }));
|
|
355
|
+
}
|
|
356
|
+
const value = backend[`${request.method} ${request.path}`] || backend[request.path];
|
|
357
|
+
const response = normalizeResponse(value, request);
|
|
358
|
+
const backendConfig = (providerConfig.backends || []).find((entry) => entry.backend === effect.backend || entry.logicalName === effect.backend) || {};
|
|
359
|
+
if (!response) {
|
|
360
|
+
return createErrorResponse(404, makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectBackendResponseMissing, `Fastly route-effect provider proof has no response fixture for ${effect.backend} ${request.path}.`, 'error', { backend: effect.backend, method: request.method, path: request.path }));
|
|
361
|
+
}
|
|
362
|
+
return responseHandle(response, { backend: effect.backend, request, bindingName: backendConfig.bindingName, baseUrlRef: backendConfig.baseUrlRef, serviceRef: backendConfig.serviceRef, authTokenRef: backendConfig.authTokenRef });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function executeResolve(routeOrSelector, resolveOrOptions, paramsInput) {
|
|
366
|
+
let route = routeOrSelector;
|
|
367
|
+
let resolve = resolveOrOptions;
|
|
368
|
+
let params = paramsInput || {};
|
|
369
|
+
if (!route || !Array.isArray(route.resolves)) {
|
|
370
|
+
const selector = routeOrSelector || {};
|
|
371
|
+
route = (plan && plan.routes || []).find((candidate) => routeMatches(candidate, selector));
|
|
372
|
+
resolve = route && (route.resolves || [])[selector.resolveIndex || 0];
|
|
373
|
+
params = selector.params || params;
|
|
374
|
+
}
|
|
375
|
+
if (!plan) return { status: 'error', diagnostics: diagnostics.map(clone), resolved: {}, finalResult: undefined };
|
|
376
|
+
if (!route) return { status: 'error', diagnostics: [makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectRouteNotFound, 'Fastly route-effect provider proof could not find a matching planned route.', 'error', { selector: routeOrSelector })], resolved: {}, finalResult: undefined };
|
|
377
|
+
if (!resolve) return { status: 'error', route: { routeId: route.routeId, method: route.method, path: route.path, handlerName: route.handlerName }, diagnostics: [makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectNoPlannedEffects, 'Fastly route-effect provider proof requires at least one ctx.resolve boundary for the selected route.', 'error', { routeId: route.routeId, path: route.path })], resolved: {}, finalResult: undefined };
|
|
378
|
+
|
|
379
|
+
const mergedParams = { ...defaultParamsForRoute(route), ...params };
|
|
380
|
+
const resolved = {};
|
|
381
|
+
const diagnosticsForRun = [];
|
|
382
|
+
for (const effect of resolve.effects || []) {
|
|
383
|
+
if (!effect || effect.kind !== 'backend-fetch') continue;
|
|
384
|
+
const request = requestFromEffect(effect, { route, params: mergedParams });
|
|
385
|
+
const handle = executeFetch(effect, { route, params: mergedParams });
|
|
386
|
+
const snapshot = handle.toJSON();
|
|
387
|
+
const name = effect.name || '$default';
|
|
388
|
+
resolved[name] = {
|
|
389
|
+
effect: clone(effect),
|
|
390
|
+
backend: effect.backend,
|
|
391
|
+
request,
|
|
392
|
+
status: snapshot.status,
|
|
393
|
+
ok: snapshot.ok,
|
|
394
|
+
kind: snapshot.kind,
|
|
395
|
+
headers: snapshot.headers,
|
|
396
|
+
body: snapshot.body,
|
|
397
|
+
diagnostics: snapshot.diagnostics,
|
|
398
|
+
bindingName: snapshot.bindingName,
|
|
399
|
+
baseUrlRef: snapshot.baseUrlRef,
|
|
400
|
+
serviceRef: snapshot.serviceRef,
|
|
401
|
+
authTokenRef: snapshot.authTokenRef,
|
|
402
|
+
response: { status: snapshot.status, kind: snapshot.kind, headers: snapshot.headers, body: snapshot.body, diagnostics: snapshot.diagnostics }
|
|
403
|
+
};
|
|
404
|
+
diagnosticsForRun.push(...(snapshot.diagnostics || []));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const continuationName = resolve.continuation && resolve.continuation.name;
|
|
408
|
+
const plannedContinuation = (plan.continuations || []).find((entry) => entry && entry.name === continuationName);
|
|
409
|
+
const continuation = continuationName ? continuations[continuationName] : undefined;
|
|
410
|
+
let finalResult;
|
|
411
|
+
if (!plannedContinuation) {
|
|
412
|
+
const diagnostic = makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectContinuationMissing, 'Fastly route-effect provider proof could not find the planned top-level continuation.', 'error', { continuation: continuationName });
|
|
413
|
+
diagnosticsForRun.push(diagnostic);
|
|
414
|
+
finalResult = { status: 500, kind: 'text', body: '', headers: [], diagnostics: [diagnostic] };
|
|
415
|
+
} else if (continuation) {
|
|
416
|
+
try {
|
|
417
|
+
finalResult = continuation(createFastlyRouteEffectContinuationContext(resolved));
|
|
418
|
+
} catch (error) {
|
|
419
|
+
const diagnostic = makeDiagnostic(routeEffects.ROUTE_HANDLER_EFFECT_DIAGNOSTICS.fastlyEffectSmokeFailed, `Fastly route-effect continuation threw during proof execution: ${error && error.message ? error.message : String(error)}.`, 'error', { continuation: continuationName });
|
|
420
|
+
diagnosticsForRun.push(diagnostic);
|
|
421
|
+
finalResult = { status: 500, kind: 'text', body: '', headers: [], diagnostics: [diagnostic] };
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
const accessNames = (plannedContinuation.resultAccesses || []).map((access) => access.name || '$default');
|
|
425
|
+
const names = accessNames.length > 0 ? accessNames : Object.keys(resolved);
|
|
426
|
+
let status = 200;
|
|
427
|
+
let body = '';
|
|
428
|
+
for (const name of names) {
|
|
429
|
+
const entry = resolved[name];
|
|
430
|
+
if (!entry) continue;
|
|
431
|
+
if (!entry.ok && status === 200) status = entry.status;
|
|
432
|
+
body += entry.body || '';
|
|
433
|
+
}
|
|
434
|
+
finalResult = { status, kind: 'text', body, headers: [['Content-Type', 'text/plain; charset=utf-8']], diagnostics: [] };
|
|
435
|
+
}
|
|
436
|
+
const finalDiagnostics = [...diagnosticsForRun, ...((finalResult && finalResult.diagnostics) || [])];
|
|
437
|
+
const hasErrors = finalDiagnostics.some((entry) => (entry.severity || 'error') === 'error');
|
|
438
|
+
return {
|
|
439
|
+
status: hasErrors ? 'error' : 'resolved',
|
|
440
|
+
provider: 'fastly',
|
|
441
|
+
runtimeVersion: routeEffects.ROUTE_HANDLER_FASTLY_EFFECT_RUNTIME_VERSION,
|
|
442
|
+
routeId: route && route.routeId,
|
|
443
|
+
route: route ? { routeId: route.routeId, method: route.method, path: route.path, handlerName: route.handlerName } : undefined,
|
|
444
|
+
groupShape: resolve.groupShape,
|
|
445
|
+
continuation: continuationName,
|
|
446
|
+
continuationPlanned: Boolean(plannedContinuation),
|
|
447
|
+
effectCount: (resolve.effects || []).length,
|
|
448
|
+
effects: Object.entries(resolved).map(([name, value]) => ({ name, backend: value.backend, bindingName: value.bindingName, baseUrlRef: value.baseUrlRef, serviceRef: value.serviceRef, authTokenRef: value.authTokenRef, request: value.request, response: { status: value.status, ok: value.ok, kind: value.kind, headers: value.headers, body: value.body, diagnostics: value.diagnostics || [] } })),
|
|
449
|
+
resolved,
|
|
450
|
+
finalResult: finalResult || { status: 204, kind: 'empty', headers: [], body: '' },
|
|
451
|
+
diagnostics: finalDiagnostics,
|
|
452
|
+
responseHandleSurface: [...routeEffects.ROUTE_HANDLER_CONTEXT_SURFACE.resolvedFetchResponse],
|
|
453
|
+
continuationResultAccesses: plannedContinuation ? clone(plannedContinuation.resultAccesses || []) : [],
|
|
454
|
+
continuationResponseMethodCalls: plannedContinuation ? clone(plannedContinuation.responseMethodCalls || []) : [],
|
|
455
|
+
providerConfig: clone(providerConfig),
|
|
456
|
+
lifecycle: {
|
|
457
|
+
handlerReturnedResolveRequest: true,
|
|
458
|
+
fastlyExecutedEffectGroup: !hasErrors,
|
|
459
|
+
hostExecutedEffectGroup: !hasErrors,
|
|
460
|
+
continuationReentered: !hasErrors && Boolean(plannedContinuation),
|
|
461
|
+
continuationReadResolvedHandles: !hasErrors && Boolean(plannedContinuation),
|
|
462
|
+
compiledWasmSuspendResumeBridgeUsed: false,
|
|
463
|
+
symbolicBackendRefs: true,
|
|
464
|
+
actualNetworkFetch: false
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function executeRoute(selector = {}) {
|
|
470
|
+
return executeResolve(selector);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const plannedResolveBoundaries = [];
|
|
474
|
+
for (const route of plan && plan.routes || []) {
|
|
475
|
+
for (const resolve of route.resolves || []) if (resolve && Array.isArray(resolve.effects) && resolve.effects.length > 0) plannedResolveBoundaries.push({ route, resolve });
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
version: routeEffects.ROUTE_HANDLER_FASTLY_EFFECT_RUNTIME_VERSION,
|
|
479
|
+
provider: 'fastly',
|
|
480
|
+
mode: options.mode || 'fastly-backend-fetch-fixture',
|
|
481
|
+
plan,
|
|
482
|
+
backends,
|
|
483
|
+
providerConfig,
|
|
484
|
+
diagnostics,
|
|
485
|
+
plannedResolveBoundaries,
|
|
486
|
+
executeFetch,
|
|
487
|
+
executeFetchEffect: executeFetch,
|
|
488
|
+
executeResolve,
|
|
489
|
+
executeResolveBoundary: executeResolve,
|
|
490
|
+
executeRoute,
|
|
491
|
+
createContinuationContext: createFastlyRouteEffectContinuationContext
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
module.exports = {
|
|
496
|
+
createFastlyRouteHandlerEffectProvider,
|
|
497
|
+
createFastlyRouteEffectProvider: createFastlyRouteHandlerEffectProvider,
|
|
498
|
+
createFastlyRouteEffectContinuationContext,
|
|
499
|
+
createDefaultBackendsFromPlan,
|
|
500
|
+
buildDefaultBackends: createDefaultBackendsFromPlan,
|
|
501
|
+
buildFastlyRouteEffectProviderConfig,
|
|
502
|
+
buildFastlyRouteHandlerEffectProviderConfig: buildFastlyRouteEffectProviderConfig,
|
|
503
|
+
evaluateExpression,
|
|
504
|
+
requestFromEffect,
|
|
505
|
+
normalizeHeaders,
|
|
506
|
+
headerValue
|
|
507
|
+
};
|