@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
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ function runtimeSource() {
4
+ return `
5
+ let __pulse_request_body_failure: i32 = 0
6
+ // Validate the bounded, assembled request bytes before any text projection.
7
+ function __pulse_request_utf8_valid(bytes: Uint8Array): bool {
8
+ for (let i = 0; i < bytes.length;) {
9
+ const lead = bytes[i++]; if (lead < 128) continue;
10
+ let count = 0, minimum = 128, maximum = 191;
11
+ if (lead >= 194 && lead <= 223) count = 1;
12
+ else if (lead >= 224 && lead <= 239) { count = 2; if (lead == 224) minimum = 160; if (lead == 237) maximum = 159; }
13
+ else if (lead >= 240 && lead <= 244) { count = 3; if (lead == 240) minimum = 144; if (lead == 244) maximum = 143; }
14
+ else return false;
15
+ if (i + count > bytes.length || i32(bytes[i]) < minimum || i32(bytes[i]) > maximum) return false;
16
+ i++; for (let n = 1; n < count; n++, i++) if (bytes[i] < 128 || bytes[i] > 191) return false;
17
+ }
18
+ return true;
19
+ }
20
+ function __pulse_request_send_body_error(): bool {
21
+ if (__pulse_fastly_last_error != PULSE_ERROR_REQUEST_BODY) return false;
22
+ const output = new __PulseFastlyValue(); output.kind = PULSE_VALUE_RESPONSE;
23
+ output.status = __pulse_fastly_error_stage == 21 ? 413 : 400;
24
+ output.text = output.status == 413 ? "Payload Too Large" : "Bad Request";
25
+ output.headers.push(new __PulseFastlyHeader("content-type", "text/plain; charset=utf-8"));
26
+ __pulse_fastly_last_error = 0; __pulse_fastly_error_stage = 0; __pulse_fastly_error_effect = -1;
27
+ const status = __pulse_fastly_send_result(__pulse_fastly_put(output));
28
+ if (status != FASTLY_STATUS_OK) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 104, -1);
29
+ return true;
30
+ }
31
+ `;
32
+ }
33
+
34
+ function instrument(source, applicationErrors) {
35
+ if (!applicationErrors) source = source.replace(/function (host_\w+)\(([^)]*)\): (i32|void) \{/g,
36
+ (declaration, _name, _args, result) => `${declaration} if (__pulse_fastly_last_error == PULSE_ERROR_REQUEST_BODY) return${result === 'void' ? '' : ' 0'};`);
37
+ // The existing terminal failure path also covers applications without an
38
+ // error handler. Request-budget instrumentation subsequently takes priority.
39
+ const marker = 'function __pulse_fastly_jwt_send_error(): void {';
40
+ if (!source.includes(marker)) throw new Error('Fastly request-body error boundary drift.');
41
+ return source.replace(marker, marker + ' if (__pulse_request_send_body_error()) return;');
42
+ }
43
+
44
+ module.exports = { runtimeSource, instrument };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ // Fastly's multi-value ABI returns NUL-delimited pages and an i64 next cursor
4
+ // (-1 marks completion). See https://docs.rs/fastly/latest/src/fastly/abi.rs.html
5
+ // Keep this independent of outbound response headers.
6
+ function runtimeSource() {
7
+ return `
8
+ @external("fastly_http_req", "header_names_get")
9
+ declare function __pulse_request_header_names(handle: i32, buffer: usize, size: i32, cursor: i32, end: usize, written: usize): i32
10
+ @external("fastly_http_req", "header_values_get")
11
+ declare function __pulse_request_header_values(handle: i32, name: usize, length: i32, buffer: usize, size: i32, cursor: i32, end: usize, written: usize): i32
12
+ let __pulse_request_headers_snapshot: i32 = 0
13
+ let __pulse_request_headers_failed: bool = false
14
+ let __pulse_request_headers_bytes: i32 = 0
15
+ function __pulse_request_headers_fail(): void {
16
+ __pulse_request_headers_failed = true;
17
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 24, -1);
18
+ }
19
+ function __pulse_request_header_list(name: string, names: bool): Array<string> {
20
+ const output = new Array<string>();
21
+ const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES);
22
+ const end = new StaticArray<i64>(1), written = new StaticArray<i32>(1);
23
+ const key = __pulse_fastly_utf8(name);
24
+ let cursor: i64 = 0;
25
+ let bytes = 0;
26
+ for (let page = 0; page < 256; page++) {
27
+ end[0] = 0; written[0] = -1;
28
+ const status = names
29
+ ? __pulse_request_header_names(__pulse_fastly_request_handle, buffer.dataStart, buffer.length, i32(cursor), changetype<usize>(end), changetype<usize>(written))
30
+ : __pulse_request_header_values(__pulse_fastly_request_handle, changetype<usize>(key), key.byteLength, buffer.dataStart, buffer.length, i32(cursor), changetype<usize>(end), changetype<usize>(written));
31
+ const count = written[0], next = end[0];
32
+ if (status == FASTLY_STATUS_OK && count == 0 && cursor == 0 && next == 0) return output;
33
+ if (status != FASTLY_STATUS_OK || count < 0 || count > buffer.length
34
+ || (next != -1 && (next <= cursor || next > 4294967295 || count == 0))) {
35
+ __pulse_request_headers_fail(); return output;
36
+ }
37
+ bytes += count;
38
+ if (bytes > PULSE_FASTLY_BUFFER_BYTES) { __pulse_request_headers_fail(); return output; }
39
+ let start = 0;
40
+ for (let i = 0; i < count; i++) {
41
+ if (buffer[i] != 0) continue;
42
+ const size = i - start;
43
+ // Count name bytes once per returned pair, including both terminators.
44
+ if (!names) __pulse_request_headers_bytes += key.byteLength + size + 2;
45
+ if (output.length >= 256 || (names && size == 0) || __pulse_request_headers_bytes > PULSE_FASTLY_BUFFER_BYTES) {
46
+ __pulse_request_headers_fail(); return output;
47
+ }
48
+ output.push(String.UTF8.decodeUnsafe(buffer.dataStart + start, size, false));
49
+ start = i + 1;
50
+ }
51
+ if (start != count) { __pulse_request_headers_fail(); return output; }
52
+ if (next == -1) return output;
53
+ cursor = next;
54
+ }
55
+ __pulse_request_headers_fail(); return output;
56
+ }
57
+ function __pulse_request_headers_read(): i32 {
58
+ if (__pulse_request_headers_failed) { __pulse_request_headers_fail(); return 0; }
59
+ if (__pulse_request_headers_snapshot != 0) return __pulse_request_headers_snapshot;
60
+ const names = __pulse_request_header_list("", true);
61
+ if (__pulse_request_headers_failed) return 0;
62
+ const output = host_value_array();
63
+ let pairs = 0;
64
+ for (let n = 0; n < names.length; n++) {
65
+ const name = names[n].toLowerCase();
66
+ for (let previous = 0; previous < n; previous++) {
67
+ if (names[previous].toLowerCase() == name) { __pulse_request_headers_fail(); return 0; }
68
+ }
69
+ const values = __pulse_request_header_list(name, false);
70
+ if (__pulse_request_headers_failed) return 0;
71
+ if (values.length == 0 || pairs + values.length > 256) { __pulse_request_headers_fail(); return 0; }
72
+ pairs += values.length;
73
+ for (let v = 0; v < values.length; v++) {
74
+ const pair = host_value_array();
75
+ host_value_array_push(pair, __pulse_fastly_string_value(name));
76
+ host_value_array_push(pair, __pulse_fastly_string_value(values[v]));
77
+ host_value_array_push(output, pair);
78
+ }
79
+ }
80
+ __pulse_fastly_deep_freeze(output);
81
+ __pulse_request_headers_snapshot = output;
82
+ return output;
83
+ }
84
+ `;
85
+ }
86
+
87
+ function instrument(source, applicationErrors, used) {
88
+ if (applicationErrors || !used) return source;
89
+ return source.replace(/function (host_\w+)\(([^)]*)\): (i32|void) \{/g,
90
+ (declaration, _name, _args, result) => `${declaration} if (__pulse_request_headers_failed) return${result === 'void' ? '' : ' 0'};`);
91
+ }
92
+
93
+ module.exports = { runtimeSource, instrument };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ // Host value handles live for the invocation. Store concatenation as shared,
4
+ // immutable prefixes instead of retaining a flat copy for every intermediate.
5
+ // This changes representation only: coercion happens at the original operation,
6
+ // and aliases keep their original string even after a later append.
7
+ const nativeStringFields = `
8
+ textLeaf: string = ""
9
+ textLeft: __PulseFastlyValue | null = null
10
+ textRight: __PulseFastlyValue | null = null
11
+ textLength: i32 = 0
12
+ get text(): string {
13
+ if (this.textLeft === null) return this.textLeaf
14
+ const pending = new Array<__PulseFastlyValue>()
15
+ const parts = new Array<string>()
16
+ pending.push(this)
17
+ while (pending.length > 0) {
18
+ const node = pending.pop()
19
+ if (node.textLeft === null) parts.push(node.textLeaf)
20
+ else { pending.push(node.textRight!); pending.push(node.textLeft!) }
21
+ }
22
+ this.textLeaf = parts.join("")
23
+ this.textLeft = null
24
+ this.textRight = null
25
+ return this.textLeaf
26
+ }
27
+ set text(value: string) {
28
+ this.textLeaf = value
29
+ this.textLength = value.length
30
+ this.textLeft = null
31
+ this.textRight = null
32
+ }
33
+ `;
34
+ const nativeStringConcat = `
35
+ function __pulse_fastly_concat(leftHandle: i32, rightHandle: i32): i32 {
36
+ let left = __pulse_fastly_value(leftHandle)
37
+ let right = __pulse_fastly_value(rightHandle)
38
+ // Snapshot non-string coercions now; a referenced object can change later.
39
+ if (left.kind != PULSE_VALUE_STRING) {
40
+ leftHandle = __pulse_fastly_string_value(__pulse_fastly_string(leftHandle))
41
+ left = __pulse_fastly_value(leftHandle)
42
+ }
43
+ if (right.kind != PULSE_VALUE_STRING) {
44
+ rightHandle = __pulse_fastly_string_value(__pulse_fastly_string(rightHandle))
45
+ right = __pulse_fastly_value(rightHandle)
46
+ }
47
+ if (left.textLength == 0) return rightHandle
48
+ if (right.textLength == 0) return leftHandle
49
+ const length = i64(left.textLength) + i64(right.textLength)
50
+ if (length > String.MAX_LENGTH) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 57, -1); return 0 }
51
+ const value = new __PulseFastlyValue()
52
+ value.kind = PULSE_VALUE_STRING
53
+ value.textLength = i32(length)
54
+ value.textLeft = left
55
+ value.textRight = right
56
+ return __pulse_fastly_put(value)
57
+ }
58
+ `;
59
+
60
+ // ECMAScript WhiteSpace + LineTerminator code units. Keep this explicit rather
61
+ // than inheriting the AssemblyScript library's Unicode whitespace policy.
62
+ const nativeStringTrim = `
63
+ function __pulse_fastly_trim_space(code: i32): bool {
64
+ return (code >= 9 && code <= 13) || code == 32 || code == 160 || code == 5760
65
+ || (code >= 8192 && code <= 8202) || code == 8232 || code == 8233
66
+ || code == 8239 || code == 8287 || code == 12288 || code == 65279
67
+ }
68
+ function host_value_string_trim(handle: i32): i32 {
69
+ const value = __pulse_fastly_value(handle)
70
+ if (value.kind != PULSE_VALUE_STRING) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 56, -1); return 0 }
71
+ let start = 0
72
+ let end = value.text.length
73
+ while (start < end && __pulse_fastly_trim_space(value.text.charCodeAt(start))) start += 1
74
+ while (end > start && __pulse_fastly_trim_space(value.text.charCodeAt(end - 1))) end -= 1
75
+ return __pulse_fastly_string_value(value.text.substring(start, end))
76
+ }
77
+ `;
78
+
79
+ // Indexed reads expose one UTF-16 code unit, including an isolated surrogate.
80
+ // Validate property spelling before converting to i32: "01", fractions,
81
+ // negative indices and nonnumeric keys are ordinary missing properties.
82
+ const nativeStringIndex = `
83
+ function __pulse_fastly_string_index(value: __PulseFastlyValue, keyHandle: i32): i32 {
84
+ const key = __pulse_fastly_value(keyHandle)
85
+ if (key.kind == PULSE_VALUE_STRING && key.text == "length") return host_value_number(value.textLength)
86
+ if (key.kind != PULSE_VALUE_NUMBER && key.kind != PULSE_VALUE_STRING) return host_value_undefined()
87
+ const index = __pulse_fastly_number(keyHandle)
88
+ if (!(index >= 0 && index < value.textLength)) return host_value_undefined()
89
+ const position = i32(index)
90
+ if (index != f64(position)) return host_value_undefined()
91
+ if (key.kind == PULSE_VALUE_STRING && key.text != position.toString()) return host_value_undefined()
92
+ let node = value
93
+ let offset = position
94
+ while (node.textLeft !== null) {
95
+ const left = node.textLeft!
96
+ if (offset < left.textLength) node = left
97
+ else { offset -= left.textLength; node = node.textRight! }
98
+ }
99
+ return __pulse_fastly_string_value(node.textLeaf.charAt(offset))
100
+ }
101
+ `;
102
+
103
+ function needsNativeValueFailureGuard(plan) {
104
+ if ((plan.schemas?.references || []).some(entry => ['value-encode', 'text-decode'].includes(entry.usage))) return true;
105
+ function containsValueOperation(node) {
106
+ if (!node || typeof node !== 'object') return false;
107
+ if (node.kind === 'pure-loop' || (node.kind === 'method-call' && node.method === 'string.trim')) return true;
108
+ return Object.values(node).some(containsValueOperation);
109
+ }
110
+ return containsValueOperation(plan.entry?.body) || containsValueOperation(plan.handlers) || containsValueOperation(plan.stages) || containsValueOperation(plan.helpers) || containsValueOperation(plan.effects);
111
+ }
112
+
113
+ module.exports = { nativeStringFields, nativeStringConcat, nativeStringTrim, nativeStringIndex, needsNativeValueFailureGuard };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { CANONICAL_NATIVE_READ_LOOP_MEMORY: policy, hasBoundedReadLoop } = require('@pulse-compute/wasm-contracts/handler/canonical-native-runtime');
4
+
5
+ function runtimeSource(limits = policy) {
6
+ return `
7
+ let __pulse_memory_bytes: i64 = 0
8
+ let __pulse_memory_values: i64 = 0
9
+ let __pulse_memory_failed: bool = false
10
+ function __pulse_memory_charge(values: i64, bytes: i64): void {
11
+ if (__pulse_memory_failed) unreachable()
12
+ if (values > ${limits.maxValues} - __pulse_memory_values || bytes > ${limits.maxBytes} - __pulse_memory_bytes) {
13
+ __pulse_memory_failed = true
14
+ __pulse_fastly_fail(1010, values > ${limits.maxValues} - __pulse_memory_values ? 173 : 172, -1)
15
+ __pulse_invocation_close()
16
+ unreachable()
17
+ }
18
+ __pulse_memory_values += values
19
+ __pulse_memory_bytes += bytes
20
+ }
21
+ function __pulse_memory_value_push(target: Array<i32>, value: i32): void {
22
+ __pulse_memory_charge(1, ${limits.edgeBytes})
23
+ target.push(value)
24
+ }
25
+ function __pulse_memory_key_push(target: Array<string>, value: string): void {
26
+ __pulse_memory_charge(1, ${limits.edgeBytes} + i64(value.length) * 2)
27
+ target.push(value)
28
+ }
29
+ function __pulse_memory_header_push(target: Array<__PulseFastlyHeader>, value: __PulseFastlyHeader): void {
30
+ __pulse_memory_charge(1, ${limits.valueBytes} + i64(value.name.length + value.value.length) * 2)
31
+ target.push(value)
32
+ }
33
+ export function pulse_fastly_memory_bytes(): i64 { return __pulse_memory_bytes }
34
+ export function pulse_fastly_memory_values(): i64 { return __pulse_memory_values }
35
+ `;
36
+ }
37
+
38
+ function instrument(source, plan) {
39
+ if (!hasBoundedReadLoop(plan)) return source;
40
+ function insert(needle, extra) {
41
+ if (source.split(needle).length !== 2) throw new Error('Native value budget instrumentation mismatch: ' + needle);
42
+ source = source.replace(needle, needle + extra);
43
+ }
44
+ // Text assignments include lazy fetch bodies and response bodies as well as
45
+ // strings. Ropes reserve their eventual flat size before materialization.
46
+ insert('\n set text(value: string) {', '\n __pulse_memory_charge(0, i64(value.length) * 2)');
47
+ insert('\nfunction __pulse_fastly_put(value: __PulseFastlyValue): i32 {',
48
+ ` __pulse_memory_charge(1, ${policy.valueBytes} + (value.textLeft === null ? 0 : i64(value.textLength) * 2));`);
49
+ const helpers = { keys: 'key', values: 'value', headers: 'header' };
50
+ // Skip quoted data and comments: authored text can contain source-like text.
51
+ const tokens = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/[^\n]*|\/\*[\s\S]*?\*\/)|\b([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.(keys|values|headers)\.push\(/g;
52
+ source = source.replace(tokens, (match, quoted, object, field) => quoted || `__pulse_memory_${helpers[field]}_push(${object}.${field}, `);
53
+ const code = source.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, '');
54
+ if (/\.(keys|values|headers)\.push\(/.test(code)) throw new Error('Unmetered Native container growth.');
55
+ return source + runtimeSource();
56
+ }
57
+
58
+ module.exports = { policy, hasBoundedReadLoop, runtimeSource, instrument };
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const {
5
+ CANONICAL_NATIVE_IMPORT_NAMES,
6
+ CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES
7
+ } = require('@pulse-compute/wasm-contracts/handler/canonical-native-runtime');
8
+
9
+ function enabled(plan) { return Boolean(plan.packages && plan.packages.application); }
10
+
11
+ // The package owns request dispatch, codecs and continuations. This adapter only
12
+ // binds its canonical host imports to the same Fastly runtime as ordinary handlers.
13
+ function portable(plan) {
14
+ const application = plan.packages.application;
15
+ if (application.contractId !== 'pulse.entities'
16
+ || application.version !== 'pulse.package-native-application.v1'
17
+ || application.effectFailure !== 'package-completion'
18
+ || application.automaticFallback !== false
19
+ || crypto.createHash('sha256').update(application.source).digest('hex') !== application.sourceHash) {
20
+ throw new Error('Unsupported or invalid Fastly package Native application.');
21
+ }
22
+ const source = application.source.replace(
23
+ /^@external\("pulse_host", "([a-z_]+)"\) declare function (\w+)\(([^)]*)\): (i32|void)$/gm,
24
+ (_declaration, name, symbol, parameters, result) => {
25
+ if (!CANONICAL_NATIVE_IMPORT_NAMES.includes(name)) throw new Error('Unsupported package host import: ' + name);
26
+ const args = parameters ? parameters.split(',').map(parameter => parameter.trim().split(':')[0]).join(', ') : '';
27
+ if (name === 'value_json') return `function ${symbol}(${parameters}): i32 {
28
+ // A failed request read must reach the provider's terminal error response,
29
+ // without trapping JSON.parse<string> in the guest first.
30
+ if (handle <= 0) return changetype<i32>('""')
31
+ const text = __pulse_fastly_json(handle, 0)
32
+ if (String.UTF8.byteLength(text) > ${CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES}) {
33
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 172, -1); return changetype<i32>('null')
34
+ }
35
+ return changetype<i32>(text)
36
+ }`;
37
+ const guard = name === 'log' ? `if (level <= 0 || level > ${Number(plan.logging.reporting.level)}) return; ` : '';
38
+ return `function ${symbol}(${parameters}): ${result} { ${guard}${result === 'void' ? '' : 'return '}host_${name}(${args}) }`;
39
+ }
40
+ );
41
+ if (source.includes('@external("pulse_host",')) throw new Error('Unbound package Native host import.');
42
+ const codecs = (plan.schemas?.registry?.schemas || []).map((_schema, index) => `
43
+ function __pulse_schema_decode_${index}(input: string): string { return changetype<string>(pulse_schema_decode(${index}, changetype<i32>(input))) }
44
+ function __pulse_schema_encode_${index}(input: string): string { return changetype<string>(pulse_schema_encode(${index}, changetype<i32>(input))) }`).join('\n');
45
+ return { ...application, source: source + codecs + `\nconst __pulse_application_plan_hash: string = ${JSON.stringify(plan.planHash)}\n` };
46
+ }
47
+
48
+ function runtimeSource() {
49
+ return `
50
+ const __pulse_package_failed = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
51
+ function __pulse_package_take_effect_failure(): bool {
52
+ const code = __pulse_fastly_last_error
53
+ // Deadlines, invalid lifecycle transitions and request/body failures remain
54
+ // provider-terminal. Only an admitted effect's operational failure is framed.
55
+ if (__pulse_fastly_error_stage == 260 || __pulse_fastly_error_stage == 261) return false
56
+ if (code != PULSE_ERROR_HOSTCALL && code != PULSE_ERROR_TRANSPORT
57
+ && code != PULSE_ERROR_JSON && code != PULSE_ERROR_SCHEMA && code != PULSE_ERROR_VALUE) return false
58
+ __pulse_fastly_last_error = 0; __pulse_fastly_error_stage = 0; __pulse_fastly_error_effect = -1
59
+ return true
60
+ }
61
+ function host_effect_begin(effectIndex: i32, payload: i32): void {
62
+ if (__pulse_fastly_last_error != 0) return
63
+ __pulse_package_effect_begin(effectIndex, payload)
64
+ if (__pulse_package_take_effect_failure()) unchecked(__pulse_package_failed[effectIndex] = 1)
65
+ }
66
+ `;
67
+ }
68
+
69
+ function instrument(source) {
70
+ const signature = 'function host_effect_begin(effectIndex: i32, payload: i32): void {';
71
+ if (!source.includes(signature)) throw new Error('Fastly package effect admission boundary drift.');
72
+ return source.replace(signature, 'function __pulse_package_effect_begin(effectIndex: i32, payload: i32): void {') + runtimeSource();
73
+ }
74
+
75
+ function driverLoop() {
76
+ return ` while (runStatus == 1 && __pulse_fastly_last_error == 0) {
77
+ for (let effectIndex = 0; effectIndex < PULSE_FASTLY_EFFECT_COUNT; effectIndex += 1) {
78
+ const invocation = unchecked(__pulse_invocation_tickets[effectIndex])
79
+ if (invocation == 0) continue
80
+ let failed = unchecked(__pulse_package_failed[effectIndex]) != 0
81
+ unchecked(__pulse_package_failed[effectIndex] = 0)
82
+ let result = failed ? 0 : __pulse_fastly_resolve_effect(effectIndex)
83
+ if (__pulse_package_take_effect_failure()) { failed = true; result = 0 }
84
+ if (__pulse_fastly_last_error != 0) { __pulse_fastly_jwt_send_error(); return }
85
+ if (!failed && result <= 0) { __pulse_fastly_fail(PULSE_ERROR_STATE, 101, effectIndex); __pulse_fastly_jwt_send_error(); return }
86
+ if (__pulse_invocation_settle(effectIndex, invocation, result) != 1 || __pulse_fastly_last_error != 0) {
87
+ __pulse_fastly_fail(PULSE_ERROR_STATE, 101, effectIndex); __pulse_fastly_jwt_send_error(); return
88
+ }
89
+ }
90
+ runStatus = pulse_resume()
91
+ }`;
92
+ }
93
+
94
+ module.exports = { enabled, portable, instrument, driverLoop };
@@ -0,0 +1,49 @@
1
+ // Provider-owned request lifecycle. No wall time or caller header grants time.
2
+ @external('fastly_async_io', 'select') declare function __request_select(handles: usize, count: i32, timeout: i32, done: usize): i32
3
+ let __request_deadline: i64 = -1;
4
+ let __request_last_clock: i64 = -1;
5
+ let __request_expired: bool = false;
6
+ let __request_error_response: bool = false;
7
+ export function pulse_fastly_request_expired(): i32 { return __request_expired ? 1 : 0; }
8
+ function __request_clock(): i64 {
9
+ const out = new StaticArray<i64>(1);
10
+ if (wasi_snapshot_preview1_clock_time_get(1, 1, changetype<usize>(out)) != 0 || out[0] < __request_last_clock) {
11
+ __request_expired = true;
12
+ return -1;
13
+ }
14
+ __request_last_clock = out[0];
15
+ return out[0];
16
+ }
17
+ function __request_remaining(): i32 {
18
+ if (__request_expired) return 0;
19
+ const now = __request_clock();
20
+ if (now < 0 || now >= __request_deadline) { __request_expired = true; return 0; }
21
+ return i32((__request_deadline - now + 999999) / 1000000);
22
+ }
23
+ function __request_check(index: i32 = -1): bool {
24
+ if (__request_error_response) return true;
25
+ if (__request_remaining() > 0) return true;
26
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 260, index);
27
+ return false;
28
+ }
29
+ function __request_wait(handle: i32, index: i32): bool {
30
+ const remaining = __request_remaining();
31
+ if (remaining <= 0) return __request_check(index);
32
+ const handles = new StaticArray<i32>(1), ready = new StaticArray<i32>(1);
33
+ handles[0] = handle;
34
+ const status = __request_select(changetype<usize>(handles), 1, remaining, changetype<usize>(ready));
35
+ if (status != 0) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 261, index); return false; }
36
+ if (ready[0] == -1) __request_expired = true;
37
+ return __request_check(index);
38
+ }
39
+ function __request_send_timeout(): void {
40
+ // A timeout says nothing about remote acceptance of dispatched writes.
41
+ __request_error_response = true;
42
+ __pulse_fastly_last_error = 0;
43
+ const options = host_value_object();
44
+ host_value_object_set(options, __pulse_fastly_string_value('status'), host_value_number(504.0));
45
+ const response = host_response_text(__pulse_fastly_string_value('Gateway Timeout'), options);
46
+ __pulse_fastly_last_error = 0;
47
+ const status = __pulse_fastly_send_result(response);
48
+ if (status != 0) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 262, -1);
49
+ }
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const {normalizeRequestDuration} = require('@pulse-compute/runtime/host');
5
+
6
+ function instrumentRequestBudget(input, duration, plan) {
7
+ const ms = normalizeRequestDuration(duration);
8
+ if (ms === undefined) return input;
9
+ let source = input;
10
+ const replace = (before, after) => {
11
+ if (!source.includes(before)) throw new Error('Fastly request budget boundary drift: ' + before.slice(0, 100));
12
+ source = source.replace(before, after);
13
+ };
14
+ replace(' __pulse_fastly_last_error = fastly_abi_init', ` __request_expired = false; __request_error_response = false; __request_last_clock = -1;
15
+ __request_deadline = __request_clock() + i64(${ms}) * 1000000;
16
+ __pulse_fastly_last_error = fastly_abi_init`);
17
+ replace(' const handles = new StaticArray<i32>(2)\n const downstreamStatus', ` if (!__request_check()) { __request_send_timeout(); return; }
18
+ const handles = new StaticArray<i32>(2)
19
+ const downstreamStatus`);
20
+ replace('function host_effect_begin(effectIndex: i32, payload: i32): void {', 'function host_effect_begin(effectIndex: i32, payload: i32): void {\n if (!__request_check(effectIndex)) return;');
21
+ replace('function __pulse_invocation_settle(index: i32, ticket: i32, result: i32): i32 {', 'function __pulse_invocation_settle(index: i32, ticket: i32, result: i32): i32 {\n if (!__request_check(index)) return 0;');
22
+ replace(' let runStatus = pulse_start()', ' if (!__request_check()) { __request_send_timeout(); return; }\n let runStatus = pulse_start()');
23
+ replace('function __pulse_fastly_resolve_effect(effectIndex: i32): i32 {', `function __pulse_fastly_resolve_effect(effectIndex: i32): i32 {
24
+ if (!__request_check(effectIndex)) return 0;
25
+ const kind = __pulse_fastly_effect_kind(effectIndex);
26
+ if (unchecked(__pulse_fastly_pending_mode[effectIndex]) == PULSE_FASTLY_PENDING_ASYNC && kind != 12 && kind != 13 && kind != 14 && kind != 21 && ${plan.effects.filter(e=>e.kind.startsWith('kv.')&&['kv.getVersioned','kv.insertIfAbsent','kv.compareAndSwap'].includes(e.kind)).map(e=>'effectIndex != '+plan.effects.indexOf(e)).join(' && ')||'true'}) {
27
+ if (!__request_wait(unchecked(__pulse_fastly_pending[effectIndex]), effectIndex)) return 0;
28
+ }`);
29
+ replace(' const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES), read = __pulse_fastly_out_i32();', ' if (!__request_wait(handle, effectIndex)) return "";\n const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES), read = __pulse_fastly_out_i32();');
30
+ replace('while (offset < bytes.byteLength) { const written', 'while (offset < bytes.byteLength) { if (!__request_check()) return 1; const written');
31
+ replace('function __pulse_fastly_send_result(handle: i32): i32 {', 'function __pulse_fastly_send_result(handle: i32): i32 { if (!__request_check()) return 1;');
32
+ replace('return fastly_http_resp_send_downstream(responseHandle, bodyHandle, 0)', 'if (!__request_check()) return 1; return fastly_http_resp_send_downstream(responseHandle, bodyHandle, 0)');
33
+ replace('if (sendStatus != FASTLY_STATUS_OK) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 104, -1)', 'if (sendStatus != FASTLY_STATUS_OK) { if (__request_expired) { __request_send_timeout(); return; } __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 104, -1); }');
34
+ replace('status = fastly_http_req_send_async(requestHandle', 'if (!__request_check(effectIndex)) return; status = fastly_http_req_send_async(requestHandle');
35
+ replace('function __pulse_fastly_ready_effect(effectIndex: i32, handle: i32): void {', 'function __pulse_fastly_ready_effect(effectIndex: i32, handle: i32): void { if (!__request_check(effectIndex)) return;');
36
+ source = source.replace('if (fatal) return', 'if (fatal) { if (__request_expired || !__request_check()) __request_send_timeout(); return; }');
37
+ replace('function __pulse_fastly_jwt_send_error(): void {', 'function __pulse_fastly_jwt_send_error(): void { if (__request_expired || !__request_check()) { __request_send_timeout(); return; }');
38
+ source = source.replaceAll('runStatus = pulse_resume()', 'if (!__request_check()) { __request_send_timeout(); return; }\n runStatus = pulse_resume()');
39
+ replace(' const result = pulse_result_handle()', ' if (!__request_check()) { __request_send_timeout(); return; }\n const result = pulse_result_handle()');
40
+ if (source.includes('__kv_deadline[index] = start +')) replace('__kv_deadline[index] = start + i64(__KV_timeoutMs) * 1000000;', '__kv_deadline[index] = min<i64>(__request_deadline, start + i64(__KV_timeoutMs) * 1000000);');
41
+ if (source.includes('__s3_deadlines[index] = start +')) replace('__s3_deadlines[index] = start + i64(binding.timeout) * 1000000;', '__s3_deadlines[index] = min<i64>(__request_deadline, start + i64(binding.timeout) * 1000000);');
42
+ if (source.includes('function __pulse_fastly_write_binary(')) replace('const written = new StaticArray<i32>(1); let offset = 0;\n while (offset < data.length) {', 'const written = new StaticArray<i32>(1); let offset = 0;\n while (offset < data.length) { if (!__request_check()) return 1;');
43
+ return source + '\n' + fs.readFileSync(path.join(__dirname,'request-budget.as.ts'),'utf8');
44
+ }
45
+ module.exports = {instrumentRequestBudget};
@@ -0,0 +1,97 @@
1
+ // Fastly owns opaque body handles. No bytes enter an application value or text codec.
2
+ @lazy const __s3_bodies = new Array<__PulseFastlyValue>()
3
+ function __s3_body_failure(reason: string): i32 {
4
+ const output = new __PulseFastlyValue(); output.kind = PULSE_VALUE_RESPONSE;
5
+ output.status = reason == 'timeout' ? 504 : 502;
6
+ output.headers.push(new __PulseFastlyHeader('x-pulse-s3-error', reason));
7
+ return __pulse_fastly_put(output);
8
+ }
9
+ function __s3_close_bodies(): void {
10
+ for (let i = 0; i < __s3_bodies.length; i++) {
11
+ const value = __s3_bodies[i];
12
+ if (value.s3Body) { __s3_close_body(value.bodyHandle); value.s3Body = false; }
13
+ }
14
+ __s3_bodies.length = 0;
15
+ }
16
+ function __s3_body_response(index: i32, response: i32, body: i32, status: i32): i32 {
17
+ const output = new __PulseFastlyValue(); output.kind = PULSE_VALUE_RESPONSE; output.status = status;
18
+ const buffer = new Uint8Array(16384), end = new StaticArray<i64>(1), size = new StaticArray<i32>(1);
19
+ if (__s3_header_names(response, buffer.dataStart, buffer.length, 0, changetype<usize>(end), changetype<usize>(size)) != 0 || end[0] >= 0) return __s3_body_failure('protocol');
20
+ const names = __s3_list(buffer, size[0]); if (names === null) return __s3_body_failure('protocol');
21
+ let total = 0, length: f64 = -1, contentRange = ''; const seen = new Set<string>();
22
+ for (let i = 0; i < names.length; i++) {
23
+ const name = names[i].toLowerCase(), bytes = __pulse_s3_bytes(names[i]);
24
+ if (__s3_header_values(response, bytes.dataStart, bytes.length, buffer.dataStart, buffer.length, 0, changetype<usize>(end), changetype<usize>(size)) != 0 || end[0] >= 0) return __s3_body_failure('protocol');
25
+ const values = __s3_list(buffer, size[0]); if (values === null) return __s3_body_failure('protocol');
26
+ const selected = name == 'content-length' || name == 'content-type' || name == 'etag' || name == 'content-encoding' || name == 'content-range';
27
+ if (selected && (seen.has(name) || values.length != 1)) return __s3_body_failure('protocol');
28
+ seen.add(name);
29
+ for (let v = 0; v < values.length; v++) {
30
+ const value = values[v], count = __pulse_s3_bytes(value).length;
31
+ total += bytes.length + count + 4; if (total > 16384) return __s3_body_failure('protocol');
32
+ if (!selected) continue;
33
+ if (count > 1024 || !__pulse_s3_scalar(value, true)) return __s3_body_failure('protocol');
34
+ if (name == 'content-encoding') { if (value.toLowerCase() != 'identity') return __s3_body_failure('protocol'); continue; }
35
+ if (name == 'content-length') {
36
+ length = __pulse_s3_decimal(value); if (length < 0) return __s3_body_failure('protocol');
37
+ if (status != 200 && status != 206) continue;
38
+ }
39
+ if (name == 'etag' && !value.length) return __s3_body_failure('protocol');
40
+ if (name == 'content-range') { if (!value.length || count > 128) return __s3_body_failure('protocol'); contentRange = value; }
41
+ output.headers.push(new __PulseFastlyHeader(name, value));
42
+ }
43
+ }
44
+ const binding = __pulse_fastly_s3_binding(index);
45
+ if ((status == 200 || status == 206) && length < 0) return __s3_body_failure('protocol');
46
+ if (!__pulse_s3_body_range(status, length, contentRange, binding.range, binding.ifNoneMatch.length > 0)) return __s3_body_failure('protocol');
47
+ if (__s3_remaining(index) == 0) return __s3_body_failure('timeout');
48
+ if (binding.method != 'HEAD' && (status == 200 || status == 206)) {
49
+ if (length > binding.max) return __s3_body_failure('too-large');
50
+ output.s3Body = true; output.bodyHandle = body; output.s3Length = i32(length); output.s3Deadline = __s3_deadlines[index];
51
+ __s3_bodies.push(output);
52
+ }
53
+ return __pulse_fastly_put(output);
54
+ }
55
+ function __s3_body_remaining(value: __PulseFastlyValue): i32 {
56
+ const now = __s3_clock(1), ns = value.s3Deadline - now;
57
+ return now < 0 || ns <= 0 ? 0 : i32((ns + 999999) / 1000000);
58
+ }
59
+ function __s3_body_wait(value: __PulseFastlyValue, handle: i32): bool {
60
+ const timeout = __s3_body_remaining(value); if (!timeout) return false;
61
+ const handles = new StaticArray<i32>(1), done = new StaticArray<i32>(1); handles[0] = handle;
62
+ return __s3_select(changetype<usize>(handles), 1, timeout, changetype<usize>(done)) == 0 && done[0] != -1 && __s3_body_remaining(value) > 0;
63
+ }
64
+ function __s3_copy_body(value: __PulseFastlyValue, output: i32): bool {
65
+ const buffer = new Uint8Array(16384), size = new StaticArray<i32>(1), written = new StaticArray<i32>(1);
66
+ let count = 0;
67
+ while (true) {
68
+ if (!__s3_body_wait(value, value.bodyHandle)) return false;
69
+ if (fastly_http_body_read(value.bodyHandle, buffer.dataStart, buffer.length, changetype<usize>(size)) != 0 || size[0] < 0 || size[0] > buffer.length) return false;
70
+ if (__s3_body_remaining(value) == 0) return false;
71
+ if (!size[0]) return count == value.s3Length;
72
+ count += size[0]; if (count > value.s3Length) return false;
73
+ let offset = 0;
74
+ while (offset < size[0]) {
75
+ if (!__s3_body_wait(value, output)) return false;
76
+ if (fastly_http_body_write(output, buffer.dataStart + offset, size[0] - offset, 0, changetype<usize>(written)) != 0 || written[0] <= 0 || written[0] > size[0] - offset) return false;
77
+ offset += written[0];
78
+ }
79
+ }
80
+ }
81
+ function __s3_send_body(value: __PulseFastlyValue): i32 {
82
+ if (!__s3_body_remaining(value)) return __pulse_fastly_send_result(__s3_body_failure('timeout'));
83
+ const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1);
84
+ if (fastly_http_resp_new(changetype<usize>(response)) != 0) return 1;
85
+ if (fastly_http_body_new(changetype<usize>(body)) != 0) { __s3_close_resp(response[0]); return 1; }
86
+ let ok = fastly_http_resp_status_set(response[0], value.status) == 0;
87
+ for (let i = 0; ok && i < value.headers.length; i++) {
88
+ const header = value.headers[i], name = __pulse_s3_bytes(header.name), data = __pulse_s3_bytes(header.value);
89
+ ok = fastly_http_resp_header_append(response[0], name.dataStart, name.length, data.dataStart, data.length) == 0;
90
+ }
91
+ if (!ok || fastly_http_resp_send_downstream(response[0], body[0], 1) != 0) { __s3_close_resp(response[0]); __s3_close_body(body[0]); return 1; }
92
+ ok = __s3_copy_body(value, body[0]);
93
+ __s3_close_body(value.bodyHandle); value.s3Body = false;
94
+ // close is successful EOF for a streaming handle. On failure leave it unfinished:
95
+ // Compute aborts the downstream stream when this invocation exits (SDK semantics).
96
+ return ok ? __s3_close_body(body[0]) : 1;
97
+ }