@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -1
- package/dist/config-schema.json +77 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -5
- package/src/build/canonical-target.js +436 -0
- package/src/build/entities-native.js +564 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/kv-native.as.ts +229 -0
- package/src/build/kv-native.js +20 -0
- package/src/build/native-http-effects.js +871 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2196 -0
- package/src/build/s3-native.as.ts +215 -0
- package/src/build/s3-native.js +30 -0
- package/src/compiler/assets-package-out-parity-proof.js +338 -0
- package/src/compiler/assets-provider-proof.js +499 -0
- package/src/compiler/config-provider-resolution.js +573 -0
- package/src/compiler/fastly-adapter.js +564 -0
- package/src/compiler/fastly-command-entry.js +390 -0
- package/src/compiler/fastly-config-secret-serve.js +323 -0
- package/src/compiler/fastly-hostcall-binding.js +598 -0
- package/src/compiler/fastly-readiness.js +477 -0
- package/src/compiler/index.js +40 -0
- package/src/compiler/lifecycle-parity-contract.js +70 -0
- package/src/compiler/lifecycle-parity-proof.js +260 -0
- package/src/compiler/route-handler-effect-proof.js +255 -0
- package/src/compiler/runtime-provider-kv.js +237 -0
- package/src/compiler/runtime-provider-shape.js +109 -0
- package/src/config-api.js +63 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +77 -0
- package/src/index.js +3 -0
- package/src/index.ts +118 -0
- package/src/javascript/assets-adapter.js +122 -0
- package/src/javascript/bindings-adapter.js +237 -0
- package/src/javascript/errors.js +26 -0
- package/src/javascript/fetch-adapter.js +93 -0
- package/src/javascript/grip-broadcast.js +261 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +26 -0
- package/src/javascript/package-effects.js +68 -0
- package/src/javascript/runtime-host.js +108 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +776 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +264 -0
- package/src/javascript/target.js +73 -0
- package/src/javascript/test-runtime.js +407 -0
- package/src/provider-contract.js +79 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +601 -0
- package/src/runtime/index.js +28 -0
- package/src/runtime/provider-primitives-contract.js +272 -0
- package/src/runtime/provider-primitives.js +82 -0
- package/src/runtime/route-handler-effects.js +507 -0
- package/src/testing/conditional-kv-host.js +109 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-http-effects-host.js +390 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +676 -0
- package/src/toolchain/config.js +131 -0
- package/src/toolchain/index.js +345 -0
- package/src/toolchain/s3.js +26 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const FASTLY_STATUS_OK = 0;
|
|
7
|
+
const FASTLY_STATUS_ERROR = 1;
|
|
8
|
+
const FASTLY_STATUS_BADF = 3;
|
|
9
|
+
const FASTLY_STATUS_BUFLEN = 4;
|
|
10
|
+
const FASTLY_STATUS_NONE = 10;
|
|
11
|
+
const FASTLY_KV_ERROR_OK = 1;
|
|
12
|
+
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', 'x-amz-security-token', 'x-amz-content-sha256']);
|
|
14
|
+
|
|
15
|
+
class FastlyNativePlatformCapabilitiesMockError extends Error {
|
|
16
|
+
constructor(message, code = 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_FAILED', detail = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'FastlyNativePlatformCapabilitiesMockError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.detail = Object.freeze({ ...detail });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeHeaders(input) {
|
|
25
|
+
if (!input) return [];
|
|
26
|
+
if (Array.isArray(input)) return input.map(([name, value]) => [String(name), String(value)]);
|
|
27
|
+
return Object.entries(input).flatMap(([name, value]) => Array.isArray(value)
|
|
28
|
+
? value.map((entry) => [String(name), String(entry)])
|
|
29
|
+
: [[String(name), String(value)]]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeBody(input) {
|
|
33
|
+
if (Buffer.isBuffer(input)) return Buffer.from(input);
|
|
34
|
+
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
35
|
+
if (input === undefined || input === null) return Buffer.alloc(0);
|
|
36
|
+
if (typeof input === 'string') return Buffer.from(input);
|
|
37
|
+
return Buffer.from(JSON.stringify(input));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function redactHeaders(headers) {
|
|
41
|
+
return headers.map(([name, value]) => [name, SENSITIVE_HEADERS.has(String(name).toLowerCase()) ? '[REDACTED]' : value]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeNamedStores(input, fallbackName, fallbackValues) {
|
|
45
|
+
const stores = new Map();
|
|
46
|
+
if (input instanceof Map) {
|
|
47
|
+
for (const [name, values] of input) stores.set(String(name), new Map(Object.entries(values || {})));
|
|
48
|
+
} else if (input && typeof input === 'object') {
|
|
49
|
+
for (const [name, values] of Object.entries(input)) stores.set(String(name), new Map(Object.entries(values || {})));
|
|
50
|
+
}
|
|
51
|
+
if (fallbackValues && typeof fallbackValues === 'object') stores.set(String(fallbackName), new Map(Object.entries(fallbackValues)));
|
|
52
|
+
return stores;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function encodeKvValue(value) {
|
|
56
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
|
|
57
|
+
if (typeof value === 'string' && value.startsWith('{"__pulseKv":')) return Buffer.from(value);
|
|
58
|
+
return Buffer.from(JSON.stringify({ __pulseKv: 1, value }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function snapshotStores(stores, decode = false) {
|
|
62
|
+
const output = {};
|
|
63
|
+
for (const [name, values] of stores) {
|
|
64
|
+
output[name] = {};
|
|
65
|
+
for (const [key, value] of values) {
|
|
66
|
+
if (!decode) output[name][key] = value;
|
|
67
|
+
else {
|
|
68
|
+
const text = Buffer.from(value).toString('utf8');
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(text);
|
|
71
|
+
output[name][key] = parsed && parsed.__pulseKv === 1 ? parsed.value : parsed;
|
|
72
|
+
} catch (_) { output[name][key] = text; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fixtureKey(backend, method, url) {
|
|
80
|
+
return `${backend} ${method.toUpperCase()} ${url}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveFixture(fixtures, backend, method, url) {
|
|
84
|
+
if (!fixtures) return undefined;
|
|
85
|
+
if (fixtures instanceof Map) {
|
|
86
|
+
return fixtures.get(fixtureKey(backend, method, url))
|
|
87
|
+
|| fixtures.get(`${method.toUpperCase()} ${url}`)
|
|
88
|
+
|| fixtures.get(url);
|
|
89
|
+
}
|
|
90
|
+
return fixtures[fixtureKey(backend, method, url)]
|
|
91
|
+
|| fixtures[`${method.toUpperCase()} ${url}`]
|
|
92
|
+
|| fixtures[url]
|
|
93
|
+
|| (fixtures[backend] && (
|
|
94
|
+
fixtures[backend][`${method.toUpperCase()} ${url}`]
|
|
95
|
+
|| fixtures[backend][url]
|
|
96
|
+
|| fixtures[backend][new URL(url).pathname]
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
101
|
+
const wasm = Buffer.isBuffer(input)
|
|
102
|
+
? input
|
|
103
|
+
: input && Buffer.isBuffer(input.wasm)
|
|
104
|
+
? input.wasm
|
|
105
|
+
: fs.readFileSync(path.resolve(input));
|
|
106
|
+
if (!WebAssembly.validate(wasm)) {
|
|
107
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Pass98 mock host requires valid WebAssembly.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_INVALID_WASM');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const requestInput = options.request || {};
|
|
111
|
+
const method = String(requestInput.method || 'GET').toUpperCase();
|
|
112
|
+
const pathValue = String(requestInput.path || '/');
|
|
113
|
+
const url = String(requestInput.url || `https://pulse.test${pathValue.startsWith('/') ? pathValue : `/${pathValue}`}`);
|
|
114
|
+
const requestHeaders = normalizeHeaders(requestInput.headers);
|
|
115
|
+
const trace = [];
|
|
116
|
+
let monotonicMs = 0;
|
|
117
|
+
const privateUrls = new Set();
|
|
118
|
+
let nextHandle = 10;
|
|
119
|
+
const typedHandleStarts = options.handleStarts && typeof options.handleStarts === 'object' ? options.handleStarts : {};
|
|
120
|
+
const typedHandleNext = new Map();
|
|
121
|
+
let instance;
|
|
122
|
+
|
|
123
|
+
const requests = new Map();
|
|
124
|
+
const bodies = new Map();
|
|
125
|
+
const responses = new Map();
|
|
126
|
+
const pending = new Map();
|
|
127
|
+
const outboundRequests = [];
|
|
128
|
+
const configStores = normalizeNamedStores(options.configStores, options.configStore || 'app_config', options.config);
|
|
129
|
+
const secretStores = normalizeNamedStores(options.secretStores, options.secretStore || 'app_secrets', options.secrets);
|
|
130
|
+
const secretValues = [...secretStores.values()].flatMap((values) => [...values.values()].map((value) => String(value))).filter(Boolean);
|
|
131
|
+
const redactText = (value) => secretValues.reduce((output, secret) => output.split(secret).join('[REDACTED]'), String(value));
|
|
132
|
+
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)]);
|
|
133
|
+
const kvStores = normalizeNamedStores(options.kvStores, options.kvStore || 'app_sessions', undefined);
|
|
134
|
+
for (const values of kvStores.values()) for (const [key, value] of values) values.set(key, encodeKvValue(value));
|
|
135
|
+
const configHandles = new Map();
|
|
136
|
+
const secretStoreHandles = new Map();
|
|
137
|
+
const secretHandles = new Map();
|
|
138
|
+
const kvStoreHandles = new Map();
|
|
139
|
+
const pendingKvLookups = new Map();
|
|
140
|
+
const pendingKvInserts = new Map();
|
|
141
|
+
const logHandles = new Map();
|
|
142
|
+
const logs = [];
|
|
143
|
+
const downstreamRequestHandle = 1;
|
|
144
|
+
const downstreamBodyHandle = 2;
|
|
145
|
+
requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
|
|
146
|
+
bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: [] });
|
|
147
|
+
let downstream;
|
|
148
|
+
|
|
149
|
+
function alloc(kind = 'default') {
|
|
150
|
+
if (Object.prototype.hasOwnProperty.call(typedHandleStarts, kind)) {
|
|
151
|
+
const current = typedHandleNext.has(kind) ? typedHandleNext.get(kind) : Number(typedHandleStarts[kind]);
|
|
152
|
+
if (!Number.isInteger(current) || current < 0) {
|
|
153
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Mock host handle starts must be non-negative integers.', 'PULSE_FASTLY_NATIVE_MOCK_HANDLE_START_INVALID', { kind, value: typedHandleStarts[kind] });
|
|
154
|
+
}
|
|
155
|
+
typedHandleNext.set(kind, current + 1);
|
|
156
|
+
return current;
|
|
157
|
+
}
|
|
158
|
+
return nextHandle++;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function memory() {
|
|
162
|
+
const value = instance && instance.exports && instance.exports.memory;
|
|
163
|
+
if (!(value instanceof WebAssembly.Memory)) {
|
|
164
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module did not export memory.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_MEMORY_MISSING');
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function view() { return new DataView(memory().buffer); }
|
|
170
|
+
function writeU32(pointer, value) { view().setUint32(Number(pointer), Number(value) >>> 0, true); }
|
|
171
|
+
function writeU64(pointer, value) { view().setBigUint64(Number(pointer), BigInt(value), true); }
|
|
172
|
+
function readBytes(pointer, length) { return Buffer.from(new Uint8Array(memory().buffer, Number(pointer), Number(length))); }
|
|
173
|
+
function readUtf8(pointer, length) { return readBytes(pointer, length).toString('utf8'); }
|
|
174
|
+
|
|
175
|
+
function writeUtf8(value, pointer, maxLength, writtenOut) {
|
|
176
|
+
const bytes = Buffer.from(String(value), 'utf8');
|
|
177
|
+
writeU32(writtenOut, bytes.length);
|
|
178
|
+
if (bytes.length > Number(maxLength)) return FASTLY_STATUS_BUFLEN;
|
|
179
|
+
new Uint8Array(memory().buffer, Number(pointer), bytes.length).set(bytes);
|
|
180
|
+
return FASTLY_STATUS_OK;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function headerValue(headers, name) {
|
|
184
|
+
const target = String(name).toLowerCase();
|
|
185
|
+
const found = headers.find(([headerName]) => String(headerName).toLowerCase() === target);
|
|
186
|
+
return found && found[1];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bodyBytes(handle) {
|
|
190
|
+
const body = bodies.get(Number(handle));
|
|
191
|
+
if (!body) return undefined;
|
|
192
|
+
if (body.writes.length > 0) return Buffer.concat(body.writes);
|
|
193
|
+
return body.bytes;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function makeOriginResponse(fixture) {
|
|
197
|
+
const responseHandle = alloc('response');
|
|
198
|
+
const bodyHandle = alloc('body');
|
|
199
|
+
const normalized = fixture || {};
|
|
200
|
+
const headers = normalizeHeaders(normalized.headers);
|
|
201
|
+
let body = normalized.body;
|
|
202
|
+
if (Object.prototype.hasOwnProperty.call(normalized, 'json')) {
|
|
203
|
+
body = JSON.stringify(normalized.json);
|
|
204
|
+
if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
205
|
+
}
|
|
206
|
+
responses.set(responseHandle, {
|
|
207
|
+
status: Number(normalized.status || 200),
|
|
208
|
+
headers,
|
|
209
|
+
origin: true
|
|
210
|
+
});
|
|
211
|
+
bodies.set(bodyHandle, { bytes: normalizeBody(body), readOffset: 0, writes: [], fixture: normalized, readyAt: monotonicMs + Number(normalized.bodyDelayMs || 0) });
|
|
212
|
+
return { responseHandle, bodyHandle };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function headerList(values, buffer, size, end, written) {
|
|
216
|
+
const output = Buffer.from(values.map((value) => String(value) + '\0').join(''));
|
|
217
|
+
writeU32(written, output.length); view().setBigInt64(Number(end), -1n, true);
|
|
218
|
+
if (output.length > Number(size)) return FASTLY_STATUS_BUFLEN;
|
|
219
|
+
new Uint8Array(memory().buffer, Number(buffer), output.length).set(output);
|
|
220
|
+
return FASTLY_STATUS_OK;
|
|
221
|
+
}
|
|
222
|
+
const imports = {
|
|
223
|
+
fastly_async_io: {
|
|
224
|
+
select(handles, count, timeout, done) {
|
|
225
|
+
if (Number(count) !== 1 || Number(timeout) <= 0) return FASTLY_STATUS_ERROR;
|
|
226
|
+
const handle = view().getUint32(Number(handles), true);
|
|
227
|
+
const item = pending.get(handle) || bodies.get(handle);
|
|
228
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
229
|
+
const wait = Math.max(0, Number(item.readyAt || 0) - monotonicMs);
|
|
230
|
+
monotonicMs += Math.min(wait, Number(timeout));
|
|
231
|
+
writeU32(done, wait >= Number(timeout) ? 0xffffffff : 0);
|
|
232
|
+
trace.push({ module: 'fastly_async_io', name: 'select', timeout: Number(timeout), timedOut: wait >= Number(timeout) });
|
|
233
|
+
return FASTLY_STATUS_OK;
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
wasi_snapshot_preview1: {
|
|
237
|
+
clock_time_get(clockId, precision, timeOut) {
|
|
238
|
+
const seconds = Number.isFinite(Number(options.clockUnixSeconds))
|
|
239
|
+
? Number(options.clockUnixSeconds)
|
|
240
|
+
: Math.floor(Date.now() / 1000);
|
|
241
|
+
writeU64(timeOut, BigInt(Math.trunc((Number(clockId) === 1 ? monotonicMs : seconds * 1000 + monotonicMs) * 1_000_000)));
|
|
242
|
+
trace.push({
|
|
243
|
+
module: 'wasi_snapshot_preview1',
|
|
244
|
+
name: 'clock_time_get',
|
|
245
|
+
clockId: Number(clockId),
|
|
246
|
+
precision: String(precision),
|
|
247
|
+
unixEpochSeconds: seconds
|
|
248
|
+
});
|
|
249
|
+
return FASTLY_STATUS_OK;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
env: {
|
|
253
|
+
abort(message, fileName, line, column) {
|
|
254
|
+
throw new FastlyNativePlatformCapabilitiesMockError('AssemblyScript aborted in the Pass98 Fastly effects module.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_ABORT', { message, fileName, line, column, trace });
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
fastly_abi: {
|
|
258
|
+
init(version) {
|
|
259
|
+
trace.push({ module: 'fastly_abi', name: 'init', version: String(version) });
|
|
260
|
+
return version === 1n ? FASTLY_STATUS_OK : FASTLY_STATUS_ERROR;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
fastly_log: {
|
|
264
|
+
endpoint_get(namePointer, nameLength, handleOut) {
|
|
265
|
+
const endpoint = readUtf8(namePointer, nameLength);
|
|
266
|
+
if (options.logEndpointFailure === true) {
|
|
267
|
+
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, opened: false });
|
|
268
|
+
return FASTLY_STATUS_ERROR;
|
|
269
|
+
}
|
|
270
|
+
const handle = alloc('log');
|
|
271
|
+
logHandles.set(handle, endpoint);
|
|
272
|
+
writeU32(handleOut, handle);
|
|
273
|
+
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, handle, opened: true });
|
|
274
|
+
return FASTLY_STATUS_OK;
|
|
275
|
+
},
|
|
276
|
+
write(handle, messagePointer, messageLength, writtenOut) {
|
|
277
|
+
const endpoint = logHandles.get(Number(handle));
|
|
278
|
+
if (!endpoint) return FASTLY_STATUS_BADF;
|
|
279
|
+
if (options.logWriteFailure === true) {
|
|
280
|
+
trace.push({ module: 'fastly_log', name: 'write', endpoint, accepted: false });
|
|
281
|
+
return FASTLY_STATUS_ERROR;
|
|
282
|
+
}
|
|
283
|
+
const message = redactText(readUtf8(messagePointer, messageLength));
|
|
284
|
+
writeU32(writtenOut, Buffer.byteLength(message));
|
|
285
|
+
logs.push(Object.freeze({ endpoint, message }));
|
|
286
|
+
trace.push({ module: 'fastly_log', name: 'write', endpoint, message, accepted: true });
|
|
287
|
+
return FASTLY_STATUS_OK;
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
fastly_http_req: {
|
|
291
|
+
body_downstream_get(requestOut, bodyOut) {
|
|
292
|
+
trace.push({ module: 'fastly_http_req', name: 'body_downstream_get' });
|
|
293
|
+
writeU32(requestOut, downstreamRequestHandle);
|
|
294
|
+
writeU32(bodyOut, downstreamBodyHandle);
|
|
295
|
+
return FASTLY_STATUS_OK;
|
|
296
|
+
},
|
|
297
|
+
method_get(handle, buffer, bufferLength, writtenOut) {
|
|
298
|
+
const request = requests.get(Number(handle));
|
|
299
|
+
trace.push({ module: 'fastly_http_req', name: 'method_get', handle: Number(handle) });
|
|
300
|
+
return request ? writeUtf8(request.method, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
|
|
301
|
+
},
|
|
302
|
+
uri_get(handle, buffer, bufferLength, writtenOut) {
|
|
303
|
+
const request = requests.get(Number(handle));
|
|
304
|
+
trace.push({ module: 'fastly_http_req', name: 'uri_get', handle: Number(handle) });
|
|
305
|
+
return request ? writeUtf8(request.url, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
|
|
306
|
+
},
|
|
307
|
+
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
308
|
+
const request = requests.get(Number(handle));
|
|
309
|
+
const name = readUtf8(namePointer, nameLength);
|
|
310
|
+
trace.push({ module: 'fastly_http_req', name: 'header_value_get', handle: Number(handle), header: name });
|
|
311
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
312
|
+
const value = headerValue(request.headers, name);
|
|
313
|
+
return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
314
|
+
},
|
|
315
|
+
new(handleOut) {
|
|
316
|
+
const handle = alloc('request');
|
|
317
|
+
requests.set(handle, { method: 'GET', url: '', headers: [] });
|
|
318
|
+
writeU32(handleOut, handle);
|
|
319
|
+
trace.push({ module: 'fastly_http_req', name: 'new', handle });
|
|
320
|
+
return FASTLY_STATUS_OK;
|
|
321
|
+
},
|
|
322
|
+
method_set(handle, pointer, length) {
|
|
323
|
+
const request = requests.get(Number(handle));
|
|
324
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
325
|
+
request.method = readUtf8(pointer, length).toUpperCase();
|
|
326
|
+
trace.push({ module: 'fastly_http_req', name: 'method_set', handle: Number(handle), method: request.method });
|
|
327
|
+
return FASTLY_STATUS_OK;
|
|
328
|
+
},
|
|
329
|
+
uri_set(handle, pointer, length) {
|
|
330
|
+
const request = requests.get(Number(handle));
|
|
331
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
332
|
+
request.url = readUtf8(pointer, length);
|
|
333
|
+
trace.push({ module: 'fastly_http_req', name: 'uri_set', handle: Number(handle), url: redactText(request.url) });
|
|
334
|
+
return FASTLY_STATUS_OK;
|
|
335
|
+
},
|
|
336
|
+
header_insert(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
337
|
+
const request = requests.get(Number(handle));
|
|
338
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
339
|
+
const name = readUtf8(namePointer, nameLength);
|
|
340
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
341
|
+
if (name.toLowerCase() === 'authorization' && value.includes('/s3/aws4_request')) privateUrls.add(request.url);
|
|
342
|
+
request.headers = request.headers.filter(([headerName]) => headerName.toLowerCase() !== name.toLowerCase());
|
|
343
|
+
request.headers.push([name, value]);
|
|
344
|
+
trace.push({ module: 'fastly_http_req', name: 'header_insert', handle: Number(handle), header: redactRuntimeHeaders([[name, value]], privateUrls.has(request.url))[0] });
|
|
345
|
+
return FASTLY_STATUS_OK;
|
|
346
|
+
},
|
|
347
|
+
cache_override_set(handle, tag, ttl, swr) {
|
|
348
|
+
const request = requests.get(Number(handle)); if (!request) return FASTLY_STATUS_BADF;
|
|
349
|
+
request.cacheOverride = Number(tag); return FASTLY_STATUS_OK;
|
|
350
|
+
},
|
|
351
|
+
auto_decompress_response_set(handle, encodings) {
|
|
352
|
+
const request = requests.get(Number(handle)); if (!request) return FASTLY_STATUS_BADF;
|
|
353
|
+
request.decompression = Number(encodings); return FASTLY_STATUS_OK;
|
|
354
|
+
},
|
|
355
|
+
close(handle) { return requests.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
|
|
356
|
+
pending_req_poll_v2(handle, detail, done, responseOut, bodyOut) {
|
|
357
|
+
const item = pending.get(Number(handle)); if (!item) return FASTLY_STATUS_BADF;
|
|
358
|
+
if (monotonicMs < item.readyAt) { writeU32(done, 0); return FASTLY_STATUS_OK; }
|
|
359
|
+
writeU32(done, 1); return imports.fastly_http_req.pending_req_wait(handle, responseOut, bodyOut);
|
|
360
|
+
},
|
|
361
|
+
send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
362
|
+
const request = requests.get(Number(handle));
|
|
363
|
+
const body = bodies.get(Number(bodyHandle));
|
|
364
|
+
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
365
|
+
const backend = readUtf8(backendPointer, backendLength);
|
|
366
|
+
const signedS3 = request.headers.some(([name, value]) => name.toLowerCase() === 'authorization' && value.startsWith('AWS4-HMAC-SHA256 '));
|
|
367
|
+
if (signedS3) privateUrls.add(request.url);
|
|
368
|
+
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
369
|
+
if (!fixture) {
|
|
370
|
+
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: signedS3 ? '[REDACTED]' : request.url, missingFixture: true });
|
|
371
|
+
return FASTLY_STATUS_ERROR;
|
|
372
|
+
}
|
|
373
|
+
const pendingHandle = alloc('pending');
|
|
374
|
+
const capturedBody = bodyBytes(bodyHandle);
|
|
375
|
+
pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: capturedBody }, backend, fixture, readyAt: monotonicMs + Number(fixture.delayMs || 0) });
|
|
376
|
+
if (typeof options.onOutboundRequest === 'function') options.onOutboundRequest({ ...request, backend, headers: request.headers.map((entry) => [...entry]), body: capturedBody });
|
|
377
|
+
if (fixture.sendStatus) { pending.delete(pendingHandle); return Number(fixture.sendStatus); }
|
|
378
|
+
if (request.headers.some(([name, value]) => name.toLowerCase() === 'authorization' && value.startsWith('AWS4-HMAC-SHA256 '))) privateUrls.add(request.url);
|
|
379
|
+
requests.delete(Number(handle)); bodies.delete(Number(bodyHandle));
|
|
380
|
+
outboundRequests.push(Object.freeze({
|
|
381
|
+
backend,
|
|
382
|
+
method: request.method,
|
|
383
|
+
url: privateUrls.has(request.url) ? '[REDACTED]' : redactText(request.url),
|
|
384
|
+
headers: Object.freeze(redactRuntimeHeaders(request.headers, signedS3).map((header) => Object.freeze(header))),
|
|
385
|
+
body: signedS3 ? '[REDACTED]' : redactText(capturedBody.toString('utf8'))
|
|
386
|
+
}));
|
|
387
|
+
writeU32(pendingOut, pendingHandle);
|
|
388
|
+
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 });
|
|
389
|
+
return FASTLY_STATUS_OK;
|
|
390
|
+
},
|
|
391
|
+
pending_req_wait(handle, responseOut, bodyOut) {
|
|
392
|
+
const item = pending.get(Number(handle));
|
|
393
|
+
trace.push({ module: 'fastly_http_req', name: 'pending_req_wait', pendingHandle: Number(handle), backend: item && item.backend, url: item && item.request.url });
|
|
394
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
395
|
+
pending.delete(Number(handle));
|
|
396
|
+
const transportStatus = Number(item.fixture.transportStatus || 0);
|
|
397
|
+
if (transportStatus !== FASTLY_STATUS_OK) return transportStatus;
|
|
398
|
+
const origin = makeOriginResponse(item.fixture);
|
|
399
|
+
writeU32(responseOut, origin.responseHandle);
|
|
400
|
+
writeU32(bodyOut, origin.bodyHandle);
|
|
401
|
+
return FASTLY_STATUS_OK;
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
fastly_config_store: {
|
|
405
|
+
open(namePointer, nameLength, handleOut) {
|
|
406
|
+
const name = readUtf8(namePointer, nameLength);
|
|
407
|
+
const values = configStores.get(name);
|
|
408
|
+
trace.push({ module: 'fastly_config_store', name: 'open', store: name, found: Boolean(values) });
|
|
409
|
+
if (!values) return FASTLY_STATUS_NONE;
|
|
410
|
+
const handle = alloc('configStore'); configHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
411
|
+
},
|
|
412
|
+
get(handle, keyPointer, keyLength, valuePointer, valueLength, writtenOut) {
|
|
413
|
+
const values = configHandles.get(Number(handle));
|
|
414
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
415
|
+
const value = values && values.get(key);
|
|
416
|
+
trace.push({ module: 'fastly_config_store', name: 'get', handle: Number(handle), key, found: value !== undefined });
|
|
417
|
+
if (!values) return FASTLY_STATUS_BADF;
|
|
418
|
+
return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
fastly_secret_store: {
|
|
422
|
+
open(namePointer, nameLength, handleOut) {
|
|
423
|
+
const name = readUtf8(namePointer, nameLength);
|
|
424
|
+
const values = secretStores.get(name);
|
|
425
|
+
trace.push({ module: 'fastly_secret_store', name: 'open', store: name, found: Boolean(values) });
|
|
426
|
+
if (!values) return FASTLY_STATUS_NONE;
|
|
427
|
+
const handle = alloc('secretStore'); secretStoreHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
428
|
+
},
|
|
429
|
+
get(handle, keyPointer, keyLength, secretOut) {
|
|
430
|
+
monotonicMs += Number(options.secretDelayMs || 0);
|
|
431
|
+
if (typeof options.onSecretLookup === 'function') options.onSecretLookup();
|
|
432
|
+
const values = secretStoreHandles.get(Number(handle));
|
|
433
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
434
|
+
const value = values && values.get(key);
|
|
435
|
+
trace.push({ module: 'fastly_secret_store', name: 'get', handle: Number(handle), key, found: value !== undefined });
|
|
436
|
+
if (!values) return FASTLY_STATUS_BADF;
|
|
437
|
+
if (value === undefined) return FASTLY_STATUS_NONE;
|
|
438
|
+
const secretHandle = alloc('secret'); secretHandles.set(secretHandle, String(value)); writeU32(secretOut, secretHandle); return FASTLY_STATUS_OK;
|
|
439
|
+
},
|
|
440
|
+
plaintext(handle, valuePointer, valueLength, writtenOut) {
|
|
441
|
+
const value = secretHandles.get(Number(handle));
|
|
442
|
+
trace.push({ module: 'fastly_secret_store', name: 'plaintext', handle: Number(handle), value: '[REDACTED]' });
|
|
443
|
+
return value === undefined ? FASTLY_STATUS_BADF : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
fastly_kv_store: {
|
|
447
|
+
open(namePointer, nameLength, handleOut) {
|
|
448
|
+
const name = readUtf8(namePointer, nameLength);
|
|
449
|
+
const values = kvStores.get(name);
|
|
450
|
+
trace.push({ module: 'fastly_kv_store', name: 'open', store: name, found: Boolean(values) });
|
|
451
|
+
if (!values) return FASTLY_STATUS_NONE;
|
|
452
|
+
const handle = alloc('kvStore'); kvStoreHandles.set(handle, { name, values }); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
|
|
453
|
+
},
|
|
454
|
+
lookup(storeHandle, keyPointer, keyLength, configMask, configPointer, lookupOut) {
|
|
455
|
+
const store = kvStoreHandles.get(Number(storeHandle));
|
|
456
|
+
if (!store) return FASTLY_STATUS_BADF;
|
|
457
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
458
|
+
const handle = alloc('kvLookup'); pendingKvLookups.set(handle, { store, key }); writeU32(lookupOut, handle);
|
|
459
|
+
trace.push({ module: 'fastly_kv_store', name: 'lookup', store: store.name, key, pendingHandle: handle, configMask: Number(configMask) });
|
|
460
|
+
return FASTLY_STATUS_OK;
|
|
461
|
+
},
|
|
462
|
+
lookup_wait_v2(handle, bodyOut, metadataPointer, metadataLength, writtenOut, generationOut, kvErrorOut) {
|
|
463
|
+
const item = pendingKvLookups.get(Number(handle));
|
|
464
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
465
|
+
pendingKvLookups.delete(Number(handle));
|
|
466
|
+
const value = item.store.values.get(item.key);
|
|
467
|
+
writeU32(writtenOut, 0); writeU64(generationOut, 0n);
|
|
468
|
+
if (value === undefined) {
|
|
469
|
+
writeU32(kvErrorOut, FASTLY_KV_ERROR_NOT_FOUND);
|
|
470
|
+
trace.push({ module: 'fastly_kv_store', name: 'lookup_wait_v2', store: item.store.name, key: item.key, found: false });
|
|
471
|
+
return FASTLY_STATUS_OK;
|
|
472
|
+
}
|
|
473
|
+
const bodyHandle = alloc('body'); bodies.set(bodyHandle, { bytes: Buffer.from(value), readOffset: 0, writes: [] }); writeU32(bodyOut, bodyHandle); writeU32(kvErrorOut, FASTLY_KV_ERROR_OK);
|
|
474
|
+
trace.push({ module: 'fastly_kv_store', name: 'lookup_wait_v2', store: item.store.name, key: item.key, found: true, bodyHandle });
|
|
475
|
+
return FASTLY_STATUS_OK;
|
|
476
|
+
},
|
|
477
|
+
insert(storeHandle, keyPointer, keyLength, bodyHandle, configMask, configPointer, insertOut) {
|
|
478
|
+
const store = kvStoreHandles.get(Number(storeHandle));
|
|
479
|
+
const body = bodies.get(Number(bodyHandle));
|
|
480
|
+
if (!store || !body) return FASTLY_STATUS_BADF;
|
|
481
|
+
const key = readUtf8(keyPointer, keyLength);
|
|
482
|
+
const handle = alloc('kvInsert'); pendingKvInserts.set(handle, { store, key, bytes: bodyBytes(bodyHandle) }); writeU32(insertOut, handle);
|
|
483
|
+
trace.push({ module: 'fastly_kv_store', name: 'insert', store: store.name, key, pendingHandle: handle, bytes: bodyBytes(bodyHandle).length, configMask: Number(configMask) });
|
|
484
|
+
return FASTLY_STATUS_OK;
|
|
485
|
+
},
|
|
486
|
+
insert_wait(handle, kvErrorOut) {
|
|
487
|
+
const item = pendingKvInserts.get(Number(handle));
|
|
488
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
489
|
+
pendingKvInserts.delete(Number(handle)); item.store.values.set(item.key, Buffer.from(item.bytes)); writeU32(kvErrorOut, FASTLY_KV_ERROR_OK);
|
|
490
|
+
trace.push({ module: 'fastly_kv_store', name: 'insert_wait', store: item.store.name, key: item.key, bytes: item.bytes.length });
|
|
491
|
+
return FASTLY_STATUS_OK;
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
fastly_http_resp: {
|
|
495
|
+
close(handle) { trace.push({ module: 'fastly_http_resp', name: 'close' }); return responses.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
|
|
496
|
+
header_names_get(handle, buffer, size, cursor, end, written) {
|
|
497
|
+
const response = responses.get(Number(handle)); if (!response || cursor !== 0) return FASTLY_STATUS_BADF;
|
|
498
|
+
return headerList([...new Set(response.headers.map(([name]) => name.toLowerCase()))], buffer, size, end, written);
|
|
499
|
+
},
|
|
500
|
+
header_values_get(handle, pointer, length, buffer, size, cursor, end, written) {
|
|
501
|
+
const response = responses.get(Number(handle)); if (!response || cursor !== 0) return FASTLY_STATUS_BADF;
|
|
502
|
+
const name = readUtf8(pointer, length).toLowerCase();
|
|
503
|
+
return headerList(response.headers.filter(([key]) => key.toLowerCase() === name).map(([, value]) => value), buffer, size, end, written);
|
|
504
|
+
},
|
|
505
|
+
new(handleOut) {
|
|
506
|
+
const handle = alloc('response');
|
|
507
|
+
responses.set(handle, { status: 200, headers: [], origin: false });
|
|
508
|
+
writeU32(handleOut, handle);
|
|
509
|
+
trace.push({ module: 'fastly_http_resp', name: 'new', handle });
|
|
510
|
+
return FASTLY_STATUS_OK;
|
|
511
|
+
},
|
|
512
|
+
header_append(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
513
|
+
const response = responses.get(Number(handle));
|
|
514
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
515
|
+
const name = readUtf8(namePointer, nameLength);
|
|
516
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
517
|
+
response.headers.push([name, value]);
|
|
518
|
+
trace.push({ module: 'fastly_http_resp', name: 'header_append', handle: Number(handle), header: [name, value] });
|
|
519
|
+
return FASTLY_STATUS_OK;
|
|
520
|
+
},
|
|
521
|
+
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
522
|
+
const response = responses.get(Number(handle));
|
|
523
|
+
const name = readUtf8(namePointer, nameLength);
|
|
524
|
+
trace.push({ module: 'fastly_http_resp', name: 'header_value_get', handle: Number(handle), header: name });
|
|
525
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
526
|
+
const value = headerValue(response.headers, name);
|
|
527
|
+
return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
528
|
+
},
|
|
529
|
+
status_get(handle, statusOut) {
|
|
530
|
+
const response = responses.get(Number(handle));
|
|
531
|
+
trace.push({ module: 'fastly_http_resp', name: 'status_get', handle: Number(handle) });
|
|
532
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
533
|
+
writeU32(statusOut, response.status);
|
|
534
|
+
return FASTLY_STATUS_OK;
|
|
535
|
+
},
|
|
536
|
+
status_set(handle, status) {
|
|
537
|
+
const response = responses.get(Number(handle));
|
|
538
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
539
|
+
response.status = Number(status);
|
|
540
|
+
trace.push({ module: 'fastly_http_resp', name: 'status_set', handle: Number(handle), status: Number(status) });
|
|
541
|
+
return FASTLY_STATUS_OK;
|
|
542
|
+
},
|
|
543
|
+
send_downstream(handle, bodyHandle, streaming) {
|
|
544
|
+
const response = responses.get(Number(handle));
|
|
545
|
+
const body = bodies.get(Number(bodyHandle));
|
|
546
|
+
trace.push({ module: 'fastly_http_resp', name: 'send_downstream', handle: Number(handle), bodyHandle: Number(bodyHandle), streaming: Number(streaming), origin: response && response.origin });
|
|
547
|
+
if (!response || !body) return FASTLY_STATUS_BADF;
|
|
548
|
+
downstream = {
|
|
549
|
+
status: response.status,
|
|
550
|
+
headers: response.headers.map((entry) => [...entry]),
|
|
551
|
+
bodyBytes: bodyBytes(bodyHandle),
|
|
552
|
+
streaming: Number(streaming),
|
|
553
|
+
origin: response.origin === true,
|
|
554
|
+
responseHandle: Number(handle),
|
|
555
|
+
bodyHandle: Number(bodyHandle)
|
|
556
|
+
};
|
|
557
|
+
return FASTLY_STATUS_OK;
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
fastly_http_body: {
|
|
561
|
+
close(handle) { trace.push({ module: 'fastly_http_body', name: 'close' }); return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
|
|
562
|
+
new(handleOut) {
|
|
563
|
+
const handle = alloc('body');
|
|
564
|
+
bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
|
|
565
|
+
writeU32(handleOut, handle);
|
|
566
|
+
trace.push({ module: 'fastly_http_body', name: 'new', handle });
|
|
567
|
+
return FASTLY_STATUS_OK;
|
|
568
|
+
},
|
|
569
|
+
read(handle, buffer, bufferLength, readOut) {
|
|
570
|
+
const body = bodies.get(Number(handle));
|
|
571
|
+
if (!body) return FASTLY_STATUS_BADF;
|
|
572
|
+
if (body.fixture && body.fixture.bodyReadStatus) return Number(body.fixture.bodyReadStatus);
|
|
573
|
+
const source = bodyBytes(handle);
|
|
574
|
+
const offset = body.readOffset || 0;
|
|
575
|
+
const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset), Number(body.fixture && body.fixture.chunkBytes || 65536));
|
|
576
|
+
if (count > 0) new Uint8Array(memory().buffer, Number(buffer), count).set(source.subarray(offset, offset + count));
|
|
577
|
+
body.readOffset = offset + count;
|
|
578
|
+
body.readyAt = monotonicMs + Number(body.fixture && body.fixture.chunkDelayMs || 0);
|
|
579
|
+
writeU32(readOut, count);
|
|
580
|
+
trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: count });
|
|
581
|
+
return FASTLY_STATUS_OK;
|
|
582
|
+
},
|
|
583
|
+
write(handle, buffer, bufferLength, end, writtenOut) {
|
|
584
|
+
const body = bodies.get(Number(handle));
|
|
585
|
+
if (!body) return FASTLY_STATUS_BADF;
|
|
586
|
+
if (requests.size > 1 && options.outboundBodyWriteStatus) return Number(options.outboundBodyWriteStatus);
|
|
587
|
+
const length = Math.min(Number(bufferLength), Number(options.bodyWriteChunkBytes || bufferLength));
|
|
588
|
+
const bytes = readBytes(buffer, length);
|
|
589
|
+
body.writes.push(bytes);
|
|
590
|
+
writeU32(writtenOut, bytes.length);
|
|
591
|
+
trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end) });
|
|
592
|
+
return FASTLY_STATUS_OK;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const kvEvidence = options.conditionalKv ? require('./conditional-kv-host.js').attachConditionalKvHost(imports, {
|
|
598
|
+
view, writeU32, writeU64, readBytes, readUtf8, alloc, bodies, bodyBytes, trace,
|
|
599
|
+
now: () => monotonicMs, advance: ms => { monotonicMs += ms; }
|
|
600
|
+
}, options.conditionalKv) : undefined;
|
|
601
|
+
|
|
602
|
+
// Model invocation termination, including cancellation raised by a host call.
|
|
603
|
+
// Throwing stops the Wasm invocation; it never fabricates an effect result.
|
|
604
|
+
const checkActive = () => { if (options.signal && options.signal.aborted) throw options.signal.reason || new Error('Request cancelled.'); };
|
|
605
|
+
checkActive();
|
|
606
|
+
if (options.signal) for (const namespace of Object.values(imports)) {
|
|
607
|
+
for (const [name, hostcall] of Object.entries(namespace)) if (typeof hostcall === 'function') namespace[name] = (...args) => {
|
|
608
|
+
checkActive(); const result = hostcall(...args); checkActive(); return result;
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const module = new WebAssembly.Module(wasm);
|
|
612
|
+
instance = new WebAssembly.Instance(module, imports);
|
|
613
|
+
if (typeof instance.exports._start !== 'function') {
|
|
614
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Pass98 native module is missing _start.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_START_MISSING');
|
|
615
|
+
}
|
|
616
|
+
if (options.conditionalKv && options.conditionalKv.deadlineNs !== undefined) instance.exports.pulse_fastly_kv_request_deadline(BigInt(options.conditionalKv.deadlineNs));
|
|
617
|
+
instance.exports._start();
|
|
618
|
+
for (const entry of trace) if (entry.url && privateUrls.has(entry.url)) entry.url = '[REDACTED]';
|
|
619
|
+
const lastError = typeof instance.exports.pulse_fastly_last_error === 'function'
|
|
620
|
+
? Number(instance.exports.pulse_fastly_last_error())
|
|
621
|
+
: undefined;
|
|
622
|
+
const errorStage = typeof instance.exports.pulse_fastly_error_stage === 'function'
|
|
623
|
+
? Number(instance.exports.pulse_fastly_error_stage())
|
|
624
|
+
: undefined;
|
|
625
|
+
const errorEffect = typeof instance.exports.pulse_fastly_error_effect === 'function'
|
|
626
|
+
? Number(instance.exports.pulse_fastly_error_effect())
|
|
627
|
+
: undefined;
|
|
628
|
+
const pulseLastError = typeof instance.exports.pulse_last_error_code === 'function'
|
|
629
|
+
? Number(instance.exports.pulse_last_error_code())
|
|
630
|
+
: undefined;
|
|
631
|
+
const handledJwtError = options.allowHandledJwtError === true
|
|
632
|
+
&& lastError === 1008
|
|
633
|
+
&& Boolean(downstream);
|
|
634
|
+
if (lastError !== FASTLY_STATUS_OK && !handledJwtError) {
|
|
635
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module reported an execution error.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_EXECUTION_FAILED', { lastError, errorStage, errorEffect, pulseLastError, trace });
|
|
636
|
+
}
|
|
637
|
+
if (!downstream) {
|
|
638
|
+
throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module did not send a downstream response.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_RESPONSE_NOT_SENT', { trace });
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return Object.freeze({
|
|
642
|
+
version: 'pulse.fastly-native-platform-capabilities-mock.v1',
|
|
643
|
+
request: Object.freeze({ method, url, path: pathValue, headers: Object.freeze(requestHeaders.map((entry) => Object.freeze(entry))) }),
|
|
644
|
+
response: Object.freeze({
|
|
645
|
+
status: downstream.status,
|
|
646
|
+
headers: Object.freeze(downstream.headers.map((entry) => Object.freeze(entry))),
|
|
647
|
+
body: downstream.bodyBytes.toString('utf8'),
|
|
648
|
+
bodyBytes: downstream.bodyBytes,
|
|
649
|
+
sent: true,
|
|
650
|
+
streaming: downstream.streaming,
|
|
651
|
+
origin: downstream.origin,
|
|
652
|
+
responseHandle: downstream.responseHandle,
|
|
653
|
+
bodyHandle: downstream.bodyHandle
|
|
654
|
+
}),
|
|
655
|
+
trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
|
|
656
|
+
logs: Object.freeze(logs),
|
|
657
|
+
outboundRequests: Object.freeze(outboundRequests),
|
|
658
|
+
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) }),
|
|
659
|
+
kvEvidence,
|
|
660
|
+
instance
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
module.exports = Object.freeze({
|
|
665
|
+
FASTLY_STATUS_OK,
|
|
666
|
+
FASTLY_STATUS_ERROR,
|
|
667
|
+
FASTLY_STATUS_BADF,
|
|
668
|
+
FASTLY_STATUS_BUFLEN,
|
|
669
|
+
FASTLY_STATUS_NONE,
|
|
670
|
+
FASTLY_KV_ERROR_OK,
|
|
671
|
+
FASTLY_KV_ERROR_NOT_FOUND,
|
|
672
|
+
FastlyNativePlatformCapabilitiesMockError,
|
|
673
|
+
normalizeHeaders,
|
|
674
|
+
fixtureKey,
|
|
675
|
+
executeFastlyNativePlatformCapabilities
|
|
676
|
+
});
|