@pulse-compute/provider-fastly 1.0.0-beta.1 → 1.0.0-beta.10

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 (68) hide show
  1. package/README.md +7 -6
  2. package/dist/config-schema.json +30 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +11 -10
  8. package/src/build/canonical-target.js +35 -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/entities-native.js +6 -11
  13. package/src/build/kv-native.as.ts +331 -0
  14. package/src/build/kv-native.js +27 -0
  15. package/src/build/native-application-errors.js +159 -0
  16. package/src/build/native-binary-resources.js +30 -0
  17. package/src/build/native-body-forwarding.as.ts +341 -0
  18. package/src/build/native-body-forwarding.js +69 -0
  19. package/src/build/native-dynamic-backends.as.ts +115 -0
  20. package/src/build/native-dynamic-backends.js +52 -0
  21. package/src/build/native-fetch-deadlines.as.ts +164 -0
  22. package/src/build/native-fetch-deadlines.js +27 -0
  23. package/src/build/native-fetch-errors.js +38 -0
  24. package/src/build/native-handle-ownership.js +127 -0
  25. package/src/build/native-header-lookup.js +50 -0
  26. package/src/build/native-http-effects.js +155 -32
  27. package/src/build/native-platform-capabilities.js +755 -210
  28. package/src/build/native-request-body.js +44 -0
  29. package/src/build/native-request-headers.js +93 -0
  30. package/src/build/native-send-error-detail.as.ts +12 -0
  31. package/src/build/native-stdio.js +45 -0
  32. package/src/build/native-string-values.js +143 -0
  33. package/src/build/native-transport-details.as.ts +84 -0
  34. package/src/build/native-value-budget.js +90 -0
  35. package/src/build/package-native-application.js +114 -0
  36. package/src/build/request-budget.as.ts +50 -0
  37. package/src/build/request-budget.js +47 -0
  38. package/src/build/s3-body.as.ts +97 -0
  39. package/src/build/s3-native.as.ts +228 -0
  40. package/src/build/s3-native.js +30 -0
  41. package/src/build/schema-admission.js +83 -0
  42. package/src/build/schema-nested-json.js +93 -0
  43. package/src/build/schema-scalar-record.js +41 -0
  44. package/src/build/time-native.js +49 -0
  45. package/src/build/wasm-budget.js +28 -0
  46. package/src/config-api.js +6 -0
  47. package/src/config-schema.json +30 -0
  48. package/src/index.ts +25 -0
  49. package/src/javascript/jwt-verifier.js +14 -4
  50. package/src/javascript/logging.js +11 -2
  51. package/src/javascript/package-effects.js +5 -2
  52. package/src/javascript/runtime-host.js +7 -3
  53. package/src/javascript/source-package.js +4 -2
  54. package/src/javascript/target-support-policy.js +20 -3
  55. package/src/javascript/target.js +17 -1
  56. package/src/javascript/test-runtime.js +1 -0
  57. package/src/provider-contract.js +22 -6
  58. package/src/runtime/canonical-api-runtime.js +6 -0
  59. package/src/testing/conditional-kv-host.js +155 -0
  60. package/src/testing/native-dynamic-backends-host.js +39 -0
  61. package/src/testing/native-http-effects-host.js +9 -1
  62. package/src/testing/native-platform-capabilities-host.js +349 -24
  63. package/src/toolchain/body-forwarding.js +25 -0
  64. package/src/toolchain/config.js +16 -0
  65. package/src/toolchain/index.js +48 -5
  66. package/src/toolchain/logging.js +18 -0
  67. package/src/toolchain/native-execution.js +78 -0
  68. package/src/toolchain/s3.js +26 -0
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { schemaHasScalarRecord } = require('@pulse-compute/wasm-contracts/schema-json/registry');
4
+
5
+ function hasScalarRecords(plan) {
6
+ return (plan.schemas?.registry?.schemas || []).some(schema => schemaHasScalarRecord(schema.root));
7
+ }
8
+
9
+ // Both Fastly Native realizations validate handles before json-as sees them.
10
+ function scalarRecordProjectionLines(node) {
11
+ const limits = node.limits;
12
+ return [
13
+ ' if (input.kind != PULSE_VALUE_OBJECT || (checkDuplicates && input.duplicateJsonKeys)) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }',
14
+ ` if (input.keys.length > ${limits.maxKeys}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
15
+ ' const keys = input.keys.slice().sort()',
16
+ ' const output = host_value_object()',
17
+ ' let bytes = 2',
18
+ ' for (let i = 0; i < keys.length; i++) {',
19
+ ' const key = keys[i]',
20
+ ` if (key.length > ${limits.maxKeyLength}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
21
+ ' const keyHandle = __pulse_fastly_string_value(key)',
22
+ ' const itemHandle = host_value_property(valueHandle, keyHandle)',
23
+ ' const item = __pulse_fastly_value(itemHandle)',
24
+ ' let size = 0',
25
+ ' if (item.kind == PULSE_VALUE_STRING) {',
26
+ ` if (item.text.length > ${limits.maxStringLength}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
27
+ ' size = __pulse_scalar_record_string_bytes(item.text)',
28
+ ' } else if (item.kind == PULSE_VALUE_NUMBER && isFinite(item.number)) {',
29
+ ` size = ${limits.numberBytes}`,
30
+ ' } else if (item.kind == PULSE_VALUE_BOOLEAN) size = item.boolean != 0 ? 4 : 5',
31
+ ' else if (item.kind == PULSE_VALUE_NULL) size = 4',
32
+ ' else { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }',
33
+ ' bytes += (i == 0 ? 0 : 1) + __pulse_scalar_record_string_bytes(key) + 1 + size',
34
+ ` if (bytes > ${limits.maxBytes}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
35
+ ' host_value_object_set(output, keyHandle, itemHandle)',
36
+ ' }',
37
+ ' return output'
38
+ ];
39
+ }
40
+
41
+ module.exports = { hasScalarRecords, scalarRecordProjectionLines };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ // Provider-owned realtime sampling. The portable guest still suspends through
4
+ // effect_begin; no clock import is exposed to application-authored code.
5
+ function timeNativeSource(plan) {
6
+ if (!(plan.effects || []).some(effect => effect.kind === 'time.now')) return '';
7
+ return `
8
+ function __pulse_time_pad(value: i32, width: i32): string {
9
+ let text = value.toString()
10
+ while (text.length < width) text = "0" + text
11
+ return text
12
+ }
13
+ function __pulse_time_iso(ms: u64): string {
14
+ // Gregorian civil date from whole days since the Unix epoch.
15
+ const z = <i32>(ms / 86400000) + 719468
16
+ const era = z / 146097
17
+ const doe = z - era * 146097
18
+ const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
19
+ let year = yoe + era * 400
20
+ const doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
21
+ const mp = (5 * doy + 2) / 153
22
+ const day = doy - (153 * mp + 2) / 5 + 1
23
+ const month = mp + (mp < 10 ? 3 : -9)
24
+ if (month <= 2) year += 1
25
+ const rest = <i32>(ms % 86400000)
26
+ return __pulse_time_pad(year, 4) + "-" + __pulse_time_pad(month, 2) + "-" + __pulse_time_pad(day, 2)
27
+ + "T" + __pulse_time_pad(rest / 3600000, 2) + ":" + __pulse_time_pad((rest / 60000) % 60, 2)
28
+ + ":" + __pulse_time_pad((rest / 1000) % 60, 2) + "." + __pulse_time_pad(rest % 1000, 3) + "Z"
29
+ }
30
+ function __pulse_fastly_time_begin(effectIndex: i32, payload: __PulseFastlyValue): void {
31
+ const out = new StaticArray<u64>(1)
32
+ const status = wasi_snapshot_preview1_clock_time_get(0, 1000000, changetype<usize>(out))
33
+ const result = host_value_object()
34
+ if (status != 0) {
35
+ host_value_object_set(result, __pulse_fastly_string_value("status"), __pulse_fastly_string_value("failed"))
36
+ host_value_object_set(result, __pulse_fastly_string_value("reason"), __pulse_fastly_string_value("unavailable"))
37
+ } else {
38
+ // Divide integer nanoseconds before conversion to f64 to retain millisecond precision.
39
+ const ms = unchecked(out[0]) / 1000000
40
+ host_value_object_set(result, __pulse_fastly_string_value("status"), __pulse_fastly_string_value("ok"))
41
+ host_value_object_set(result, __pulse_fastly_string_value("unixEpochMs"), host_value_number(<f64>ms))
42
+ host_value_object_set(result, __pulse_fastly_string_value("iso8601"), __pulse_fastly_string_value(__pulse_time_iso(ms)))
43
+ }
44
+ __pulse_fastly_ready_effect(effectIndex, result)
45
+ }
46
+ `;
47
+ }
48
+
49
+ module.exports = Object.freeze({ timeNativeSource });
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { fastlyConfigDefault } = require('../config-schema.js');
4
+ const DEFAULT_FASTLY_MAX_WASM_BYTES = fastlyConfigDefault('fastly.build.maxWasmBytes');
5
+
6
+ function normalizeFastlyMaxWasmBytes(value) {
7
+ if (value === undefined) return DEFAULT_FASTLY_MAX_WASM_BYTES;
8
+ if (!Number.isSafeInteger(value) || value <= 0) {
9
+ const error = new TypeError('fastly.build.maxWasmBytes must be a positive safe integer byte count.');
10
+ error.code = 'PULSE_FASTLY_MAX_WASM_BYTES_INVALID';
11
+ throw error;
12
+ }
13
+ return value;
14
+ }
15
+
16
+ function assertFastlyWasmBudget(bytes, value) {
17
+ const maxBytes = normalizeFastlyMaxWasmBytes(value);
18
+ if (bytes > maxBytes) {
19
+ const error = new Error('Generated Fastly Native module exceeds the configured Pulse compilation budget.');
20
+ error.name = 'FastlyNativePlatformCapabilitiesError';
21
+ error.code = 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_WASM_TOO_LARGE';
22
+ error.detail = Object.freeze({ bytes, maxBytes });
23
+ throw error;
24
+ }
25
+ return maxBytes;
26
+ }
27
+
28
+ module.exports = Object.freeze({ DEFAULT_FASTLY_MAX_WASM_BYTES, normalizeFastlyMaxWasmBytes, assertFastlyWasmBudget });
package/src/config-api.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeFastlyLogEndpoint } = require('./toolchain/logging.js');
4
+ const { normalizeFastlyMaxWasmBytes } = require('./build/wasm-budget.js');
3
5
  const { fastlyConfigDefault } = require('./config-schema.js');
4
6
 
5
7
  const FASTLY_PROVIDER_API_VERSION = 'pulse.provider-fastly-api.v2';
@@ -38,8 +40,11 @@ function fastly(options = {}) {
38
40
  const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
39
41
  return Object.freeze({
40
42
  kind: 'fastly',
43
+ ...(options.bodyForwarding === undefined ? {} : {bodyForwarding: require('./toolchain/body-forwarding.js').normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs)}),
44
+ ...(options.maxDurationMs === undefined ? {} : { maxDurationMs: require('@pulse-compute/runtime/host').normalizeRequestDuration(options.maxDurationMs) }),
41
45
  version: FASTLY_PROVIDER_API_VERSION,
42
46
  bindings: Object.freeze({
47
+ ...(options.logEndpoint === undefined ? {} : { logEndpoint: normalizeFastlyLogEndpoint(options.logEndpoint) }),
43
48
  configStore: String(options.configStore ?? fastlyConfigDefault('fastly.configStore')),
44
49
  secretStore: String(options.secretStore ?? fastlyConfigDefault('fastly.secretStore')),
45
50
  kv: stringMap(options.kv),
@@ -48,6 +53,7 @@ function fastly(options = {}) {
48
53
  dynamicBackends: options.dynamicBackends === undefined ? fastlyConfigDefault('fastly.dynamicBackends') : options.dynamicBackends === true
49
54
  }),
50
55
  build: Object.freeze({
56
+ maxWasmBytes: normalizeFastlyMaxWasmBytes(options.maxWasmBytes),
51
57
  name: String(options.name ?? fastlyConfigDefault('fastly.name')),
52
58
  description: String(options.description ?? fastlyConfigDefault('fastly.description')),
53
59
  authors: Object.freeze((Array.isArray(options.authors) ? options.authors : fastlyConfigDefault('fastly.authors')).map(String)),
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "schemaVersion": "pulse.provider-fastly-config-schema.v1",
3
3
  "fields": {
4
+ "fastly.bindings.s3": {
5
+ "kind": "object",
6
+ "default": {}
7
+ },
4
8
  "fastly.configStore": {
5
9
  "kind": "string",
6
10
  "default": "pulse_config"
@@ -68,6 +72,32 @@
68
72
  "fastly.local.networkFetch": {
69
73
  "kind": "boolean",
70
74
  "default": false
75
+ },
76
+ "fastly.maxDurationMs": {
77
+ "kind": "optional-number"
78
+ },
79
+ "fastly.build.maxWasmBytes": {
80
+ "kind": "positive-safe-integer",
81
+ "default": 4194304
82
+ },
83
+ "fastly.maxWasmBytes": {
84
+ "kind": "positive-safe-integer"
85
+ },
86
+ "fastly.bodyForwarding": {
87
+ "kind": "object"
88
+ },
89
+ "fastly.bodyForwarding.requestMaxBytes": {
90
+ "kind": "integer",
91
+ "minimum": 1,
92
+ "maximum": 1048576
93
+ },
94
+ "fastly.bodyForwarding.responseMaxBytes": {
95
+ "kind": "integer",
96
+ "minimum": 1,
97
+ "maximum": 1048576
98
+ },
99
+ "fastly.logEndpoint": {
100
+ "kind": "optional-string"
71
101
  }
72
102
  }
73
103
  }
package/src/index.ts CHANGED
@@ -1,3 +1,7 @@
1
+ const { normalizeFastlyLogEndpoint } = require('../src/toolchain/logging.js') as { normalizeFastlyLogEndpoint(value?: string): string | undefined };
2
+ const { normalizeBodyForwarding } = require('../src/toolchain/body-forwarding.js') as { normalizeBodyForwarding(value: PulseFastlyBodyForwardingOptions | undefined, maxDurationMs?: number): PulseFastlyBodyForwardingOptions | undefined };
3
+ const { normalizeFastlyMaxWasmBytes } = require('../src/build/wasm-budget.js') as { normalizeFastlyMaxWasmBytes(value?: number): number };
4
+ const { normalizeRequestDuration } = require('@pulse-compute/runtime/host') as { normalizeRequestDuration(value?: number): number | undefined };
1
5
  import FASTLY_CONFIG_SCHEMA from './config-schema.json';
2
6
 
3
7
  type FastlySchemaRule = Readonly<{ kind: string; default?: unknown }>;
@@ -22,7 +26,19 @@ export interface PulseFastlyLocalOptions {
22
26
  readonly networkFetch?: boolean;
23
27
  }
24
28
 
29
+ export interface PulseFastlyBodyForwardingOptions {
30
+ readonly requestMaxBytes: number;
31
+ readonly responseMaxBytes: number;
32
+ }
33
+
25
34
  export interface PulseFastlyProviderOptions {
35
+ /** Inclusive final Native Wasm byte budget; default 4 MiB. */
36
+ readonly maxWasmBytes?: number;
37
+ readonly maxDurationMs?: number;
38
+ /** Experimental Native metered pump; final close hands delivery to Fastly. */
39
+ readonly bodyForwarding?: PulseFastlyBodyForwardingOptions;
40
+ /** Named production log stream; omitted preserves stdout. */
41
+ readonly logEndpoint?: string;
26
42
  readonly configStore?: string;
27
43
  readonly secretStore?: string;
28
44
  readonly kv?: Readonly<Record<string, string>>;
@@ -36,9 +52,13 @@ export interface PulseFastlyProviderOptions {
36
52
  }
37
53
 
38
54
  export interface PulseFastlyProviderConfig {
55
+ readonly maxDurationMs?: number;
56
+ /** Experimental Native metered pump; final close hands delivery to Fastly. */
57
+ readonly bodyForwarding?: PulseFastlyBodyForwardingOptions;
39
58
  readonly kind: 'fastly';
40
59
  readonly version: typeof FASTLY_PROVIDER_API_VERSION;
41
60
  readonly bindings: {
61
+ readonly logEndpoint?: string;
42
62
  readonly configStore: string;
43
63
  readonly secretStore: string;
44
64
  readonly kv: Readonly<Record<string, string>>;
@@ -57,6 +77,7 @@ export interface PulseFastlyProviderConfig {
57
77
  readonly dynamicBackends: boolean;
58
78
  };
59
79
  readonly build: {
80
+ readonly maxWasmBytes: number;
60
81
  readonly name: string;
61
82
  readonly description: string;
62
83
  readonly authors: readonly string[];
@@ -97,8 +118,11 @@ export function fastly(options: PulseFastlyProviderOptions = {}): PulseFastlyPro
97
118
  const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
98
119
  return Object.freeze({
99
120
  kind: 'fastly',
121
+ maxDurationMs: normalizeRequestDuration(options.maxDurationMs),
122
+ bodyForwarding: normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs),
100
123
  version: FASTLY_PROVIDER_API_VERSION,
101
124
  bindings: Object.freeze({
125
+ ...(options.logEndpoint === undefined ? {} : { logEndpoint: normalizeFastlyLogEndpoint(options.logEndpoint) }),
102
126
  configStore: String(options.configStore ?? fastlyDefaults['fastly.configStore']),
103
127
  secretStore: String(options.secretStore ?? fastlyDefaults['fastly.secretStore']),
104
128
  kv: stringMap(options.kv),
@@ -107,6 +131,7 @@ export function fastly(options: PulseFastlyProviderOptions = {}): PulseFastlyPro
107
131
  dynamicBackends: options.dynamicBackends === undefined ? Boolean(fastlyDefaults['fastly.dynamicBackends']) : options.dynamicBackends === true
108
132
  }),
109
133
  build: Object.freeze({
134
+ maxWasmBytes: normalizeFastlyMaxWasmBytes(options.maxWasmBytes),
110
135
  name: String(options.name ?? fastlyDefaults['fastly.name']),
111
136
  description: String(options.description ?? fastlyDefaults['fastly.description']),
112
137
  authors: Object.freeze((options.authors ?? (Array.isArray(fastlyDefaults['fastly.authors']) ? fastlyDefaults['fastly.authors'] : [])).map(String)),
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
+ normalizeJwtSignEffect,
4
5
  normalizeJwtVerifyEffect
5
6
  } = require('@pulse-compute/wasm-contracts/jwt/contracts');
6
7
  const {
7
8
  CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
9
+ CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION,
8
10
  CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION
9
11
  } = require('@pulse-compute/wasm-contracts/crypto/contracts');
10
12
 
@@ -16,6 +18,7 @@ const FASTLY_JAVASCRIPT_JWT_REALIZATIONS = Object.freeze({
16
18
  implementation: CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION,
17
19
  automaticFallback: false
18
20
  }),
21
+ RS256: Object.freeze({ algorithm: 'RS256', realization: 'runtime-builtin', implementation: CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION, automaticFallback: false }),
19
22
  ES256: Object.freeze({
20
23
  algorithm: 'ES256',
21
24
  realization: 'runtime-builtin',
@@ -83,12 +86,19 @@ function createFastlyJavascriptJwtVerify(options = {}) {
83
86
  return async function verifyFastlyJavascriptJwt(effect, execution) {
84
87
  const { jwtProvider, cryptoProvider } = await loadRuntimeModules();
85
88
  assertRequestActive(execution, jwtProvider);
86
- const input = normalizeJwtVerifyEffect(effect);
87
- if (execution && typeof execution.registerRedactionValue === 'function') {
89
+ const signing = effect.operation === 'sign';
90
+ const input = signing ? normalizeJwtSignEffect(effect) : normalizeJwtVerifyEffect(effect);
91
+ if (!signing && execution && typeof execution.registerRedactionValue === 'function') {
88
92
  execution.registerRedactionValue(input.token);
89
93
  }
90
94
  assertRequestActive(execution, jwtProvider);
91
- const selectedCrypto = assertRuntimeBuiltin(cryptoProvider, jwtProvider);
95
+ const selectedCrypto = signing
96
+ ? (input.options.algorithm === 'RS256'
97
+ ? require('@pulse-compute/crypto/provider').bindJavascriptRs256Signer()
98
+ : input.options.algorithm === 'ES256'
99
+ ? require('@pulse-compute/crypto/provider').bindJavascriptEs256Signer()
100
+ : require('@pulse-compute/crypto/provider').bindJavascriptDigestMac(['HMAC-SHA256']))
101
+ : assertRuntimeBuiltin(cryptoProvider, jwtProvider);
92
102
  const host = {
93
103
  async captureWallClock() {
94
104
  assertRequestActive(execution, jwtProvider);
@@ -118,7 +128,7 @@ function createFastlyJavascriptJwtVerify(options = {}) {
118
128
  return result;
119
129
  };
120
130
  }
121
- const result = await jwtProvider.verifyJwtWithCrypto(
131
+ const result = await (signing ? jwtProvider.signJwtWithCrypto : jwtProvider.verifyJwtWithCrypto)(
122
132
  input,
123
133
  Object.freeze(host),
124
134
  selectedCrypto
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeFastlyLogEndpoint, MAX_LOG_BYTES } = require('../toolchain/logging.js');
4
+
3
5
  const FASTLY_JAVASCRIPT_LOGGING_VERSION = 'pulse.fastly-javascript-logging.v1';
4
6
 
5
7
  function createFastlyJavascriptLogEmitter(options = {}) {
8
+ const endpoint = normalizeFastlyLogEndpoint(options.logEndpoint);
6
9
  const output = options.console || globalThis.console;
7
10
  const methods = Object.freeze({
8
11
  1: 'error',
@@ -11,12 +14,18 @@ function createFastlyJavascriptLogEmitter(options = {}) {
11
14
  4: 'debug'
12
15
  });
13
16
  return function fastlyJavascriptLog(level, message, event) {
14
- if (!output) return;
15
17
  const method = methods[Number(level)] || 'log';
18
+ const record = `[pulse][${event && event.name || method}] ${String(message)}`;
19
+ if (endpoint !== undefined) {
20
+ if (new TextEncoder().encode(record).byteLength > MAX_LOG_BYTES) return;
21
+ try { new options.Logger(endpoint).log(record); } catch { /* Best effort, no fallback or retry. */ }
22
+ return;
23
+ }
24
+ if (!output) return;
16
25
  const write = typeof output[method] === 'function'
17
26
  ? output[method]
18
27
  : (typeof output.log === 'function' ? output.log : undefined);
19
- if (write) write.call(output, `[pulse][${event && event.name || method}] ${String(message)}`);
28
+ try { if (write) write.call(output, record); } catch { /* Logging does not change the HTTP outcome. */ }
20
29
  };
21
30
  }
22
31
 
@@ -15,10 +15,12 @@ function unavailable(effect = {}) {
15
15
  function createFastlyJavascriptPackageEffectCapabilities(options = {}) {
16
16
  const assetsLookup = options.assetsLookup;
17
17
  const gripBroadcast = options.gripBroadcast;
18
+ const digestText = require('@pulse-compute/crypto/provider').createJavascriptTextDigest(options);
18
19
  const jwtVerify = options.jwtVerify;
19
20
  return Object.freeze({
20
21
  version: FASTLY_JAVASCRIPT_PACKAGE_EFFECTS_VERSION,
21
22
  effect(effect, execution) {
23
+ if (effect && effect.contractId === 'pulse.crypto' && effect.operation === 'digestText') return digestText(effect, execution);
22
24
  if (effect && effect.contractId === 'pulse.assets' && effect.operation === 'lookup') {
23
25
  if (typeof assetsLookup !== 'function') return unavailable(effect);
24
26
  return assetsLookup(effect.payload, execution);
@@ -33,7 +35,7 @@ function createFastlyJavascriptPackageEffectCapabilities(options = {}) {
33
35
  }
34
36
  return gripBroadcast(effect.payload, execution);
35
37
  }
36
- if (effect && effect.contractId === 'pulse.jwt' && effect.operation === 'verify') {
38
+ if (effect && effect.contractId === 'pulse.jwt' && ['verify', 'sign'].includes(effect.operation)) {
37
39
  if (typeof jwtVerify !== 'function') return unavailable(effect);
38
40
  return jwtVerify(effect, execution);
39
41
  }
@@ -49,7 +51,8 @@ function withFastlyPackageEffectCapabilities(capabilities, options = {}) {
49
51
  ...base,
50
52
  effect(effect, execution) {
51
53
  if (effect && (
52
- effect.contractId === 'pulse.assets'
54
+ effect.contractId === 'pulse.crypto'
55
+ || effect.contractId === 'pulse.assets'
53
56
  || effect.contractId === 'pulse.grip'
54
57
  || effect.contractId === 'pulse.jwt'
55
58
  )) {
@@ -45,10 +45,10 @@ function createFastlyJavascriptCapabilities(options = {}) {
45
45
  : undefined;
46
46
  return withFastlyPackageEffectCapabilities(
47
47
  withFastlyFetchCapability(
48
- withFastlyBindingCapabilities(options.capabilities, { ...options, bindingCapabilities }),
48
+ withFastlyBindingCapabilities({ time: () => runtimeHost.readWallTime(options.wallClock === undefined ? () => Date.now() : options.wallClock), ...options.capabilities }, { ...options, bindingCapabilities }),
49
49
  { ...options, fetchImplementation: options.fetchImplementation }
50
50
  ),
51
- { assetsLookup, gripBroadcast, jwtVerify }
51
+ { assetsLookup, gripBroadcast, jwtVerify, digestSubtle: options.digestSubtle }
52
52
  );
53
53
  }
54
54
 
@@ -57,6 +57,9 @@ function requestHeaderPairs(request) {
57
57
  }
58
58
 
59
59
  async function executeFastlyJavascriptApplication(application, request, options = {}) {
60
+ if (options.maxDurationMs !== undefined || options.requestBudget !== undefined) {
61
+ throw new runtimeHost.PulseRuntimeContractError('PULSE_REQUEST_DURATION_UNSUPPORTED', 'Fastly JavaScript does not implement the request deadline; select Native.');
62
+ }
60
63
  const capabilities = options.effectAdapter === undefined
61
64
  ? createFastlyJavascriptCapabilities(options)
62
65
  : undefined;
@@ -77,12 +80,13 @@ async function executeFastlyJavascriptApplication(application, request, options
77
80
  maxKvValueBytes: options.maxKvValueBytes,
78
81
  maxKvValueDepth: options.maxKvValueDepth,
79
82
  maxKvValueEntries: options.maxKvValueEntries,
83
+ binaryResources: options.binaryResources,
80
84
  schemaCodecs: options.schemaCodecs,
81
85
  strict: options.strict === true,
82
86
  target: 'javascript',
83
87
  provider: 'fastly',
84
88
  reporting: options.reporting,
85
- log: options.log || createFastlyJavascriptLogEmitter({ console: options.console }),
89
+ log: options.log || createFastlyJavascriptLogEmitter({ console: options.console, logEndpoint: options.bindings?.logEndpoint, Logger: options.apis?.Logger }),
86
90
  redactionValues: options.redactionValues,
87
91
  onLogObservation: options.onLogObservation,
88
92
  onJsonTrace: options.onJsonTrace,
@@ -153,6 +153,7 @@ function abortControllerCompatibilitySource() {
153
153
  function bootstrapSource(deployment, runtimeConfig) {
154
154
  return [
155
155
  "import { enforceExplicitBackends } from 'fastly:backend';",
156
+ "import { Logger } from 'fastly:logger';",
156
157
  "import { ConfigStore } from 'fastly:config-store';",
157
158
  "import { KVStore } from 'fastly:kv-store';",
158
159
  "import { SecretStore } from 'fastly:secret-store';",
@@ -165,7 +166,7 @@ function bootstrapSource(deployment, runtimeConfig) {
165
166
  'if (pulseRuntimeConfig.bindings.dynamicBackends !== true) enforceExplicitBackends();',
166
167
  'export const handler = createFastlyJavascriptHandler(application, {',
167
168
  ' ...pulseRuntimeConfig,',
168
- ' apis: { ConfigStore, SecretStore, KVStore },',
169
+ ' apis: { ConfigStore, SecretStore, KVStore, Logger },',
169
170
  ' fetchImplementation: globalThis.fetch.bind(globalThis),',
170
171
  ' console: globalThis.console,',
171
172
  ' schemaCodecs',
@@ -332,6 +333,7 @@ function bundleFastlyJavascriptApplication(plan, projectRoot, schemaBundle, opti
332
333
  ]
333
334
  : ['const schemaCodecs = null;']),
334
335
  'const { createFastlyJavascriptHandler } = fastlyRuntime;',
336
+ ...(options.binaryResources ? [require('@pulse-compute/wasm-host-runtime/compiler/binary-resources').resourceJavascriptSource(options.binaryResources, 'application', 'resourceHost'), "import resourceHost from '@pulse-compute/runtime/host';"] : []),
335
337
  'export { application, createFastlyJavascriptHandler, schemaCodecs };',
336
338
  'export default application;',
337
339
  ''
@@ -501,7 +503,7 @@ function writeFastlyJavascriptSourcePackage(options = {}) {
501
503
  });
502
504
  const schemaBundle = options.schemaBundle;
503
505
  const schemaActive = Boolean(schemaBundle && schemaBundle.active);
504
- const bundle = bundleFastlyJavascriptApplication(plan, projectRoot, schemaBundle);
506
+ const bundle = bundleFastlyJavascriptApplication(plan, projectRoot, schemaBundle, { binaryResources: options.binaryResources });
505
507
  const providerConfig = options.providerConfig || {};
506
508
  const providerBindings = providerConfig.bindings && typeof providerConfig.bindings === 'object'
507
509
  ? providerConfig.bindings
@@ -16,6 +16,11 @@ function blockedRestriction(id, restriction) {
16
16
 
17
17
  function classifyFastlyJavascriptCapability(id, restrictions = {}) {
18
18
  const capability = String(id);
19
+ if (capability === 'request.body.transform') return decision(capability, 'blocked', 'fastly-body-transform-unavailable', 'provider-fastly');
20
+ if (capability === 'request.body.forward' || capability === 'req.body') return decision(capability, 'blocked', 'fastly-incoming-body-forwarding-unavailable', 'provider-fastly');
21
+ if (capability === 'crypto.digestText') return decision(capability, 'eligible', 'fastly-javascript-crypto-sha256-runtime-builtin', 'provider-fastly');
22
+ if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'kv.list', 'kv.delete'].includes(capability)) return decision(capability, 'blocked', 'fastly-conditional-kv-incomplete', 'provider-fastly');
23
+ if (['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(capability)) return decision(capability, 'blocked', 'fastly-javascript-s3-raw-headers-unavailable', 'provider-fastly');
19
24
  if (isJavascriptCoreCapability(capability)) {
20
25
  return decision(capability, 'eligible', 'fastly-request-router-runtime', 'provider-fastly');
21
26
  }
@@ -79,9 +84,11 @@ function classifyFastlyJavascriptCapability(id, restrictions = {}) {
79
84
  return decision(capability, 'eligible', 'fastly-javascript-provider-wall-clock-authority', 'provider-fastly');
80
85
  }
81
86
  if (
82
- capability === 'jwt.verify'
87
+ capability === 'jwt.sign'
88
+ || capability === 'jwt.verify'
83
89
  || capability === 'jwt.verify.hs256'
84
90
  || capability === 'jwt.verify.es256'
91
+ || capability === 'jwt.verify.rs256'
85
92
  ) {
86
93
  return blockedRestriction(capability, restrictions.jwt)
87
94
  || decision(capability, 'eligible', 'fastly-javascript-jwt-crypto-runtime-builtin', 'provider-fastly');
@@ -100,6 +107,9 @@ function classifyFastlyJavascriptCapability(id, restrictions = {}) {
100
107
 
101
108
  function classifyFastlyJavascriptProviderRequirement(id, restrictions = {}) {
102
109
  const requirement = String(id);
110
+ if (requirement === 'crypto.digestText') return decision(requirement, 'eligible', 'fastly-javascript-crypto-sha256-runtime-builtin', 'provider-fastly');
111
+ if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'kv.list', 'kv.delete'].includes(requirement)) return decision(requirement, 'blocked', 'fastly-conditional-kv-incomplete', 'provider-fastly');
112
+ if (['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(requirement)) return decision(requirement, 'blocked', 'fastly-javascript-s3-raw-headers-unavailable', 'provider-fastly');
103
113
  if (requirement === 'request' || ['response.json', 'response.text', 'response.custom'].includes(requirement)) {
104
114
  return decision(requirement, 'eligible', 'fastly-request-response-lifecycle', 'provider-fastly');
105
115
  }
@@ -141,9 +151,11 @@ function classifyFastlyJavascriptProviderRequirement(id, restrictions = {}) {
141
151
  return decision(requirement, 'eligible', 'fastly-javascript-provider-wall-clock-authority', 'provider-fastly');
142
152
  }
143
153
  if (
144
- requirement === 'jwt.verify'
154
+ requirement === 'jwt.sign'
155
+ || requirement === 'jwt.verify'
145
156
  || requirement === 'jwt.verify.hs256'
146
157
  || requirement === 'jwt.verify.es256'
158
+ || requirement === 'jwt.verify.rs256'
147
159
  ) {
148
160
  return blockedRestriction(requirement, restrictions.jwt)
149
161
  || decision(requirement, 'eligible', 'fastly-javascript-jwt-crypto-runtime-builtin', 'provider-fastly');
@@ -165,6 +177,11 @@ function restriction(reasonId, configuration) {
165
177
  }
166
178
 
167
179
  function fastlyJavascriptProjectRestrictions(compiled, project) {
180
+ if (project.providerConfig?.maxDurationMs !== undefined) {
181
+ const error = new TypeError('fastly.maxDurationMs requires the Native target; Fastly JavaScript does not implement the request deadline.');
182
+ error.code = 'PULSE_REQUEST_DURATION_UNSUPPORTED';
183
+ throw error;
184
+ }
168
185
  const bindings = project.providerConfig && project.providerConfig.bindings || {};
169
186
  const backends = bindings.backends && typeof bindings.backends === 'object' ? bindings.backends : {};
170
187
  const kv = bindings.kv && typeof bindings.kv === 'object' ? bindings.kv : {};
@@ -177,7 +194,7 @@ function fastlyJavascriptProjectRestrictions(compiled, project) {
177
194
  const secretSites = sites.filter((entry) => entry.kind === 'secret.get');
178
195
  const jwtSites = sites.filter((entry) => (
179
196
  entry.contractId === 'pulse.jwt'
180
- && (entry.operation === 'verify' || entry.kind === 'jwt.verify')
197
+ && (['verify', 'sign'].includes(entry.operation) || ['jwt.verify', 'jwt.sign'].includes(entry.kind))
181
198
  ));
182
199
  const output = {};
183
200
 
@@ -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',
@@ -57,6 +72,7 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
57
72
  supported: true,
58
73
  default: 'info',
59
74
  levels: ['off', 'error', 'warn', 'info', 'debug'],
75
+ destinations: ['stdout', 'configured-named-endpoint'],
60
76
  destination: 'provider-owned',
61
77
  betaDefault: 'stdio-log-tail'
62
78
  },
@@ -331,6 +331,7 @@ async function executeFastlyJavascriptLocalRequest(application, request, options
331
331
  maxRequestBodyBytes: options.maxRequestBodyBytes ?? options.maxBodyBytes,
332
332
  maxFetchBodyBytes: options.maxFetchBodyBytes ?? options.maxBodyBytes,
333
333
  maxStructuredBodyBytes: options.maxStructuredBodyBytes,
334
+ binaryResources: options.binaryResources,
334
335
  schemaCodecs: options.schemaCodecs,
335
336
  strict: options.strict === true,
336
337
  reporting: options.reporting,
@@ -12,27 +12,40 @@ const FASTLY_PROVIDER_DESCRIPTOR = canonical.normalizeDescriptor({
12
12
  deployable: true,
13
13
  localExecution: true,
14
14
  capabilities: [
15
- 'request', 'response.json', 'response.text', 'response.custom', 'fetch',
16
- 'config.get', 'secret.get', 'kv.get', 'kv.put', 'assets.lookup',
17
- 'grip.channel', 'grip.hold', 'grip.publish', 'grip.broadcast', 'jwt.verify',
18
- 'opaque.pass-through'
15
+ 'request', 'request.body.forward', 'response.json', 'response.text', 'response.custom', 'fetch',
16
+ 'time.wall-clock', 'crypto.digestText', 'config.get', 'secret.get', 'kv.get', 'kv.put', 'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'kv.list', 'kv.delete', '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',
22
+ 'request.body.forward': 'fastly.native.body-forwarding',
22
23
  'response.json': 'fastly.compute.response.json',
23
24
  'response.text': 'fastly.compute.response.text',
24
25
  'response.custom': 'fastly.compute.response.custom',
25
26
  fetch: 'fastly.backend.fetch',
27
+ 'time.wall-clock': 'wasi.clock_time_get.realtime',
28
+ 'crypto.digestText': 'pulse.crypto.guest-source.sha256',
26
29
  'config.get': 'fastly.config-store.get',
27
30
  'secret.get': 'fastly.secret-store.get',
28
31
  'kv.get': 'fastly.kv-store.get',
29
32
  'kv.put': 'fastly.kv-store.put',
33
+ 'kv.list': 'fastly.native.kv-store.list',
34
+ 'kv.delete': 'fastly.native.kv-store.delete',
35
+ 'kv.getVersioned': 'fastly.native.kv-store.getVersioned',
36
+ 'kv.insertIfAbsent': 'fastly.native.kv-store.insertIfAbsent',
37
+ 'kv.compareAndSwap': 'fastly.native.kv-store.compareAndSwap',
30
38
  'assets.lookup': 'fastly.kv-store.assets.lookup',
31
39
  'grip.channel': 'fastly.fanout.channel',
32
40
  'grip.hold': 'fastly.fanout.hold',
33
41
  'grip.publish': 'fastly.fanout.publish',
34
42
  'grip.broadcast': 'fastly.grip.publish-control',
35
43
  'jwt.verify': 'fastly.native.jwt.verify',
44
+ 'jwt.sign': 'fastly.native.jwt.sign',
45
+ 's3.head': 'fastly.native.s3.head',
46
+ 's3.getText': 'fastly.native.s3.getText',
47
+ 's3.getBody': 'fastly.native.s3.getBody',
48
+ 's3.putText': 'fastly.native.s3.putText',
36
49
  'opaque.pass-through': 'fastly.response-body.stream'
37
50
  }
38
51
  });
@@ -52,9 +65,12 @@ function validateStaticBackendBindings(metadata = {}, bindings = {}) {
52
65
  }
53
66
  }
54
67
 
55
- function createFastlyLoweringPlan(metadata, bindings = {}) {
68
+ function createFastlyLoweringPlan(metadata, bindings = {}, options = {}) {
69
+ const forwarding = require('./toolchain/body-forwarding.js').normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs);
70
+ const descriptor = forwarding ? FASTLY_PROVIDER_DESCRIPTOR : {...FASTLY_PROVIDER_DESCRIPTOR, capabilities: FASTLY_PROVIDER_DESCRIPTOR.capabilities.filter(id => id !== 'request.body.forward')};
71
+ require('./toolchain/s3.js').resolveFastlyS3(metadata.providerOperations || [], bindings.s3);
56
72
  validateStaticBackendBindings(metadata, bindings);
57
- return canonical.createProviderLoweringPlan(metadata, FASTLY_PROVIDER_DESCRIPTOR, {
73
+ return canonical.createProviderLoweringPlan(metadata, descriptor, {
58
74
  ...bindings,
59
75
  grip: bindings.grip || 'fastly-fanout'
60
76
  });