@pulse-compute/provider-fastly 1.0.0-beta.1 → 1.0.0-beta.11
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 +41 -2
- package/src/build/digest-native.js +25 -0
- package/src/build/effect-invocations.js +51 -0
- package/src/build/embedded-assets.as.ts +32 -0
- package/src/build/entities-native.js +6 -11
- package/src/build/host-extensions.js +254 -0
- package/src/build/kv-native.as.ts +331 -0
- package/src/build/kv-native.js +27 -0
- package/src/build/lifecycle.js +50 -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 +325 -0
- package/src/build/native-body-forwarding.js +73 -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-accounting.as.ts +133 -0
- package/src/build/native-fetch-accounting.js +84 -0
- package/src/build/native-fetch-bounds.js +47 -0
- package/src/build/native-fetch-cohorts.as.ts +126 -0
- package/src/build/native-fetch-cohorts.js +65 -0
- package/src/build/native-fetch-deadlines.as.ts +169 -0
- package/src/build/native-fetch-deadlines.js +36 -0
- package/src/build/native-fetch-errors.js +38 -0
- package/src/build/native-fetch-headers.js +52 -0
- package/src/build/native-fetch-outcomes.js +87 -0
- package/src/build/native-fetch-timing.as.ts +74 -0
- package/src/build/native-fetch-timing.js +50 -0
- package/src/build/native-forwarding-accounting.as.ts +153 -0
- package/src/build/native-forwarding-accounting.js +47 -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 +164 -35
- package/src/build/native-platform-capabilities.js +815 -220
- package/src/build/native-request-body.js +45 -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-source.js +58 -0
- package/src/build/request-budget.js +46 -0
- package/src/build/response-authority.js +56 -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/fetch-adapter.js +3 -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 +8 -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 +23 -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 +18 -1
- package/src/testing/native-platform-capabilities-host.js +359 -24
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +16 -0
- package/src/toolchain/index.js +49 -5
- package/src/toolchain/logging.js +18 -0
- package/src/toolchain/native-execution.js +83 -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,56 @@ 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
|
+
header_append(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
487
|
+
const request = requests.get(Number(handle));
|
|
488
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
489
|
+
const name = readUtf8(namePointer, nameLength);
|
|
490
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
491
|
+
if (name.toLowerCase() === 'authorization' && value.includes('/s3/aws4_request')) privateUrls.add(request.url);
|
|
492
|
+
request.headers.push([name, value]);
|
|
493
|
+
trace.push({ module: 'fastly_http_req', name: 'header_append', handle: Number(handle), header: redactRuntimeHeaders([[name, value]], privateUrls.has(request.url))[0] });
|
|
494
|
+
return FASTLY_STATUS_OK;
|
|
495
|
+
},
|
|
496
|
+
cache_override_set(handle, tag, ttl, swr) {
|
|
497
|
+
const request = requests.get(Number(handle)); if (!request) return FASTLY_STATUS_BADF;
|
|
498
|
+
request.cacheOverride = Number(tag); return FASTLY_STATUS_OK;
|
|
499
|
+
},
|
|
500
|
+
auto_decompress_response_set(handle, encodings) {
|
|
501
|
+
const request = requests.get(Number(handle)); if (!request) return FASTLY_STATUS_BADF;
|
|
502
|
+
request.decompression = Number(encodings); return FASTLY_STATUS_OK;
|
|
503
|
+
},
|
|
504
|
+
close(handle) { return requests.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
|
|
505
|
+
pending_req_poll_v2(handle, detail, done, responseOut, bodyOut) {
|
|
506
|
+
const item = pending.get(Number(handle)); if (!item) return FASTLY_STATUS_BADF;
|
|
507
|
+
writeSendErrorDetail(item, handle, detail, [done, responseOut, bodyOut], 'pending_req_poll_v2');
|
|
508
|
+
// Exercise a readiness race: select/is_ready can precede a not-ready poll.
|
|
509
|
+
if (monotonicMs >= item.readyAt && item.fixture.pollNotReadyMs && !item.delayedPoll) {
|
|
510
|
+
item.delayedPoll = true; item.readyAt = monotonicMs + Number(item.fixture.pollNotReadyMs);
|
|
511
|
+
}
|
|
512
|
+
if (monotonicMs < item.readyAt) { writeU32(done, 0); return FASTLY_STATUS_OK; }
|
|
513
|
+
writeU32(done, 1); return imports.fastly_http_req.pending_req_wait(handle, responseOut, bodyOut);
|
|
514
|
+
},
|
|
515
|
+
send_async_streaming(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
516
|
+
const request = requests.get(Number(handle)), body = bodies.get(Number(bodyHandle));
|
|
517
|
+
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
518
|
+
const backend = readUtf8(backendPointer, backendLength);
|
|
519
|
+
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
520
|
+
requests.delete(Number(handle));
|
|
521
|
+
if (!fixture || fixture.sendStatus) { bodies.delete(Number(bodyHandle)); return Number(fixture?.sendStatus || 1); }
|
|
522
|
+
const pendingHandle = alloc('pending');
|
|
523
|
+
const record = {backend, method:request.method, url:redactText(request.url), headers:redactRuntimeHeaders(request.headers), bodyBytes:Buffer.alloc(0)};
|
|
524
|
+
outboundRequests.push(record);
|
|
525
|
+
const item = {request, backend, fixture, readyAt:fixture.earlyResponse ? monotonicMs + Number(fixture.delayMs || 0) : Infinity};
|
|
526
|
+
pending.set(pendingHandle, item);
|
|
527
|
+
body.streamingUpload = {item, record}; body.readyAt = monotonicMs + Number(options.uploadReadyDelayMs || 0);
|
|
528
|
+
writeU32(pendingOut, pendingHandle);
|
|
529
|
+
trace.push({module:'fastly_http_req',name:'send_async_streaming',bodyHandle:Number(bodyHandle),pendingHandle});
|
|
322
530
|
return FASTLY_STATUS_OK;
|
|
323
531
|
},
|
|
324
532
|
send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
@@ -326,25 +534,37 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
326
534
|
const body = bodies.get(Number(bodyHandle));
|
|
327
535
|
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
328
536
|
const backend = readUtf8(backendPointer, backendLength);
|
|
537
|
+
dynamicBackends.route(backend, request);
|
|
538
|
+
const signedS3 = request.headers.some(([name, value]) => name.toLowerCase() === 'authorization' && value.startsWith('AWS4-HMAC-SHA256 '));
|
|
539
|
+
if (signedS3) privateUrls.add(request.url);
|
|
329
540
|
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
330
541
|
if (!fixture) {
|
|
331
|
-
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: request.url, missingFixture: true });
|
|
542
|
+
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: signedS3 ? '[REDACTED]' : request.url, missingFixture: true });
|
|
332
543
|
return FASTLY_STATUS_ERROR;
|
|
333
544
|
}
|
|
334
545
|
const pendingHandle = alloc('pending');
|
|
335
546
|
const capturedBody = bodyBytes(bodyHandle);
|
|
336
|
-
pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: capturedBody }, backend, fixture });
|
|
547
|
+
pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: capturedBody }, backend, fixture, readyAt: fixture.neverHeaders ? Infinity : monotonicMs + Number(fixture.delayMs || 0) });
|
|
548
|
+
if (typeof options.onOutboundRequest === 'function') options.onOutboundRequest({ ...request, backend, headers: request.headers.map((entry) => [...entry]), body: capturedBody });
|
|
549
|
+
if (fixture.sendStatus) { pending.delete(pendingHandle); return Number(fixture.sendStatus); }
|
|
550
|
+
if (request.headers.some(([name, value]) => name.toLowerCase() === 'authorization' && value.startsWith('AWS4-HMAC-SHA256 '))) privateUrls.add(request.url);
|
|
551
|
+
requests.delete(Number(handle)); bodies.delete(Number(bodyHandle));
|
|
337
552
|
outboundRequests.push(Object.freeze({
|
|
338
553
|
backend,
|
|
339
554
|
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'))
|
|
555
|
+
url: privateUrls.has(request.url) ? '[REDACTED]' : redactText(request.url),
|
|
556
|
+
headers: Object.freeze(redactRuntimeHeaders(request.headers, signedS3).map((header) => Object.freeze(header))),
|
|
557
|
+
body: signedS3 ? '[REDACTED]' : redactText(capturedBody.toString('utf8'))
|
|
343
558
|
}));
|
|
344
559
|
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 });
|
|
560
|
+
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
561
|
return FASTLY_STATUS_OK;
|
|
347
562
|
},
|
|
563
|
+
pending_req_wait_v2(handle, detail, responseOut, bodyOut) {
|
|
564
|
+
const item = pending.get(Number(handle)); if (!item) return FASTLY_STATUS_BADF;
|
|
565
|
+
writeSendErrorDetail(item, handle, detail, [responseOut, bodyOut], 'pending_req_wait_v2');
|
|
566
|
+
return imports.fastly_http_req.pending_req_wait(handle, responseOut, bodyOut);
|
|
567
|
+
},
|
|
348
568
|
pending_req_wait(handle, responseOut, bodyOut) {
|
|
349
569
|
const item = pending.get(Number(handle));
|
|
350
570
|
trace.push({ module: 'fastly_http_req', name: 'pending_req_wait', pendingHandle: Number(handle), backend: item && item.backend, url: item && item.request.url });
|
|
@@ -384,6 +604,8 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
384
604
|
const handle = alloc('secretStore'); secretStoreHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
385
605
|
},
|
|
386
606
|
get(handle, keyPointer, keyLength, secretOut) {
|
|
607
|
+
monotonicMs += Number(options.secretDelayMs || 0);
|
|
608
|
+
if (typeof options.onSecretLookup === 'function') options.onSecretLookup();
|
|
387
609
|
const values = secretStoreHandles.get(Number(handle));
|
|
388
610
|
const key = readUtf8(keyPointer, keyLength);
|
|
389
611
|
const value = values && values.get(key);
|
|
@@ -447,6 +669,16 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
447
669
|
}
|
|
448
670
|
},
|
|
449
671
|
fastly_http_resp: {
|
|
672
|
+
close(handle) { trace.push({ module: 'fastly_http_resp', name: 'close' }); return responses.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
|
|
673
|
+
header_names_get(handle, buffer, size, cursor, end, written) {
|
|
674
|
+
const response = responses.get(Number(handle)); if (!response || cursor !== 0) return FASTLY_STATUS_BADF;
|
|
675
|
+
return headerList([...new Set(response.headers.map(([name]) => name.toLowerCase()))], buffer, size, end, written);
|
|
676
|
+
},
|
|
677
|
+
header_values_get(handle, pointer, length, buffer, size, cursor, end, written) {
|
|
678
|
+
const response = responses.get(Number(handle)); if (!response || cursor !== 0) return FASTLY_STATUS_BADF;
|
|
679
|
+
const name = readUtf8(pointer, length).toLowerCase();
|
|
680
|
+
return headerList(response.headers.filter(([key]) => key.toLowerCase() === name).map(([, value]) => value), buffer, size, end, written);
|
|
681
|
+
},
|
|
450
682
|
new(handleOut) {
|
|
451
683
|
const handle = alloc('response');
|
|
452
684
|
responses.set(handle, { status: 200, headers: [], origin: false });
|
|
@@ -463,13 +695,21 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
463
695
|
trace.push({ module: 'fastly_http_resp', name: 'header_append', handle: Number(handle), header: [name, value] });
|
|
464
696
|
return FASTLY_STATUS_OK;
|
|
465
697
|
},
|
|
698
|
+
header_values_get(handle, pointer, length, buffer, size, cursor, end, written) {
|
|
699
|
+
trace.push({module:'fastly_http_resp', name:'header_values_get', cursor:Number(cursor)});
|
|
700
|
+
const response = responses.get(Number(handle));
|
|
701
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
702
|
+
const name = readUtf8(pointer, length).toLowerCase();
|
|
703
|
+
if (!/^[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name)) return FASTLY_STATUS_INVALID_ARGUMENT;
|
|
704
|
+
return requestHeaderList(response.headers.filter(([key]) => key.toLowerCase() === name).map(([, value]) => value), buffer, size, cursor, end, written, 'response-values', 'response');
|
|
705
|
+
},
|
|
466
706
|
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
467
707
|
const response = responses.get(Number(handle));
|
|
468
708
|
const name = readUtf8(namePointer, nameLength);
|
|
469
709
|
trace.push({ module: 'fastly_http_resp', name: 'header_value_get', handle: Number(handle), header: name });
|
|
470
710
|
if (!response) return FASTLY_STATUS_BADF;
|
|
471
711
|
const value = headerValue(response.headers, name);
|
|
472
|
-
return value === undefined ?
|
|
712
|
+
return value === undefined ? FASTLY_STATUS_INVALID_ARGUMENT : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
473
713
|
},
|
|
474
714
|
status_get(handle, statusOut) {
|
|
475
715
|
const response = responses.get(Number(handle));
|
|
@@ -499,10 +739,30 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
499
739
|
responseHandle: Number(handle),
|
|
500
740
|
bodyHandle: Number(bodyHandle)
|
|
501
741
|
};
|
|
742
|
+
if (Number(streaming) === 1) responses.delete(Number(handle));
|
|
502
743
|
return FASTLY_STATUS_OK;
|
|
503
744
|
}
|
|
504
745
|
},
|
|
505
746
|
fastly_http_body: {
|
|
747
|
+
close(handle) {
|
|
748
|
+
trace.push({module:'fastly_http_body',name:'close',handle:Number(handle)});
|
|
749
|
+
const body = bodies.get(Number(handle));
|
|
750
|
+
if (body?.streamingUpload) {
|
|
751
|
+
const {item,record} = body.streamingUpload;
|
|
752
|
+
record.bodyBytes = bodyBytes(handle);
|
|
753
|
+
if (item.fixture.echoBody) item.fixture = {...item.fixture,body:record.bodyBytes};
|
|
754
|
+
item.readyAt = item.fixture.neverHeaders ? Infinity : monotonicMs + Number(item.fixture.delayMs || 0);
|
|
755
|
+
}
|
|
756
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) { downstream.bodyBytes = bodyBytes(handle); downstream.finished = true; }
|
|
757
|
+
return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF;
|
|
758
|
+
},
|
|
759
|
+
abandon(handle) {
|
|
760
|
+
trace.push({module:'fastly_http_body',name:'abandon',handle:Number(handle)});
|
|
761
|
+
const body = bodies.get(Number(handle));
|
|
762
|
+
if (body?.streamingUpload) { body.streamingUpload.record.bodyBytes = bodyBytes(handle); body.streamingUpload.record.aborted = true; }
|
|
763
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) { downstream.bodyBytes = bodyBytes(handle); downstream.aborted = true; }
|
|
764
|
+
return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF;
|
|
765
|
+
},
|
|
506
766
|
new(handleOut) {
|
|
507
767
|
const handle = alloc('body');
|
|
508
768
|
bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
|
|
@@ -513,33 +773,81 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
513
773
|
read(handle, buffer, bufferLength, readOut) {
|
|
514
774
|
const body = bodies.get(Number(handle));
|
|
515
775
|
if (!body) return FASTLY_STATUS_BADF;
|
|
776
|
+
if (body.fixture && body.fixture.bodyReadStatus) return Number(body.fixture.bodyReadStatus);
|
|
516
777
|
const source = bodyBytes(handle);
|
|
517
778
|
const offset = body.readOffset || 0;
|
|
518
|
-
const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset));
|
|
779
|
+
const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset), Number(body.fixture && body.fixture.chunkBytes || 65536));
|
|
519
780
|
if (count > 0) new Uint8Array(memory().buffer, Number(buffer), count).set(source.subarray(offset, offset + count));
|
|
520
781
|
body.readOffset = offset + count;
|
|
782
|
+
body.readyAt = monotonicMs + Number(body.fixture && body.fixture.chunkDelayMs || 0);
|
|
521
783
|
writeU32(readOut, count);
|
|
522
|
-
trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: count });
|
|
784
|
+
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
785
|
return FASTLY_STATUS_OK;
|
|
524
786
|
},
|
|
525
787
|
write(handle, buffer, bufferLength, end, writtenOut) {
|
|
526
788
|
const body = bodies.get(Number(handle));
|
|
527
789
|
if (!body) return FASTLY_STATUS_BADF;
|
|
528
|
-
|
|
790
|
+
if (downstream?.streaming && Number(handle) === downstream.bodyHandle && options.downstreamBodyWriteStatus) return Number(options.downstreamBodyWriteStatus);
|
|
791
|
+
if ((requests.size > 1 || body.streamingUpload) && options.outboundBodyWriteStatus) return Number(options.outboundBodyWriteStatus);
|
|
792
|
+
const length = Math.min(Number(bufferLength), Number(options.bodyWriteChunkBytes ?? bufferLength));
|
|
793
|
+
const bytes = readBytes(buffer, length);
|
|
529
794
|
body.writes.push(bytes);
|
|
795
|
+
if (body.streamingUpload) body.readyAt = monotonicMs + Number(options.uploadWriteDelayMs || 0);
|
|
796
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) body.readyAt = monotonicMs + Number(options.downstreamWriteDelayMs || 0);
|
|
530
797
|
writeU32(writtenOut, bytes.length);
|
|
531
|
-
trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end) });
|
|
798
|
+
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
799
|
return FASTLY_STATUS_OK;
|
|
533
800
|
}
|
|
534
801
|
}
|
|
535
802
|
};
|
|
536
803
|
|
|
804
|
+
const kvEvidence = options.conditionalKv ? require('./conditional-kv-host.js').attachConditionalKvHost(imports, {
|
|
805
|
+
view, writeU32, writeU64, readBytes, readUtf8, alloc, bodies, bodyBytes, trace,
|
|
806
|
+
now: () => monotonicMs, advance: ms => { monotonicMs += ms; }
|
|
807
|
+
}, options.conditionalKv) : undefined;
|
|
808
|
+
|
|
809
|
+
// Model invocation termination, including cancellation raised by a host call.
|
|
810
|
+
// Throwing stops the Wasm invocation; it never fabricates an effect result.
|
|
811
|
+
const checkActive = () => { if (options.signal && options.signal.aborted) throw options.signal.reason || new Error('Request cancelled.'); };
|
|
812
|
+
checkActive();
|
|
813
|
+
// A test-only post-call hook can corrupt ABI outputs or advance the clock.
|
|
814
|
+
// It observes the real generated guest; it is never used by provider execution.
|
|
815
|
+
if (options.signal || options.hostcallDelayMs || options.afterHostcall) for (const [moduleName, namespace] of Object.entries(imports)) {
|
|
816
|
+
for (const [name, hostcall] of Object.entries(namespace)) if (typeof hostcall === 'function') namespace[name] = (...args) => {
|
|
817
|
+
checkActive(); const result = hostcall(...args);
|
|
818
|
+
monotonicMs += Number(options.hostcallDelayMs?.[`${moduleName}.${name}`] || 0);
|
|
819
|
+
const replacement = options.afterHostcall?.({module:moduleName,name,args,result,trace,
|
|
820
|
+
writeU32,writeU64,readU32:pointer=>view().getUint32(Number(pointer),true),
|
|
821
|
+
advance:ms=>{monotonicMs+=ms;}});
|
|
822
|
+
checkActive(); return replacement ?? result;
|
|
823
|
+
};
|
|
824
|
+
}
|
|
537
825
|
const module = new WebAssembly.Module(wasm);
|
|
538
826
|
instance = new WebAssembly.Instance(module, imports);
|
|
539
827
|
if (typeof instance.exports._start !== 'function') {
|
|
540
828
|
throw new FastlyNativePlatformCapabilitiesMockError('Pass98 native module is missing _start.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_START_MISSING');
|
|
541
829
|
}
|
|
542
|
-
instance.exports.
|
|
830
|
+
if (options.conditionalKv && options.conditionalKv.deadlineNs !== undefined) instance.exports.pulse_fastly_kv_request_deadline(BigInt(options.conditionalKv.deadlineNs));
|
|
831
|
+
const callableEvidence = () => input?.plan?.callables?.length ? {
|
|
832
|
+
guestMemoryBytes: instance.exports.memory.buffer.byteLength,
|
|
833
|
+
memory: { bytes: Number(instance.exports.pulse_fastly_memory_bytes()),
|
|
834
|
+
values: Number(instance.exports.pulse_fastly_memory_values()) },
|
|
835
|
+
responseStatus: downstream?.status
|
|
836
|
+
} : {};
|
|
837
|
+
try { instance.exports._start(); }
|
|
838
|
+
catch (error) {
|
|
839
|
+
if (Number(instance.exports.pulse_fastly_last_error?.()) !== 1010) {
|
|
840
|
+
Object.defineProperty(error, 'execution', { value: Object.freeze(callableEvidence()), configurable: true });
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Native retained-value budget exceeded.',
|
|
844
|
+
'PULSE_RUNTIME_MEMORY_LIMIT_EXCEEDED', {
|
|
845
|
+
...callableEvidence(), lastError: 1010, errorStage: Number(instance.exports.pulse_fastly_error_stage()), trace,
|
|
846
|
+
bytes: Number(instance.exports.pulse_fastly_memory_bytes()),
|
|
847
|
+
values: Number(instance.exports.pulse_fastly_memory_values())
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
for (const entry of trace) if (entry.url && privateUrls.has(entry.url)) entry.url = '[REDACTED]';
|
|
543
851
|
const lastError = typeof instance.exports.pulse_fastly_last_error === 'function'
|
|
544
852
|
? Number(instance.exports.pulse_fastly_last_error())
|
|
545
853
|
: undefined;
|
|
@@ -552,17 +860,32 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
552
860
|
const pulseLastError = typeof instance.exports.pulse_last_error_code === 'function'
|
|
553
861
|
? Number(instance.exports.pulse_last_error_code())
|
|
554
862
|
: undefined;
|
|
863
|
+
if (downstream?.streaming && !downstream.finished) { downstream.bodyBytes = bodyBytes(downstream.bodyHandle) || downstream.bodyBytes; downstream.aborted = true; }
|
|
555
864
|
const handledJwtError = options.allowHandledJwtError === true
|
|
556
865
|
&& lastError === 1008
|
|
557
866
|
&& Boolean(downstream);
|
|
558
|
-
|
|
559
|
-
|
|
867
|
+
const handledHeaderError = options.allowHandledHeaderError === true
|
|
868
|
+
&& lastError === 1003 && (errorStage === 22 || errorStage === 23) && downstream?.status === 500;
|
|
869
|
+
const handledTransportError = options.allowHandledTransportError === true
|
|
870
|
+
&& (errorStage === 77 || errorStage === 81)
|
|
871
|
+
&& ((lastError === 1006 && downstream?.status === 502) || (lastError === 1003 && downstream?.status === 500));
|
|
872
|
+
const handledDeadlineError = options.allowHandledFetchDeadlineError === true
|
|
873
|
+
&& ((lastError === 1006 && errorStage === 271 && downstream?.status === 504)
|
|
874
|
+
|| (lastError === 1001 && errorStage === 270 && downstream?.status === 400)
|
|
875
|
+
|| (lastError === 1003 && errorStage === 272 && downstream?.status === 500));
|
|
876
|
+
const handledDynamicBackendError = options.allowHandledDynamicBackendError === true
|
|
877
|
+
&& ((lastError === 1001 && errorStage === 273 && downstream?.status === 400)
|
|
878
|
+
|| (lastError === 1001 && errorStage === 276 && downstream?.status === 500)
|
|
879
|
+
|| (lastError === 1003 && (errorStage === 274 || errorStage === 275) && downstream?.status === 500));
|
|
880
|
+
if (lastError !== FASTLY_STATUS_OK && !handledDynamicBackendError && !handledDeadlineError && !handledTransportError && !handledHeaderError && !handledJwtError && !(options.allowAbortedResponse === true && downstream?.aborted)) {
|
|
881
|
+
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
882
|
}
|
|
561
883
|
if (!downstream) {
|
|
562
884
|
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
885
|
}
|
|
564
886
|
|
|
565
887
|
return Object.freeze({
|
|
888
|
+
...callableEvidence(),
|
|
566
889
|
version: 'pulse.fastly-native-platform-capabilities-mock.v1',
|
|
567
890
|
request: Object.freeze({ method, url, path: pathValue, headers: Object.freeze(requestHeaders.map((entry) => Object.freeze(entry))) }),
|
|
568
891
|
response: Object.freeze({
|
|
@@ -572,6 +895,7 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
572
895
|
bodyBytes: downstream.bodyBytes,
|
|
573
896
|
sent: true,
|
|
574
897
|
streaming: downstream.streaming,
|
|
898
|
+
aborted: downstream.aborted === true,
|
|
575
899
|
origin: downstream.origin,
|
|
576
900
|
responseHandle: downstream.responseHandle,
|
|
577
901
|
bodyHandle: downstream.bodyHandle
|
|
@@ -580,6 +904,17 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
580
904
|
logs: Object.freeze(logs),
|
|
581
905
|
outboundRequests: Object.freeze(outboundRequests),
|
|
582
906
|
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) }),
|
|
907
|
+
kvEvidence,
|
|
908
|
+
monotonicMs,
|
|
909
|
+
transportResources: Object.freeze({pendingRequests: pending.size,
|
|
910
|
+
outboundRequests: requests.size - Number(requests.has(downstreamRequestHandle))}),
|
|
911
|
+
// Non-streaming send transfers these two handles even though the fixture
|
|
912
|
+
// keeps their bytes for assertions. Pending work is counted separately:
|
|
913
|
+
// Fastly has no cancellation hostcall for an unresolved pending request.
|
|
914
|
+
forwardingResources: Object.freeze({
|
|
915
|
+
bodies: bodies.size - Number(!downstream?.streaming && bodies.has(downstream?.bodyHandle)),
|
|
916
|
+
responses: responses.size - Number(!downstream?.streaming && responses.has(downstream?.responseHandle))
|
|
917
|
+
}),
|
|
583
918
|
instance
|
|
584
919
|
});
|
|
585
920
|
}
|
|
@@ -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};
|