@pulse-compute/provider-fastly 1.0.0-beta.1 → 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 +7 -6
- package/dist/config-schema.json +30 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
- package/src/build/canonical-target.js +35 -2
- 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 +6 -11
- 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 +155 -32
- package/src/build/native-platform-capabilities.js +755 -210
- 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/config-api.js +6 -0
- package/src/config-schema.json +30 -0
- package/src/index.ts +25 -0
- package/src/javascript/jwt-verifier.js +14 -4
- package/src/javascript/logging.js +11 -2
- package/src/javascript/package-effects.js +5 -2
- package/src/javascript/runtime-host.js +7 -3
- package/src/javascript/source-package.js +4 -2
- package/src/javascript/target-support-policy.js +20 -3
- package/src/javascript/target.js +17 -1
- package/src/javascript/test-runtime.js +1 -0
- package/src/provider-contract.js +22 -6
- package/src/runtime/canonical-api-runtime.js +6 -0
- package/src/testing/conditional-kv-host.js +155 -0
- package/src/testing/native-dynamic-backends-host.js +39 -0
- package/src/testing/native-http-effects-host.js +9 -1
- package/src/testing/native-platform-capabilities-host.js +349 -24
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +16 -0
- package/src/toolchain/index.js +48 -5
- package/src/toolchain/logging.js +18 -0
- package/src/toolchain/native-execution.js +78 -0
- package/src/toolchain/s3.js +26 -0
|
@@ -4,13 +4,14 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const FASTLY_STATUS_OK = 0;
|
|
7
|
+
const FASTLY_STATUS_INVALID_ARGUMENT = 2;
|
|
7
8
|
const FASTLY_STATUS_ERROR = 1;
|
|
8
9
|
const FASTLY_STATUS_BADF = 3;
|
|
9
10
|
const FASTLY_STATUS_BUFLEN = 4;
|
|
10
11
|
const FASTLY_STATUS_NONE = 10;
|
|
11
12
|
const FASTLY_KV_ERROR_OK = 1;
|
|
12
13
|
const FASTLY_KV_ERROR_NOT_FOUND = 3;
|
|
13
|
-
const SENSITIVE_HEADERS = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'x-auth-token', 'cookie', 'set-cookie']);
|
|
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']);
|
|
14
15
|
|
|
15
16
|
class FastlyNativePlatformCapabilitiesMockError extends Error {
|
|
16
17
|
constructor(message, code = 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_FAILED', detail = {}) {
|
|
@@ -113,6 +114,8 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
113
114
|
const url = String(requestInput.url || `https://pulse.test${pathValue.startsWith('/') ? pathValue : `/${pathValue}`}`);
|
|
114
115
|
const requestHeaders = normalizeHeaders(requestInput.headers);
|
|
115
116
|
const trace = [];
|
|
117
|
+
let monotonicMs = 0;
|
|
118
|
+
const privateUrls = new Set();
|
|
116
119
|
let nextHandle = 10;
|
|
117
120
|
const typedHandleStarts = options.handleStarts && typeof options.handleStarts === 'object' ? options.handleStarts : {};
|
|
118
121
|
const typedHandleNext = new Map();
|
|
@@ -127,7 +130,7 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
127
130
|
const secretStores = normalizeNamedStores(options.secretStores, options.secretStore || 'app_secrets', options.secrets);
|
|
128
131
|
const secretValues = [...secretStores.values()].flatMap((values) => [...values.values()].map((value) => String(value))).filter(Boolean);
|
|
129
132
|
const redactText = (value) => secretValues.reduce((output, secret) => output.split(secret).join('[REDACTED]'), String(value));
|
|
130
|
-
const redactRuntimeHeaders = (headers) => headers.map(([name, value]) => [name, SENSITIVE_HEADERS.has(String(name).toLowerCase()) ? '[REDACTED]' : redactText(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)]);
|
|
131
134
|
const kvStores = normalizeNamedStores(options.kvStores, options.kvStore || 'app_sessions', undefined);
|
|
132
135
|
for (const values of kvStores.values()) for (const [key, value] of values) values.set(key, encodeKvValue(value));
|
|
133
136
|
const configHandles = new Map();
|
|
@@ -141,7 +144,8 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
141
144
|
const downstreamRequestHandle = 1;
|
|
142
145
|
const downstreamBodyHandle = 2;
|
|
143
146
|
requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
|
|
144
|
-
bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: []
|
|
147
|
+
bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: [],
|
|
148
|
+
readyAt: monotonicMs + Number(requestInput.readyDelayMs || 0), fixture: requestInput });
|
|
145
149
|
let downstream;
|
|
146
150
|
|
|
147
151
|
function alloc(kind = 'default') {
|
|
@@ -170,6 +174,8 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
170
174
|
function readBytes(pointer, length) { return Buffer.from(new Uint8Array(memory().buffer, Number(pointer), Number(length))); }
|
|
171
175
|
function readUtf8(pointer, length) { return readBytes(pointer, length).toString('utf8'); }
|
|
172
176
|
|
|
177
|
+
const dynamicBackends = require('./native-dynamic-backends-host.js').createDynamicBackendHost({ view, readUtf8, trace, options, beforeRegister: () => { monotonicMs += Number(options.dynamicBackendRegistrationMs || 0); } });
|
|
178
|
+
|
|
173
179
|
function writeUtf8(value, pointer, maxLength, writtenOut) {
|
|
174
180
|
const bytes = Buffer.from(String(value), 'utf8');
|
|
175
181
|
writeU32(writtenOut, bytes.length);
|
|
@@ -201,22 +207,152 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
201
207
|
body = JSON.stringify(normalized.json);
|
|
202
208
|
if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
203
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
|
+
}
|
|
204
219
|
responses.set(responseHandle, {
|
|
205
220
|
status: Number(normalized.status || 200),
|
|
206
221
|
headers,
|
|
207
222
|
origin: true
|
|
208
223
|
});
|
|
209
|
-
bodies.set(bodyHandle, { bytes: normalizeBody(body), readOffset: 0, writes: [] });
|
|
224
|
+
bodies.set(bodyHandle, { bytes: normalizeBody(body), readOffset: 0, writes: [], fixture: normalized, readyAt: monotonicMs + Number(normalized.bodyDelayMs || 0) });
|
|
210
225
|
return { responseHandle, bodyHandle };
|
|
211
226
|
}
|
|
212
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
|
+
}
|
|
213
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
|
+
},
|
|
214
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
|
+
},
|
|
215
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
|
+
}
|
|
216
352
|
const seconds = Number.isFinite(Number(options.clockUnixSeconds))
|
|
217
353
|
? Number(options.clockUnixSeconds)
|
|
218
354
|
: Math.floor(Date.now() / 1000);
|
|
219
|
-
writeU64(timeOut, BigInt(Math.trunc(seconds *
|
|
355
|
+
writeU64(timeOut, BigInt(Math.trunc((Number(clockId) === 1 ? monotonicMs : seconds * 1000 + monotonicMs) * 1_000_000)));
|
|
220
356
|
trace.push({
|
|
221
357
|
module: 'wasi_snapshot_preview1',
|
|
222
358
|
name: 'clock_time_get',
|
|
@@ -241,7 +377,12 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
241
377
|
fastly_log: {
|
|
242
378
|
endpoint_get(namePointer, nameLength, handleOut) {
|
|
243
379
|
const endpoint = readUtf8(namePointer, nameLength);
|
|
244
|
-
|
|
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))) {
|
|
245
386
|
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, opened: false });
|
|
246
387
|
return FASTLY_STATUS_ERROR;
|
|
247
388
|
}
|
|
@@ -258,14 +399,20 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
258
399
|
trace.push({ module: 'fastly_log', name: 'write', endpoint, accepted: false });
|
|
259
400
|
return FASTLY_STATUS_ERROR;
|
|
260
401
|
}
|
|
261
|
-
const
|
|
262
|
-
|
|
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);
|
|
263
409
|
logs.push(Object.freeze({ endpoint, message }));
|
|
264
410
|
trace.push({ module: 'fastly_log', name: 'write', endpoint, message, accepted: true });
|
|
265
411
|
return FASTLY_STATUS_OK;
|
|
266
412
|
}
|
|
267
413
|
},
|
|
268
414
|
fastly_http_req: {
|
|
415
|
+
register_dynamic_backend: dynamicBackends.register,
|
|
269
416
|
body_downstream_get(requestOut, bodyOut) {
|
|
270
417
|
trace.push({ module: 'fastly_http_req', name: 'body_downstream_get' });
|
|
271
418
|
writeU32(requestOut, downstreamRequestHandle);
|
|
@@ -282,13 +429,27 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
282
429
|
trace.push({ module: 'fastly_http_req', name: 'uri_get', handle: Number(handle) });
|
|
283
430
|
return request ? writeUtf8(request.url, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
|
|
284
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
|
+
},
|
|
285
446
|
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
286
447
|
const request = requests.get(Number(handle));
|
|
287
448
|
const name = readUtf8(namePointer, nameLength);
|
|
288
449
|
trace.push({ module: 'fastly_http_req', name: 'header_value_get', handle: Number(handle), header: name });
|
|
289
450
|
if (!request) return FASTLY_STATUS_BADF;
|
|
290
451
|
const value = headerValue(request.headers, name);
|
|
291
|
-
return value === undefined ?
|
|
452
|
+
return value === undefined ? FASTLY_STATUS_INVALID_ARGUMENT : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
292
453
|
},
|
|
293
454
|
new(handleOut) {
|
|
294
455
|
const handle = alloc('request');
|
|
@@ -316,9 +477,46 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
316
477
|
if (!request) return FASTLY_STATUS_BADF;
|
|
317
478
|
const name = readUtf8(namePointer, nameLength);
|
|
318
479
|
const value = readUtf8(valuePointer, valueLength);
|
|
480
|
+
if (name.toLowerCase() === 'authorization' && value.includes('/s3/aws4_request')) privateUrls.add(request.url);
|
|
319
481
|
request.headers = request.headers.filter(([headerName]) => headerName.toLowerCase() !== name.toLowerCase());
|
|
320
482
|
request.headers.push([name, value]);
|
|
321
|
-
trace.push({ module: 'fastly_http_req', name: 'header_insert', handle: Number(handle), header: [name,
|
|
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});
|
|
322
520
|
return FASTLY_STATUS_OK;
|
|
323
521
|
},
|
|
324
522
|
send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
@@ -326,25 +524,37 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
326
524
|
const body = bodies.get(Number(bodyHandle));
|
|
327
525
|
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
328
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);
|
|
329
530
|
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
330
531
|
if (!fixture) {
|
|
331
|
-
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: request.url, missingFixture: true });
|
|
532
|
+
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: signedS3 ? '[REDACTED]' : request.url, missingFixture: true });
|
|
332
533
|
return FASTLY_STATUS_ERROR;
|
|
333
534
|
}
|
|
334
535
|
const pendingHandle = alloc('pending');
|
|
335
536
|
const capturedBody = bodyBytes(bodyHandle);
|
|
336
|
-
pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: capturedBody }, backend, fixture });
|
|
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));
|
|
337
542
|
outboundRequests.push(Object.freeze({
|
|
338
543
|
backend,
|
|
339
544
|
method: request.method,
|
|
340
|
-
url: redactText(request.url),
|
|
341
|
-
headers: Object.freeze(redactRuntimeHeaders(request.headers).map((header) => Object.freeze(header))),
|
|
342
|
-
body: redactText(capturedBody.toString('utf8'))
|
|
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'))
|
|
343
548
|
}));
|
|
344
549
|
writeU32(pendingOut, pendingHandle);
|
|
345
|
-
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), bodyBytes: capturedBody.length });
|
|
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 });
|
|
346
551
|
return FASTLY_STATUS_OK;
|
|
347
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
|
+
},
|
|
348
558
|
pending_req_wait(handle, responseOut, bodyOut) {
|
|
349
559
|
const item = pending.get(Number(handle));
|
|
350
560
|
trace.push({ module: 'fastly_http_req', name: 'pending_req_wait', pendingHandle: Number(handle), backend: item && item.backend, url: item && item.request.url });
|
|
@@ -384,6 +594,8 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
384
594
|
const handle = alloc('secretStore'); secretStoreHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
385
595
|
},
|
|
386
596
|
get(handle, keyPointer, keyLength, secretOut) {
|
|
597
|
+
monotonicMs += Number(options.secretDelayMs || 0);
|
|
598
|
+
if (typeof options.onSecretLookup === 'function') options.onSecretLookup();
|
|
387
599
|
const values = secretStoreHandles.get(Number(handle));
|
|
388
600
|
const key = readUtf8(keyPointer, keyLength);
|
|
389
601
|
const value = values && values.get(key);
|
|
@@ -447,6 +659,16 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
447
659
|
}
|
|
448
660
|
},
|
|
449
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
|
+
},
|
|
450
672
|
new(handleOut) {
|
|
451
673
|
const handle = alloc('response');
|
|
452
674
|
responses.set(handle, { status: 200, headers: [], origin: false });
|
|
@@ -463,13 +685,21 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
463
685
|
trace.push({ module: 'fastly_http_resp', name: 'header_append', handle: Number(handle), header: [name, value] });
|
|
464
686
|
return FASTLY_STATUS_OK;
|
|
465
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
|
+
},
|
|
466
696
|
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
467
697
|
const response = responses.get(Number(handle));
|
|
468
698
|
const name = readUtf8(namePointer, nameLength);
|
|
469
699
|
trace.push({ module: 'fastly_http_resp', name: 'header_value_get', handle: Number(handle), header: name });
|
|
470
700
|
if (!response) return FASTLY_STATUS_BADF;
|
|
471
701
|
const value = headerValue(response.headers, name);
|
|
472
|
-
return value === undefined ?
|
|
702
|
+
return value === undefined ? FASTLY_STATUS_INVALID_ARGUMENT : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
473
703
|
},
|
|
474
704
|
status_get(handle, statusOut) {
|
|
475
705
|
const response = responses.get(Number(handle));
|
|
@@ -499,10 +729,30 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
499
729
|
responseHandle: Number(handle),
|
|
500
730
|
bodyHandle: Number(bodyHandle)
|
|
501
731
|
};
|
|
732
|
+
if (Number(streaming) === 1) responses.delete(Number(handle));
|
|
502
733
|
return FASTLY_STATUS_OK;
|
|
503
734
|
}
|
|
504
735
|
},
|
|
505
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
|
+
},
|
|
506
756
|
new(handleOut) {
|
|
507
757
|
const handle = alloc('body');
|
|
508
758
|
bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
|
|
@@ -513,33 +763,81 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
513
763
|
read(handle, buffer, bufferLength, readOut) {
|
|
514
764
|
const body = bodies.get(Number(handle));
|
|
515
765
|
if (!body) return FASTLY_STATUS_BADF;
|
|
766
|
+
if (body.fixture && body.fixture.bodyReadStatus) return Number(body.fixture.bodyReadStatus);
|
|
516
767
|
const source = bodyBytes(handle);
|
|
517
768
|
const offset = body.readOffset || 0;
|
|
518
|
-
const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset));
|
|
769
|
+
const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset), Number(body.fixture && body.fixture.chunkBytes || 65536));
|
|
519
770
|
if (count > 0) new Uint8Array(memory().buffer, Number(buffer), count).set(source.subarray(offset, offset + count));
|
|
520
771
|
body.readOffset = offset + count;
|
|
772
|
+
body.readyAt = monotonicMs + Number(body.fixture && body.fixture.chunkDelayMs || 0);
|
|
521
773
|
writeU32(readOut, count);
|
|
522
|
-
trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: 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} : {}) });
|
|
523
775
|
return FASTLY_STATUS_OK;
|
|
524
776
|
},
|
|
525
777
|
write(handle, buffer, bufferLength, end, writtenOut) {
|
|
526
778
|
const body = bodies.get(Number(handle));
|
|
527
779
|
if (!body) return FASTLY_STATUS_BADF;
|
|
528
|
-
|
|
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);
|
|
529
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);
|
|
530
787
|
writeU32(writtenOut, bytes.length);
|
|
531
|
-
trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end) });
|
|
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} : {}) });
|
|
532
789
|
return FASTLY_STATUS_OK;
|
|
533
790
|
}
|
|
534
791
|
}
|
|
535
792
|
};
|
|
536
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
|
+
}
|
|
537
815
|
const module = new WebAssembly.Module(wasm);
|
|
538
816
|
instance = new WebAssembly.Instance(module, imports);
|
|
539
817
|
if (typeof instance.exports._start !== 'function') {
|
|
540
818
|
throw new FastlyNativePlatformCapabilitiesMockError('Pass98 native module is missing _start.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_START_MISSING');
|
|
541
819
|
}
|
|
542
|
-
instance.exports.
|
|
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]';
|
|
543
841
|
const lastError = typeof instance.exports.pulse_fastly_last_error === 'function'
|
|
544
842
|
? Number(instance.exports.pulse_fastly_last_error())
|
|
545
843
|
: undefined;
|
|
@@ -552,17 +850,32 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
552
850
|
const pulseLastError = typeof instance.exports.pulse_last_error_code === 'function'
|
|
553
851
|
? Number(instance.exports.pulse_last_error_code())
|
|
554
852
|
: undefined;
|
|
853
|
+
if (downstream?.streaming && !downstream.finished) { downstream.bodyBytes = bodyBytes(downstream.bodyHandle) || downstream.bodyBytes; downstream.aborted = true; }
|
|
555
854
|
const handledJwtError = options.allowHandledJwtError === true
|
|
556
855
|
&& lastError === 1008
|
|
557
856
|
&& Boolean(downstream);
|
|
558
|
-
|
|
559
|
-
|
|
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 });
|
|
560
872
|
}
|
|
561
873
|
if (!downstream) {
|
|
562
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 });
|
|
563
875
|
}
|
|
564
876
|
|
|
565
877
|
return Object.freeze({
|
|
878
|
+
...callableEvidence(),
|
|
566
879
|
version: 'pulse.fastly-native-platform-capabilities-mock.v1',
|
|
567
880
|
request: Object.freeze({ method, url, path: pathValue, headers: Object.freeze(requestHeaders.map((entry) => Object.freeze(entry))) }),
|
|
568
881
|
response: Object.freeze({
|
|
@@ -572,6 +885,7 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
572
885
|
bodyBytes: downstream.bodyBytes,
|
|
573
886
|
sent: true,
|
|
574
887
|
streaming: downstream.streaming,
|
|
888
|
+
aborted: downstream.aborted === true,
|
|
575
889
|
origin: downstream.origin,
|
|
576
890
|
responseHandle: downstream.responseHandle,
|
|
577
891
|
bodyHandle: downstream.bodyHandle
|
|
@@ -580,6 +894,17 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
580
894
|
logs: Object.freeze(logs),
|
|
581
895
|
outboundRequests: Object.freeze(outboundRequests),
|
|
582
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
|
+
}),
|
|
583
908
|
instance
|
|
584
909
|
});
|
|
585
910
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_BODY_BYTES = 1048576;
|
|
4
|
+
function forwardingError(message) {
|
|
5
|
+
const error = new TypeError(message);
|
|
6
|
+
error.code = 'PULSE_FASTLY_BODY_FORWARDING_INVALID';
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
function normalizeBodyForwarding(value, maxDurationMs) {
|
|
10
|
+
if (value === undefined) return undefined;
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
12
|
+
|| Object.keys(value).some(key => !['requestMaxBytes', 'responseMaxBytes'].includes(key))) {
|
|
13
|
+
throw forwardingError('fastly.bodyForwarding requires requestMaxBytes and responseMaxBytes.');
|
|
14
|
+
}
|
|
15
|
+
for (const key of ['requestMaxBytes', 'responseMaxBytes']) {
|
|
16
|
+
if (!Number.isInteger(value[key]) || value[key] < 1 || value[key] > MAX_BODY_BYTES) {
|
|
17
|
+
throw forwardingError(`fastly.bodyForwarding.${key} must be an integer from 1 to ${MAX_BODY_BYTES}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isInteger(maxDurationMs) || maxDurationMs < 1 || maxDurationMs > 30000) {
|
|
21
|
+
throw forwardingError('fastly.bodyForwarding requires maxDurationMs from 1 to 30000.');
|
|
22
|
+
}
|
|
23
|
+
return Object.freeze({requestMaxBytes: value.requestMaxBytes, responseMaxBytes: value.responseMaxBytes});
|
|
24
|
+
}
|
|
25
|
+
module.exports = {normalizeBodyForwarding, forwardingError, MAX_BODY_BYTES};
|