@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,597 @@
1
+ 'use strict';
2
+
3
+ const { Readable } = require('node:stream');
4
+ const { setTimeout: delay } = require('node:timers/promises');
5
+ const hostRuntime = require('@pulse-compute/wasm-host-runtime/runtime/canonical-api-runtime');
6
+ const { createFastlyProviderPrimitiveSurface } = require('./provider-primitives.js');
7
+ const { FASTLY_PROVIDER_DESCRIPTOR, createFastlyLoweringPlan } = require('../provider-contract.js');
8
+
9
+ const CANONICAL_FASTLY_RUNTIME_VERSION = 'pulse.canonical-fastly-runtime.v3';
10
+
11
+ function normalizeHeaders(headers) {
12
+ if (!headers) return [];
13
+ if (Array.isArray(headers)) return headers.map(([name, value]) => [String(name), String(value)]);
14
+ if (typeof headers.entries === 'function') return [...headers.entries()].map(([name, value]) => [String(name), String(value)]);
15
+ return Object.entries(headers).flatMap(([name, value]) => Array.isArray(value) ? value.map((entry) => [String(name), String(entry)]) : [[String(name), String(value)]]);
16
+ }
17
+
18
+ function headerValue(headers, name) {
19
+ const lower = String(name).toLowerCase();
20
+ const found = (headers || []).find(([key]) => String(key).toLowerCase() === lower);
21
+ return found && String(found[1]);
22
+ }
23
+
24
+ function responseIsStructured(method, headers) {
25
+ if (String(method).toUpperCase() === 'HEAD') return true;
26
+ const contentType = String(headerValue(headers, 'content-type') || '').toLowerCase();
27
+ return contentType.startsWith('text/')
28
+ || contentType.includes('application/json')
29
+ || contentType.includes('+json')
30
+ || contentType.includes('application/xml')
31
+ || contentType.includes('+xml')
32
+ || contentType.includes('application/x-www-form-urlencoded');
33
+ }
34
+
35
+ function normalizeFetchFixtures(fetches = {}) {
36
+ return fetches instanceof Map ? new Map(fetches) : new Map(Object.entries(fetches || {}));
37
+ }
38
+
39
+ function responseSpec(input = {}) {
40
+ const spec = input && typeof input === 'object' ? input : { body: input };
41
+ if (spec.opaque || spec.kind === 'stream' || spec.bodyStream !== undefined || spec.bodyHandle !== undefined) {
42
+ return {
43
+ status: Number(Object.prototype.hasOwnProperty.call(spec, 'status') ? spec.status : 200),
44
+ kind: 'stream',
45
+ bodyClass: 'opaque',
46
+ headers: normalizeHeaders(spec.headers || [['content-type', 'application/octet-stream']]),
47
+ bodyStream: spec.bodyStream || { chunks: Array.isArray(spec.chunks) ? [...spec.chunks] : [] },
48
+ bodyHandle: spec.bodyHandle,
49
+ responseRef: spec.responseRef,
50
+ streamRef: spec.streamRef,
51
+ providerSource: 'fastly'
52
+ };
53
+ }
54
+ const body = Object.prototype.hasOwnProperty.call(spec, 'body') ? spec.body : JSON.stringify(Object.prototype.hasOwnProperty.call(spec, 'value') ? spec.value : {});
55
+ return {
56
+ status: Number(Object.prototype.hasOwnProperty.call(spec, 'status') ? spec.status : 200),
57
+ kind: spec.kind || 'text',
58
+ bodyClass: 'structured',
59
+ headers: normalizeHeaders(spec.headers || [['content-type', 'application/json; charset=utf-8']]),
60
+ body: typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body),
61
+ providerSource: 'fastly'
62
+ };
63
+ }
64
+
65
+ function fixtureWebResponse(spec = {}, method = 'GET') {
66
+ const response = responseSpec(spec);
67
+ const status = Number(response.status || 200);
68
+ const bodyAllowed = String(method).toUpperCase() !== 'HEAD'
69
+ && status !== 204
70
+ && status !== 205
71
+ && status !== 304
72
+ && !(status >= 100 && status < 200);
73
+ return new Response(bodyAllowed ? response.body : null, {
74
+ status,
75
+ headers: normalizeHeaders(response.headers)
76
+ });
77
+ }
78
+
79
+ function fixtureFetchImplementation(fetches) {
80
+ const fixtures = normalizeFetchFixtures(fetches);
81
+ return async function fastlyCanonicalFixtureFetch(url, init = {}) {
82
+ const method = String(init.method || 'GET').toUpperCase();
83
+ const fixture = fixtures.get(`${method} ${String(url)}`) || fixtures.get(String(url));
84
+ if (!fixture) throw new Error(`No Fastly provider fixture is configured for ${method} ${String(url)}.`);
85
+ if (fixture.kind === 'network-error') throw new Error('Fastly provider fixture produced a network failure.');
86
+ if (fixture.kind === 'timeout') {
87
+ await new Promise((_resolve, reject) => {
88
+ const signal = init.signal;
89
+ const onAbort = () => reject(signal.reason || new DOMException('Aborted', 'AbortError'));
90
+ if (signal && signal.aborted) onAbort();
91
+ else if (signal) signal.addEventListener('abort', onAbort, { once: true });
92
+ });
93
+ }
94
+ const delayMs = Math.max(0, Number(fixture.delayMs) || 0);
95
+ if (delayMs > 0) await delay(delayMs, undefined, { signal: init.signal });
96
+ return fixtureWebResponse(fixture, method);
97
+ };
98
+ }
99
+
100
+ function assetContentType(key) {
101
+ const extension = String(key).toLowerCase().split('.').pop();
102
+ return Object.freeze({
103
+ css: 'text/css; charset=utf-8',
104
+ html: 'text/html; charset=utf-8',
105
+ js: 'text/javascript; charset=utf-8',
106
+ json: 'application/json; charset=utf-8',
107
+ mjs: 'text/javascript; charset=utf-8',
108
+ svg: 'image/svg+xml',
109
+ txt: 'text/plain; charset=utf-8',
110
+ webp: 'image/webp',
111
+ png: 'image/png',
112
+ jpg: 'image/jpeg',
113
+ jpeg: 'image/jpeg',
114
+ gif: 'image/gif',
115
+ ico: 'image/x-icon'
116
+ })[extension] || 'application/octet-stream';
117
+ }
118
+
119
+ function responseHeaders(response) {
120
+ const headers = [];
121
+ if (response && response.headers && typeof response.headers.entries === 'function') {
122
+ for (const [name, value] of response.headers.entries()) {
123
+ if (String(name).toLowerCase() === 'set-cookie' && typeof response.headers.getSetCookie === 'function') continue;
124
+ headers.push([String(name), String(value)]);
125
+ }
126
+ if (typeof response.headers.getSetCookie === 'function') {
127
+ for (const value of response.headers.getSetCookie()) headers.push(['set-cookie', String(value)]);
128
+ }
129
+ }
130
+ return headers;
131
+ }
132
+
133
+ async function readBoundedResponseText(response, maxBytes) {
134
+ const contentLength = Number(response.headers && response.headers.get && response.headers.get('content-length'));
135
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
136
+ throw new hostRuntime.CanonicalRuntimeError(
137
+ 'ProviderCapabilityError',
138
+ 'PULSE_GRIP_BROADCAST_RESPONSE_TOO_LARGE',
139
+ 'Fastly GRIP broadcast response exceeded its configured byte limit.',
140
+ { provider: 'fastly', maxResponseBytes: maxBytes }
141
+ );
142
+ }
143
+ if (!response.body) return '';
144
+ const reader = response.body.getReader();
145
+ const decoder = new TextDecoder();
146
+ let size = 0;
147
+ let output = '';
148
+ try {
149
+ for (;;) {
150
+ const { done, value } = await reader.read();
151
+ if (done) break;
152
+ size += value.byteLength;
153
+ if (size > maxBytes) {
154
+ await reader.cancel();
155
+ throw new hostRuntime.CanonicalRuntimeError(
156
+ 'ProviderCapabilityError',
157
+ 'PULSE_GRIP_BROADCAST_RESPONSE_TOO_LARGE',
158
+ 'Fastly GRIP broadcast response exceeded its configured byte limit.',
159
+ { provider: 'fastly', maxResponseBytes: maxBytes }
160
+ );
161
+ }
162
+ output += decoder.decode(value, { stream: true });
163
+ }
164
+ return output + decoder.decode();
165
+ } finally {
166
+ reader.releaseLock();
167
+ }
168
+ }
169
+
170
+ async function executeLocalLiveFetch(normalized, fetchImplementation) {
171
+ const timeoutMs = Number(normalized.init.timeoutMs);
172
+ const controller = Number.isFinite(timeoutMs) && timeoutMs > 0 ? new AbortController() : undefined;
173
+ const timeout = controller ? setTimeout(() => controller.abort(new Error(`Fetch timed out after ${timeoutMs}ms.`)), timeoutMs) : undefined;
174
+ try {
175
+ const response = await fetchImplementation(normalized.parts.url, {
176
+ method: normalized.init.method,
177
+ headers: normalized.init.headers,
178
+ body: ['GET', 'HEAD'].includes(normalized.init.method) ? undefined : normalized.init.body,
179
+ signal: controller && controller.signal,
180
+ redirect: 'follow'
181
+ });
182
+ const headers = responseHeaders(response);
183
+ if (responseIsStructured(normalized.init.method, headers)) {
184
+ return { status: response.status, kind: 'text', bodyClass: 'structured', headers, body: normalized.init.method === 'HEAD' ? '' : await response.text(), providerSource: 'fastly' };
185
+ }
186
+ return {
187
+ status: response.status,
188
+ kind: 'stream',
189
+ bodyClass: 'opaque',
190
+ headers,
191
+ bodyStream: response.body && typeof Readable.fromWeb === 'function' ? Readable.fromWeb(response.body) : response.body,
192
+ responseRef: response,
193
+ providerSource: 'fastly'
194
+ };
195
+ } catch (error) {
196
+ if ((controller && controller.signal.aborted) || error && error.name === 'AbortError') {
197
+ throw new hostRuntime.CanonicalRuntimeError('FetchTimeoutError', 'PULSE_FETCH_TIMEOUT', 'Fastly local fetch exceeded the configured timeout.', { effectId: normalized.id, url: normalized.parts.url, timeoutMs });
198
+ }
199
+ if (error instanceof hostRuntime.CanonicalRuntimeError) throw error;
200
+ throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', `Fastly local fetch failed for ${normalized.init.method} ${normalized.parts.url}.`, { effectId: normalized.id, url: normalized.parts.url, message: error && error.message ? error.message : String(error) });
201
+ } finally {
202
+ if (timeout) clearTimeout(timeout);
203
+ }
204
+ }
205
+
206
+ function valueMap(value = {}) {
207
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
208
+ }
209
+
210
+ function gripBound(value, fallback, field, effectId) {
211
+ if (value === undefined) return fallback;
212
+ const number = Number(value);
213
+ if (!Number.isSafeInteger(number) || number <= 0) {
214
+ throw new hostRuntime.CanonicalRuntimeError(
215
+ 'ProviderCapabilityError',
216
+ 'PULSE_GRIP_BROADCAST_CAPABILITY_REQUIRED',
217
+ `Fastly GRIP broadcast ${field} must be a positive safe integer.`,
218
+ { provider: 'fastly', effectId, configuration: `grip.${field}` }
219
+ );
220
+ }
221
+ return number;
222
+ }
223
+
224
+ function gripBroadcastBody(payload, effectId) {
225
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
226
+ throw new hostRuntime.CanonicalRuntimeError(
227
+ 'ProviderCapabilityError',
228
+ 'PULSE_GRIP_BROADCAST_MESSAGE_INVALID',
229
+ 'Fastly GRIP broadcast requires a structured message.',
230
+ { provider: 'fastly', effectId }
231
+ );
232
+ }
233
+ const channel = typeof payload.channel === 'string' ? payload.channel.trim() : '';
234
+ if (!channel || /[,\r\n]/.test(channel)) {
235
+ throw new hostRuntime.CanonicalRuntimeError(
236
+ 'ProviderCapabilityError',
237
+ 'PULSE_GRIP_BROADCAST_CHANNEL_INVALID',
238
+ 'Fastly GRIP broadcast channel must be a non-empty string without commas or line breaks.',
239
+ { provider: 'fastly', effectId }
240
+ );
241
+ }
242
+ if (!Object.prototype.hasOwnProperty.call(payload, 'data') || payload.data === undefined) {
243
+ throw new hostRuntime.CanonicalRuntimeError(
244
+ 'ProviderCapabilityError',
245
+ 'PULSE_GRIP_BROADCAST_MESSAGE_INVALID',
246
+ 'Fastly GRIP broadcast requires a JSON-compatible data value.',
247
+ { provider: 'fastly', effectId }
248
+ );
249
+ }
250
+ try {
251
+ const body = JSON.stringify({
252
+ channel,
253
+ data: payload.data,
254
+ ...(payload.event === undefined ? {} : { event: String(payload.event) }),
255
+ ...(payload.id === undefined ? {} : { id: String(payload.id) })
256
+ });
257
+ if (typeof body !== 'string') throw new TypeError('not JSON serializable');
258
+ return body;
259
+ } catch {
260
+ throw new hostRuntime.CanonicalRuntimeError(
261
+ 'ProviderCapabilityError',
262
+ 'PULSE_GRIP_BROADCAST_MESSAGE_INVALID',
263
+ 'Fastly GRIP broadcast message must be JSON-compatible.',
264
+ { provider: 'fastly', effectId }
265
+ );
266
+ }
267
+ }
268
+
269
+ function createFastlyProviderAdapter(baseOptions = {}) {
270
+ const states = new Map();
271
+ const bindings = baseOptions.bindings || {};
272
+
273
+ function physicalKvName(logical) {
274
+ const key = String(logical);
275
+ return bindings.kv && bindings.kv[key] ? String(bindings.kv[key]) : key;
276
+ }
277
+
278
+ function stateFor(executionOptions = {}) {
279
+ const id = String(executionOptions.executionId || 'default');
280
+ if (!states.has(id)) {
281
+ const config = valueMap(executionOptions.config || baseOptions.config);
282
+ const secrets = valueMap(executionOptions.secrets || baseOptions.secrets);
283
+ const inputKv = valueMap(executionOptions.kv || baseOptions.kv);
284
+ const physicalKv = {};
285
+ for (const [logical, entries] of Object.entries(inputKv)) {
286
+ const physical = physicalKvName(logical);
287
+ physicalKv[physical] = { ...(physicalKv[physical] || {}), ...valueMap(entries) };
288
+ }
289
+ for (const [logical, physical] of Object.entries(bindings.kv || {})) {
290
+ if (inputKv[physical]) physicalKv[physical] = { ...(physicalKv[physical] || {}), ...valueMap(inputKv[physical]) };
291
+ if (inputKv[logical]) physicalKv[physical] = { ...(physicalKv[physical] || {}), ...valueMap(inputKv[logical]) };
292
+ }
293
+ const surface = createFastlyProviderPrimitiveSurface({
294
+ configStore: bindings.configStore,
295
+ secretStore: bindings.secretStore,
296
+ kvStore: 'pulse_kv',
297
+ configValues: config,
298
+ secretValues: secrets,
299
+ kvStores: physicalKv
300
+ });
301
+ const grip = {
302
+ channels: [],
303
+ holds: [],
304
+ publishes: [],
305
+ broadcasts: []
306
+ };
307
+ states.set(id, Object.freeze({ surface, config, secrets, physicalKv, grip }));
308
+ }
309
+ return states.get(id);
310
+ }
311
+
312
+ async function dispatchFetch(normalized, executionOptions = {}) {
313
+ const state = stateFor(executionOptions);
314
+ const mappedBackend = bindings.backends && bindings.backends[normalized.parts.origin];
315
+ state.surface.fetch({ url: normalized.parts.url, method: normalized.init.method, headers: normalized.init.headers, body: normalized.init.body, timeoutMs: normalized.init.timeoutMs, backend: mappedBackend });
316
+ const fixtures = normalizeFetchFixtures(executionOptions.fetches || baseOptions.fetches);
317
+ const fixture = fixtures.get(`${normalized.init.method} ${normalized.parts.url}`) || fixtures.get(normalized.parts.url);
318
+ const liveFetchEnabled = executionOptions.liveFetch === true || baseOptions.liveFetch === true || typeof executionOptions.fetchImplementation === 'function' || typeof baseOptions.fetchImplementation === 'function';
319
+ if (!fixture && liveFetchEnabled) {
320
+ const implementation = executionOptions.fetchImplementation || baseOptions.fetchImplementation || globalThis.fetch;
321
+ if (typeof implementation !== 'function') throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', 'Fastly local fetch implementation is unavailable.', { effectId: normalized.id, url: normalized.parts.url });
322
+ return executeLocalLiveFetch(normalized, implementation);
323
+ }
324
+ if (!fixture) throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', `No Fastly provider fixture is configured for ${normalized.init.method} ${normalized.parts.url}.`, { effectId: normalized.id, url: normalized.parts.url, backend: mappedBackend });
325
+ if (fixture.kind === 'network-error') throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', 'Fastly provider fixture produced a network failure.', { effectId: normalized.id, url: normalized.parts.url });
326
+ if (fixture.kind === 'timeout') throw new hostRuntime.CanonicalRuntimeError('FetchTimeoutError', 'PULSE_FETCH_TIMEOUT', 'Fastly provider fixture exceeded the fetch timeout.', { effectId: normalized.id, url: normalized.parts.url });
327
+ const delayMs = Math.max(0, Number(fixture.delayMs) || 0);
328
+ const timeoutMs = Number(normalized.init.timeoutMs);
329
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0 && delayMs > timeoutMs) {
330
+ await delay(timeoutMs);
331
+ throw new hostRuntime.CanonicalRuntimeError('FetchTimeoutError', 'PULSE_FETCH_TIMEOUT', 'Fastly provider fixture exceeded the fetch timeout.', { effectId: normalized.id, url: normalized.parts.url, timeoutMs, delayMs });
332
+ }
333
+ if (delayMs > 0) await delay(delayMs);
334
+ return responseSpec(fixture);
335
+ }
336
+
337
+ return Object.freeze({
338
+ id: 'fastly',
339
+ version: CANONICAL_FASTLY_RUNTIME_VERSION,
340
+ async dispatchEffect(effect, executionOptions = {}) {
341
+ const state = stateFor(executionOptions);
342
+ if (effect.kind === 'fetch') return dispatchFetch(effect, executionOptions);
343
+ if (effect.kind === 'config.get') return state.surface.config.adapter.get(effect.name);
344
+ if (effect.kind === 'secret.get') {
345
+ const secret = state.surface.secret.adapter.get(effect.name);
346
+ return secret && secret.value;
347
+ }
348
+ if (effect.kind === 'kv.get') {
349
+ const result = state.surface.kv.get(physicalKvName(effect.store), effect.key);
350
+ return result.hit ? result.value : undefined;
351
+ }
352
+ if (effect.kind === 'kv.put') return state.surface.kv.put(physicalKvName(effect.store), effect.key, effect.value).valueStored;
353
+ if (effect.kind === 'assets.lookup') {
354
+ const payload = valueMap(effect.payload);
355
+ const result = state.surface.kv.get(physicalKvName(payload.store), payload.key);
356
+ const found = result.hit === true;
357
+ const headers = normalizeHeaders(payload.headers);
358
+ if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) {
359
+ headers.push(['content-type', found ? assetContentType(payload.key) : 'text/plain; charset=utf-8']);
360
+ }
361
+ if (payload.cacheControl !== undefined) headers.push(['cache-control', String(payload.cacheControl)]);
362
+ const method = String(payload.method || 'GET').toUpperCase();
363
+ const body = method === 'HEAD' || !found
364
+ ? ''
365
+ : (typeof result.value === 'string' || Buffer.isBuffer(result.value) || result.value instanceof Uint8Array)
366
+ ? result.value
367
+ : JSON.stringify(result.value);
368
+ return Object.freeze({
369
+ status: found ? 200 : 404,
370
+ kind: 'text',
371
+ bodyClass: 'structured',
372
+ headers: Object.freeze(headers.map((entry) => Object.freeze(entry))),
373
+ body,
374
+ providerSource: 'fastly'
375
+ });
376
+ }
377
+ if (effect.kind === 'grip.channel') {
378
+ const payload = valueMap(effect.payload);
379
+ const declaration = Object.freeze({
380
+ channel: String(payload.channel || ''),
381
+ prefix: payload.prefix === undefined ? undefined : String(payload.prefix),
382
+ fanout: payload.fanout !== false
383
+ });
384
+ if (!declaration.channel) {
385
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_FASTLY_GRIP_CHANNEL_REQUIRED', 'Fastly GRIP channel declarations require a non-empty channel.', { provider: 'fastly', effectId: effect.id });
386
+ }
387
+ state.grip.channels.push(declaration);
388
+ return Object.freeze({ declared: true, ...declaration });
389
+ }
390
+ if (effect.kind === 'grip.hold') {
391
+ const payload = valueMap(effect.payload);
392
+ const declared = state.grip.channels.map((entry) => entry.prefix ? `${entry.prefix}${entry.channel}` : entry.channel);
393
+ const explicit = Array.isArray(payload.channels) ? payload.channels.map(String) : [];
394
+ const channels = [...new Set([...declared, ...explicit].filter(Boolean))];
395
+ const hold = Object.freeze({
396
+ mode: String(payload.mode || 'stream'),
397
+ channels: Object.freeze(channels),
398
+ timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
399
+ hostOwned: true
400
+ });
401
+ state.grip.holds.push(hold);
402
+ const gripOptions = valueMap(executionOptions.grip || baseOptions.grip);
403
+ const chunks = Array.isArray(gripOptions.holdChunks)
404
+ ? gripOptions.holdChunks
405
+ : [Buffer.from(String(gripOptions.holdBody || 'pulse-grip-hold'), 'utf8')];
406
+ return responseSpec({
407
+ opaque: true,
408
+ status: Number(gripOptions.holdStatus || 200),
409
+ headers: [
410
+ ['content-type', String(gripOptions.holdContentType || 'application/octet-stream')],
411
+ ['grip-hold', hold.mode],
412
+ ...channels.map((channel) => ['grip-channel', channel]),
413
+ ...(hold.timeoutMs === undefined ? [] : [['grip-timeout', String(Math.max(0, hold.timeoutMs))]])
414
+ ],
415
+ chunks,
416
+ responseRef: Object.freeze({ provider: 'fastly', kind: 'grip-hold', channels, hostOwned: true })
417
+ });
418
+ }
419
+ if (effect.kind === 'grip.publish') {
420
+ const publication = Object.freeze({
421
+ channel: String(effect.payload.channel),
422
+ message: String(effect.payload.message),
423
+ event: effect.payload.event === undefined ? undefined : String(effect.payload.event),
424
+ id: effect.payload.id === undefined ? undefined : String(effect.payload.id)
425
+ });
426
+ state.grip.publishes.push(publication);
427
+ return Object.freeze({
428
+ status: 202,
429
+ kind: 'json',
430
+ bodyClass: 'structured',
431
+ headers: Object.freeze([['content-type', 'application/json; charset=utf-8']]),
432
+ body: JSON.stringify({ published: true, ...publication })
433
+ });
434
+ }
435
+ if (effect.kind === 'grip.broadcast') {
436
+ const gripBindings = valueMap(bindings.grip);
437
+ const endpoint = String(gripBindings.publishEndpoint || '').trim();
438
+ const implementation = executionOptions.fetchImplementation
439
+ || baseOptions.fetchImplementation
440
+ || fixtureFetchImplementation(executionOptions.fetches || baseOptions.fetches);
441
+ if (!endpoint || typeof implementation !== 'function') {
442
+ throw new hostRuntime.CanonicalRuntimeError(
443
+ 'ProviderCapabilityError',
444
+ 'PULSE_GRIP_BROADCAST_CAPABILITY_REQUIRED',
445
+ 'Fastly GRIP broadcast requires an explicit publish endpoint and local provider fetch implementation.',
446
+ { provider: 'fastly', effectId: effect.id }
447
+ );
448
+ }
449
+ try {
450
+ const parsedEndpoint = new URL(endpoint);
451
+ if (!['http:', 'https:'].includes(parsedEndpoint.protocol)) throw new TypeError('unsupported protocol');
452
+ } catch {
453
+ throw new hostRuntime.CanonicalRuntimeError(
454
+ 'ProviderCapabilityError',
455
+ 'PULSE_GRIP_BROADCAST_ENDPOINT_INVALID',
456
+ 'Fastly GRIP broadcast publishEndpoint must be an absolute HTTP or HTTPS URL.',
457
+ { provider: 'fastly', effectId: effect.id }
458
+ );
459
+ }
460
+ const body = gripBroadcastBody(effect.payload, effect.id);
461
+ const maxRequestBytes = gripBound(gripBindings.maxRequestBytes, 65536, 'maxRequestBytes', effect.id);
462
+ if (new TextEncoder().encode(body).byteLength > maxRequestBytes) {
463
+ throw new hostRuntime.CanonicalRuntimeError(
464
+ 'ProviderCapabilityError',
465
+ 'PULSE_GRIP_BROADCAST_REQUEST_TOO_LARGE',
466
+ 'Fastly GRIP broadcast request exceeded its configured byte limit.',
467
+ { provider: 'fastly', effectId: effect.id, maxRequestBytes }
468
+ );
469
+ }
470
+ const headers = new Headers({
471
+ accept: 'application/json',
472
+ 'content-type': 'application/json; charset=utf-8'
473
+ });
474
+ const authentication = valueMap(gripBindings.authentication);
475
+ if (authentication.scheme !== undefined && String(authentication.scheme).toLowerCase() !== 'bearer') {
476
+ throw new hostRuntime.CanonicalRuntimeError(
477
+ 'ProviderCapabilityError',
478
+ 'PULSE_GRIP_BROADCAST_AUTH_SCHEME_UNSUPPORTED',
479
+ 'Fastly GRIP broadcast supports only bearer authentication.',
480
+ { provider: 'fastly', effectId: effect.id }
481
+ );
482
+ }
483
+ if (gripBindings.authentication !== undefined && !String(authentication.secretRef || '').trim()) {
484
+ throw new hostRuntime.CanonicalRuntimeError(
485
+ 'ProviderCapabilityError',
486
+ 'PULSE_GRIP_BROADCAST_SECRET_REFERENCE_REQUIRED',
487
+ 'Fastly GRIP bearer authentication requires a named secret reference.',
488
+ { provider: 'fastly', effectId: effect.id }
489
+ );
490
+ }
491
+ if (authentication.secretRef) {
492
+ const secret = state.surface.secret.adapter.get(String(authentication.secretRef));
493
+ if (!secret || typeof secret.value !== 'string' || secret.value.length === 0) {
494
+ throw new hostRuntime.CanonicalRuntimeError(
495
+ 'ProviderCapabilityError',
496
+ 'PULSE_GRIP_BROADCAST_SECRET_REQUIRED',
497
+ 'Fastly GRIP broadcast authentication secret is unavailable.',
498
+ { provider: 'fastly', effectId: effect.id, secretRef: String(authentication.secretRef) }
499
+ );
500
+ }
501
+ headers.set('authorization', `Bearer ${secret.value}`);
502
+ }
503
+ let response;
504
+ let text;
505
+ try {
506
+ response = await implementation(endpoint, {
507
+ method: 'POST',
508
+ headers,
509
+ body,
510
+ signal: executionOptions.signal,
511
+ redirect: 'error'
512
+ });
513
+ if (!(response instanceof Response)) {
514
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_GRIP_BROADCAST_RESPONSE_INVALID', 'Fastly GRIP broadcast provider must return a Web Response.', { provider: 'fastly', effectId: effect.id });
515
+ }
516
+ text = await readBoundedResponseText(
517
+ response,
518
+ gripBound(gripBindings.maxResponseBytes, 65536, 'maxResponseBytes', effect.id)
519
+ );
520
+ } catch (error) {
521
+ if (error instanceof hostRuntime.CanonicalRuntimeError) throw error;
522
+ if (executionOptions.signal && executionOptions.signal.aborted) {
523
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_GRIP_BROADCAST_ABORTED', 'Fastly GRIP broadcast was cancelled with its owning request.', { provider: 'fastly', effectId: effect.id });
524
+ }
525
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_GRIP_BROADCAST_NETWORK', 'Fastly GRIP broadcast provider request failed.', { provider: 'fastly', effectId: effect.id });
526
+ }
527
+ if (response.status < 200 || response.status > 299) {
528
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_GRIP_BROADCAST_REJECTED', 'Fastly GRIP broadcast provider rejected the publication.', { provider: 'fastly', effectId: effect.id, status: response && response.status });
529
+ }
530
+ let providerAck = {};
531
+ if (text.trim()) {
532
+ try { providerAck = JSON.parse(text); }
533
+ catch {
534
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_GRIP_BROADCAST_RESPONSE_INVALID', 'Fastly GRIP broadcast provider returned an invalid acknowledgement.', { provider: 'fastly', effectId: effect.id, status: response.status });
535
+ }
536
+ }
537
+ if (!providerAck || typeof providerAck !== 'object' || Array.isArray(providerAck)) {
538
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_GRIP_BROADCAST_RESPONSE_INVALID', 'Fastly GRIP broadcast provider acknowledgement must be an object.', { provider: 'fastly', effectId: effect.id, status: response.status });
539
+ }
540
+ const ack = Object.freeze({
541
+ accepted: providerAck.accepted !== false,
542
+ status: response.status,
543
+ ...(providerAck.messageId === undefined ? {} : { messageId: String(providerAck.messageId) })
544
+ });
545
+ state.grip.broadcasts.push(Object.freeze({ channel: String(effect.payload.channel), accepted: ack.accepted, status: ack.status }));
546
+ return ack;
547
+ }
548
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_PROVIDER_CAPABILITY_UNSUPPORTED', `Fastly local runtime does not implement ${effect.kind}.`, { provider: 'fastly', kind: effect.kind, effectId: effect.id });
549
+ },
550
+ disposeExecution({ executionId } = {}) {
551
+ states.delete(String(executionId || 'default'));
552
+ },
553
+ resultMetadata(executionOptions = {}) {
554
+ const state = stateFor(executionOptions);
555
+ return Object.freeze({
556
+ contract: FASTLY_PROVIDER_DESCRIPTOR,
557
+ lowering: createFastlyLoweringPlan(executionOptions.metadata, bindings),
558
+ manifest: state.surface.manifest,
559
+ localConformanceRuntime: true,
560
+ executionMode: 'local-conformance',
561
+ deploymentValidated: false,
562
+ deployable: true,
563
+ providerSpecificUserland: false,
564
+ providerSdkUserland: false,
565
+ grip: Object.freeze({
566
+ channels: Object.freeze([...state.grip.channels]),
567
+ holds: Object.freeze([...state.grip.holds]),
568
+ publishes: Object.freeze([...state.grip.publishes]),
569
+ broadcasts: Object.freeze([...state.grip.broadcasts])
570
+ })
571
+ });
572
+ }
573
+ });
574
+ }
575
+
576
+ function createCanonicalFastlyRuntime(options = {}) {
577
+ return hostRuntime.createCanonicalHostRuntime({
578
+ ...options,
579
+ runtimeVersion: CANONICAL_FASTLY_RUNTIME_VERSION,
580
+ providerAdapter: createFastlyProviderAdapter(options)
581
+ });
582
+ }
583
+
584
+ async function executeCanonicalProgram(programModule, options = {}) {
585
+ return createCanonicalFastlyRuntime(options).execute(programModule, options);
586
+ }
587
+
588
+ module.exports = {
589
+ CANONICAL_FASTLY_RUNTIME_VERSION,
590
+ FASTLY_PROVIDER_DESCRIPTOR,
591
+ createFastlyProviderAdapter,
592
+ createCanonicalFastlyRuntime,
593
+ executeCanonicalProgram,
594
+ executeLocalLiveFetch,
595
+ responseIsStructured,
596
+ responseSpec
597
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const assetsProvider = require('./assets-provider.js');
4
+ const routeHandlerEffects = require('./route-handler-effects.js');
5
+ const providerPrimitives = require('./provider-primitives.js');
6
+ const canonicalApiRuntime = require('./canonical-api-runtime.js');
7
+
8
+ module.exports = {
9
+ assetsProvider,
10
+ routeHandlerEffects,
11
+ providerPrimitives,
12
+ canonicalApiRuntime,
13
+ createFastlyAssetsProvider: assetsProvider.createFastlyAssetsProvider,
14
+ createDefaultStoresFromPlan: assetsProvider.createDefaultStoresFromPlan,
15
+ buildFastlyAssetsProviderConfig: assetsProvider.buildFastlyAssetsProviderConfig,
16
+ normalizeHeaders: assetsProvider.normalizeHeaders,
17
+ normalizeFastlyPath: assetsProvider.normalizeFastlyPath,
18
+ createFastlyRouteHandlerEffectProvider: routeHandlerEffects.createFastlyRouteHandlerEffectProvider,
19
+ createFastlyRouteEffectProvider: routeHandlerEffects.createFastlyRouteEffectProvider,
20
+ createFastlyRouteEffectContinuationContext: routeHandlerEffects.createFastlyRouteEffectContinuationContext,
21
+ createDefaultRouteEffectBackendsFromPlan: routeHandlerEffects.createDefaultBackendsFromPlan,
22
+ buildFastlyRouteEffectProviderConfig: routeHandlerEffects.buildFastlyRouteEffectProviderConfig,
23
+ buildFastlyRouteHandlerEffectProviderConfig: routeHandlerEffects.buildFastlyRouteHandlerEffectProviderConfig,
24
+ createFastlyProviderPrimitiveSurface: providerPrimitives.createFastlyProviderPrimitiveSurface,
25
+ buildFastlyProviderPrimitiveManifest: providerPrimitives.buildFastlyProviderPrimitiveManifest,
26
+ createCanonicalFastlyRuntime: canonicalApiRuntime.createCanonicalFastlyRuntime,
27
+ executeCanonicalProgram: canonicalApiRuntime.executeCanonicalProgram
28
+ };