@pulse-compute/provider-fastly 1.0.0-beta.4 → 1.0.0-beta.6

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 (46) hide show
  1. package/README.md +6 -6
  2. package/dist/config-schema.json +10 -0
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +4 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +11 -11
  8. package/src/build/canonical-target.js +15 -2
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +49 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/kv-native.as.ts +18 -8
  13. package/src/build/native-application-errors.js +140 -0
  14. package/src/build/native-http-effects.js +87 -22
  15. package/src/build/native-platform-capabilities.js +480 -151
  16. package/src/build/native-request-body.js +44 -0
  17. package/src/build/native-request-headers.js +93 -0
  18. package/src/build/native-string-values.js +113 -0
  19. package/src/build/native-value-budget.js +58 -0
  20. package/src/build/package-native-application.js +94 -0
  21. package/src/build/request-budget.as.ts +49 -0
  22. package/src/build/request-budget.js +45 -0
  23. package/src/build/s3-body.as.ts +97 -0
  24. package/src/build/s3-native.as.ts +12 -4
  25. package/src/build/s3-native.js +2 -2
  26. package/src/build/schema-admission.js +83 -0
  27. package/src/build/schema-nested-json.js +93 -0
  28. package/src/build/schema-scalar-record.js +41 -0
  29. package/src/build/time-native.js +49 -0
  30. package/src/build/wasm-budget.js +28 -0
  31. package/src/config-api.js +3 -0
  32. package/src/config-schema.json +10 -0
  33. package/src/index.ts +9 -0
  34. package/src/javascript/jwt-verifier.js +14 -4
  35. package/src/javascript/package-effects.js +5 -2
  36. package/src/javascript/runtime-host.js +5 -2
  37. package/src/javascript/target-support-policy.js +18 -5
  38. package/src/javascript/target.js +16 -1
  39. package/src/provider-contract.js +7 -3
  40. package/src/runtime/canonical-api-runtime.js +2 -0
  41. package/src/testing/native-http-effects-host.js +5 -1
  42. package/src/testing/native-platform-capabilities-host.js +78 -6
  43. package/src/toolchain/config.js +8 -1
  44. package/src/toolchain/index.js +31 -2
  45. package/src/toolchain/native-execution.js +1 -0
  46. package/src/toolchain/s3.js +2 -2
@@ -14,6 +14,16 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
14
14
  crypto: defineCryptoTargetCapabilities({
15
15
  target: 'javascript',
16
16
  algorithms: [{
17
+ algorithm: 'HMAC-SHA256',
18
+ realization: 'runtime-builtin',
19
+ implemented: true,
20
+ status: 'implemented-jwt-sign'
21
+ }, {
22
+ algorithm: 'SHA-256',
23
+ realization: 'runtime-builtin',
24
+ implemented: true,
25
+ status: 'implemented-d1'
26
+ }, {
17
27
  algorithm: 'HS256',
18
28
  realization: 'runtime-builtin',
19
29
  implemented: true,
@@ -23,6 +33,11 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
23
33
  realization: 'runtime-builtin',
24
34
  implemented: true,
25
35
  status: 'implemented-h4'
36
+ }, {
37
+ algorithm: 'RS256',
38
+ realization: 'runtime-builtin',
39
+ implemented: true,
40
+ status: 'implemented-h4'
26
41
  }]
27
42
  }),
28
43
  capabilities: {
@@ -45,7 +60,7 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
45
60
  kv: true,
46
61
  parallel: true
47
62
  },
