@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.
Files changed (63) hide show
  1. package/README.md +52 -1
  2. package/dist/config-schema.json +73 -0
  3. package/dist/index.d.ts +61 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +59 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +87 -5
  8. package/src/build/canonical-target.js +436 -0
  9. package/src/build/entities-native.js +564 -0
  10. package/src/build/fixed-memory-runtime.as.ts +148 -0
  11. package/src/build/native-http-effects.js +871 -0
  12. package/src/build/native-http-shell.js +703 -0
  13. package/src/build/native-platform-capabilities.js +2109 -0
  14. package/src/compiler/assets-package-out-parity-proof.js +338 -0
  15. package/src/compiler/assets-provider-proof.js +499 -0
  16. package/src/compiler/config-provider-resolution.js +573 -0
  17. package/src/compiler/fastly-adapter.js +564 -0
  18. package/src/compiler/fastly-command-entry.js +390 -0
  19. package/src/compiler/fastly-config-secret-serve.js +323 -0
  20. package/src/compiler/fastly-hostcall-binding.js +598 -0
  21. package/src/compiler/fastly-readiness.js +477 -0
  22. package/src/compiler/index.js +40 -0
  23. package/src/compiler/lifecycle-parity-contract.js +70 -0
  24. package/src/compiler/lifecycle-parity-proof.js +260 -0
  25. package/src/compiler/route-handler-effect-proof.js +255 -0
  26. package/src/compiler/runtime-provider-kv.js +237 -0
  27. package/src/compiler/runtime-provider-shape.js +109 -0
  28. package/src/config-api.js +63 -0
  29. package/src/config-schema.d.ts +13 -0
  30. package/src/config-schema.js +27 -0
  31. package/src/config-schema.json +73 -0
  32. package/src/index.js +3 -0
  33. package/src/index.ts +118 -0
  34. package/src/javascript/assets-adapter.js +122 -0
  35. package/src/javascript/bindings-adapter.js +237 -0
  36. package/src/javascript/errors.js +26 -0
  37. package/src/javascript/fetch-adapter.js +93 -0
  38. package/src/javascript/grip-broadcast.js +261 -0
  39. package/src/javascript/jwt-verifier.js +137 -0
  40. package/src/javascript/lifecycle.js +55 -0
  41. package/src/javascript/local-loader.js +138 -0
  42. package/src/javascript/logging.js +26 -0
  43. package/src/javascript/package-effects.js +68 -0
  44. package/src/javascript/runtime-host.js +108 -0
  45. package/src/javascript/sha256.js +128 -0
  46. package/src/javascript/source-package.js +776 -0
  47. package/src/javascript/support.js +142 -0
  48. package/src/javascript/target-support-policy.js +260 -0
  49. package/src/javascript/target.js +73 -0
  50. package/src/javascript/test-runtime.js +407 -0
  51. package/src/provider-contract.js +72 -0
  52. package/src/runtime/assets-provider.js +464 -0
  53. package/src/runtime/canonical-api-runtime.js +597 -0
  54. package/src/runtime/index.js +28 -0
  55. package/src/runtime/provider-primitives-contract.js +272 -0
  56. package/src/runtime/provider-primitives.js +82 -0
  57. package/src/runtime/route-handler-effects.js +507 -0
  58. package/src/testing/fastly-cli.js +538 -0
  59. package/src/testing/native-http-effects-host.js +390 -0
  60. package/src/testing/native-http-shell-host.js +190 -0
  61. package/src/testing/native-platform-capabilities-host.js +599 -0
  62. package/src/toolchain/config.js +128 -0
  63. package/src/toolchain/index.js +343 -0
