@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/dist/config-schema.json +73 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -5
- package/src/build/canonical-target.js +436 -0
- package/src/build/entities-native.js +564 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/native-http-effects.js +871 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2109 -0
- package/src/compiler/assets-package-out-parity-proof.js +338 -0
- package/src/compiler/assets-provider-proof.js +499 -0
- package/src/compiler/config-provider-resolution.js +573 -0
- package/src/compiler/fastly-adapter.js +564 -0
- package/src/compiler/fastly-command-entry.js +390 -0
- package/src/compiler/fastly-config-secret-serve.js +323 -0
- package/src/compiler/fastly-hostcall-binding.js +598 -0
- package/src/compiler/fastly-readiness.js +477 -0
- package/src/compiler/index.js +40 -0
- package/src/compiler/lifecycle-parity-contract.js +70 -0
- package/src/compiler/lifecycle-parity-proof.js +260 -0
- package/src/compiler/route-handler-effect-proof.js +255 -0
- package/src/compiler/runtime-provider-kv.js +237 -0
- package/src/compiler/runtime-provider-shape.js +109 -0
- package/src/config-api.js +63 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +73 -0
- package/src/index.js +3 -0
- package/src/index.ts +118 -0
- package/src/javascript/assets-adapter.js +122 -0
- package/src/javascript/bindings-adapter.js +237 -0
- package/src/javascript/errors.js +26 -0
- package/src/javascript/fetch-adapter.js +93 -0
- package/src/javascript/grip-broadcast.js +261 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +26 -0
- package/src/javascript/package-effects.js +68 -0
- package/src/javascript/runtime-host.js +108 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +776 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +260 -0
- package/src/javascript/target.js +73 -0
- package/src/javascript/test-runtime.js +407 -0
- package/src/provider-contract.js +72 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +597 -0
- package/src/runtime/index.js +28 -0
- package/src/runtime/provider-primitives-contract.js +272 -0
- package/src/runtime/provider-primitives.js +82 -0
- package/src/runtime/route-handler-effects.js +507 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-http-effects-host.js +390 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +599 -0
- package/src/toolchain/config.js +128 -0
- package/src/toolchain/index.js +343 -0
|
@@ -0,0 +1,390 @@
|
|
|
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
|
+
|
|
12
|
+
class FastlyNativeHttpEffectsMockError extends Error {
|
|
13
|
+
constructor(message, code = 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_MOCK_FAILED', detail = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'FastlyNativeHttpEffectsMockError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.detail = Object.freeze({ ...detail });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeHeaders(input) {
|
|
22
|
+
if (!input) return [];
|
|
23
|
+
if (Array.isArray(input)) return input.map(([name, value]) => [String(name), String(value)]);
|
|
24
|
+
return Object.entries(input).flatMap(([name, value]) => Array.isArray(value)
|
|
25
|
+
? value.map((entry) => [String(name), String(entry)])
|
|
26
|
+
: [[String(name), String(value)]]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeBody(input) {
|
|
30
|
+
if (Buffer.isBuffer(input)) return Buffer.from(input);
|
|
31
|
+
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
32
|
+
if (input === undefined || input === null) return Buffer.alloc(0);
|
|
33
|
+
if (typeof input === 'string') return Buffer.from(input);
|
|
34
|
+
return Buffer.from(JSON.stringify(input));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fixtureKey(backend, method, url) {
|
|
38
|
+
return `${backend} ${method.toUpperCase()} ${url}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveFixture(fixtures, backend, method, url) {
|
|
42
|
+
if (!fixtures) return undefined;
|
|
43
|
+
if (fixtures instanceof Map) {
|
|
44
|
+
return fixtures.get(fixtureKey(backend, method, url))
|
|
45
|
+
|| fixtures.get(`${method.toUpperCase()} ${url}`)
|
|
46
|
+
|| fixtures.get(url);
|
|
47
|
+
}
|
|
48
|
+
return fixtures[fixtureKey(backend, method, url)]
|
|
49
|
+
|| fixtures[`${method.toUpperCase()} ${url}`]
|
|
50
|
+
|| fixtures[url]
|
|
51
|
+
|| (fixtures[backend] && (
|
|
52
|
+
fixtures[backend][`${method.toUpperCase()} ${url}`]
|
|
53
|
+
|| fixtures[backend][url]
|
|
54
|
+
|| fixtures[backend][new URL(url).pathname]
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function executeFastlyNativeHttpEffects(input, options = {}) {
|
|
59
|
+
const wasm = Buffer.isBuffer(input)
|
|
60
|
+
? input
|
|
61
|
+
: input && Buffer.isBuffer(input.wasm)
|
|
62
|
+
? input.wasm
|
|
63
|
+
: fs.readFileSync(path.resolve(input));
|
|
64
|
+
if (!WebAssembly.validate(wasm)) {
|
|
65
|
+
throw new FastlyNativeHttpEffectsMockError('Pass97 mock host requires valid WebAssembly.', 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_MOCK_INVALID_WASM');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const requestInput = options.request || {};
|
|
69
|
+
const method = String(requestInput.method || 'GET').toUpperCase();
|
|
70
|
+
const pathValue = String(requestInput.path || '/');
|
|
71
|
+
const url = String(requestInput.url || `https://pulse.test${pathValue.startsWith('/') ? pathValue : `/${pathValue}`}`);
|
|
72
|
+
const requestHeaders = normalizeHeaders(requestInput.headers);
|
|
73
|
+
const trace = [];
|
|
74
|
+
let nextHandle = 10;
|
|
75
|
+
const typedHandleStarts = options.handleStarts && typeof options.handleStarts === 'object' ? options.handleStarts : {};
|
|
76
|
+
const typedHandleNext = new Map();
|
|
77
|
+
let instance;
|
|
78
|
+
|
|
79
|
+
const requests = new Map();
|
|
80
|
+
const bodies = new Map();
|
|
81
|
+
const responses = new Map();
|
|
82
|
+
const pending = new Map();
|
|
83
|
+
const downstreamRequestHandle = 1;
|
|
84
|
+
const downstreamBodyHandle = 2;
|
|
85
|
+
requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
|
|
86
|
+
bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: [] });
|
|
87
|
+
let downstream;
|
|
88
|
+
|
|
89
|
+
function alloc(kind = 'default') {
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(typedHandleStarts, kind)) {
|
|
91
|
+
const current = typedHandleNext.has(kind) ? typedHandleNext.get(kind) : Number(typedHandleStarts[kind]);
|
|
92
|
+
if (!Number.isInteger(current) || current < 0) {
|
|
93
|
+
throw new FastlyNativeHttpEffectsMockError('Mock host handle starts must be non-negative integers.', 'PULSE_FASTLY_NATIVE_MOCK_HANDLE_START_INVALID', { kind, value: typedHandleStarts[kind] });
|
|
94
|
+
}
|
|
95
|
+
typedHandleNext.set(kind, current + 1);
|
|
96
|
+
return current;
|
|
97
|
+
}
|
|
98
|
+
return nextHandle++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function memory() {
|
|
102
|
+
const value = instance && instance.exports && instance.exports.memory;
|
|
103
|
+
if (!(value instanceof WebAssembly.Memory)) {
|
|
104
|
+
throw new FastlyNativeHttpEffectsMockError('Fastly native effects module did not export memory.', 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_MOCK_MEMORY_MISSING');
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function view() { return new DataView(memory().buffer); }
|
|
110
|
+
function writeU32(pointer, value) { view().setUint32(Number(pointer), Number(value) >>> 0, true); }
|
|
111
|
+
function readBytes(pointer, length) { return Buffer.from(new Uint8Array(memory().buffer, Number(pointer), Number(length))); }
|
|
112
|
+
function readUtf8(pointer, length) { return readBytes(pointer, length).toString('utf8'); }
|
|
113
|
+
|
|
114
|
+
function writeUtf8(value, pointer, maxLength, writtenOut) {
|
|
115
|
+
const bytes = Buffer.from(String(value), 'utf8');
|
|
116
|
+
writeU32(writtenOut, bytes.length);
|
|
117
|
+
if (bytes.length > Number(maxLength)) return FASTLY_STATUS_BUFLEN;
|
|
118
|
+
new Uint8Array(memory().buffer, Number(pointer), bytes.length).set(bytes);
|
|
119
|
+
return FASTLY_STATUS_OK;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function headerValue(headers, name) {
|
|
123
|
+
const target = String(name).toLowerCase();
|
|
124
|
+
const found = headers.find(([headerName]) => String(headerName).toLowerCase() === target);
|
|
125
|
+
return found && found[1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function bodyBytes(handle) {
|
|
129
|
+
const body = bodies.get(Number(handle));
|
|
130
|
+
if (!body) return undefined;
|
|
131
|
+
if (body.writes.length > 0) return Buffer.concat(body.writes);
|
|
132
|
+
return body.bytes;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function makeOriginResponse(fixture) {
|
|
136
|
+
const responseHandle = alloc('response');
|
|
137
|
+
const bodyHandle = alloc('body');
|
|
138
|
+
const normalized = fixture || {};
|
|
139
|
+
const headers = normalizeHeaders(normalized.headers);
|
|
140
|
+
let body = normalized.body;
|
|
141
|
+
if (Object.prototype.hasOwnProperty.call(normalized, 'json')) {
|
|
142
|
+
body = JSON.stringify(normalized.json);
|
|
143
|
+
if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
144
|
+
}
|
|
145
|
+
responses.set(responseHandle, {
|
|
146
|
+
status: Number(normalized.status || 200),
|
|
147
|
+
headers,
|
|
148
|
+
origin: true
|
|
149
|
+
});
|
|
150
|
+
bodies.set(bodyHandle, { bytes: normalizeBody(body), readOffset: 0, writes: [] });
|
|
151
|
+
return { responseHandle, bodyHandle };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const imports = {
|
|
155
|
+
env: {
|
|
156
|
+
abort(message, fileName, line, column) {
|
|
157
|
+
throw new FastlyNativeHttpEffectsMockError('AssemblyScript aborted in the Pass97 Fastly effects module.', 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_MOCK_ABORT', { message, fileName, line, column, trace });
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
fastly_abi: {
|
|
161
|
+
init(version) {
|
|
162
|
+
trace.push({ module: 'fastly_abi', name: 'init', version: String(version) });
|
|
163
|
+
return version === 1n ? FASTLY_STATUS_OK : FASTLY_STATUS_ERROR;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
fastly_http_req: {
|
|
167
|
+
body_downstream_get(requestOut, bodyOut) {
|
|
168
|
+
trace.push({ module: 'fastly_http_req', name: 'body_downstream_get' });
|
|
169
|
+
writeU32(requestOut, downstreamRequestHandle);
|
|
170
|
+
writeU32(bodyOut, downstreamBodyHandle);
|
|
171
|
+
return FASTLY_STATUS_OK;
|
|
172
|
+
},
|
|
173
|
+
method_get(handle, buffer, bufferLength, writtenOut) {
|
|
174
|
+
const request = requests.get(Number(handle));
|
|
175
|
+
trace.push({ module: 'fastly_http_req', name: 'method_get', handle: Number(handle) });
|
|
176
|
+
return request ? writeUtf8(request.method, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
|
|
177
|
+
},
|
|
178
|
+
uri_get(handle, buffer, bufferLength, writtenOut) {
|
|
179
|
+
const request = requests.get(Number(handle));
|
|
180
|
+
trace.push({ module: 'fastly_http_req', name: 'uri_get', handle: Number(handle) });
|
|
181
|
+
return request ? writeUtf8(request.url, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
|
|
182
|
+
},
|
|
183
|
+
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
184
|
+
const request = requests.get(Number(handle));
|
|
185
|
+
const name = readUtf8(namePointer, nameLength);
|
|
186
|
+
trace.push({ module: 'fastly_http_req', name: 'header_value_get', handle: Number(handle), header: name });
|
|
187
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
188
|
+
const value = headerValue(request.headers, name);
|
|
189
|
+
return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
190
|
+
},
|
|
191
|
+
new(handleOut) {
|
|
192
|
+
const handle = alloc('request');
|
|
193
|
+
requests.set(handle, { method: 'GET', url: '', headers: [] });
|
|
194
|
+
writeU32(handleOut, handle);
|
|
195
|
+
trace.push({ module: 'fastly_http_req', name: 'new', handle });
|
|
196
|
+
return FASTLY_STATUS_OK;
|
|
197
|
+
},
|
|
198
|
+
method_set(handle, pointer, length) {
|
|
199
|
+
const request = requests.get(Number(handle));
|
|
200
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
201
|
+
request.method = readUtf8(pointer, length).toUpperCase();
|
|
202
|
+
trace.push({ module: 'fastly_http_req', name: 'method_set', handle: Number(handle), method: request.method });
|
|
203
|
+
return FASTLY_STATUS_OK;
|
|
204
|
+
},
|
|
205
|
+
uri_set(handle, pointer, length) {
|
|
206
|
+
const request = requests.get(Number(handle));
|
|
207
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
208
|
+
request.url = readUtf8(pointer, length);
|
|
209
|
+
trace.push({ module: 'fastly_http_req', name: 'uri_set', handle: Number(handle), url: request.url });
|
|
210
|
+
return FASTLY_STATUS_OK;
|
|
211
|
+
},
|
|
212
|
+
header_insert(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
213
|
+
const request = requests.get(Number(handle));
|
|
214
|
+
if (!request) return FASTLY_STATUS_BADF;
|
|
215
|
+
const name = readUtf8(namePointer, nameLength);
|
|
216
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
217
|
+
request.headers = request.headers.filter(([headerName]) => headerName.toLowerCase() !== name.toLowerCase());
|
|
218
|
+
request.headers.push([name, value]);
|
|
219
|
+
trace.push({ module: 'fastly_http_req', name: 'header_insert', handle: Number(handle), header: [name, value] });
|
|
220
|
+
return FASTLY_STATUS_OK;
|
|
221
|
+
},
|
|
222
|
+
send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
223
|
+
const request = requests.get(Number(handle));
|
|
224
|
+
const body = bodies.get(Number(bodyHandle));
|
|
225
|
+
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
226
|
+
const backend = readUtf8(backendPointer, backendLength);
|
|
227
|
+
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
228
|
+
if (!fixture) {
|
|
229
|
+
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: request.url, missingFixture: true });
|
|
230
|
+
return FASTLY_STATUS_ERROR;
|
|
231
|
+
}
|
|
232
|
+
const pendingHandle = alloc('pending');
|
|
233
|
+
pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: bodyBytes(bodyHandle) }, backend, fixture });
|
|
234
|
+
writeU32(pendingOut, pendingHandle);
|
|
235
|
+
trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), bodyHandle: Number(bodyHandle), pendingHandle, backend, method: request.method, url: request.url });
|
|
236
|
+
return FASTLY_STATUS_OK;
|
|
237
|
+
},
|
|
238
|
+
pending_req_wait(handle, responseOut, bodyOut) {
|
|
239
|
+
const item = pending.get(Number(handle));
|
|
240
|
+
trace.push({ module: 'fastly_http_req', name: 'pending_req_wait', pendingHandle: Number(handle), backend: item && item.backend, url: item && item.request.url });
|
|
241
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
242
|
+
pending.delete(Number(handle));
|
|
243
|
+
const transportStatus = Number(item.fixture.transportStatus || 0);
|
|
244
|
+
if (transportStatus !== FASTLY_STATUS_OK) return transportStatus;
|
|
245
|
+
const origin = makeOriginResponse(item.fixture);
|
|
246
|
+
writeU32(responseOut, origin.responseHandle);
|
|
247
|
+
writeU32(bodyOut, origin.bodyHandle);
|
|
248
|
+
return FASTLY_STATUS_OK;
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
fastly_http_resp: {
|
|
252
|
+
new(handleOut) {
|
|
253
|
+
const handle = alloc('response');
|
|
254
|
+
responses.set(handle, { status: 200, headers: [], origin: false });
|
|
255
|
+
writeU32(handleOut, handle);
|
|
256
|
+
trace.push({ module: 'fastly_http_resp', name: 'new', handle });
|
|
257
|
+
return FASTLY_STATUS_OK;
|
|
258
|
+
},
|
|
259
|
+
header_append(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
260
|
+
const response = responses.get(Number(handle));
|
|
261
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
262
|
+
const name = readUtf8(namePointer, nameLength);
|
|
263
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
264
|
+
response.headers.push([name, value]);
|
|
265
|
+
trace.push({ module: 'fastly_http_resp', name: 'header_append', handle: Number(handle), header: [name, value] });
|
|
266
|
+
return FASTLY_STATUS_OK;
|
|
267
|
+
},
|
|
268
|
+
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
269
|
+
const response = responses.get(Number(handle));
|
|
270
|
+
const name = readUtf8(namePointer, nameLength);
|
|
271
|
+
trace.push({ module: 'fastly_http_resp', name: 'header_value_get', handle: Number(handle), header: name });
|
|
272
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
273
|
+
const value = headerValue(response.headers, name);
|
|
274
|
+
return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
|
|
275
|
+
},
|
|
276
|
+
status_get(handle, statusOut) {
|
|
277
|
+
const response = responses.get(Number(handle));
|
|
278
|
+
trace.push({ module: 'fastly_http_resp', name: 'status_get', handle: Number(handle) });
|
|
279
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
280
|
+
writeU32(statusOut, response.status);
|
|
281
|
+
return FASTLY_STATUS_OK;
|
|
282
|
+
},
|
|
283
|
+
status_set(handle, status) {
|
|
284
|
+
const response = responses.get(Number(handle));
|
|
285
|
+
if (!response) return FASTLY_STATUS_BADF;
|
|
286
|
+
response.status = Number(status);
|
|
287
|
+
trace.push({ module: 'fastly_http_resp', name: 'status_set', handle: Number(handle), status: Number(status) });
|
|
288
|
+
return FASTLY_STATUS_OK;
|
|
289
|
+
},
|
|
290
|
+
send_downstream(handle, bodyHandle, streaming) {
|
|
291
|
+
const response = responses.get(Number(handle));
|
|
292
|
+
const body = bodies.get(Number(bodyHandle));
|
|
293
|
+
trace.push({ module: 'fastly_http_resp', name: 'send_downstream', handle: Number(handle), bodyHandle: Number(bodyHandle), streaming: Number(streaming), origin: response && response.origin });
|
|
294
|
+
if (!response || !body) return FASTLY_STATUS_BADF;
|
|
295
|
+
downstream = {
|
|
296
|
+
status: response.status,
|
|
297
|
+
headers: response.headers.map((entry) => [...entry]),
|
|
298
|
+
bodyBytes: bodyBytes(bodyHandle),
|
|
299
|
+
streaming: Number(streaming),
|
|
300
|
+
origin: response.origin === true,
|
|
301
|
+
responseHandle: Number(handle),
|
|
302
|
+
bodyHandle: Number(bodyHandle)
|
|
303
|
+
};
|
|
304
|
+
return FASTLY_STATUS_OK;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
fastly_http_body: {
|
|
308
|
+
new(handleOut) {
|
|
309
|
+
const handle = alloc('body');
|
|
310
|
+
bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
|
|
311
|
+
writeU32(handleOut, handle);
|
|
312
|
+
trace.push({ module: 'fastly_http_body', name: 'new', handle });
|
|
313
|
+
return FASTLY_STATUS_OK;
|
|
314
|
+
},
|
|
315
|
+
read(handle, buffer, bufferLength, readOut) {
|
|
316
|
+
const body = bodies.get(Number(handle));
|
|
317
|
+
if (!body) return FASTLY_STATUS_BADF;
|
|
318
|
+
const source = bodyBytes(handle);
|
|
319
|
+
const offset = body.readOffset || 0;
|
|
320
|
+
const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset));
|
|
321
|
+
if (count > 0) new Uint8Array(memory().buffer, Number(buffer), count).set(source.subarray(offset, offset + count));
|
|
322
|
+
body.readOffset = offset + count;
|
|
323
|
+
writeU32(readOut, count);
|
|
324
|
+
trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: count });
|
|
325
|
+
return FASTLY_STATUS_OK;
|
|
326
|
+
},
|
|
327
|
+
write(handle, buffer, bufferLength, end, writtenOut) {
|
|
328
|
+
const body = bodies.get(Number(handle));
|
|
329
|
+
if (!body) return FASTLY_STATUS_BADF;
|
|
330
|
+
const bytes = readBytes(buffer, bufferLength);
|
|
331
|
+
body.writes.push(bytes);
|
|
332
|
+
writeU32(writtenOut, bytes.length);
|
|
333
|
+
trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end) });
|
|
334
|
+
return FASTLY_STATUS_OK;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const module = new WebAssembly.Module(wasm);
|
|
340
|
+
instance = new WebAssembly.Instance(module, imports);
|
|
341
|
+
if (typeof instance.exports._start !== 'function') {
|
|
342
|
+
throw new FastlyNativeHttpEffectsMockError('Pass97 native module is missing _start.', 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_MOCK_START_MISSING');
|
|
343
|
+
}
|
|
344
|
+
instance.exports._start();
|
|
345
|
+
const lastError = typeof instance.exports.pulse_fastly_last_error === 'function'
|
|
346
|
+
? Number(instance.exports.pulse_fastly_last_error())
|
|
347
|
+
: undefined;
|
|
348
|
+
const errorStage = typeof instance.exports.pulse_fastly_error_stage === 'function'
|
|
349
|
+
? Number(instance.exports.pulse_fastly_error_stage())
|
|
350
|
+
: undefined;
|
|
351
|
+
const errorEffect = typeof instance.exports.pulse_fastly_error_effect === 'function'
|
|
352
|
+
? Number(instance.exports.pulse_fastly_error_effect())
|
|
353
|
+
: undefined;
|
|
354
|
+
if (lastError !== FASTLY_STATUS_OK) {
|
|
355
|
+
throw new FastlyNativeHttpEffectsMockError('Fastly native HTTP effects module reported an execution error.', 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_MOCK_EXECUTION_FAILED', { lastError, errorStage, errorEffect, trace });
|
|
356
|
+
}
|
|
357
|
+
if (!downstream) {
|
|
358
|
+
throw new FastlyNativeHttpEffectsMockError('Fastly native HTTP effects module did not send a downstream response.', 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_MOCK_RESPONSE_NOT_SENT', { trace });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return Object.freeze({
|
|
362
|
+
version: 'pulse.fastly-native-http-effects-mock.v1',
|
|
363
|
+
request: Object.freeze({ method, url, path: pathValue, headers: Object.freeze(requestHeaders.map((entry) => Object.freeze(entry))) }),
|
|
364
|
+
response: Object.freeze({
|
|
365
|
+
status: downstream.status,
|
|
366
|
+
headers: Object.freeze(downstream.headers.map((entry) => Object.freeze(entry))),
|
|
367
|
+
body: downstream.bodyBytes.toString('utf8'),
|
|
368
|
+
bodyBytes: downstream.bodyBytes,
|
|
369
|
+
sent: true,
|
|
370
|
+
streaming: downstream.streaming,
|
|
371
|
+
origin: downstream.origin,
|
|
372
|
+
responseHandle: downstream.responseHandle,
|
|
373
|
+
bodyHandle: downstream.bodyHandle
|
|
374
|
+
}),
|
|
375
|
+
trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
|
|
376
|
+
instance
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = Object.freeze({
|
|
381
|
+
FASTLY_STATUS_OK,
|
|
382
|
+
FASTLY_STATUS_ERROR,
|
|
383
|
+
FASTLY_STATUS_BADF,
|
|
384
|
+
FASTLY_STATUS_BUFLEN,
|
|
385
|
+
FASTLY_STATUS_NONE,
|
|
386
|
+
FastlyNativeHttpEffectsMockError,
|
|
387
|
+
normalizeHeaders,
|
|
388
|
+
fixtureKey,
|
|
389
|
+
executeFastlyNativeHttpEffects
|
|
390
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
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_BUFLEN = 4;
|
|
8
|
+
const FASTLY_STATUS_NONE = 10;
|
|
9
|
+
|
|
10
|
+
class FastlyNativeHttpMockError extends Error {
|
|
11
|
+
constructor(message, code = 'PULSE_FASTLY_NATIVE_HTTP_MOCK_FAILED', detail = {}) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'FastlyNativeHttpMockError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.detail = Object.freeze({ ...detail });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeHeaders(input) {
|
|
20
|
+
if (!input) return [];
|
|
21
|
+
if (Array.isArray(input)) return input.map(([name, value]) => [String(name), String(value)]);
|
|
22
|
+
return Object.entries(input).flatMap(([name, value]) => Array.isArray(value) ? value.map((entry) => [String(name), String(entry)]) : [[String(name), String(value)]]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function executeFastlyNativeHttpShell(input, options = {}) {
|
|
26
|
+
const wasm = Buffer.isBuffer(input) ? input : input && Buffer.isBuffer(input.wasm) ? input.wasm : fs.readFileSync(path.resolve(input));
|
|
27
|
+
if (!WebAssembly.validate(wasm)) throw new FastlyNativeHttpMockError('Pass96 mock host requires valid WebAssembly.', 'PULSE_FASTLY_NATIVE_HTTP_MOCK_INVALID_WASM');
|
|
28
|
+
const requestInput = options.request || {};
|
|
29
|
+
const method = String(requestInput.method || 'GET').toUpperCase();
|
|
30
|
+
const pathValue = String(requestInput.path || '/');
|
|
31
|
+
const url = String(requestInput.url || `https://pulse.test${pathValue.startsWith('/') ? pathValue : `/${pathValue}`}`);
|
|
32
|
+
const requestHeaders = normalizeHeaders(requestInput.headers);
|
|
33
|
+
const trace = [];
|
|
34
|
+
const response = { status: 200, headers: [], bodyChunks: [], sent: false, streaming: 0 };
|
|
35
|
+
const handles = Object.freeze({ request: 1, requestBody: 2, response: 3, responseBody: 4 });
|
|
36
|
+
let instance;
|
|
37
|
+
|
|
38
|
+
function memory() {
|
|
39
|
+
const value = instance && instance.exports && instance.exports.memory;
|
|
40
|
+
if (!(value instanceof WebAssembly.Memory)) throw new FastlyNativeHttpMockError('Fastly native module did not export memory.', 'PULSE_FASTLY_NATIVE_HTTP_MOCK_MEMORY_MISSING');
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function view() {
|
|
45
|
+
return new DataView(memory().buffer);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeU32(pointer, value) {
|
|
49
|
+
view().setUint32(Number(pointer), Number(value) >>> 0, true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readBytes(pointer, length) {
|
|
53
|
+
return Buffer.from(new Uint8Array(memory().buffer, Number(pointer), Number(length)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readUtf8(pointer, length) {
|
|
57
|
+
return readBytes(pointer, length).toString('utf8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeUtf8(value, pointer, maxLength, writtenOut) {
|
|
61
|
+
const bytes = Buffer.from(String(value), 'utf8');
|
|
62
|
+
if (bytes.length > Number(maxLength)) {
|
|
63
|
+
writeU32(writtenOut, bytes.length);
|
|
64
|
+
return FASTLY_STATUS_BUFLEN;
|
|
65
|
+
}
|
|
66
|
+
new Uint8Array(memory().buffer, Number(pointer), bytes.length).set(bytes);
|
|
67
|
+
writeU32(writtenOut, bytes.length);
|
|
68
|
+
return FASTLY_STATUS_OK;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findHeader(name) {
|
|
72
|
+
const target = String(name).toLowerCase();
|
|
73
|
+
return requestHeaders.find(([headerName]) => headerName.toLowerCase() === target);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const imports = {
|
|
77
|
+
env: {
|
|
78
|
+
abort(message, fileName, line, column) {
|
|
79
|
+
throw new FastlyNativeHttpMockError('AssemblyScript aborted in the Pass96 Fastly shell.', 'PULSE_FASTLY_NATIVE_HTTP_MOCK_ABORT', { message, fileName, line, column });
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
fastly_abi: {
|
|
83
|
+
init(version) {
|
|
84
|
+
trace.push({ module: 'fastly_abi', name: 'init', version: String(version) });
|
|
85
|
+
return version === 1n ? FASTLY_STATUS_OK : 1;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
fastly_http_req: {
|
|
89
|
+
body_downstream_get(requestOut, bodyOut) {
|
|
90
|
+
trace.push({ module: 'fastly_http_req', name: 'body_downstream_get' });
|
|
91
|
+
writeU32(requestOut, handles.request);
|
|
92
|
+
writeU32(bodyOut, handles.requestBody);
|
|
93
|
+
return FASTLY_STATUS_OK;
|
|
94
|
+
},
|
|
95
|
+
method_get(handle, buffer, bufferLength, writtenOut) {
|
|
96
|
+
trace.push({ module: 'fastly_http_req', name: 'method_get', handle });
|
|
97
|
+
if (handle !== handles.request) return 3;
|
|
98
|
+
return writeUtf8(method, buffer, bufferLength, writtenOut);
|
|
99
|
+
},
|
|
100
|
+
uri_get(handle, buffer, bufferLength, writtenOut) {
|
|
101
|
+
trace.push({ module: 'fastly_http_req', name: 'uri_get', handle });
|
|
102
|
+
if (handle !== handles.request) return 3;
|
|
103
|
+
return writeUtf8(url, buffer, bufferLength, writtenOut);
|
|
104
|
+
},
|
|
105
|
+
header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
|
|
106
|
+
const name = readUtf8(namePointer, nameLength);
|
|
107
|
+
trace.push({ module: 'fastly_http_req', name: 'header_value_get', handle, header: name });
|
|
108
|
+
if (handle !== handles.request) return 3;
|
|
109
|
+
const found = findHeader(name);
|
|
110
|
+
if (!found) return FASTLY_STATUS_NONE;
|
|
111
|
+
return writeUtf8(found[1], valuePointer, valueLength, writtenOut);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
fastly_http_resp: {
|
|
115
|
+
new(handleOut) {
|
|
116
|
+
trace.push({ module: 'fastly_http_resp', name: 'new' });
|
|
117
|
+
writeU32(handleOut, handles.response);
|
|
118
|
+
return FASTLY_STATUS_OK;
|
|
119
|
+
},
|
|
120
|
+
header_append(handle, namePointer, nameLength, valuePointer, valueLength) {
|
|
121
|
+
const name = readUtf8(namePointer, nameLength);
|
|
122
|
+
const value = readUtf8(valuePointer, valueLength);
|
|
123
|
+
trace.push({ module: 'fastly_http_resp', name: 'header_append', handle, header: [name, value] });
|
|
124
|
+
if (handle !== handles.response) return 3;
|
|
125
|
+
response.headers.push([name, value]);
|
|
126
|
+
return FASTLY_STATUS_OK;
|
|
127
|
+
},
|
|
128
|
+
status_set(handle, status) {
|
|
129
|
+
trace.push({ module: 'fastly_http_resp', name: 'status_set', handle, status });
|
|
130
|
+
if (handle !== handles.response) return 3;
|
|
131
|
+
response.status = Number(status);
|
|
132
|
+
return FASTLY_STATUS_OK;
|
|
133
|
+
},
|
|
134
|
+
send_downstream(handle, bodyHandle, streaming) {
|
|
135
|
+
trace.push({ module: 'fastly_http_resp', name: 'send_downstream', handle, bodyHandle, streaming });
|
|
136
|
+
if (handle !== handles.response || bodyHandle !== handles.responseBody) return 3;
|
|
137
|
+
response.sent = true;
|
|
138
|
+
response.streaming = Number(streaming);
|
|
139
|
+
return FASTLY_STATUS_OK;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
fastly_http_body: {
|
|
143
|
+
new(handleOut) {
|
|
144
|
+
trace.push({ module: 'fastly_http_body', name: 'new' });
|
|
145
|
+
writeU32(handleOut, handles.responseBody);
|
|
146
|
+
return FASTLY_STATUS_OK;
|
|
147
|
+
},
|
|
148
|
+
write(handle, buffer, bufferLength, end, writtenOut) {
|
|
149
|
+
trace.push({ module: 'fastly_http_body', name: 'write', handle, bytes: Number(bufferLength), end });
|
|
150
|
+
if (handle !== handles.responseBody) return 3;
|
|
151
|
+
const bytes = readBytes(buffer, bufferLength);
|
|
152
|
+
response.bodyChunks.push(bytes);
|
|
153
|
+
writeU32(writtenOut, bytes.length);
|
|
154
|
+
return FASTLY_STATUS_OK;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const module = new WebAssembly.Module(wasm);
|
|
160
|
+
instance = new WebAssembly.Instance(module, imports);
|
|
161
|
+
if (typeof instance.exports._start !== 'function') throw new FastlyNativeHttpMockError('Pass96 native module is missing _start.', 'PULSE_FASTLY_NATIVE_HTTP_MOCK_START_MISSING');
|
|
162
|
+
instance.exports._start();
|
|
163
|
+
const lastError = typeof instance.exports.pulse_fastly_last_error === 'function' ? Number(instance.exports.pulse_fastly_last_error()) : undefined;
|
|
164
|
+
if (lastError !== FASTLY_STATUS_OK) throw new FastlyNativeHttpMockError('Fastly native HTTP shell reported a hostcall error.', 'PULSE_FASTLY_NATIVE_HTTP_MOCK_HOSTCALL_FAILED', { lastError, trace });
|
|
165
|
+
if (!response.sent) throw new FastlyNativeHttpMockError('Fastly native HTTP shell did not send a downstream response.', 'PULSE_FASTLY_NATIVE_HTTP_MOCK_RESPONSE_NOT_SENT', { trace });
|
|
166
|
+
const bodyBytes = Buffer.concat(response.bodyChunks);
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
version: 'pulse.fastly-native-http-mock.v1',
|
|
169
|
+
request: Object.freeze({ method, url, path: pathValue, headers: Object.freeze(requestHeaders.map((pair) => Object.freeze(pair))) }),
|
|
170
|
+
response: Object.freeze({
|
|
171
|
+
status: response.status,
|
|
172
|
+
headers: Object.freeze(response.headers.map((pair) => Object.freeze(pair))),
|
|
173
|
+
body: bodyBytes.toString('utf8'),
|
|
174
|
+
bodyBytes,
|
|
175
|
+
sent: response.sent,
|
|
176
|
+
streaming: response.streaming
|
|
177
|
+
}),
|
|
178
|
+
trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
|
|
179
|
+
instance
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = Object.freeze({
|
|
184
|
+
FASTLY_STATUS_OK,
|
|
185
|
+
FASTLY_STATUS_BUFLEN,
|
|
186
|
+
FASTLY_STATUS_NONE,
|
|
187
|
+
FastlyNativeHttpMockError,
|
|
188
|
+
normalizeHeaders,
|
|
189
|
+
executeFastlyNativeHttpShell
|
|
190
|
+
});
|