48
- packages: ['pulse.assets', 'pulse.grip', 'pulse.jwt'],
63
+ packages: ['pulse.assets', 'pulse.crypto', 'pulse.grip', 'pulse.jwt'],
49
64
  restrictions: {
50
65
  fetch: 'origin-backend-map-or-dynamic-opt-in',
51
66
  kv: 'logical-store-map-required',
@@ -13,9 +13,9 @@ const FASTLY_PROVIDER_DESCRIPTOR = canonical.normalizeDescriptor({
13
13
  localExecution: true,
14
14
  capabilities: [
15
15
  'request', 'response.json', 'response.text', 'response.custom', 'fetch',
16
- 'config.get', 'secret.get', 'kv.get', 'kv.put', 'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'assets.lookup',
17
- 'grip.channel', 'grip.hold', 'grip.publish', 'grip.broadcast', 'jwt.verify',
18
- 'opaque.pass-through', 's3.head', 's3.getText', 's3.putText'
16
+ 'time.wall-clock', 'crypto.digestText', 'config.get', 'secret.get', 'kv.get', 'kv.put', 'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'assets.lookup',
17
+ 'grip.channel', 'grip.hold', 'grip.publish', 'grip.broadcast', 'jwt.verify', 'jwt.sign',
18
+ 'opaque.pass-through', 's3.head', 's3.getText', 's3.putText', 's3.getBody'
19
19
  ],
20
20
  lowering: {
21
21
  request: 'fastly.compute.request',
@@ -23,6 +23,8 @@ const FASTLY_PROVIDER_DESCRIPTOR = canonical.normalizeDescriptor({
23
23
  'response.text': 'fastly.compute.response.text',
24
24
  'response.custom': 'fastly.compute.response.custom',
25
25
  fetch: 'fastly.backend.fetch',
26
+ 'time.wall-clock': 'wasi.clock_time_get.realtime',
27
+ 'crypto.digestText': 'pulse.crypto.guest-source.sha256',
26
28
  'config.get': 'fastly.config-store.get',
27
29
  'secret.get': 'fastly.secret-store.get',
28
30
  'kv.get': 'fastly.kv-store.get',
@@ -36,8 +38,10 @@ const FASTLY_PROVIDER_DESCRIPTOR = canonical.normalizeDescriptor({
36
38
  'grip.publish': 'fastly.fanout.publish',
37
39
  'grip.broadcast': 'fastly.grip.publish-control',
38
40
  'jwt.verify': 'fastly.native.jwt.verify',
41
+ 'jwt.sign': 'fastly.native.jwt.sign',
39
42
  's3.head': 'fastly.native.s3.head',
40
43
  's3.getText': 'fastly.native.s3.getText',
44
+ 's3.getBody': 'fastly.native.s3.getBody',
41
45
  's3.putText': 'fastly.native.s3.putText',
42
46
  'opaque.pass-through': 'fastly.response-body.stream'
43
47
  }
@@ -344,6 +344,8 @@ function createFastlyProviderAdapter(baseOptions = {}) {
344
344
  async dispatchEffect(effect, executionOptions = {}) {
345
345
  const state = stateFor(executionOptions);
346
346
  if (effect.kind === 'fetch') return dispatchFetch(effect, executionOptions);
347
+ if (effect.kind === 'crypto.digestText') return require('@pulse-compute/crypto/provider').createJavascriptTextDigest(baseOptions)(effect, executionOptions);
348
+ if (effect.kind === 'time.now') return require('@pulse-compute/runtime/host').readWallTime(baseOptions.wallClock === undefined ? () => Date.now() : baseOptions.wallClock);
347
349
  if (effect.kind === 'config.get') return state.surface.config.adapter.get(effect.name);
348
350
  if (effect.kind === 'secret.get') {
349
351
  const secret = state.surface.secret.adapter.get(effect.name);
@@ -80,6 +80,7 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
80
80
  const bodies = new Map();
81
81
  const responses = new Map();
82
82
  const pending = new Map();
83
+ const outboundRequests = [];
83
84
  const downstreamRequestHandle = 1;
84
85
  const downstreamBodyHandle = 2;
85
86
  requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
@@ -230,7 +231,9 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
230
231
  return FASTLY_STATUS_ERROR;
231
232
  }
232
233
  const pendingHandle = alloc('pending');
233
- pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: bodyBytes(bodyHandle) }, backend, fixture });
234
+ const outbound = { ...request, headers: request.headers.map((entry) => [...entry]), body: bodyBytes(bodyHandle) };
235
+ outboundRequests.push(outbound);
236
+ pending.set(pendingHandle, { request: outbound, backend, fixture });
234
237
  writeU32(pendingOut, pendingHandle);
235
238
  trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), bodyHandle: Number(bodyHandle), pendingHandle, backend, method: request.method, url: request.url });
236
239
  return FASTLY_STATUS_OK;
@@ -373,6 +376,7 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
373
376
  bodyHandle: downstream.bodyHandle
374
377
  }),
375
378
  trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
379
+ outboundRequests: Object.freeze(outboundRequests.map(entry => Object.freeze(entry))),
376
380
  instance
377
381
  });
378
382
  }
@@ -143,7 +143,8 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
143
143
  const downstreamRequestHandle = 1;
144
144
  const downstreamBodyHandle = 2;