@@ -0,0 +1,599 @@
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']);
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 nextHandle = 10;
117
+ const typedHandleStarts = options.handleStarts && typeof options.handleStarts === 'object' ? options.handleStarts : {};
118
+ const typedHandleNext = new Map();
119
+ let instance;
120
+
121
+ const requests = new Map();
122
+ const bodies = new Map();
123
+ const responses = new Map();
124
+ const pending = new Map();
125
+ const outboundRequests = [];
126
+ const configStores = normalizeNamedStores(options.configStores, options.configStore || 'app_config', options.config);
127
+ const secretStores = normalizeNamedStores(options.secretStores, options.secretStore || 'app_secrets', options.secrets);
128
+ const secretValues = [...secretStores.values()].flatMap((values) => [...values.values()].map((value) => String(value))).filter(Boolean);
129
+ 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)]);
131
+ const kvStores = normalizeNamedStores(options.kvStores, options.kvStore || 'app_sessions', undefined);
132
+ for (const values of kvStores.values()) for (const [key, value] of values) values.set(key, encodeKvValue(value));
133
+ const configHandles = new Map();
134
+ const secretStoreHandles = new Map();
135
+ const secretHandles = new Map();
136
+ const kvStoreHandles = new Map();
137
+ const pendingKvLookups = new Map();
138
+ const pendingKvInserts = new Map();
139
+ const logHandles = new Map();
140
+ const logs = [];
141
+ const downstreamRequestHandle = 1;
142
+ const downstreamBodyHandle = 2;
143
+ requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
144
+ bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: [] });
145
+ let downstream;
146
+
147
+ function alloc(kind = 'default') {
148
+ if (Object.prototype.hasOwnProperty.call(typedHandleStarts, kind)) {
149
+ const current = typedHandleNext.has(kind) ? typedHandleNext.get(kind) : Number(typedHandleStarts[kind]);
150
+ if (!Number.isInteger(current) || current < 0) {
151
+ throw new FastlyNativePlatformCapabilitiesMockError('Mock host handle starts must be non-negative integers.', 'PULSE_FASTLY_NATIVE_MOCK_HANDLE_START_INVALID', { kind, value: typedHandleStarts[kind] });
152
+ }
153
+ typedHandleNext.set(kind, current + 1);
154
+ return current;
155
+ }
156
+ return nextHandle++;
157
+ }
158
+
159
+ function memory() {
160
+ const value = instance && instance.exports && instance.exports.memory;
161
+ if (!(value instanceof WebAssembly.Memory)) {
162
+ throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module did not export memory.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_MEMORY_MISSING');
163
+ }
164
+ return value;
165
+ }
166
+
167
+ function view() { return new DataView(memory().buffer); }
168
+ function writeU32(pointer, value) { view().setUint32(Number(pointer), Number(value) >>> 0, true); }
169
+ function writeU64(pointer, value) { view().setBigUint64(Number(pointer), BigInt(value), true); }
170
+ function readBytes(pointer, length) { return Buffer.from(new Uint8Array(memory().buffer, Number(pointer), Number(length))); }
171
+ function readUtf8(pointer, length) { return readBytes(pointer, length).toString('utf8'); }
172
+
173
+ function writeUtf8(value, pointer, maxLength, writtenOut) {
174
+ const bytes = Buffer.from(String(value), 'utf8');
175
+ writeU32(writtenOut, bytes.length);
176
+ if (bytes.length > Number(maxLength)) return FASTLY_STATUS_BUFLEN;
177
+ new Uint8Array(memory().buffer, Number(pointer), bytes.length).set(bytes);
178
+ return FASTLY_STATUS_OK;
179
+ }
180
+
181
+ function headerValue(headers, name) {
182
+ const target = String(name).toLowerCase();
183
+ const found = headers.find(([headerName]) => String(headerName).toLowerCase() === target);
184
+ return found && found[1];
185
+ }
186
+
187
+ function bodyBytes(handle) {
188
+ const body = bodies.get(Number(handle));
189
+ if (!body) return undefined;
190
+ if (body.writes.length > 0) return Buffer.concat(body.writes);
191
+ return body.bytes;
192
+ }
193
+
194
+ function makeOriginResponse(fixture) {
195
+ const responseHandle = alloc('response');
196
+ const bodyHandle = alloc('body');
197
+ const normalized = fixture || {};
198
+ const headers = normalizeHeaders(normalized.headers);
199
+ let body = normalized.body;
200
+ if (Object.prototype.hasOwnProperty.call(normalized, 'json')) {
201
+ body = JSON.stringify(normalized.json);
202
+ if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
203
+ }
204
+ responses.set(responseHandle, {
205
+ status: Number(normalized.status || 200),
206
+ headers,
207
+ origin: true
208
+ });
209
+ bodies.set(bodyHandle, { bytes: normalizeBody(body), readOffset: 0, writes: [] });
210
+ return { responseHandle, bodyHandle };
211
+ }
212
+
213
+ const imports = {
214
+ wasi_snapshot_preview1: {
215
+ clock_time_get(clockId, precision, timeOut) {
216
+ const seconds = Number.isFinite(Number(options.clockUnixSeconds))
217
+ ? Number(options.clockUnixSeconds)
218
+ : Math.floor(Date.now() / 1000);
219
+ writeU64(timeOut, BigInt(Math.trunc(seconds * 1_000_000_000)));
220
+ trace.push({
221
+ module: 'wasi_snapshot_preview1',
222
+ name: 'clock_time_get',
223
+ clockId: Number(clockId),
224
+ precision: String(precision),
225
+ unixEpochSeconds: seconds
226
+ });
227
+ return FASTLY_STATUS_OK;
228
+ }
229
+ },
230
+ env: {
231
+ abort(message, fileName, line, column) {
232
+ throw new FastlyNativePlatformCapabilitiesMockError('AssemblyScript aborted in the Pass98 Fastly effects module.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_ABORT', { message, fileName, line, column, trace });
233
+ }
234
+ },
235
+ fastly_abi: {
236
+ init(version) {
237
+ trace.push({ module: 'fastly_abi', name: 'init', version: String(version) });
238
+ return version === 1n ? FASTLY_STATUS_OK : FASTLY_STATUS_ERROR;
239
+ }
240
+ },
241
+ fastly_log: {
242
+ endpoint_get(namePointer, nameLength, handleOut) {
243
+ const endpoint = readUtf8(namePointer, nameLength);
244
+ if (options.logEndpointFailure === true) {
245
+ trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, opened: false });
246
+ return FASTLY_STATUS_ERROR;
247
+ }
248
+ const handle = alloc('log');
249
+ logHandles.set(handle, endpoint);
250
+ writeU32(handleOut, handle);
251
+ trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, handle, opened: true });
252
+ return FASTLY_STATUS_OK;
253
+ },
254
+ write(handle, messagePointer, messageLength, writtenOut) {
255
+ const endpoint = logHandles.get(Number(handle));
256
+ if (!endpoint) return FASTLY_STATUS_BADF;
257
+ if (options.logWriteFailure === true) {
258
+ trace.push({ module: 'fastly_log', name: 'write', endpoint, accepted: false });
259
+ return FASTLY_STATUS_ERROR;
260
+ }
261
+ const message = redactText(readUtf8(messagePointer, messageLength));
262
+ writeU32(writtenOut, Buffer.byteLength(message));
263
+ logs.push(Object.freeze({ endpoint, message }));
264
+ trace.push({ module: 'fastly_log', name: 'write', endpoint, message, accepted: true });
265
+ return FASTLY_STATUS_OK;
266
+ }
267
+ },
268
+ fastly_http_req: {
269
+ body_downstream_get(requestOut, bodyOut) {
270
+ trace.push({ module: 'fastly_http_req', name: 'body_downstream_get' });
271
+ writeU32(requestOut, downstreamRequestHandle);
272
+ writeU32(bodyOut, downstreamBodyHandle);
273
+ return FASTLY_STATUS_OK;
274
+ },
275
+ method_get(handle, buffer, bufferLength, writtenOut) {
276
+ const request = requests.get(Number(handle));
277
+ trace.push({ module: 'fastly_http_req', name: 'method_get', handle: Number(handle) });
278
+ return request ? writeUtf8(request.method, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
279
+ },
280
+ uri_get(handle, buffer, bufferLength, writtenOut) {
281
+ const request = requests.get(Number(handle));
282
+ trace.push({ module: 'fastly_http_req', name: 'uri_get', handle: Number(handle) });
283
+ return request ? writeUtf8(request.url, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
284
+ },
285
+ header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
286
+ const request = requests.get(Number(handle));
287
+ const name = readUtf8(namePointer, nameLength);
288
+ trace.push({ module: 'fastly_http_req', name: 'header_value_get', handle: Number(handle), header: name });
289
+ if (!request) return FASTLY_STATUS_BADF;
290
+ const value = headerValue(request.headers, name);
291
+ return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
292
+ },
293
+ new(handleOut) {
294
+ const handle = alloc('request');
295
+ requests.set(handle, { method: 'GET', url: '', headers: [] });
296
+ writeU32(handleOut, handle);
297
+ trace.push({ module: 'fastly_http_req', name: 'new', handle });
298
+ return FASTLY_STATUS_OK;
299
+ },
300
+ method_set(handle, pointer, length) {
301
+ const request = requests.get(Number(handle));
302
+ if (!request) return FASTLY_STATUS_BADF;
303
+ request.method = readUtf8(pointer, length).toUpperCase();
304
+ trace.push({ module: 'fastly_http_req', name: 'method_set', handle: Number(handle), method: request.method });
305
+ return FASTLY_STATUS_OK;
306
+ },
307
+ uri_set(handle, pointer, length) {
308
+ const request = requests.get(Number(handle));
309
+ if (!request) return FASTLY_STATUS_BADF;
310
+ request.url = readUtf8(pointer, length);
311
+ trace.push({ module: 'fastly_http_req', name: 'uri_set', handle: Number(handle), url: redactText(request.url) });
312
+ return FASTLY_STATUS_OK;
313
+ },
314
+ header_insert(handle, namePointer, nameLength, valuePointer, valueLength) {
315
+ const request = requests.get(Number(handle));
316
+ if (!request) return FASTLY_STATUS_BADF;
317
+ const name = readUtf8(namePointer, nameLength);
318
+ const value = readUtf8(valuePointer, valueLength);
319
+ request.headers = request.headers.filter(([headerName]) => headerName.toLowerCase() !== name.toLowerCase());
320
+ request.headers.push([name, value]);
321
+ trace.push({ module: 'fastly_http_req', name: 'header_insert', handle: Number(handle), header: [name, SENSITIVE_HEADERS.has(name.toLowerCase()) ? '[REDACTED]' : redactText(value)] });
322
+ return FASTLY_STATUS_OK;
323
+ },
324
+ send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
325
+ const request = requests.get(Number(handle));
326
+ const body = bodies.get(Number(bodyHandle));
327
+ if (!request || !body) return FASTLY_STATUS_BADF;
328
+ const backend = readUtf8(backendPointer, backendLength);
329
+ const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
330
+ if (!fixture) {
331
+ trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: request.url, missingFixture: true });
332
+ return FASTLY_STATUS_ERROR;
333
+ }
334
+ const pendingHandle = alloc('pending');
335
+ const capturedBody = bodyBytes(bodyHandle);
336
+ pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: capturedBody }, backend, fixture });
337
+ outboundRequests.push(Object.freeze({
338
+ backend,
339
+ 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'))
343
+ }));
344
+ 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 });
346
+ return FASTLY_STATUS_OK;
347
+ },
348
+ pending_req_wait(handle, responseOut, bodyOut) {
349
+ const item = pending.get(Number(handle));
350
+ trace.push({ module: 'fastly_http_req', name: 'pending_req_wait', pendingHandle: Number(handle), backend: item && item.backend, url: item && item.request.url });
351
+ if (!item) return FASTLY_STATUS_BADF;
352
+ pending.delete(Number(handle));
353
+ const transportStatus = Number(item.fixture.transportStatus || 0);
354
+ if (transportStatus !== FASTLY_STATUS_OK) return transportStatus;
355
+ const origin = makeOriginResponse(item.fixture);
356
+ writeU32(responseOut, origin.responseHandle);
357
+ writeU32(bodyOut, origin.bodyHandle);
358
+ return FASTLY_STATUS_OK;
359
+ }
360
+ },
361
+ fastly_config_store: {
362
+ open(namePointer, nameLength, handleOut) {
363
+ const name = readUtf8(namePointer, nameLength);
364
+ const values = configStores.get(name);
365
+ trace.push({ module: 'fastly_config_store', name: 'open', store: name, found: Boolean(values) });
366
+ if (!values) return FASTLY_STATUS_NONE;
367
+ const handle = alloc('configStore'); configHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
368
+ },
369
+ get(handle, keyPointer, keyLength, valuePointer, valueLength, writtenOut) {
370
+ const values = configHandles.get(Number(handle));
371
+ const key = readUtf8(keyPointer, keyLength);
372
+ const value = values && values.get(key);
373
+ trace.push({ module: 'fastly_config_store', name: 'get', handle: Number(handle), key, found: value !== undefined });
374
+ if (!values) return FASTLY_STATUS_BADF;
375
+ return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
376
+ }
377
+ },
378
+ fastly_secret_store: {
379
+ open(namePointer, nameLength, handleOut) {
380
+ const name = readUtf8(namePointer, nameLength);
381
+ const values = secretStores.get(name);
382
+ trace.push({ module: 'fastly_secret_store', name: 'open', store: name, found: Boolean(values) });
383
+ if (!values) return FASTLY_STATUS_NONE;
384
+ const handle = alloc('secretStore'); secretStoreHandles.set(handle, values); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
385
+ },
386
+ get(handle, keyPointer, keyLength, secretOut) {
387
+ const values = secretStoreHandles.get(Number(handle));
388
+ const key = readUtf8(keyPointer, keyLength);
389
+ const value = values && values.get(key);
390
+ trace.push({ module: 'fastly_secret_store', name: 'get', handle: Number(handle), key, found: value !== undefined });
391
+ if (!values) return FASTLY_STATUS_BADF;
392
+ if (value === undefined) return FASTLY_STATUS_NONE;
393
+ const secretHandle = alloc('secret'); secretHandles.set(secretHandle, String(value)); writeU32(secretOut, secretHandle); return FASTLY_STATUS_OK;
394
+ },
395
+ plaintext(handle, valuePointer, valueLength, writtenOut) {
396
+ const value = secretHandles.get(Number(handle));
397
+ trace.push({ module: 'fastly_secret_store', name: 'plaintext', handle: Number(handle), value: '[REDACTED]' });
398
+ return value === undefined ? FASTLY_STATUS_BADF : writeUtf8(value, valuePointer, valueLength, writtenOut);
399
+ }
400
+ },
401
+ fastly_kv_store: {
402
+ open(namePointer, nameLength, handleOut) {
403
+ const name = readUtf8(namePointer, nameLength);
404
+ const values = kvStores.get(name);
405
+ trace.push({ module: 'fastly_kv_store', name: 'open', store: name, found: Boolean(values) });
406
+ if (!values) return FASTLY_STATUS_NONE;
407
+ const handle = alloc('kvStore'); kvStoreHandles.set(handle, { name, values }); writeU32(handleOut, handle); return FASTLY_STATUS_OK;
408
+ },
409
+ lookup(storeHandle, keyPointer, keyLength, configMask, configPointer, lookupOut) {
410
+ const store = kvStoreHandles.get(Number(storeHandle));
411
+ if (!store) return FASTLY_STATUS_BADF;
412
+ const key = readUtf8(keyPointer, keyLength);
413
+ const handle = alloc('kvLookup'); pendingKvLookups.set(handle, { store, key }); writeU32(lookupOut, handle);
414
+ trace.push({ module: 'fastly_kv_store', name: 'lookup', store: store.name, key, pendingHandle: handle, configMask: Number(configMask) });
415
+ return FASTLY_STATUS_OK;
416
+ },
417
+ lookup_wait_v2(handle, bodyOut, metadataPointer, metadataLength, writtenOut, generationOut, kvErrorOut) {
418
+ const item = pendingKvLookups.get(Number(handle));
419
+ if (!item) return FASTLY_STATUS_BADF;
420
+ pendingKvLookups.delete(Number(handle));
421
+ const value = item.store.values.get(item.key);
422
+ writeU32(writtenOut, 0); writeU64(generationOut, 0n);
423
+ if (value === undefined) {
424
+ writeU32(kvErrorOut, FASTLY_KV_ERROR_NOT_FOUND);
425
+ trace.push({ module: 'fastly_kv_store', name: 'lookup_wait_v2', store: item.store.name, key: item.key, found: false });
426
+ return FASTLY_STATUS_OK;
427
+ }
428
+ const bodyHandle = alloc('body'); bodies.set(bodyHandle, { bytes: Buffer.from(value), readOffset: 0, writes: [] }); writeU32(bodyOut, bodyHandle); writeU32(kvErrorOut, FASTLY_KV_ERROR_OK);
429
+ trace.push({ module: 'fastly_kv_store', name: 'lookup_wait_v2', store: item.store.name, key: item.key, found: true, bodyHandle });
430
+ return FASTLY_STATUS_OK;
431
+ },
432
+ insert(storeHandle, keyPointer, keyLength, bodyHandle, configMask, configPointer, insertOut) {
433
+ const store = kvStoreHandles.get(Number(storeHandle));
434
+ const body = bodies.get(Number(bodyHandle));
435
+ if (!store || !body) return FASTLY_STATUS_BADF;
436
+ const key = readUtf8(keyPointer, keyLength);
437
+ const handle = alloc('kvInsert'); pendingKvInserts.set(handle, { store, key, bytes: bodyBytes(bodyHandle) }); writeU32(insertOut, handle);
438
+ trace.push({ module: 'fastly_kv_store', name: 'insert', store: store.name, key, pendingHandle: handle, bytes: bodyBytes(bodyHandle).length, configMask: Number(configMask) });
439
+ return FASTLY_STATUS_OK;
440
+ },
441
+ insert_wait(handle, kvErrorOut) {
442
+ const item = pendingKvInserts.get(Number(handle));
443
+ if (!item) return FASTLY_STATUS_BADF;
444
+ pendingKvInserts.delete(Number(handle)); item.store.values.set(item.key, Buffer.from(item.bytes)); writeU32(kvErrorOut, FASTLY_KV_ERROR_OK);
445
+ trace.push({ module: 'fastly_kv_store', name: 'insert_wait', store: item.store.name, key: item.key, bytes: item.bytes.length });
446
+ return FASTLY_STATUS_OK;
447
+ }
448
+ },
449
+ fastly_http_resp: {
450
+ new(handleOut) {
451
+ const handle = alloc('response');
452
+ responses.set(handle, { status: 200, headers: [], origin: false });
453
+ writeU32(handleOut, handle);
454
+ trace.push({ module: 'fastly_http_resp', name: 'new', handle });
455
+ return FASTLY_STATUS_OK;
456
+ },
457
+ header_append(handle, namePointer, nameLength, valuePointer, valueLength) {
458
+ const response = responses.get(Number(handle));
459
+ if (!response) return FASTLY_STATUS_BADF;
460
+ const name = readUtf8(namePointer, nameLength);
461
+ const value = readUtf8(valuePointer, valueLength);
462
+ response.headers.push([name, value]);
463
+ trace.push({ module: 'fastly_http_resp', name: 'header_append', handle: Number(handle), header: [name, value] });
464
+ return FASTLY_STATUS_OK;
465
+ },
466
+ header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
467
+ const response = responses.get(Number(handle));
468
+ const name = readUtf8(namePointer, nameLength);
469
+ trace.push({ module: 'fastly_http_resp', name: 'header_value_get', handle: Number(handle), header: name });
470
+ if (!response) return FASTLY_STATUS_BADF;
471
+ const value = headerValue(response.headers, name);
472
+ return value === undefined ? FASTLY_STATUS_NONE : writeUtf8(value, valuePointer, valueLength, writtenOut);
473
+ },
474
+ status_get(handle, statusOut) {
475
+ const response = responses.get(Number(handle));
476
+ trace.push({ module: 'fastly_http_resp', name: 'status_get', handle: Number(handle) });
477
+ if (!response) return FASTLY_STATUS_BADF;
478
+ writeU32(statusOut, response.status);
479
+ return FASTLY_STATUS_OK;
480
+ },
481
+ status_set(handle, status) {
482
+ const response = responses.get(Number(handle));
483
+ if (!response) return FASTLY_STATUS_BADF;
484
+ response.status = Number(status);
485
+ trace.push({ module: 'fastly_http_resp', name: 'status_set', handle: Number(handle), status: Number(status) });
486
+ return FASTLY_STATUS_OK;
487
+ },
488
+ send_downstream(handle, bodyHandle, streaming) {
489
+ const response = responses.get(Number(handle));
490
+ const body = bodies.get(Number(bodyHandle));
491
+ trace.push({ module: 'fastly_http_resp', name: 'send_downstream', handle: Number(handle), bodyHandle: Number(bodyHandle), streaming: Number(streaming), origin: response && response.origin });
492
+ if (!response || !body) return FASTLY_STATUS_BADF;
493
+ downstream = {
494
+ status: response.status,
495
+ headers: response.headers.map((entry) => [...entry]),
496
+ bodyBytes: bodyBytes(bodyHandle),
497
+ streaming: Number(streaming),
498
+ origin: response.origin === true,
499
+ responseHandle: Number(handle),
500
+ bodyHandle: Number(bodyHandle)
501
+ };
502
+ return FASTLY_STATUS_OK;
503
+ }
504
+ },
505
+ fastly_http_body: {
506
+ new(handleOut) {
507
+ const handle = alloc('body');
508
+ bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
509
+ writeU32(handleOut, handle);
510
+ trace.push({ module: 'fastly_http_body', name: 'new', handle });
511
+ return FASTLY_STATUS_OK;
512
+ },
513
+ read(handle, buffer, bufferLength, readOut) {
514
+ const body = bodies.get(Number(handle));
515
+ if (!body) return FASTLY_STATUS_BADF;
516
+ const source = bodyBytes(handle);
517
+ const offset = body.readOffset || 0;
518
+ const count = Math.min(Number(bufferLength), Math.max(0, source.length - offset));
519
+ if (count > 0) new Uint8Array(memory().buffer, Number(buffer), count).set(source.subarray(offset, offset + count));
520
+ body.readOffset = offset + count;
521
+ writeU32(readOut, count);
522
+ trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: count });
523
+ return FASTLY_STATUS_OK;
524
+ },
525
+ write(handle, buffer, bufferLength, end, writtenOut) {
526
+ const body = bodies.get(Number(handle));
527
+ if (!body) return FASTLY_STATUS_BADF;
528
+ const bytes = readBytes(buffer, bufferLength);
529
+ body.writes.push(bytes);
530
+ writeU32(writtenOut, bytes.length);
531
+ trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end) });
532
+ return FASTLY_STATUS_OK;
533
+ }
534
+ }
535
+ };
536
+
537
+ const module = new WebAssembly.Module(wasm);
538
+ instance = new WebAssembly.Instance(module, imports);
539
+ if (typeof instance.exports._start !== 'function') {
540
+ throw new FastlyNativePlatformCapabilitiesMockError('Pass98 native module is missing _start.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_START_MISSING');
541
+ }
542
+ instance.exports._start();
543
+ const lastError = typeof instance.exports.pulse_fastly_last_error === 'function'
544
+ ? Number(instance.exports.pulse_fastly_last_error())
545
+ : undefined;
546
+ const errorStage = typeof instance.exports.pulse_fastly_error_stage === 'function'
547
+ ? Number(instance.exports.pulse_fastly_error_stage())
548
+ : undefined;
549
+ const errorEffect = typeof instance.exports.pulse_fastly_error_effect === 'function'
550
+ ? Number(instance.exports.pulse_fastly_error_effect())
551
+ : undefined;
552
+ const pulseLastError = typeof instance.exports.pulse_last_error_code === 'function'
553
+ ? Number(instance.exports.pulse_last_error_code())
554
+ : undefined;
555
+ const handledJwtError = options.allowHandledJwtError === true
556
+ && lastError === 1008
557
+ && Boolean(downstream);
558
+ if (lastError !== FASTLY_STATUS_OK && !handledJwtError) {
559
+ 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 });
560
+ }
561
+ if (!downstream) {
562
+ 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
+ }
564
+
565
+ return Object.freeze({
566
+ version: 'pulse.fastly-native-platform-capabilities-mock.v1',
567
+ request: Object.freeze({ method, url, path: pathValue, headers: Object.freeze(requestHeaders.map((entry) => Object.freeze(entry))) }),
568
+ response: Object.freeze({
569
+ status: downstream.status,
570
+ headers: Object.freeze(downstream.headers.map((entry) => Object.freeze(entry))),
571
+ body: downstream.bodyBytes.toString('utf8'),
572
+ bodyBytes: downstream.bodyBytes,
573
+ sent: true,
574
+ streaming: downstream.streaming,
575
+ origin: downstream.origin,
576
+ responseHandle: downstream.responseHandle,
577
+ bodyHandle: downstream.bodyHandle
578
+ }),
579
+ trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
580
+ logs: Object.freeze(logs),
581
+ outboundRequests: Object.freeze(outboundRequests),
582
+ 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) }),
583
+ instance
584
+ });
585
+ }
586
+
587
+ module.exports = Object.freeze({
588
+ FASTLY_STATUS_OK,
589
+ FASTLY_STATUS_ERROR,
590
+ FASTLY_STATUS_BADF,
591
+ FASTLY_STATUS_BUFLEN,
592
+ FASTLY_STATUS_NONE,
593
+ FASTLY_KV_ERROR_OK,
594
+ FASTLY_KV_ERROR_NOT_FOUND,
595
+ FastlyNativePlatformCapabilitiesMockError,
596
+ normalizeHeaders,
597
+ fixtureKey,
598
+ executeFastlyNativePlatformCapabilities
599
+ });