@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.10
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 +53 -1
- package/dist/config-schema.json +103 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -5
- package/src/build/canonical-target.js +469 -0
- package/src/build/digest-native.js +25 -0
- package/src/build/effect-invocations.js +49 -0
- package/src/build/embedded-assets.as.ts +32 -0
- package/src/build/entities-native.js +559 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/kv-native.as.ts +331 -0
- package/src/build/kv-native.js +27 -0
- package/src/build/native-application-errors.js +159 -0
- package/src/build/native-binary-resources.js +30 -0
- package/src/build/native-body-forwarding.as.ts +341 -0
- package/src/build/native-body-forwarding.js +69 -0
- package/src/build/native-dynamic-backends.as.ts +115 -0
- package/src/build/native-dynamic-backends.js +52 -0
- package/src/build/native-fetch-deadlines.as.ts +164 -0
- package/src/build/native-fetch-deadlines.js +27 -0
- package/src/build/native-fetch-errors.js +38 -0
- package/src/build/native-handle-ownership.js +127 -0
- package/src/build/native-header-lookup.js +50 -0
- package/src/build/native-http-effects.js +994 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2654 -0
- package/src/build/native-request-body.js +44 -0
- package/src/build/native-request-headers.js +93 -0
- package/src/build/native-send-error-detail.as.ts +12 -0
- package/src/build/native-stdio.js +45 -0
- package/src/build/native-string-values.js +143 -0
- package/src/build/native-transport-details.as.ts +84 -0
- package/src/build/native-value-budget.js +90 -0
- package/src/build/package-native-application.js +114 -0
- package/src/build/request-budget.as.ts +50 -0
- package/src/build/request-budget.js +47 -0
- package/src/build/s3-body.as.ts +97 -0
- package/src/build/s3-native.as.ts +228 -0
- package/src/build/s3-native.js +30 -0
- package/src/build/schema-admission.js +83 -0
- package/src/build/schema-nested-json.js +93 -0
- package/src/build/schema-scalar-record.js +41 -0
- package/src/build/time-native.js +49 -0
- package/src/build/wasm-budget.js +28 -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 +69 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +103 -0
- package/src/index.js +3 -0
- package/src/index.ts +143 -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 +147 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +35 -0
- package/src/javascript/package-effects.js +71 -0
- package/src/javascript/runtime-host.js +112 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +778 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +277 -0
- package/src/javascript/target.js +89 -0
- package/src/javascript/test-runtime.js +408 -0
- package/src/provider-contract.js +88 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +603 -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/conditional-kv-host.js +155 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-dynamic-backends-host.js +39 -0
- package/src/testing/native-http-effects-host.js +398 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +924 -0
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +144 -0
- package/src/toolchain/index.js +386 -0
- package/src/toolchain/logging.js +18 -0
- package/src/toolchain/native-execution.js +78 -0
- package/src/toolchain/s3.js +26 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const FASTLY_STATUS_OK = 0;
|
|
7
|
+
const FASTLY_STATUS_INVALID_ARGUMENT = 2;
|
|
8
|
+
const FASTLY_STATUS_ERROR = 1;
|
|
9
|
+
const FASTLY_STATUS_BADF = 3;
|
|
10
|
+
const FASTLY_STATUS_BUFLEN = 4;
|
|
11
|
+
const FASTLY_STATUS_NONE = 10;
|
|
12
|
+
const FASTLY_KV_ERROR_OK = 1;
|
|
13
|
+
const FASTLY_KV_ERROR_NOT_FOUND = 3;
|
|
14
|
+
const SENSITIVE_HEADERS = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'x-auth-token', 'cookie', 'set-cookie', 'x-amz-security-token', 'x-amz-content-sha256']);
|
|
15
|
+
|
|
16
|
+
class FastlyNativePlatformCapabilitiesMockError extends Error {
|
|
17
|
+
constructor(message, code = 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_FAILED', detail = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'FastlyNativePlatformCapabilitiesMockError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.detail = Object.freeze({ ...detail });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeHeaders(input) {
|
|
26
|
+
if (!input) return [];
|
|
27
|
+
if (Array.isArray(input)) return input.map(([name, value]) => [String(name), String(value)]);
|
|
28
|
+
return Object.entries(input).flatMap(([name, value]) => Array.isArray(value)
|
|
29
|
+
? value.map((entry) => [String(name), String(entry)])
|
|
30
|
+
: [[String(name), String(value)]]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeBody(input) {
|
|
34
|
+
if (Buffer.isBuffer(input)) return Buffer.from(input);
|
|
35
|
+
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
36
|
+
if (input === undefined || input === null) return Buffer.alloc(0);
|
|
37
|
+
if (typeof input === 'string') return Buffer.from(input);
|
|
38
|
+
return Buffer.from(JSON.stringify(input));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function redactHeaders(headers) {
|
|
42
|
+
return headers.map(([name, value]) => [name, SENSITIVE_HEADERS.has(String(name).toLowerCase()) ? '[REDACTED]' : value]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeNamedStores(input, fallbackName, fallbackValues) {
|
|
46
|
+
const stores = new Map();
|
|
47
|
+
if (input instanceof Map) {
|
|
48
|
+
for (const [name, values] of input) stores.set(String(name), new Map(Object.entries(values || {})));
|
|
49
|
+
} else if (input && typeof input === 'object') {
|
|
50
|
+
for (const [name, values] of Object.entries(input)) stores.set(String(name), new Map(Object.entries(values || {})));
|
|
51
|
+
}
|
|
52
|
+
if (fallbackValues && typeof fallbackValues === 'object') stores.set(String(fallbackName), new Map(Object.entries(fallbackValues)));
|
|
53
|
+
return stores;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function encodeKvValue(value) {
|
|
57
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
|
|
58
|
+
if (typeof value === 'string' && value.startsWith('{"__pulseKv":')) return Buffer.from(value);
|
|
59
|
+
return Buffer.from(JSON.stringify({ __pulseKv: 1, value }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function snapshotStores(stores, decode = false) {
|
|
63
|
+
const output = {};
|
|
64
|
+
for (const [name, values] of stores) {
|
|
65
|
+
output[name] = {};
|
|
66
|
+
for (const [key, value] of values) {
|
|
67
|
+
if (!decode) output[name][key] = value;
|
|
68
|
+
else {
|
|
69
|
+
const text = Buffer.from(value).toString('utf8');
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(text);
|
|
72
|
+
output[name][key] = parsed && parsed.__pulseKv === 1 ? parsed.value : parsed;
|
|
73
|
+
} catch (_) { output[name][key] = text; }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fixtureKey(backend, method, url) {
|
|
81
|
+
return `${backend} ${method.toUpperCase()} ${url}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveFixture(fixtures, backend, method, url) {
|
|
85
|
+
if (!fixtures) return undefined;
|
|
86
|
+
if (fixtures instanceof Map) {
|
|
87
|
+
return fixtures.get(fixtureKey(backend, method, url))
|
|
88
|
+
|| fixtures.get(`${method.toUpperCase()} ${url}`)
|
|
89
|
+
|| fixtures.get(url);
|
|
90
|
+
}
|
|
91
|
+
return fixtures[fixtureKey(backend, method, url)]
|
|
92
|
+
|| fixtures[`${method.toUpperCase()} ${url}`]
|
|
93
|
+
|| fixtures[url]
|
|
94
|
+
|| (fixtures[backend] && (
|
|
95
|
+
fixtures[backend][`${method.toUpperCase()} ${url}`]
|
|
96
|
+
|| fixtures[backend][url]
|
|
97
|
+
|| fixtures[backend][new URL(url).pathname]
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
102
|
+
const wasm = Buffer.isBuffer(input)
|
|
103
|
+
? input
|
|
104
|
+
: input && Buffer.isBuffer(input.wasm)
|
|
105
|
+
? input.wasm
|
|
106
|
+
: fs.readFileSync(path.resolve(input));
|
|
107
|
+
if (!WebAssembly.validate(wasm)) {
|
|
108
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Pass98 mock host requires valid WebAssembly.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_INVALID_WASM');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const requestInput = options.request || {};
|
|
112
|
+
const method = String(requestInput.method || 'GET').toUpperCase();
|
|
113
|
+
const pathValue = String(requestInput.path || '/');
|
|
114
|
+
const url = String(requestInput.url || `https://pulse.test${pathValue.startsWith('/') ? pathValue : `/${pathValue}`}`);
|
|
115
|
+
const requestHeaders = normalizeHeaders(requestInput.headers);
|
|
116
|
+
const trace = [];
|
|
117
|
+
let monotonicMs = 0;
|
|
118
|
+
const privateUrls = new Set();
|
|
119
|
+
let nextHandle = 10;
|
|
120
|
+
const typedHandleStarts = options.handleStarts && typeof options.handleStarts === 'object' ? options.handleStarts : {};
|
|
121
|
+
const typedHandleNext = new Map();
|
|
122
|
+
let instance;
|
|
123
|
+
|
|
124
|
+
const requests = new Map();
|
|
125
|
+
const bodies = new Map();
|
|
126
|
+
const responses = new Map();
|
|
127
|
+
const pending = new Map();
|
|
128
|
+
const outboundRequests = [];
|
|
129
|
+
const configStores = normalizeNamedStores(options.configStores, options.configStore || 'app_config', options.config);
|
|
130
|
+
const secretStores = normalizeNamedStores(options.secretStores, options.secretStore || 'app_secrets', options.secrets);
|
|
131
|
+
const secretValues = [...secretStores.values()].flatMap((values) => [...values.values()].map((value) => String(value))).filter(Boolean);
|
|
132
|
+
const redactText = (value) => secretValues.reduce((output, secret) => output.split(secret).join('[REDACTED]'), String(value));
|
|
133
|
+
const redactRuntimeHeaders = (headers, privateS3 = false) => headers.map(([name, value]) => [name, SENSITIVE_HEADERS.has(String(name).toLowerCase()) || privateS3 && ['etag', 'content-type'].includes(String(name).toLowerCase()) ? '[REDACTED]' : redactText(value)]);
|
|
134
|
+
const kvStores = normalizeNamedStores(options.kvStores, options.kvStore || 'app_sessions', undefined);
|
|
135
|
+
for (const values of kvStores.values()) for (const [key, value] of values) values.set(key, encodeKvValue(value));
|
|
136
|
+
const configHandles = new Map();
|
|
137
|
+
const secretStoreHandles = new Map();
|
|
138
|
+
const secretHandles = new Map();
|
|
139
|
+
const kvStoreHandles = new Map();
|
|
140
|
+
const pendingKvLookups = new Map();
|
|
141
|
+
const pendingKvInserts = new Map();
|
|
142
|
+
const logHandles = new Map();
|
|
143
|
+
const logs = [];
|
|
144
|
+
const downstreamRequestHandle = 1;
|
|
145
|
+
const downstreamBodyHandle = 2;
|
|
146
|
+
requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
|
|
147
|
+
bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: [],
|
|
148
|
+
readyAt: monotonicMs + Number(requestInput.readyDelayMs || 0), fixture: requestInput });
|
|
149
|
+
let downstream;
|
|
150
|
+
|
|
151
|
+
function alloc(kind = 'default') {
|
|
152
|
+
if (Object.prototype.hasOwnProperty.call(typedHandleStarts, kind)) {
|
|
153
|
+
const current = typedHandleNext.has(kind) ? typedHandleNext.get(kind) : Number(typedHandleStarts[kind]);
|
|
154
|
+
if (!Number.isInteger(current) || current < 0) {
|
|
155
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Mock host handle starts must be non-negative integers.', 'PULSE_FASTLY_NATIVE_MOCK_HANDLE_START_INVALID', { kind, value: typedHandleStarts[kind] });
|
|
156
|
+
}
|
|
157
|
+
typedHandleNext.set(kind, current + 1);
|
|
158
|
+
return current;
|
|
159
|
+
}
|
|
160
|
+
return nextHandle++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function memory() {
|
|
164
|
+
const value = instance && instance.exports && instance.exports.memory;
|
|
165
|
+
if (!(value instanceof WebAssembly.Memory)) {
|
|
166
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module did not export memory.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_MEMORY_MISSING');
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function view() { return new DataView(memory().buffer); }
|
|
172
|
+
function writeU32(pointer, value) { view().setUint32(Number(pointer), Number(value) >>> 0, true); }
|
|
173
|
+
function writeU64(pointer, value) { view().setBigUint64(Number(pointer), BigInt(value), true); }
|
|
174
|
+
function readBytes(pointer, length) { return Buffer.from(new Uint8Array(memory().buffer, Number(pointer), Number(length))); }
|
|
175
|
+
function readUtf8(pointer, length) { return readBytes(pointer, length).toString('utf8'); }
|
|
176
|
+
|
|
177
|
+
const dynamicBackends = require('./native-dynamic-backends-host.js').createDynamicBackendHost({ view, readUtf8, trace, options, beforeRegister: () => { monotonicMs += Number(options.dynamicBackendRegistrationMs || 0); } });
|
|
178
|
+
|
|
179
|
+
function writeUtf8(value, pointer, maxLength, writtenOut) {
|
|
180
|
+
const bytes = Buffer.from(String(value), 'utf8');
|
|
181
|
+
writeU32(writtenOut, bytes.length);
|
|
182
|
+
if (bytes.length > Number(maxLength)) return FASTLY_STATUS_BUFLEN;
|
|
183
|
+
new Uint8Array(memory().buffer, Number(pointer), bytes.length).set(bytes);
|
|
184
|
+
return FASTLY_STATUS_OK;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function headerValue(headers, name) {
|
|
188
|
+
const target = String(name).toLowerCase();
|
|
189
|
+
const found = headers.find(([headerName]) => String(headerName).toLowerCase() === target);
|
|
190
|
+
return found && found[1];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function bodyBytes(handle) {
|
|
194
|
+
const body = bodies.get(Number(handle));
|
|
195
|
+
if (!body) return undefined;
|
|
196
|
+
if (body.writes.length > 0) return Buffer.concat(body.writes);
|
|
197
|
+
return body.bytes;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function makeOriginResponse(fixture) {
|
|
201
|
+
const responseHandle = alloc('response');
|
|
202
|
+
const bodyHandle = alloc('body');
|
|
203
|
+
const normalized = fixture || {};
|
|
204
|
+
const headers = normalizeHeaders(normalized.headers);
|
|
205
|
+
let body = normalized.body;
|
|
206
|
+
if (Object.prototype.hasOwnProperty.call(normalized, 'json')) {
|
|
207
|
+
body = JSON.stringify(normalized.json);
|
|
208
|
+
if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
209
|
+
}
|
|
210
|
+
// Ordinary Pulse harness fixtures use value/text; preserve the ABI fixture
|
|
211
|
+
// body's explicit precedence and its existing json shorthand.
|
|
212
|
+
if (body === undefined && Object.prototype.hasOwnProperty.call(normalized, 'value')) {
|
|
213
|
+
body = JSON.stringify(normalized.value);
|
|
214
|
+
if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
215
|
+
} else if (body === undefined && Object.prototype.hasOwnProperty.call(normalized, 'text')) {
|
|
216
|
+
body = String(normalized.text);
|
|
217
|
+
if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'text/plain; charset=utf-8']);
|
|
218
|
+
}
|
|
219
|
+
responses.set(responseHandle, {
|
|
220
|
+
status: Number(normalized.status || 200),
|
|
221
|
+
headers,
|
|
222
|
+
origin: true
|
|
223
|
+
});
|
|
224
|
+
bodies.set(bodyHandle, { bytes: normalizeBody(body), readOffset: 0, writes: [], fixture: normalized, readyAt: monotonicMs + Number(normalized.bodyDelayMs || 0) });
|
|
225
|
+
return { responseHandle, bodyHandle };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function headerList(values, buffer, size, end, written) {
|
|
229
|
+
const output = Buffer.from(values.map((value) => String(value) + '\0').join(''));
|
|
230
|
+
writeU32(written, output.length); view().setBigInt64(Number(end), -1n, true);
|
|
231
|
+
if (output.length > Number(size)) return FASTLY_STATUS_BUFLEN;
|
|
232
|
+
new Uint8Array(memory().buffer, Number(buffer), output.length).set(output);
|
|
233
|
+
return FASTLY_STATUS_OK;
|
|
234
|
+
}
|
|
235
|
+
function requestHeaderList(values, buffer, size, cursor, end, written, kind, lookupKind) {
|
|
236
|
+
const limit = Math.min(Number(size), options.requestHeaderPageBytes ?? Number(size));
|
|
237
|
+
let index = Number(cursor), count = 0;
|
|
238
|
+
if (index < 0 || index > values.length) return FASTLY_STATUS_ERROR;
|
|
239
|
+
while (index < values.length) {
|
|
240
|
+
const bytes = Buffer.from(String(values[index]) + '\0');
|
|
241
|
+
if (count + bytes.length > limit) {
|
|
242
|
+
if (count === 0) { writeU32(written, bytes.length); return FASTLY_STATUS_BUFLEN; }
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
new Uint8Array(memory().buffer, Number(buffer) + count, bytes.length).set(bytes);
|
|
246
|
+
count += bytes.length; index++;
|
|
247
|
+
}
|
|
248
|
+
writeU32(written, count);
|
|
249
|
+
view().setBigInt64(Number(end), index === values.length ? -1n : BigInt(index), true);
|
|
250
|
+
// Fault injection is host-fixture-only; neither values nor faults enter traces.
|
|
251
|
+
const fault = (lookupKind && options.headerLookupFault?.({kind:lookupKind})) || options.requestHeaderFault?.({kind, cursor: Number(cursor)});
|
|
252
|
+
if (fault?.written !== undefined) writeU32(written, fault.written);
|
|
253
|
+
if (fault?.cursor !== undefined) view().setBigInt64(Number(end), BigInt(fault.cursor), true);
|
|
254
|
+
if (fault?.unterminated && count) new Uint8Array(memory().buffer)[Number(buffer) + count - 1] = 65;
|
|
255
|
+
return fault?.status ?? FASTLY_STATUS_OK;
|
|
256
|
+
}
|
|
257
|
+
function writeSendErrorDetail(item, handle, detail, outputs, name) {
|
|
258
|
+
// Independent wasm32 ABI oracle: SendErrorDetail is 20 bytes, aligned to
|
|
259
|
+
// four, even when no optional field is valid. Do not import guest constants.
|
|
260
|
+
const strict = options.sendErrorDetailLayout;
|
|
261
|
+
if (strict) {
|
|
262
|
+
const fail = message => { throw new FastlyNativePlatformCapabilitiesMockError(message, 'PULSE_FASTLY_SEND_ERROR_DETAIL_LAYOUT'); };
|
|
263
|
+
if (detail % 4) fail('SendErrorDetail must be four-byte aligned');
|
|
264
|
+
// AssemblyScript managed StaticArray rtSize is the word at data - 4.
|
|
265
|
+
if (strict === 'managed' && view().getUint32(detail - 4, true) < 20) fail('SendErrorDetail allocation is smaller than 20 bytes');
|
|
266
|
+
for (const output of outputs) if (output < detail + 20 && output + 4 > detail) fail('SendErrorDetail overlaps an output');
|
|
267
|
+
if (view().getUint32(detail, true) !== 0 || view().getUint32(detail + 4, true) !== 30) fail('SendErrorDetail tag/mask were not reset before host call');
|
|
268
|
+
for (let offset = 8; offset < 20; offset++) if (view().getUint8(detail + offset) !== 0) fail('SendErrorDetail contains stale optional fields');
|
|
269
|
+
}
|
|
270
|
+
const guards = [...new Set([detail - 4, detail + 20, ...outputs])];
|
|
271
|
+
const saved = guards.map(pointer => view().getUint32(pointer, true));
|
|
272
|
+
const canary = guards.map((_, i) => 0x13579bd0 + i);
|
|
273
|
+
if (strict) guards.forEach((pointer, i) => writeU32(pointer, canary[i]));
|
|
274
|
+
const wire = item.fixture.sendErrorDetail === null ? {tag:0,mask:0} : item.fixture.sendErrorDetail || { tag: item.fixture.transportStatus ? 22 : 1, mask: 0 };
|
|
275
|
+
// Write the entire C-layout record, including padding and the final word.
|
|
276
|
+
new Uint8Array(memory().buffer, detail, 20).fill(0);
|
|
277
|
+
writeU32(detail, wire.tag); writeU32(detail + 4, wire.mask);
|
|
278
|
+
view().setUint16(detail + 8, wire.dnsRcode || 0, true);
|
|
279
|
+
view().setUint16(detail + 10, wire.dnsInfo || 0, true);
|
|
280
|
+
view().setUint8(detail + 12, wire.tlsAlert || 0);
|
|
281
|
+
view().setUint8(detail + 13, wire.h2Frame || 0);
|
|
282
|
+
writeU32(detail + 16, wire.h2Code || 0);
|
|
283
|
+
if (strict) guards.forEach((pointer, i) => {
|
|
284
|
+
if (view().getUint32(pointer, true) !== canary[i]) throw new FastlyNativePlatformCapabilitiesMockError('SendErrorDetail overwrote an adjacent/output sentinel', 'PULSE_FASTLY_SEND_ERROR_DETAIL_LAYOUT');
|
|
285
|
+
writeU32(pointer, saved[i]);
|
|
286
|
+
});
|
|
287
|
+
trace.push({module:'fastly_http_req',name,pendingHandle:Number(handle),
|
|
288
|
+
detailBytes:20, detailTag:wire.tag, detailMask:wire.mask, detailLayoutChecked:Boolean(strict)});
|
|
289
|
+
}
|
|
290
|
+
const imports = {
|
|
291
|
+
fastly_async_io: {
|
|
292
|
+
is_ready(handle, out) {
|
|
293
|
+
const item = pending.get(Number(handle)) || bodies.get(Number(handle));
|
|
294
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
295
|
+
const ready = monotonicMs >= (item.readyAt || 0) ? 1 : 0;
|
|
296
|
+
writeU32(out, ready);
|
|
297
|
+
trace.push({module:'fastly_async_io',name:'is_ready',handle:Number(handle),ready,now:monotonicMs});
|
|
298
|
+
return FASTLY_STATUS_OK;
|
|
299
|
+
},
|
|
300
|
+
select(handles, count, timeout, done) {
|
|
301
|
+
if (Number(count) < 1 || Number(timeout) <= 0) return FASTLY_STATUS_ERROR;
|
|
302
|
+
const selected = Array.from({length:Number(count)},(_,i) => view().getUint32(Number(handles)+4*i,true));
|
|
303
|
+
const items = selected.map(handle => pending.get(handle) || bodies.get(handle));
|
|
304
|
+
if (items.some(item => !item)) return FASTLY_STATUS_BADF;
|
|
305
|
+
const waits = items.map(item => Math.max(0, Number(item.readyAt || 0) - monotonicMs));
|
|
306
|
+
const wait = Math.min(...waits), timedOut = wait >= Number(timeout);
|
|
307
|
+
const fault = options.asyncSelectFault?.({handles:selected,timeout:Number(timeout),now:monotonicMs});
|
|
308
|
+
monotonicMs += fault?.advanceMs ?? Math.min(wait,Number(timeout));
|
|
309
|
+
writeU32(done, fault?.selected ?? (timedOut ? 0xffffffff : waits.indexOf(wait)));
|
|
310
|
+
trace.push({ module: 'fastly_async_io', name: 'select', handles:selected, timeout:Number(timeout), timedOut, now:monotonicMs });
|
|
311
|
+
if (fault?.status) return fault.status;
|
|
312
|
+
return FASTLY_STATUS_OK;
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
wasi_snapshot_preview1: {
|
|
316
|
+
fd_write(fd, iovs, count, writtenOut) {
|
|
317
|
+
// WASI errno values, independent of the Fastly status enum.
|
|
318
|
+
if (Number(fd) !== 1) return 8; // BADF
|
|
319
|
+
if (options.logWriteFailure === true) {
|
|
320
|
+
trace.push({ module: 'wasi_snapshot_preview1', name: 'fd_write', fd: 1, accepted: false });
|
|
321
|
+
return 29; // IO
|
|
322
|
+
}
|
|
323
|
+
const chunks = [];
|
|
324
|
+
for (let index = 0; index < Number(count); index++) {
|
|
325
|
+
const pointer = Number(iovs) + index * 8;
|
|
326
|
+
chunks.push(readBytes(view().getUint32(pointer, true), view().getUint32(pointer + 4, true)));
|
|
327
|
+
}
|
|
328
|
+
const bytes = Buffer.concat(chunks);
|
|
329
|
+
const written = Math.min(bytes.length, options.logWriteMaxBytes ?? bytes.length);
|
|
330
|
+
const emitted = bytes.subarray(0, written).toString('utf8');
|
|
331
|
+
if (typeof options.onLogWrite === 'function') options.onLogWrite(emitted);
|
|
332
|
+
const message = redactText(emitted);
|
|
333
|
+
writeU32(writtenOut, written);
|
|
334
|
+
logs.push(Object.freeze({ endpoint: 'stdout', message }));
|
|
335
|
+
trace.push({ module: 'wasi_snapshot_preview1', name: 'fd_write', fd: 1, count: Number(count), bytes: bytes.length, written, message, accepted: true });
|
|
336
|
+
return 0;
|
|
337
|
+
},
|
|
338
|
+
clock_time_get(clockId, precision, timeOut) {
|
|
339
|
+
if (Number(clockId) === 1 && typeof options.monotonicClock === 'function') {
|
|
340
|
+
const sample = options.monotonicClock(monotonicMs);
|
|
341
|
+
if (sample.status) return sample.status;
|
|
342
|
+
writeU64(timeOut, sample.nanoseconds);
|
|
343
|
+
return FASTLY_STATUS_OK;
|
|
344
|
+
}
|
|
345
|
+
if (Number(clockId) === 0 && typeof options.realtimeClock === 'function') {
|
|
346
|
+
const sample = options.realtimeClock();
|
|
347
|
+
trace.push({ module: 'wasi_snapshot_preview1', name: 'clock_time_get', clockId: 0, precision: String(precision), status: sample.status || 0 });
|
|
348
|
+
if (sample.status) return sample.status;
|
|
349
|
+
writeU64(timeOut, sample.nanoseconds);
|
|
350
|
+
return FASTLY_STATUS_OK;
|
|
351
|
+
}
|
|
352
|
+
const seconds = Number.isFinite(Number(options.clockUnixSeconds))
|
|
353
|
+
? Number(options.clockUnixSeconds)
|
|
354
|
+
: Math.floor(Date.now() / 1000);
|
|
355
|
+
writeU64(timeOut, BigInt(Math.trunc((Number(clockId) === 1 ? monotonicMs : seconds * 1000 + monotonicMs) * 1_000_000)));
|
|
356
|
+
trace.push({
|
|
357
|
+
module: 'wasi_snapshot_preview1',
|
|
358
|
+
name: 'clock_time_get',
|
|
359
|
+
clockId: Number(clockId),
|
|
360
|
+
precision: String(precision),
|
|
361
|
+
unixEpochSeconds: seconds
|
|
362
|
+
});
|
|
363
|
+
return FASTLY_STATUS_OK;
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
env: {
|
|
367
|
+
abort(message, fileName, line, column) {
|
|
368
|
+
throw new FastlyNativePlatformCapabilitiesMockError('AssemblyScript aborted in the Pass98 Fastly effects module.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_ABORT', { message, fileName, line, column, trace });
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
fastly_abi: {
|
|
372
|
+
init(version) {
|
|
373
|
+
trace.push({ module: 'fastly_abi', name: 'init', version: String(version) });
|
|
374
|
+
return version === 1n ? FASTLY_STATUS_OK : FASTLY_STATUS_ERROR;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
fastly_log: {
|
|
378
|
+
endpoint_get(namePointer, nameLength, handleOut) {
|
|
379
|
+
const endpoint = readUtf8(namePointer, nameLength);
|
|
380
|
+
// Match Viceroy/Compute: these names belong to stdio or the platform.
|
|
381
|
+
if (/^(?:stdout$|stderr$|fst_managed_)/i.test(endpoint)) {
|
|
382
|
+
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, opened: false });
|
|
383
|
+
return FASTLY_STATUS_INVALID_ARGUMENT;
|
|
384
|
+
}
|
|
385
|
+
if (options.logEndpointFailure === true || (options.logEndpoints && !options.logEndpoints.includes(endpoint))) {
|
|
386
|
+
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, opened: false });
|
|
387
|
+
return FASTLY_STATUS_ERROR;
|
|
388
|
+
}
|
|
389
|
+
const handle = alloc('log');
|
|
390
|
+
logHandles.set(handle, endpoint);
|
|
391
|
+
writeU32(handleOut, handle);
|
|
392
|
+
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, handle, opened: true });
|
|
393
|
+
return FASTLY_STATUS_OK;
|
|
394
|
+
},
|
|
395
|
+
write(handle, messagePointer, messageLength, writtenOut) {
|
|
396
|
+
const endpoint = logHandles.get(Number(handle));
|
|
397
|
+
if (!endpoint) return FASTLY_STATUS_BADF;
|
|
398
|
+
if (options.logWriteFailure === true) {
|
|
399
|
+
trace.push({ module: 'fastly_log', name: 'write', endpoint, accepted: false });
|
|
400
|
+
return FASTLY_STATUS_ERROR;
|
|
401
|
+
}
|
|
402
|
+
const length = Math.min(Number(messageLength), options.logWriteMaxBytes ?? Number(messageLength));
|
|
403
|
+
const emitted = readUtf8(messagePointer, length);
|
|
404
|
+
// Test-only assertion seam before proof-trace redaction. Never retain raw
|
|
405
|
+
// guest bytes in the returned evidence, even when the guest regresses.
|
|
406
|
+
if (typeof options.onLogWrite === 'function') options.onLogWrite(emitted);
|
|
407
|
+
const message = redactText(emitted);
|
|
408
|
+
writeU32(writtenOut, length);
|
|
409
|
+
logs.push(Object.freeze({ endpoint, message }));
|
|
410
|
+
trace.push({ module: 'fastly_log', name: 'write', endpoint, message, accepted: true });
|
|
411
|
+
return FASTLY_STATUS_OK;
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
fastly_http_req: {
|
|
415
|
+
register_dynamic_backend: dynamicBackends.register,
|
|
416
|
+
body_downstream_get(requestOut, bodyOut) {
|
|
417
|
+
trace.push({ module: 'fastly_http_req', name: 'body_downstream_get' });
|
|
418
|
+
writeU32(requestOut, downstreamRequestHandle);
|
|
419
|
+
writeU32(bodyOut, downstreamBodyHandle);
|
|
420
|
+
return FASTLY_STATUS_OK;
|
|
421
|
+
},
|
|
422
|
+
method_get(handle, buffer, bufferLength, writtenOut) {
|
|
423
|
+
const request = requests.get(Number(handle));
|
|
424
|
+
trace.push({ module: 'fastly_http_req', name: 'method_get', handle: Number(handle) });
|
|
425
|
+
return request ? writeUtf8(request.method, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
|
|
426
|
+
},
|
|
427
|
+
uri_get(handle, buffer, bufferLength, writtenOut) {
|
|
428
|
+
const request = requests.get(Number(handle));
|
|
429
|
+
trace.push({ module: 'fastly_http_req', name: 'uri_get', handle: Number(handle) });
|
|
430
|
+
return request ? writeUtf8(request.url, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
|
|
431
|
+
},
|
|
432
|
+
header_names_get(handle, buffer, size, cursor, end, written) {
|
|
433
|
+
trace.push({module: 'fastly_http_req', name: 'header_names_get', cursor: Number(cursor)});
|
|
434
|
+
const request = requests.get(Number(handle));
|
|
435
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
436
|
+
return requestHeaderList([...new Set(request.headers.map(([name]) => name.toLowerCase()))], buffer, size, cursor, end, written, 'names');
|
|
437
|
+
},
|
|
438
|
+
header_values_get(handle, pointer, length, buffer, size, cursor, end, written) {
|
|
439
|
+
trace.push({module: 'fastly_http_req', name: 'header_values_get', cursor: Number(cursor)});
|
|
440
|
+
const request = requests.get(Number(handle));
|
|
441
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
442
|
+
const name = readUtf8(pointer, length).toLowerCase();
|
|
443
|
+
if (!/^[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name)) return FASTLY_STATUS_INVALID_ARGUMENT;
|
|
444
|
+
return requestHeaderList(request.headers.filter(([key]) => key.toLowerCase() === name).map(([, value]) => value), buffer, size, cursor, end, written, 'values', 'request');
|
|
445
|
+
},
|
|
446
|
+
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
447
|
+
const request = requests.get(Number(handle));
|
|
448
|
+
const name = readUtf8(namePointer, nameLength);
|
|
449
|
+
trace.push({ module: 'fastly_http_req', name: 'header_value_get', handle: Number(handle), header: name });
|
|
450
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
451
|
+
const value = headerValue(request.headers, name);
|
|
452
|
+
return value === undefined ? FASTLY_STATUS_INVALID_ARGUMENT : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
453
|
+
},
|
|
454
|
+
new(handleOut) {
|
|
455
|
+
const handle = alloc('request');
|
|
456
|
+
requests.set(handle, { method: 'GET', url: '', headers: [] });
|
|
457
|
+
writeU32(handleOut, handle);
|
|
458
|
+
trace.push({ module: 'fastly_http_req', name: 'new', handle });
|
|
459
|
+
return FASTLY_STATUS_OK;
|
|
460
|
+
},
|
|
461
|
+
method_set(handle, pointer, length) {
|
|
462
|
+
const request = requests.get(Number(handle));
|
|
463
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
464
|
+
request.method = readUtf8(pointer, length).toUpperCase();
|
|
465
|
+
trace.push({ module: 'fastly_http_req', name: 'method_set', handle: Number(handle), method: request.method });
|
|
466
|
+
return FASTLY_STATUS_OK;
|
|
467
|
+
},
|
|
468
|
+
uri_set(handle, pointer, length) {
|
|
469
|
+
const request = requests.get(Number(handle));
|
|
470
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
471
|
+
request.url = readUtf8(pointer, length);
|
|
472
|
+
trace.push({ module: 'fastly_http_req', name: 'uri_set', handle: Number(handle), url: redactText(request.url) });
|
|
473
|
+
return FASTLY_STATUS_OK;
|
|
474
|
+
},
|
|
475
|
+
header_insert(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
476
|
+
const request = requests.get(Number(handle));
|
|
477
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
478
|
+
const name = readUtf8(namePointer, nameLength);
|
|
479
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
480
|
+
if (name.toLowerCase() === 'authorization' && value.includes('/s3/aws4_request')) privateUrls.add(request.url);
|
|
481
|
+
request.headers = request.headers.filter(([headerName]) => headerName.toLowerCase() !== name.toLowerCase());
|
|
482
|
+
request.headers.push([name, value]);
|
|
483
|
+
trace.push({ module: 'fastly_http_req', name: 'header_insert', handle: Number(handle), header: redactRuntimeHeaders([[name, value]], privateUrls.has(request.url))[0] });
|
|
484
|
+
return FASTLY_STATUS_OK;
|
|
485
|
+
},
|
|
486
|
+
cache_override_set(handle, tag, ttl, swr) {
|
|
487
|
+
const request = requests.get(Number(handle)); if (!request) return FASTLY_STATUS_BADF;
|
|
488
|
+
request.cacheOverride = Number(tag); return FASTLY_STATUS_OK;
|
|
489
|
+
},
|
|
490
|
+
auto_decompress_response_set(handle, encodings) {
|
|
491
|
+
const request = requests.get(Number(handle)); if (!request) return FASTLY_STATUS_BADF;
|
|
492
|
+
request.decompression = Number(encodings); return FASTLY_STATUS_OK;
|
|
493
|
+
},
|
|
494
|
+
close(handle) { return requests.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
|
|
495
|
+
pending_req_poll_v2(handle, detail, done, responseOut, bodyOut) {
|
|
496
|
+
const item = pending.get(Number(handle)); if (!item) return FASTLY_STATUS_BADF;
|
|
497
|
+
writeSendErrorDetail(item, handle, detail, [done, responseOut, bodyOut], 'pending_req_poll_v2');
|
|
498
|
+
// Exercise a readiness race: select/is_ready can precede a not-ready poll.
|
|
499
|
+
if (monotonicMs >= item.readyAt && item.fixture.pollNotReadyMs && !item.delayedPoll) {
|
|
500
|
+
item.delayedPoll = true; item.readyAt = monotonicMs + Number(item.fixture.pollNotReadyMs);
|
|
501
|
+
}
|
|
502
|
+
if (monotonicMs < item.readyAt) { writeU32(done, 0); return FASTLY_STATUS_OK; }
|
|
503
|
+
writeU32(done, 1); return imports.fastly_http_req.pending_req_wait(handle, responseOut, bodyOut);
|
|
504
|
+
},
|
|
505
|
+
send_async_streaming(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
506
|
+
const request = requests.get(Number(handle)), body = bodies.get(Number(bodyHandle));
|
|
507
|
+
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
508
|
+
const backend = readUtf8(backendPointer, backendLength);
|
|
509
|
+
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
510
|
+
requests.delete(Number(handle));
|
|
511
|
+
if (!fixture || fixture.sendStatus) { bodies.delete(Number(bodyHandle)); return Number(fixture?.sendStatus || 1); }
|
|
512
|
+
const pendingHandle = alloc('pending');
|
|
513
|
+
const record = {backend, method:request.method, url:redactText(request.url), headers:redactRuntimeHeaders(request.headers), bodyBytes:Buffer.alloc(0)};
|
|
514
|
+
outboundRequests.push(record);
|
|
515
|
+
const item = {request, backend, fixture, readyAt:fixture.earlyResponse ? monotonicMs + Number(fixture.delayMs || 0) : Infinity};
|
|
516
|
+
pending.set(pendingHandle, item);
|
|
517
|
+
body.streamingUpload = {item, record}; body.readyAt = monotonicMs + Number(options.uploadReadyDelayMs || 0);
|
|
518
|
+
writeU32(pendingOut, pendingHandle);
|
|
519
|
+
trace.push({module:'fastly_http_req',name:'send_async_streaming',bodyHandle:Number(bodyHandle),pendingHandle});
|
|
520
|
+
return FASTLY_STATUS_OK;
|
|
521
|
+
},
|
|
522
|
+
send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
523
|
+
const request = requests.get(Number(handle));
|
|
524
|
+
const body = bodies.get(Number(bodyHandle));
|
|
525
|
+
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
526
|
+
const backend = readUtf8(backendPointer, backendLength);
|
|
527
|
+
dynamicBackends.route(backend, request);
|
|
528
|
+
const signedS3 = request.headers.some(([name, value]) => name.toLowerCase() === 'authorization' && value.startsWith('AWS4-HMAC-SHA256 '));
|
|
529
|
+
if (signedS3) privateUrls.add(request.url);
|
|
530
|
+
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
531
|
+
if (!fixture) {
|
|
532
|
+
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: signedS3 ? '[REDACTED]' : request.url, missingFixture: true });
|
|
533
|
+
return FASTLY_STATUS_ERROR;
|
|
534
|
+
}
|
|
535
|
+
const pendingHandle = alloc('pending');
|
|
536
|
+
const capturedBody = bodyBytes(bodyHandle);
|
|
537
|
+
pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: capturedBody }, backend, fixture, readyAt: fixture.neverHeaders ? Infinity : monotonicMs + Number(fixture.delayMs || 0) });
|
|
538
|
+
if (typeof options.onOutboundRequest === 'function') options.onOutboundRequest({ ...request, backend, headers: request.headers.map((entry) => [...entry]), body: capturedBody });
|
|
539
|
+
if (fixture.sendStatus) { pending.delete(pendingHandle); return Number(fixture.sendStatus); }
|
|
540
|
+
if (request.headers.some(([name, value]) => name.toLowerCase() === 'authorization' && value.startsWith('AWS4-HMAC-SHA256 '))) privateUrls.add(request.url);
|
|
541
|
+
requests.delete(Number(handle)); bodies.delete(Number(bodyHandle));
|
|
542
|
+
outboundRequests.push(Object.freeze({
|
|
543
|
+
backend,
|
|
544
|
+
method: request.method,
|
|
545
|
+
url: privateUrls.has(request.url) ? '[REDACTED]' : redactText(request.url),
|
|
546
|
+
headers: Object.freeze(redactRuntimeHeaders(request.headers, signedS3).map((header) => Object.freeze(header))),
|
|
547
|
+
body: signedS3 ? '[REDACTED]' : redactText(capturedBody.toString('utf8'))
|
|
548
|
+
}));
|
|
549
|
+
writeU32(pendingOut, pendingHandle);
|
|
550
|
+
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), bodyHandle: Number(bodyHandle), pendingHandle, backend, method: request.method, url: redactText(request.url), headers: redactRuntimeHeaders(request.headers, signedS3), bodyBytes: capturedBody.length });
|
|
551
|
+
return FASTLY_STATUS_OK;
|
|
552
|
+
},
|
|
553
|
+
pending_req_wait_v2(handle, detail, responseOut, bodyOut) {
|
|
554
|
+
const item = pending.get(Number(handle)); if (!item) return FASTLY_STATUS_BADF;
|
|
555
|
+
writeSendErrorDetail(item, handle, detail, [responseOut, bodyOut], 'pending_req_wait_v2');
|
|
556
|
+
return imports.fastly_http_req.pending_req_wait(handle, responseOut, bodyOut);
|
|
557
|
+
},
|
|
558
|
+
pending_req_wait(handle, responseOut, bodyOut) {
|
|
559
|
+
const item = pending.get(Number(handle));
|
|
560
|
+
trace.push({ module: 'fastly_http_req', name: 'pending_req_wait', pendingHandle: Number(handle), backend: item && item.backend, url: item && item.request.url });
|
|
561
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
562
|
+
pending.delete(Number(handle));
|
|
563
|
+
const transportStatus = Number(item.fixture.transportStatus || 0);
|
|
564
|
+
if (transportStatus !== FASTLY_STATUS_OK) return transportStatus;
|
|
565
|
+
const origin = makeOriginResponse(item.fixture);
|
|
566
|
+
writeU32(responseOut, origin.responseHandle);
|
|
567
|
+
writeU32(bodyOut, origin.bodyHandle);
|
|
568
|
+
return FASTLY_STATUS_OK;
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
fastly_config_store: {
|
|
572
|
+
open(namePointer, nameLength, handleOut) {
|
|
573
|
+
const name = readUtf8(namePointer, nameLength);
|
|
574
|
+
const values = configStores.get(name);
|
|
575
|
+
trace.push({ module: 'fastly_config_store', name: 'open', store: name, found: Boolean(values) });
|
|
576
|
+
if (!values) return FASTLY_STATUS_NONE;
|
|
577
|
+
const handle = alloc('configStore'); configHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
578
|
+
},
|
|
579
|
+
get(handle, keyPointer, keyLength, valuePointer, valueLength, writtenOut) {
|
|
580
|
+
const values = configHandles.get(Number(handle));
|
|
581
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
582
|
+
const value = values && values.get(key);
|
|
583
|
+
trace.push({ module: 'fastly_config_store', name: 'get', handle: Number(handle), key, found: value !== undefined });
|
|
584
|
+
if (!values) return FASTLY_STATUS_BADF;
|
|
585
|
+
return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
fastly_secret_store: {
|
|
589
|
+
open(namePointer, nameLength, handleOut) {
|
|
590
|
+
const name = readUtf8(namePointer, nameLength);
|
|
591
|
+
const values = secretStores.get(name);
|
|
592
|
+
trace.push({ module: 'fastly_secret_store', name: 'open', store: name, found: Boolean(values) });
|
|
593
|
+
if (!values) return FASTLY_STATUS_NONE;
|
|
594
|
+
const handle = alloc('secretStore'); secretStoreHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
595
|
+
},
|
|
596
|
+
get(handle, keyPointer, keyLength, secretOut) {
|
|
597
|
+
monotonicMs += Number(options.secretDelayMs || 0);
|
|
598
|
+
if (typeof options.onSecretLookup === 'function') options.onSecretLookup();
|
|
599
|
+
const values = secretStoreHandles.get(Number(handle));
|
|
600
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
601
|
+
const value = values && values.get(key);
|
|
602
|
+
trace.push({ module: 'fastly_secret_store', name: 'get', handle: Number(handle), key, found: value !== undefined });
|
|
603
|
+
if (!values) return FASTLY_STATUS_BADF;
|
|
604
|
+
if (value === undefined) return FASTLY_STATUS_NONE;
|
|
605
|
+
const secretHandle = alloc('secret'); secretHandles.set(secretHandle, String(value)); writeU32(secretOut, secretHandle); return FASTLY_STATUS_OK;
|
|
606
|
+
},
|
|
607
|
+
plaintext(handle, valuePointer, valueLength, writtenOut) {
|
|
608
|
+
const value = secretHandles.get(Number(handle));
|
|
609
|
+
trace.push({ module: 'fastly_secret_store', name: 'plaintext', handle: Number(handle), value: '[REDACTED]' });
|
|
610
|
+
return value === undefined ? FASTLY_STATUS_BADF : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
fastly_kv_store: {
|
|
614
|
+
open(namePointer, nameLength, handleOut) {
|
|
615
|
+
const name = readUtf8(namePointer, nameLength);
|
|
616
|
+
const values = kvStores.get(name);
|
|
617
|
+
trace.push({ module: 'fastly_kv_store', name: 'open', store: name, found: Boolean(values) });
|
|
618
|
+
if (!values) return FASTLY_STATUS_NONE;
|
|
619
|
+
const handle = alloc('kvStore'); kvStoreHandles.set(handle, { name, values }); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
620
|
+
},
|
|
621
|
+
lookup(storeHandle, keyPointer, keyLength, configMask, configPointer, lookupOut) {
|
|
622
|
+
const store = kvStoreHandles.get(Number(storeHandle));
|
|
623
|
+
if (!store) return FASTLY_STATUS_BADF;
|
|
624
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
625
|
+
const handle = alloc('kvLookup'); pendingKvLookups.set(handle, { store, key }); writeU32(lookupOut, handle);
|
|
626
|
+
trace.push({ module: 'fastly_kv_store', name: 'lookup', store: store.name, key, pendingHandle: handle, configMask: Number(configMask) });
|
|
627
|
+
return FASTLY_STATUS_OK;
|
|
628
|
+
},
|
|
629
|
+
lookup_wait_v2(handle, bodyOut, metadataPointer, metadataLength, writtenOut, generationOut, kvErrorOut) {
|
|
630
|
+
const item = pendingKvLookups.get(Number(handle));
|
|
631
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
632
|
+
pendingKvLookups.delete(Number(handle));
|
|
633
|
+
const value = item.store.values.get(item.key);
|
|
634
|
+
writeU32(writtenOut, 0); writeU64(generationOut, 0n);
|
|
635
|
+
if (value === undefined) {
|
|
636
|
+
writeU32(kvErrorOut, FASTLY_KV_ERROR_NOT_FOUND);
|
|
637
|
+
trace.push({ module: 'fastly_kv_store', name: 'lookup_wait_v2', store: item.store.name, key: item.key, found: false });
|
|
638
|
+
return FASTLY_STATUS_OK;
|
|
639
|
+
}
|
|
640
|
+
const bodyHandle = alloc('body'); bodies.set(bodyHandle, { bytes: Buffer.from(value), readOffset: 0, writes: [] }); writeU32(bodyOut, bodyHandle); writeU32(kvErrorOut, FASTLY_KV_ERROR_OK);
|
|
641
|
+
trace.push({ module: 'fastly_kv_store', name: 'lookup_wait_v2', store: item.store.name, key: item.key, found: true, bodyHandle });
|
|
642
|
+
return FASTLY_STATUS_OK;
|
|
643
|
+
},
|
|
644
|
+
insert(storeHandle, keyPointer, keyLength, bodyHandle, configMask, configPointer, insertOut) {
|
|
645
|
+
const store = kvStoreHandles.get(Number(storeHandle));
|
|
646
|
+
const body = bodies.get(Number(bodyHandle));
|
|
647
|
+
if (!store || !body) return FASTLY_STATUS_BADF;
|
|
648
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
649
|
+
const handle = alloc('kvInsert'); pendingKvInserts.set(handle, { store, key, bytes: bodyBytes(bodyHandle) }); writeU32(insertOut, handle);
|
|
650
|
+
trace.push({ module: 'fastly_kv_store', name: 'insert', store: store.name, key, pendingHandle: handle, bytes: bodyBytes(bodyHandle).length, configMask: Number(configMask) });
|
|
651
|
+
return FASTLY_STATUS_OK;
|
|
652
|
+
},
|
|
653
|
+
insert_wait(handle, kvErrorOut) {
|
|
654
|
+
const item = pendingKvInserts.get(Number(handle));
|
|
655
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
656
|
+
pendingKvInserts.delete(Number(handle)); item.store.values.set(item.key, Buffer.from(item.bytes)); writeU32(kvErrorOut, FASTLY_KV_ERROR_OK);
|
|
657
|
+
trace.push({ module: 'fastly_kv_store', name: 'insert_wait', store: item.store.name, key: item.key, bytes: item.bytes.length });
|
|
658
|
+
return FASTLY_STATUS_OK;
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
fastly_http_resp: {
|
|
662
|
+
close(handle) { trace.push({ module: 'fastly_http_resp', name: 'close' }); return responses.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
|
|
663
|
+
header_names_get(handle, buffer, size, cursor, end, written) {
|
|
664
|
+
const response = responses.get(Number(handle)); if (!response || cursor !== 0) return FASTLY_STATUS_BADF;
|
|
665
|
+
return headerList([...new Set(response.headers.map(([name]) => name.toLowerCase()))], buffer, size, end, written);
|
|
666
|
+
},
|
|
667
|
+
header_values_get(handle, pointer, length, buffer, size, cursor, end, written) {
|
|
668
|
+
const response = responses.get(Number(handle)); if (!response || cursor !== 0) return FASTLY_STATUS_BADF;
|
|
669
|
+
const name = readUtf8(pointer, length).toLowerCase();
|
|
670
|
+
return headerList(response.headers.filter(([key]) => key.toLowerCase() === name).map(([, value]) => value), buffer, size, end, written);
|
|
671
|
+
},
|
|
672
|
+
new(handleOut) {
|
|
673
|
+
const handle = alloc('response');
|
|
674
|
+
responses.set(handle, { status: 200, headers: [], origin: false });
|
|
675
|
+
writeU32(handleOut, handle);
|
|
676
|
+
trace.push({ module: 'fastly_http_resp', name: 'new', handle });
|
|
677
|
+
return FASTLY_STATUS_OK;
|
|
678
|
+
},
|
|
679
|
+
header_append(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
680
|
+
const response = responses.get(Number(handle));
|
|
681
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
682
|
+
const name = readUtf8(namePointer, nameLength);
|
|
683
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
684
|
+
response.headers.push([name, value]);
|
|
685
|
+
trace.push({ module: 'fastly_http_resp', name: 'header_append', handle: Number(handle), header: [name, value] });
|
|
686
|
+
return FASTLY_STATUS_OK;
|
|
687
|
+
},
|
|
688
|
+
header_values_get(handle, pointer, length, buffer, size, cursor, end, written) {
|
|
689
|
+
trace.push({module:'fastly_http_resp', name:'header_values_get', cursor:Number(cursor)});
|
|
690
|
+
const response = responses.get(Number(handle));
|
|
691
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
692
|
+
const name = readUtf8(pointer, length).toLowerCase();
|
|
693
|
+
if (!/^[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name)) return FASTLY_STATUS_INVALID_ARGUMENT;
|
|
694
|
+
return requestHeaderList(response.headers.filter(([key]) => key.toLowerCase() === name).map(([, value]) => value), buffer, size, cursor, end, written, 'response-values', 'response');
|
|
695
|
+
},
|
|
696
|
+
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
697
|
+
const response = responses.get(Number(handle));
|
|
698
|
+
const name = readUtf8(namePointer, nameLength);
|
|
699
|
+
trace.push({ module: 'fastly_http_resp', name: 'header_value_get', handle: Number(handle), header: name });
|
|
700
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
701
|
+
const value = headerValue(response.headers, name);
|
|
702
|
+
return value === undefined ? FASTLY_STATUS_INVALID_ARGUMENT : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
703
|
+
},
|
|
704
|
+
status_get(handle, statusOut) {
|
|
705
|
+
const response = responses.get(Number(handle));
|
|
706
|
+
trace.push({ module: 'fastly_http_resp', name: 'status_get', handle: Number(handle) });
|
|
707
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
708
|
+
writeU32(statusOut, response.status);
|
|
709
|
+
return FASTLY_STATUS_OK;
|
|
710
|
+
},
|
|
711
|
+
status_set(handle, status) {
|
|
712
|
+
const response = responses.get(Number(handle));
|
|
713
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
714
|
+
response.status = Number(status);
|
|
715
|
+
trace.push({ module: 'fastly_http_resp', name: 'status_set', handle: Number(handle), status: Number(status) });
|
|
716
|
+
return FASTLY_STATUS_OK;
|
|
717
|
+
},
|
|
718
|
+
send_downstream(handle, bodyHandle, streaming) {
|
|
719
|
+
const response = responses.get(Number(handle));
|
|
720
|
+
const body = bodies.get(Number(bodyHandle));
|
|
721
|
+
trace.push({ module: 'fastly_http_resp', name: 'send_downstream', handle: Number(handle), bodyHandle: Number(bodyHandle), streaming: Number(streaming), origin: response && response.origin });
|
|
722
|
+
if (!response || !body) return FASTLY_STATUS_BADF;
|
|
723
|
+
downstream = {
|
|
724
|
+
status: response.status,
|
|
725
|
+
headers: response.headers.map((entry) => [...entry]),
|
|
726
|
+
bodyBytes: bodyBytes(bodyHandle),
|
|
727
|
+
streaming: Number(streaming),
|
|
728
|
+
origin: response.origin === true,
|
|
729
|
+
responseHandle: Number(handle),
|
|
730
|
+
bodyHandle: Number(bodyHandle)
|
|
731
|
+
};
|
|
732
|
+
if (Number(streaming) === 1) responses.delete(Number(handle));
|
|
733
|
+
return FASTLY_STATUS_OK;
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
fastly_http_body: {
|
|
737
|
+
close(handle) {
|
|
738
|
+
trace.push({module:'fastly_http_body',name:'close',handle:Number(handle)});
|
|
739
|
+
const body = bodies.get(Number(handle));
|
|
740
|
+
if (body?.streamingUpload) {
|
|
741
|
+
const {item,record} = body.streamingUpload;
|
|
742
|
+
record.bodyBytes = bodyBytes(handle);
|
|
743
|
+
if (item.fixture.echoBody) item.fixture = {...item.fixture,body:record.bodyBytes};
|
|
744
|
+
item.readyAt = item.fixture.neverHeaders ? Infinity : monotonicMs + Number(item.fixture.delayMs || 0);
|
|
745
|
+
}
|
|
746
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) { downstream.bodyBytes = bodyBytes(handle); downstream.finished = true; }
|
|
747
|
+
return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF;
|
|
748
|
+
},
|
|
749
|
+
abandon(handle) {
|
|
750
|
+
trace.push({module:'fastly_http_body',name:'abandon',handle:Number(handle)});
|
|
751
|
+
const body = bodies.get(Number(handle));
|
|
752
|
+
if (body?.streamingUpload) { body.streamingUpload.record.bodyBytes = bodyBytes(handle); body.streamingUpload.record.aborted = true; }
|
|
753
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) { downstream.bodyBytes = bodyBytes(handle); downstream.aborted = true; }
|
|
754
|
+
return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF;
|
|
755
|
+
},
|
|
756
|
+
new(handleOut) {
|
|
757
|
+
const handle = alloc('body');
|
|
758
|
+
bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
|
|
759
|
+
writeU32(handleOut, handle);
|
|
760
|
+
trace.push({ module: 'fastly_http_body', name: 'new', handle });
|
|
761
|
+
return FASTLY_STATUS_OK;
|
|
762
|
+
},
|
|
763
|
+
read(handle, buffer, bufferLength, readOut) {
|
|
764
|
+
const body = bodies.get(Number(handle));
|
|
765
|
+
if (!body) return FASTLY_STATUS_BADF;
|
|
766
|
+
if (body.fixture && body.fixture.bodyReadStatus) return Number(body.fixture.bodyReadStatus);
|
|
767
|
+
const source = bodyBytes(handle);
|
|
768
|
+
const offset = body.readOffset || 0;
|
|
769
|
+
const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset), Number(body.fixture && body.fixture.chunkBytes || 65536));
|
|
770
|
+
if (count > 0) new Uint8Array(memory().buffer, Number(buffer), count).set(source.subarray(offset, offset + count));
|
|
771
|
+
body.readOffset = offset + count;
|
|
772
|
+
body.readyAt = monotonicMs + Number(body.fixture && body.fixture.chunkDelayMs || 0);
|
|
773
|
+
writeU32(readOut, count);
|
|
774
|
+
trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: count, ...(options.traceBodyIo === true ? {pointer:Number(buffer), capacity:Number(bufferLength), now:monotonicMs} : {}) });
|
|
775
|
+
return FASTLY_STATUS_OK;
|
|
776
|
+
},
|
|
777
|
+
write(handle, buffer, bufferLength, end, writtenOut) {
|
|
778
|
+
const body = bodies.get(Number(handle));
|
|
779
|
+
if (!body) return FASTLY_STATUS_BADF;
|
|
780
|
+
if (downstream?.streaming && Number(handle) === downstream.bodyHandle && options.downstreamBodyWriteStatus) return Number(options.downstreamBodyWriteStatus);
|
|
781
|
+
if ((requests.size > 1 || body.streamingUpload) && options.outboundBodyWriteStatus) return Number(options.outboundBodyWriteStatus);
|
|
782
|
+
const length = Math.min(Number(bufferLength), Number(options.bodyWriteChunkBytes ?? bufferLength));
|
|
783
|
+
const bytes = readBytes(buffer, length);
|
|
784
|
+
body.writes.push(bytes);
|
|
785
|
+
if (body.streamingUpload) body.readyAt = monotonicMs + Number(options.uploadWriteDelayMs || 0);
|
|
786
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) body.readyAt = monotonicMs + Number(options.downstreamWriteDelayMs || 0);
|
|
787
|
+
writeU32(writtenOut, bytes.length);
|
|
788
|
+
trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end), ...(options.traceBodyIo === true ? {pointer:Number(buffer), capacity:Number(bufferLength), now:monotonicMs} : {}) });
|
|
789
|
+
return FASTLY_STATUS_OK;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const kvEvidence = options.conditionalKv ? require('./conditional-kv-host.js').attachConditionalKvHost(imports, {
|
|
795
|
+
view, writeU32, writeU64, readBytes, readUtf8, alloc, bodies, bodyBytes, trace,
|
|
796
|
+
now: () => monotonicMs, advance: ms => { monotonicMs += ms; }
|
|
797
|
+
}, options.conditionalKv) : undefined;
|
|
798
|
+
|
|
799
|
+
// Model invocation termination, including cancellation raised by a host call.
|
|
800
|
+
// Throwing stops the Wasm invocation; it never fabricates an effect result.
|
|
801
|
+
const checkActive = () => { if (options.signal && options.signal.aborted) throw options.signal.reason || new Error('Request cancelled.'); };
|
|
802
|
+
checkActive();
|
|
803
|
+
// A test-only post-call hook can corrupt ABI outputs or advance the clock.
|
|
804
|
+
// It observes the real generated guest; it is never used by provider execution.
|
|
805
|
+
if (options.signal || options.hostcallDelayMs || options.afterHostcall) for (const [moduleName, namespace] of Object.entries(imports)) {
|
|
806
|
+
for (const [name, hostcall] of Object.entries(namespace)) if (typeof hostcall === 'function') namespace[name] = (...args) => {
|
|
807
|
+
checkActive(); const result = hostcall(...args);
|
|
808
|
+
monotonicMs += Number(options.hostcallDelayMs?.[`${moduleName}.${name}`] || 0);
|
|
809
|
+
const replacement = options.afterHostcall?.({module:moduleName,name,args,result,trace,
|
|
810
|
+
writeU32,writeU64,readU32:pointer=>view().getUint32(Number(pointer),true),
|
|
811
|
+
advance:ms=>{monotonicMs+=ms;}});
|
|
812
|
+
checkActive(); return replacement ?? result;
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const module = new WebAssembly.Module(wasm);
|
|
816
|
+
instance = new WebAssembly.Instance(module, imports);
|
|
817
|
+
if (typeof instance.exports._start !== 'function') {
|
|
818
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Pass98 native module is missing _start.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_START_MISSING');
|
|
819
|
+
}
|
|
820
|
+
if (options.conditionalKv && options.conditionalKv.deadlineNs !== undefined) instance.exports.pulse_fastly_kv_request_deadline(BigInt(options.conditionalKv.deadlineNs));
|
|
821
|
+
const callableEvidence = () => input?.plan?.callables?.length ? {
|
|
822
|
+
guestMemoryBytes: instance.exports.memory.buffer.byteLength,
|
|
823
|
+
memory: { bytes: Number(instance.exports.pulse_fastly_memory_bytes()),
|
|
824
|
+
values: Number(instance.exports.pulse_fastly_memory_values()) },
|
|
825
|
+
responseStatus: downstream?.status
|
|
826
|
+
} : {};
|
|
827
|
+
try { instance.exports._start(); }
|
|
828
|
+
catch (error) {
|
|
829
|
+
if (Number(instance.exports.pulse_fastly_last_error?.()) !== 1010) {
|
|
830
|
+
Object.defineProperty(error, 'execution', { value: Object.freeze(callableEvidence()), configurable: true });
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Native retained-value budget exceeded.',
|
|
834
|
+
'PULSE_RUNTIME_MEMORY_LIMIT_EXCEEDED', {
|
|
835
|
+
...callableEvidence(), lastError: 1010, errorStage: Number(instance.exports.pulse_fastly_error_stage()), trace,
|
|
836
|
+
bytes: Number(instance.exports.pulse_fastly_memory_bytes()),
|
|
837
|
+
values: Number(instance.exports.pulse_fastly_memory_values())
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
for (const entry of trace) if (entry.url && privateUrls.has(entry.url)) entry.url = '[REDACTED]';
|
|
841
|
+
const lastError = typeof instance.exports.pulse_fastly_last_error === 'function'
|
|
842
|
+
? Number(instance.exports.pulse_fastly_last_error())
|
|
843
|
+
: undefined;
|
|
844
|
+
const errorStage = typeof instance.exports.pulse_fastly_error_stage === 'function'
|
|
845
|
+
? Number(instance.exports.pulse_fastly_error_stage())
|
|
846
|
+
: undefined;
|
|
847
|
+
const errorEffect = typeof instance.exports.pulse_fastly_error_effect === 'function'
|
|
848
|
+
? Number(instance.exports.pulse_fastly_error_effect())
|
|
849
|
+
: undefined;
|
|
850
|
+
const pulseLastError = typeof instance.exports.pulse_last_error_code === 'function'
|
|
851
|
+
? Number(instance.exports.pulse_last_error_code())
|
|
852
|
+
: undefined;
|
|
853
|
+
if (downstream?.streaming && !downstream.finished) { downstream.bodyBytes = bodyBytes(downstream.bodyHandle) || downstream.bodyBytes; downstream.aborted = true; }
|
|
854
|
+
const handledJwtError = options.allowHandledJwtError === true
|
|
855
|
+
&& lastError === 1008
|
|
856
|
+
&& Boolean(downstream);
|
|
857
|
+
const handledHeaderError = options.allowHandledHeaderError === true
|
|
858
|
+
&& lastError === 1003 && (errorStage === 22 || errorStage === 23) && downstream?.status === 500;
|
|
859
|
+
const handledTransportError = options.allowHandledTransportError === true
|
|
860
|
+
&& (errorStage === 77 || errorStage === 81)
|
|
861
|
+
&& ((lastError === 1006 && downstream?.status === 502) || (lastError === 1003 && downstream?.status === 500));
|
|
862
|
+
const handledDeadlineError = options.allowHandledFetchDeadlineError === true
|
|
863
|
+
&& ((lastError === 1006 && errorStage === 271 && downstream?.status === 504)
|
|
864
|
+
|| (lastError === 1001 && errorStage === 270 && downstream?.status === 400)
|
|
865
|
+
|| (lastError === 1003 && errorStage === 272 && downstream?.status === 500));
|
|
866
|
+
const handledDynamicBackendError = options.allowHandledDynamicBackendError === true
|
|
867
|
+
&& ((lastError === 1001 && errorStage === 273 && downstream?.status === 400)
|
|
868
|
+
|| (lastError === 1001 && errorStage === 276 && downstream?.status === 500)
|
|
869
|
+
|| (lastError === 1003 && (errorStage === 274 || errorStage === 275) && downstream?.status === 500));
|
|
870
|
+
if (lastError !== FASTLY_STATUS_OK && !handledDynamicBackendError && !handledDeadlineError && !handledTransportError && !handledHeaderError && !handledJwtError && !(options.allowAbortedResponse === true && downstream?.aborted)) {
|
|
871
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module reported an execution error.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_EXECUTION_FAILED', { ...callableEvidence(), lastError, errorStage, errorEffect, pulseLastError, trace });
|
|
872
|
+
}
|
|
873
|
+
if (!downstream) {
|
|
874
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module did not send a downstream response.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_RESPONSE_NOT_SENT', { trace });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return Object.freeze({
|
|
878
|
+
...callableEvidence(),
|
|
879
|
+
version: 'pulse.fastly-native-platform-capabilities-mock.v1',
|
|
880
|
+
request: Object.freeze({ method, url, path: pathValue, headers: Object.freeze(requestHeaders.map((entry) => Object.freeze(entry))) }),
|
|
881
|
+
response: Object.freeze({
|
|
882
|
+
status: downstream.status,
|
|
883
|
+
headers: Object.freeze(downstream.headers.map((entry) => Object.freeze(entry))),
|
|
884
|
+
body: downstream.bodyBytes.toString('utf8'),
|
|
885
|
+
bodyBytes: downstream.bodyBytes,
|
|
886
|
+
sent: true,
|
|
887
|
+
streaming: downstream.streaming,
|
|
888
|
+
aborted: downstream.aborted === true,
|
|
889
|
+
origin: downstream.origin,
|
|
890
|
+
responseHandle: downstream.responseHandle,
|
|
891
|
+
bodyHandle: downstream.bodyHandle
|
|
892
|
+
}),
|
|
893
|
+
trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
|
|
894
|
+
logs: Object.freeze(logs),
|
|
895
|
+
outboundRequests: Object.freeze(outboundRequests),
|
|
896
|
+
stores: Object.freeze({ config: snapshotStores(configStores), secrets: Object.fromEntries([...secretStores].map(([name, values]) => [name, Object.fromEntries([...values].map(([key]) => [key, '[REDACTED]']))])), kv: snapshotStores(kvStores, true) }),
|
|
897
|
+
kvEvidence,
|
|
898
|
+
monotonicMs,
|
|
899
|
+
transportResources: Object.freeze({pendingRequests: pending.size,
|
|
900
|
+
outboundRequests: requests.size - Number(requests.has(downstreamRequestHandle))}),
|
|
901
|
+
// Non-streaming send transfers these two handles even though the fixture
|
|
902
|
+
// keeps their bytes for assertions. Pending work is counted separately:
|
|
903
|
+
// Fastly has no cancellation hostcall for an unresolved pending request.
|
|
904
|
+
forwardingResources: Object.freeze({
|
|
905
|
+
bodies: bodies.size - Number(!downstream?.streaming && bodies.has(downstream?.bodyHandle)),
|
|
906
|
+
responses: responses.size - Number(!downstream?.streaming && responses.has(downstream?.responseHandle))
|
|
907
|
+
}),
|
|
908
|
+
instance
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
module.exports = Object.freeze({
|
|
913
|
+
FASTLY_STATUS_OK,
|
|
914
|
+
FASTLY_STATUS_ERROR,
|
|
915
|
+
FASTLY_STATUS_BADF,
|
|
916
|
+
FASTLY_STATUS_BUFLEN,
|
|
917
|
+
FASTLY_STATUS_NONE,
|
|
918
|
+
FASTLY_KV_ERROR_OK,
|
|
919
|
+
FASTLY_KV_ERROR_NOT_FOUND,
|
|
920
|
+
FastlyNativePlatformCapabilitiesMockError,
|
|
921
|
+
normalizeHeaders,
|
|
922
|
+
fixtureKey,
|
|
923
|
+
executeFastlyNativePlatformCapabilities
|
|
924
|
+
});
|