145
145
  requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
146
- bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: [] });
146
+ bodies.set(downstreamBodyHandle, { bytes: normalizeBody(requestInput.body), readOffset: 0, writes: [],
147
+ readyAt: monotonicMs + Number(requestInput.readyDelayMs || 0), fixture: requestInput });
147
148
  let downstream;
148
149
 
149
150
  function alloc(kind = 'default') {
@@ -203,6 +204,15 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
203
204
  body = JSON.stringify(normalized.json);
204
205
  if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
205
206
  }
207
+ // Ordinary Pulse harness fixtures use value/text; preserve the ABI fixture
208
+ // body's explicit precedence and its existing json shorthand.
209
+ if (body === undefined && Object.prototype.hasOwnProperty.call(normalized, 'value')) {
210
+ body = JSON.stringify(normalized.value);
211
+ if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
212
+ } else if (body === undefined && Object.prototype.hasOwnProperty.call(normalized, 'text')) {
213
+ body = String(normalized.text);
214
+ if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) headers.push(['content-type', 'text/plain; charset=utf-8']);
215
+ }
206
216
  responses.set(responseHandle, {
207
217
  status: Number(normalized.status || 200),
208
218
  headers,
@@ -219,6 +229,28 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
219
229
  new Uint8Array(memory().buffer, Number(buffer), output.length).set(output);
220
230
  return FASTLY_STATUS_OK;
221
231
  }
232
+ function requestHeaderList(values, buffer, size, cursor, end, written, kind) {
233
+ const limit = Math.min(Number(size), options.requestHeaderPageBytes ?? Number(size));
234
+ let index = Number(cursor), count = 0;
235
+ if (index < 0 || index > values.length) return FASTLY_STATUS_ERROR;
236
+ while (index < values.length) {
237
+ const bytes = Buffer.from(String(values[index]) + '\0');
238
+ if (count + bytes.length > limit) {
239
+ if (count === 0) { writeU32(written, bytes.length); return FASTLY_STATUS_BUFLEN; }
240
+ break;
241
+ }
242
+ new Uint8Array(memory().buffer, Number(buffer) + count, bytes.length).set(bytes);
243
+ count += bytes.length; index++;
244
+ }
245
+ writeU32(written, count);
246
+ view().setBigInt64(Number(end), index === values.length ? -1n : BigInt(index), true);
247
+ // Fault injection is host-fixture-only; neither values nor faults enter traces.
248
+ const fault = options.requestHeaderFault?.({kind, cursor: Number(cursor)});
249
+ if (fault?.written !== undefined) writeU32(written, fault.written);
250
+ if (fault?.cursor !== undefined) view().setBigInt64(Number(end), BigInt(fault.cursor), true);
251
+ if (fault?.unterminated && count) new Uint8Array(memory().buffer)[Number(buffer) + count - 1] = 65;
252
+ return fault?.status ?? FASTLY_STATUS_OK;
253
+ }
222
254
  const imports = {
223
255
  fastly_async_io: {
224
256
  select(handles, count, timeout, done) {
@@ -235,6 +267,19 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
235
267
  },
236
268
  wasi_snapshot_preview1: {
237
269
  clock_time_get(clockId, precision, timeOut) {
270
+ if (Number(clockId) === 1 && typeof options.monotonicClock === 'function') {
271
+ const sample = options.monotonicClock(monotonicMs);
272
+ if (sample.status) return sample.status;
273
+ writeU64(timeOut, sample.nanoseconds);
274
+ return FASTLY_STATUS_OK;
275
+ }
276
+ if (Number(clockId) === 0 && typeof options.realtimeClock === 'function') {
277
+ const sample = options.realtimeClock();
278
+ trace.push({ module: 'wasi_snapshot_preview1', name: 'clock_time_get', clockId: 0, precision: String(precision), status: sample.status || 0 });
279
+ if (sample.status) return sample.status;
280
+ writeU64(timeOut, sample.nanoseconds);
281
+ return FASTLY_STATUS_OK;
282
+ }
238
283
  const seconds = Number.isFinite(Number(options.clockUnixSeconds))
239
284
  ? Number(options.clockUnixSeconds)
240
285
  : Math.floor(Date.now() / 1000);
@@ -304,6 +349,19 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
304
349
  trace.push({ module: 'fastly_http_req', name: 'uri_get', handle: Number(handle) });
305
350
  return request ? writeUtf8(request.url, buffer, bufferLength, writtenOut) : FASTLY_STATUS_BADF;
306
351
  },
352
+ header_names_get(handle, buffer, size, cursor, end, written) {
353
+ trace.push({module: 'fastly_http_req', name: 'header_names_get', cursor: Number(cursor)});
354
+ const request = requests.get(Number(handle));
355
+ if (!request) return FASTLY_STATUS_BADF;
356
+ return requestHeaderList([...new Set(request.headers.map(([name]) => name.toLowerCase()))], buffer, size, cursor, end, written, 'names');
357
+ },
358
+ header_values_get(handle, pointer, length, buffer, size, cursor, end, written) {
359
+ trace.push({module: 'fastly_http_req', name: 'header_values_get', cursor: Number(cursor)});
360
+ const request = requests.get(Number(handle));
361
+ if (!request) return FASTLY_STATUS_BADF;
362
+ const name = readUtf8(pointer, length).toLowerCase();
363
+ return requestHeaderList(request.headers.filter(([key]) => key.toLowerCase() === name).map(([, value]) => value), buffer, size, cursor, end, written, 'values');
364
+ },
307
365
  header_value_get(handle, namePointer, nameLength, valuePointer, valueLength, writtenOut) {
308
366
  const request = requests.get(Number(handle));
309
367
  const name = readUtf8(namePointer, nameLength);
@@ -558,7 +616,7 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
558
616
  }
559
617
  },
560
618
  fastly_http_body: {
561
- close(handle) { trace.push({ module: 'fastly_http_body', name: 'close' }); return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
619
+ close(handle) { trace.push({ module: 'fastly_http_body', name: 'close', handle: Number(handle) }); if (downstream?.streaming && downstream.bodyHandle === Number(handle)) { downstream.bodyBytes = bodyBytes(handle); downstream.finished = true; } return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF; },
562
620
  new(handleOut) {
563
621
  const handle = alloc('body');
564
622
  bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
@@ -583,6 +641,7 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
583
641
  write(handle, buffer, bufferLength, end, writtenOut) {
584
642
  const body = bodies.get(Number(handle));
585
643
  if (!body) return FASTLY_STATUS_BADF;
644
+ if (downstream?.streaming && Number(handle) === downstream.bodyHandle && options.downstreamBodyWriteStatus) return Number(options.downstreamBodyWriteStatus);
586
645
  if (requests.size > 1 && options.outboundBodyWriteStatus) return Number(options.outboundBodyWriteStatus);
587
646
  const length = Math.min(Number(bufferLength), Number(options.bodyWriteChunkBytes || bufferLength));
588
647
  const bytes = readBytes(buffer, length);
@@ -603,9 +662,11 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
603
662
  // Throwing stops the Wasm invocation; it never fabricates an effect result.
604
663
  const checkActive = () => { if (options.signal && options.signal.aborted) throw options.signal.reason || new Error('Request cancelled.'); };
605
664
  checkActive();
606
- if (options.signal) for (const namespace of Object.values(imports)) {
665
+ if (options.signal || options.hostcallDelayMs) for (const [moduleName, namespace] of Object.entries(imports)) {
607
666
  for (const [name, hostcall] of Object.entries(namespace)) if (typeof hostcall === 'function') namespace[name] = (...args) => {
608
- checkActive(); const result = hostcall(...args); checkActive(); return result;
667
+ checkActive(); const result = hostcall(...args);
668
+ monotonicMs += Number(options.hostcallDelayMs?.[`${moduleName}.${name}`] || 0);
669
+ checkActive(); return result;
609
670
  };
610
671
  }
611
672
  const module = new WebAssembly.Module(wasm);
@@ -614,7 +675,16 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
614
675
  throw new FastlyNativePlatformCapabilitiesMockError('Pass98 native module is missing _start.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_START_MISSING');
615
676
  }
616
677
  if (options.conditionalKv && options.conditionalKv.deadlineNs !== undefined) instance.exports.pulse_fastly_kv_request_deadline(BigInt(options.conditionalKv.deadlineNs));
617
- instance.exports._start();
678
+ try { instance.exports._start(); }
679
+ catch (error) {
680
+ if (Number(instance.exports.pulse_fastly_last_error?.()) !== 1010) throw error;
681
+ throw new FastlyNativePlatformCapabilitiesMockError('Native retained-value budget exceeded.',
682
+ 'PULSE_RUNTIME_MEMORY_LIMIT_EXCEEDED', {
683
+ lastError: 1010, errorStage: Number(instance.exports.pulse_fastly_error_stage()), trace,
684
+ bytes: Number(instance.exports.pulse_fastly_memory_bytes()),
685
+ values: Number(instance.exports.pulse_fastly_memory_values())
686
+ });
687
+ }
618
688
  for (const entry of trace) if (entry.url && privateUrls.has(entry.url)) entry.url = '[REDACTED]';
619
689
  const lastError = typeof instance.exports.pulse_fastly_last_error === 'function'
620
690
  ? Number(instance.exports.pulse_fastly_last_error())
@@ -628,10 +698,11 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
628
698
  const pulseLastError = typeof instance.exports.pulse_last_error_code === 'function'
629
699
  ? Number(instance.exports.pulse_last_error_code())
630
700
  : undefined;
701
+ if (downstream?.streaming && !downstream.finished) { downstream.bodyBytes = bodyBytes(downstream.bodyHandle); downstream.aborted = true; }
631
702
  const handledJwtError = options.allowHandledJwtError === true
632
703
  && lastError === 1008
633
704
  && Boolean(downstream);
634
- if (lastError !== FASTLY_STATUS_OK && !handledJwtError) {
705
+ if (lastError !== FASTLY_STATUS_OK && !handledJwtError && !(options.allowAbortedResponse === true && downstream?.aborted)) {
635
706
  throw new FastlyNativePlatformCapabilitiesMockError('Fastly native platform capabilities module reported an execution error.', 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_MOCK_EXECUTION_FAILED', { lastError, errorStage, errorEffect, pulseLastError, trace });
636
707
  }
637
708
  if (!downstream) {
@@ -648,6 +719,7 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
648
719
  bodyBytes: downstream.bodyBytes,
649
720
  sent: true,
650
721
  streaming: downstream.streaming,
722
+ aborted: downstream.aborted === true,
651
723
  origin: downstream.origin,
652
724
  responseHandle: downstream.responseHandle,
653
725
  bodyHandle: downstream.bodyHandle
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const { normalizeFastlyMaxWasmBytes } = require('../build/wasm-budget.js');
2
3
  const { normalizeFastlyS3 } = require('./s3.js');
3
4
 
4
5
  function providerConfigError(code, message, detail) {
@@ -33,6 +34,7 @@ function normalizeFastlyProviderConfig(value) {
33
34
  : {};
34
35
  return Object.freeze({
35
36
  kind: 'fastly',
37
+ maxDurationMs: require('@pulse-compute/runtime/host').normalizeRequestDuration(input.maxDurationMs),
36
38
  version: input.version ? String(input.version) : undefined,
37
39
  bindings: Object.freeze({
38
40
  configStore: String(bindings.configStore || 'pulse_config'),
@@ -54,6 +56,7 @@ function normalizeFastlyProviderConfig(value) {
54
56
  dynamicBackends: bindings.dynamicBackends === true
55
57
  }),
56
58
  build: Object.freeze({
59
+ maxWasmBytes: normalizeFastlyMaxWasmBytes(input.build?.maxWasmBytes),
57
60
  name: String(input.build && input.build.name || 'pulse-app'),
58
61
  description: String(input.build && input.build.description || 'Pulse application'),
59
62
  authors: Object.freeze(Array.isArray(input.build && input.build.authors) ? input.build.authors.map(String) : [])
@@ -67,6 +70,7 @@ function fastlyProjectConfigDocument(config) {
67
70
  return Object.freeze({
68
71
  kind: 'fastly',
69
72
  bindings: config.bindings,
73
+ maxDurationMs: config.maxDurationMs,
70
74
  build: config.build,
71
75
  local: config.local,
72
76
  providerSpecificUserland: false
@@ -100,7 +104,10 @@ const FASTLY_PROJECT_CONFIG_REFERENCE = Object.freeze({
100
104
  })
101
105
  ]),
102
106
  fields: Object.freeze([
103
- Object.freeze({ section: 'fastly', path: 'fastly.bindings.s3', type: 'Readonly<Record<string, S3Binding & { backend: string }>>', default: '`{}`', scope: 'Fastly Native S3', description: 'Maps logical names to fixed HTTPS endpoint, bucket, region, named static backend, accessKeyIdSecret, secretAccessKeySecret, optional sessionTokenSecret, maxTextBytes (1–32768, default 32768) and timeoutMs (1–30000, default 10000).', security: 'Credentials resolve through the configured Secret Store at execution. Dynamic backend authority and guest endpoint overrides are forbidden.' }),
107
+ Object.freeze({section:'fastly',path:'fastly.maxWasmBytes',type:'integer',allowed:'positive safe integer',default:'4194304 (4 MiB)',scope:'fastly() facade',description:'Facade option projected to build.maxWasmBytes. Conventional profiles configure fastly.build.maxWasmBytes.'}),
108
+ Object.freeze({section:'fastly',path:'fastly.build.maxWasmBytes',type:'integer',allowed:'positive safe integer',default:'4194304 (4 MiB)',scope:'Native final Wasm artifact',description:'Inclusive byte budget checked for newly compiled and reused artifacts. Raising it does not increase runtime memory or change Fastly service limits. The fastly() facade accepts maxWasmBytes.'}),
109
+ Object.freeze({section:'fastly',path:'fastly.maxDurationMs',type:'integer',allowed:'1–30000',default:'omitted',scope:'HTTP request execution',description:'One provider-owned monotonic budget through buffered response handoff; expiry attempts 504 before headers commit and never asserts rollback of dispatched writes. No CPU preemption or post-handoff delivery guarantee.'}),
110
+ Object.freeze({ section: 'fastly', path: 'fastly.bindings.s3', type: 'Readonly<Record<string, S3Binding & { backend: string }>>', default: '`{}`', scope: 'Fastly Native S3', description: 'Maps logical names to fixed HTTPS endpoint, bucket, region, named static backend, accessKeyIdSecret, secretAccessKeySecret, optional sessionTokenSecret, maxTextBytes (1–2097152, default 32768) and timeoutMs (1–30000, default 10000).', security: 'Credentials resolve through the configured Secret Store at execution. Dynamic backend authority and guest endpoint overrides are forbidden.' }),
104
111
  Object.freeze({ section: 'fastly', path: 'fastly.configStore', type: 'string', default: '`pulse_config`', scope: 'Fastly config capability', description: 'Fastly Config Store resource name.' }),
105
112
  Object.freeze({ section: 'fastly', path: 'fastly.secretStore', type: 'string', default: '`pulse_secrets`', scope: 'Fastly secret capability', description: 'Fastly Secret Store resource name.', security: 'The name is build metadata; secret values are never embedded by this option.' }),
106
113
  Object.freeze({ section: 'fastly', path: 'fastly.kv', type: 'Readonly<Record<string, string>>', default: '`{}`', scope: 'Fastly KV capability', description: 'Maps logical ctx.kv names to Fastly KV Store resource names.', diagnostics: Object.freeze(['PULSE_FASTLY_KV_BINDINGS_INVALID']) }),
@@ -169,16 +169,25 @@ function createDriver() {
169
169
  runtimeClass: 'native',
170
170
  status: 'supported',
171
171
  capabilities: Object.freeze([
172
- 's3.head', 's3.getText',
172
+ 's3.head', 's3.getText', 's3.getBody',
173
173
  'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap',
174
174
  'jwt.verify',
175
+ 'jwt.sign',
175
176
  'jwt.verify.hs256',
176
177
  'jwt.verify.es256',
178
+ 'jwt.verify.rs256',
177
179
  'secret.get',
178
180
  'time.wall-clock'
179
181
  ]),
180
182
  keyTypes: Object.freeze(['secret', 'jwk', 'jwks']),
181
183
  realizations: Object.freeze([
184
+ Object.freeze({
185
+ kind: 'package-native-application',
186
+ contractId: 'pulse.entities',
187
+ implementation: 'pulse.package-native-application.v1',
188
+ implemented: true,
189
+ automaticFallback: false
190
+ }),
182
191
  Object.freeze({
183
192
  kind: 'crypto-composed',
184
193
  realization: 'guest-source:pulse-hmac-as',
@@ -204,6 +213,19 @@ function createDriver() {
204
213
  guestUnitRequired: true,
205
214
  portable: true,
206
215
  automaticFallback: false
216
+ }),
217
+ Object.freeze({
218
+ kind: 'crypto-composed',
219
+ realization: 'guest-linked:pulse-rs256-bearssl-i31',
220
+ implementation: 'bearssl.0.6.rsa-i31.sha256.v1',
221
+ algorithms: Object.freeze(['RS256']),
222
+ keyTypes: Object.freeze(['jwk', 'jwks']),
223
+ implemented: true,
224
+ status: 'implemented-g4',
225
+ semanticOwner: '@pulse-compute/crypto',
226
+ guestUnitRequired: true,
227
+ portable: true,
228
+ automaticFallback: false
207
229
  })
208
230
  ]),
209
231
  jwt: Object.freeze({
@@ -233,6 +255,11 @@ function createDriver() {
233
255
  realization: 'guest-linked:pulse-es256-rustcrypto-p256',
234
256
  implemented: true,
235
257
  status: 'implemented-g4'
258
+ }, {
259
+ algorithm: 'RS256',
260
+ realization: 'guest-linked:pulse-rs256-bearssl-i31',
261
+ implemented: true,
262
+ status: 'implemented-g4'
236
263
  }]
237
264
  }),
238
265
  finalWasmPolicy: fastlyFinalWasmPolicy,
@@ -258,6 +285,7 @@ function createDriver() {
258
285
  realizationArtifacts: invocation.nativeArtifact.realizationArtifacts,
259
286
  guestUnits: invocation.nativeArtifact.guestUnits,
260
287
  nativeOptimization: invocation.optimization,
288
+ emitWat: invocation.nativeArtifact.manifest.wat?.emitted === true,
261
289
  compileTimeoutMs: invocation.timeoutMs
262
290
  });
263
291
  const { native, ...publicRealization } = realization;
@@ -286,6 +314,7 @@ function createDriver() {
286
314
  realizationArtifacts: invocation.nativeArtifact.realizationArtifacts,
287
315
  guestUnits: invocation.nativeArtifact.guestUnits,
288
316
  nativeOptimization: invocation.optimization,
317
+ emitWat: invocation.nativeArtifact.manifest.wat?.emitted === true,
289
318
  compileTimeoutMs: invocation.timeoutMs
290
319
  });
291
320
  const providerBuild = writeFastlyCanonicalTarget({
@@ -332,7 +361,7 @@ function createDriver() {
332
361
  });
333
362
  },
334
363
  executionOptions(providerConfig = {}, values = {}) {
335
- return { ...values, bindings: providerConfig.bindings };
364
+ return { ...values, maxDurationMs: providerConfig.maxDurationMs, bindings: providerConfig.bindings };
336
365
  }
337
366
  });
338
367
  }
@@ -18,6 +18,7 @@ function prepareFastlyNativeExecution(invocation) {
18
18
  realizationArtifacts: invocation.nativeArtifact.realizationArtifacts,
19
19
  guestUnits: invocation.nativeArtifact.guestUnits,
20
20
  nativeOptimization: invocation.optimization,
21
+ emitWat: invocation.nativeArtifact.manifest.wat?.emitted === true,
21
22
  compileTimeoutMs: invocation.timeoutMs || undefined
22
23
  });
23
24
  const evidence = Object.freeze({
@@ -17,10 +17,10 @@ function normalizeFastlyS3(input = {}) {
17
17
  function resolveFastlyS3(effects, input) {
18
18
  const configured = normalizeFastlyS3(input);
19
19
  return Object.freeze(effects.flatMap((effect, index) => {
20
- if (!['s3.head', 's3.getText', 's3.putText'].includes(effect.kind)) return [];
20
+ if (!['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(effect.kind)) return [];
21
21
  const name = effect.resource && effect.resource.binding;
22
22
  if (!Object.hasOwn(configured, name)) throw new TypeError('The required Fastly S3 binding is missing.');
23
- return [Object.freeze({ index, ...configured[name], contentType: effect.operation === 'putText' ? require('@pulse-compute/s3/provider').normalizePutOptions({ contentType: effect.payload && effect.payload.contentType }).contentType : '' })];
23
+ return [Object.freeze({ index, ...configured[name], ...(effect.operation === 'getBody' ? require('@pulse-compute/s3/provider').normalizeBodyOptions(Object.fromEntries(['method', 'range', 'ifNoneMatch'].filter(key => effect.payload?.[key] !== undefined).map(key => [key, effect.payload[key]]))) : {}), contentType: effect.operation === 'putText' ? require('@pulse-compute/s3/provider').normalizePutOptions({ contentType: effect.payload && effect.payload.contentType }).contentType : '' })];
24
24
  }));
25
25
  }
26
26
  module.exports = { normalizeFastlyS3, resolveFastlyS3 };