@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,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,12 @@
1
+ // Fastly wasm32 SendErrorDetail, pinned to Viceroy v0.21.1 / compute-at-edge-abi:
2
+ // tag:u32 @0, mask:u32 @4, DNS rcode:u16 @8, DNS info:u16 @10,
3
+ // TLS alert:u8 @12, H2 frame:u8 @13, padding @14..15, H2 code:u32 @16.
4
+ // https://github.com/fastly/Viceroy/blob/ba6fb78c02b2f40b70f8c65ab86fa86c66f0641f/wasm_abi/compute-at-edge-abi/typenames.witx
5
+ const __PULSE_SEND_ERROR_DETAIL_BYTES: i32 = 20
6
+ const __PULSE_SEND_ERROR_DETAIL_WORDS: i32 = __PULSE_SEND_ERROR_DETAIL_BYTES / 4
7
+ function __pulse_send_error_detail_reset(detail: usize): void {
8
+ memory.fill(detail, 0, __PULSE_SEND_ERROR_DETAIL_BYTES)
9
+ // Advertise storage for all known optional fields (bits 1..4); bit 0 is reserved.
10
+ // Diagnostics consume only returned fields authorized by both tag and mask.
11
+ store<u32>(detail + 4, 0x1e)
12
+ }
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ // Shared by both Native emitters. Fastly's named logging endpoint ABI rejects
4
+ // stdout/stderr; Compute stdio uses WASI fd_write with stdout descriptor 1.
5
+ const nativeStdioSource = String.raw`
6
+ @external("wasi_snapshot_preview1", "fd_write")
7
+ declare function __pulse_fastly_fd_write(fd: i32, iovs: usize, count: i32, written: usize): i32
8
+
9
+ function __pulse_fastly_write_stdout(bytes: ArrayBuffer): void {
10
+ const iov = new StaticArray<usize>(2)
11
+ unchecked(iov[0] = changetype<usize>(bytes))
12
+ unchecked(iov[1] = bytes.byteLength)
13
+ const written = new StaticArray<i32>(1)
14
+ // One best-effort attempt: errors, zero progress, and short writes never
15
+ // retry, spin, change the response, or disable subsequent log statements.
16
+ __pulse_fastly_fd_write(1, changetype<usize>(iov), 1, changetype<usize>(written))
17
+ }
18
+ `;
19
+
20
+ function nativeLoggingSource(endpoint) {
21
+ const { normalizeFastlyLogEndpoint, MAX_LOG_BYTES } = require('../toolchain/logging.js');
22
+ const name = normalizeFastlyLogEndpoint(endpoint);
23
+ if (name === undefined) return nativeStdioSource;
24
+ return String.raw`
25
+ @external("fastly_log", "endpoint_get")
26
+ declare function __pulse_fastly_log_endpoint_get(name: usize, length: usize, handle: usize): i32
27
+ @external("fastly_log", "write")
28
+ declare function __pulse_fastly_log_write(handle: i32, message: usize, length: usize, written: usize): i32
29
+
30
+ // Retain the shared emitter entry point; destination is selected at build time.
31
+ function __pulse_fastly_write_stdout(bytes: ArrayBuffer): void {
32
+ if (bytes.byteLength > ${MAX_LOG_BYTES}) return
33
+ const name = String.UTF8.encode(${JSON.stringify(name)}, false)
34
+ const handle = new StaticArray<i32>(1)
35
+ unchecked(handle[0] = -1)
36
+ if (__pulse_fastly_log_endpoint_get(changetype<usize>(name), name.byteLength, changetype<usize>(handle)) != 0) return
37
+ if (unchecked(handle[0]) == -1) return
38
+ const written = new StaticArray<usize>(1)
39
+ // One best-effort write. No retry or fallback, including zero/short writes.
40
+ __pulse_fastly_log_write(unchecked(handle[0]), changetype<usize>(bytes), bytes.byteLength, changetype<usize>(written))
41
+ }
42
+ `;
43
+ }
44
+
45
+ module.exports = { nativeStdioSource, nativeLoggingSource };
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ // Array<string>.join uses repeated concatenation at AssemblyScript shrink >= 1.
4
+ // Own the count/allocate/copy path so stub-runtime allocation stays linear at
5
+ // every optimization level. Copy UTF-16 bytes without decoding or coercion.
6
+ const nativeStringAssembly = `
7
+ function __pulse_fastly_string_bytes(length: i64): i32 {
8
+ if (length < 0 || length > i64(String.MAX_LENGTH)) {
9
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 57, -1); return -1
10
+ }
11
+ return i32(length * 2)
12
+ }
13
+ function __pulse_fastly_join_strings(parts: Array<string>): string {
14
+ // Both array and string lengths are bounded i32 values: their product fits
15
+ // in i64. Validate before narrowing or allocating the UTF-16 byte length.
16
+ let length: i64 = 0
17
+ for (let i = 0; i < parts.length; i++) length += i64(unchecked(parts[i]).length)
18
+ const bytes = __pulse_fastly_string_bytes(length)
19
+ if (bytes <= 0) return ""
20
+ if (parts.length == 1) return unchecked(parts[0])
21
+ const result = changetype<string>(__new(usize(bytes), idof<string>()))
22
+ let cursor = changetype<usize>(result)
23
+ for (let i = 0; i < parts.length; i++) {
24
+ const part = unchecked(parts[i])
25
+ const size = usize(part.length) * 2
26
+ memory.copy(cursor, changetype<usize>(part), size)
27
+ cursor += size
28
+ }
29
+ return result
30
+ }
31
+ `;
32
+
33
+ // Store concatenation as shared, immutable prefixes instead of retaining a flat
34
+ // copy for every intermediate. Direct object edges preserve live rope children.
35
+ // This changes representation only: coercion happens at the original operation,
36
+ // and aliases keep their original string even after a later append.
37
+ const nativeStringFields = `
38
+ textLeaf: string = ""
39
+ textLeft: __PulseFastlyValue | null = null
40
+ textRight: __PulseFastlyValue | null = null
41
+ textLength: i32 = 0
42
+ get text(): string {
43
+ if (this.textLeft === null) return this.textLeaf
44
+ const pending = new Array<__PulseFastlyValue>()
45
+ const parts = new Array<string>()
46
+ pending.push(this)
47
+ while (pending.length > 0) {
48
+ const node = pending.pop()
49
+ if (node.textLeft === null) parts.push(node.textLeaf)
50
+ else { pending.push(node.textRight!); pending.push(node.textLeft!) }
51
+ }
52
+ this.textLeaf = __pulse_fastly_join_strings(parts)
53
+ this.textLeft = null
54
+ this.textRight = null
55
+ return this.textLeaf
56
+ }
57
+ set text(value: string) {
58
+ this.textLeaf = value
59
+ this.textLength = value.length
60
+ this.textLeft = null
61
+ this.textRight = null
62
+ }
63
+ `;
64
+ const nativeStringConcat = `
65
+ function __pulse_fastly_concat(leftHandle: i32, rightHandle: i32): i32 {
66
+ let left = __pulse_fastly_value(leftHandle)
67
+ let right = __pulse_fastly_value(rightHandle)
68
+ // Snapshot non-string coercions now; a referenced object can change later.
69
+ if (left.kind != PULSE_VALUE_STRING) {
70
+ leftHandle = __pulse_fastly_string_value(__pulse_fastly_string(leftHandle))
71
+ left = __pulse_fastly_value(leftHandle)
72
+ }
73
+ if (right.kind != PULSE_VALUE_STRING) {
74
+ rightHandle = __pulse_fastly_string_value(__pulse_fastly_string(rightHandle))
75
+ right = __pulse_fastly_value(rightHandle)
76
+ }
77
+ if (left.textLength == 0) return rightHandle
78
+ if (right.textLength == 0) return leftHandle
79
+ const length = i64(left.textLength) + i64(right.textLength)
80
+ if (length > String.MAX_LENGTH) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 57, -1); return 0 }
81
+ const value = new __PulseFastlyValue()
82
+ value.kind = PULSE_VALUE_STRING
83
+ value.textLength = i32(length)
84
+ value.textLeft = left
85
+ value.textRight = right
86
+ return __pulse_fastly_put(value)
87
+ }
88
+ `;
89
+
90
+ // ECMAScript WhiteSpace + LineTerminator code units. Keep this explicit rather
91
+ // than inheriting the AssemblyScript library's Unicode whitespace policy.
92
+ const nativeStringTrim = `
93
+ function __pulse_fastly_trim_space(code: i32): bool {
94
+ return (code >= 9 && code <= 13) || code == 32 || code == 160 || code == 5760
95
+ || (code >= 8192 && code <= 8202) || code == 8232 || code == 8233
96
+ || code == 8239 || code == 8287 || code == 12288 || code == 65279
97
+ }
98
+ function host_value_string_trim(handle: i32): i32 {
99
+ const value = __pulse_fastly_value(handle)
100
+ if (value.kind != PULSE_VALUE_STRING) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 56, -1); return 0 }
101
+ let start = 0
102
+ let end = value.text.length
103
+ while (start < end && __pulse_fastly_trim_space(value.text.charCodeAt(start))) start += 1
104
+ while (end > start && __pulse_fastly_trim_space(value.text.charCodeAt(end - 1))) end -= 1
105
+ return __pulse_fastly_string_value(value.text.substring(start, end))
106
+ }
107
+ `;
108
+
109
+ // Indexed reads expose one UTF-16 code unit, including an isolated surrogate.
110
+ // Validate property spelling before converting to i32: "01", fractions,
111
+ // negative indices and nonnumeric keys are ordinary missing properties.
112
+ const nativeStringIndex = `
113
+ function __pulse_fastly_string_index(value: __PulseFastlyValue, keyHandle: i32): i32 {
114
+ const key = __pulse_fastly_value(keyHandle)
115
+ if (key.kind == PULSE_VALUE_STRING && key.text == "length") return host_value_number(value.textLength)
116
+ if (key.kind != PULSE_VALUE_NUMBER && key.kind != PULSE_VALUE_STRING) return host_value_undefined()
117
+ const index = __pulse_fastly_number(keyHandle)
118
+ if (!(index >= 0 && index < value.textLength)) return host_value_undefined()
119
+ const position = i32(index)
120
+ if (index != f64(position)) return host_value_undefined()
121
+ if (key.kind == PULSE_VALUE_STRING && key.text != position.toString()) return host_value_undefined()
122
+ let node = value
123
+ let offset = position
124
+ while (node.textLeft !== null) {
125
+ const left = node.textLeft!
126
+ if (offset < left.textLength) node = left
127
+ else { offset -= left.textLength; node = node.textRight! }
128
+ }
129
+ return __pulse_fastly_string_value(node.textLeaf.charAt(offset))
130
+ }
131
+ `;
132
+
133
+ function needsNativeValueFailureGuard(plan) {
134
+ if ((plan.schemas?.references || []).some(entry => ['value-encode', 'text-decode'].includes(entry.usage))) return true;
135
+ function containsValueOperation(node) {
136
+ if (!node || typeof node !== 'object') return false;
137
+ if (node.kind === 'pure-loop' || (node.kind === 'method-call' && node.method === 'string.trim')) return true;
138
+ return Object.values(node).some(containsValueOperation);
139
+ }
140
+ return containsValueOperation(plan.entry?.body) || containsValueOperation(plan.handlers) || containsValueOperation(plan.stages) || containsValueOperation(plan.helpers) || containsValueOperation(plan.effects);
141
+ }
142
+
143
+ module.exports = { nativeStringAssembly, nativeStringFields, nativeStringConcat, nativeStringTrim, nativeStringIndex, needsNativeValueFailureGuard };
@@ -0,0 +1,84 @@
1
+ // Fixed scalar-only host provenance. Never retain pointers, URLs, headers or bodies.
2
+ class __PulseTransportDetail {
3
+ phase: string = ''
4
+ status: u32 = 0
5
+ tag: i64 = -1
6
+ mask: i64 = -1
7
+ kind: string = 'unavailable'
8
+ dnsRcode: i32 = -1
9
+ dnsInfo: i32 = -1
10
+ tlsAlert: i32 = -1
11
+ h2Frame: i32 = -1
12
+ h2Code: i64 = -1
13
+ }
14
+ @lazy const __pulse_transport_slots = new StaticArray<__PulseTransportDetail | null>(PULSE_FASTLY_EFFECT_COUNT)
15
+ let __pulse_transport_error: __PulseTransportDetail | null = null
16
+ function __pulse_transport_kind(tag: u32): string {
17
+ switch (tag) {
18
+ case 1: return 'ok'
19
+ case 2: return 'dns_timeout'
20
+ case 3: return 'dns_error'
21
+ case 4: return 'destination_not_found'
22
+ case 5: return 'destination_unavailable'
23
+ case 6: return 'destination_ip_unroutable'
24
+ case 7: return 'connection_refused'
25
+ case 8: return 'connection_terminated'
26
+ case 9: return 'connection_timeout'
27
+ case 10: return 'connection_limit_reached'
28
+ case 11: return 'tls_certificate_error'
29
+ case 12: return 'tls_configuration_error'
30
+ case 13: return 'http_incomplete_response'
31
+ case 14: return 'http_response_header_section_too_large'
32
+ case 15: return 'http_response_body_too_large'
33
+ case 16: return 'http_response_timeout'
34
+ case 17: return 'http_response_status_invalid'
35
+ case 18: return 'http_upgrade_failed'
36
+ case 19: return 'http_protocol_error'
37
+ case 20: return 'http_request_cache_key_invalid'
38
+ case 21: return 'http_request_uri_invalid'
39
+ case 22: return 'internal_error'
40
+ case 23: return 'tls_alert_received'
41
+ case 24: return 'tls_protocol_error'
42
+ case 25: return 'h2_error'
43
+ default: return 'unknown'
44
+ }
45
+ }
46
+ function __pulse_transport_integer(value: i64): string { return value < 0 ? 'null' : value.toString() }
47
+ function __pulse_transport_capture(index: i32, status: i32, phase: string, ptr: usize = 0): void {
48
+ const d = new __PulseTransportDetail(); d.status = u32(status); d.phase = phase;
49
+ // Uninitialized means no host detail, even if stale/unknown mask bits exist.
50
+ if (ptr != 0 && load<u32>(ptr) != 0) {
51
+ const tag = load<u32>(ptr), mask = load<u32>(ptr + 4);
52
+ d.tag = i64(tag); d.mask = i64(mask); d.kind = __pulse_transport_kind(tag);
53
+ if (tag == 3) {
54
+ if ((mask & 2) != 0) d.dnsRcode = load<u16>(ptr + 8);
55
+ if ((mask & 4) != 0) d.dnsInfo = load<u16>(ptr + 10);
56
+ }
57
+ if (tag == 23 && (mask & 8) != 0) d.tlsAlert = load<u8>(ptr + 12);
58
+ if (tag == 25 && (mask & 16) != 0) { d.h2Frame = load<u8>(ptr + 13); d.h2Code = i64(load<u32>(ptr + 16)); }
59
+ }
60
+ if (index >= 0 && index < PULSE_FASTLY_EFFECT_COUNT) __pulse_transport_slots[index] = d;
61
+ // One bounded best-effort diagnostic through the configured existing log sink.
62
+ // Only fixed enum strings and unsigned integers enter this record (<512 bytes).
63
+ const text = '[pulse:error] {"event":"pulse.transport","provider":"fastly","phase":"' + d.phase
64
+ + '","status":' + d.status.toString() + ',"tag":' + __pulse_transport_integer(d.tag)
65
+ + ',"mask":' + __pulse_transport_integer(d.mask) + ',"kind":"' + d.kind
66
+ + '","dnsRcode":' + __pulse_transport_integer(d.dnsRcode) + ',"dnsInfo":' + __pulse_transport_integer(d.dnsInfo)
67
+ + ',"tlsAlert":' + __pulse_transport_integer(d.tlsAlert) + ',"h2Frame":' + __pulse_transport_integer(d.h2Frame)
68
+ + ',"h2Code":' + __pulse_transport_integer(d.h2Code) + '}\n';
69
+ __pulse_fastly_write_stdout(String.UTF8.encode(text, false));
70
+ }
71
+ function __pulse_transport_field(result: i32, key: string, value: i64): void {
72
+ host_value_object_set(result, __pulse_fastly_string_value(key), value < 0 ? host_value_null() : host_value_number(f64(value)));
73
+ }
74
+ function __pulse_transport_value(d: __PulseTransportDetail): i32 {
75
+ const result = host_value_object();
76
+ host_value_object_set(result, __pulse_fastly_string_value('provider'), __pulse_fastly_string_value('fastly'));
77
+ host_value_object_set(result, __pulse_fastly_string_value('phase'), __pulse_fastly_string_value(d.phase));
78
+ host_value_object_set(result, __pulse_fastly_string_value('kind'), __pulse_fastly_string_value(d.kind));
79
+ __pulse_transport_field(result, 'status', i64(d.status)); __pulse_transport_field(result, 'tag', d.tag);
80
+ __pulse_transport_field(result, 'mask', d.mask); __pulse_transport_field(result, 'dnsRcode', d.dnsRcode);
81
+ __pulse_transport_field(result, 'dnsInfo', d.dnsInfo); __pulse_transport_field(result, 'tlsAlert', d.tlsAlert);
82
+ __pulse_transport_field(result, 'h2Frame', d.h2Frame); __pulse_transport_field(result, 'h2Code', d.h2Code);
83
+ return result;
84
+ }
@@ -0,0 +1,90 @@
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
+ // These helpers are private to proven regions in an unlinked read-loop plan.
39
+ // Cache admissions use the original operations, so later boxed code sees the
40
+ // same cache and charges. Uncached numbers pay at each original evaluation;
41
+ // only their physical materialization is delayed until a region exit.
42
+ function numericRegionSource() {
43
+ return `
44
+ function __pulse_numeric_cacheable(value: f64): bool {
45
+ return value >= 0 && value < 65536 && Math.floor(value) == value && (value != 0 || 1.0 / value > 0)
46
+ }
47
+ function __pulse_numeric_charge_number(value: f64): f64 {
48
+ __pulse_memory_charge(0, 0)
49
+ if (__pulse_numeric_cacheable(value)) host_value_number(value)
50
+ else __pulse_memory_charge(1, ${policy.valueBytes})
51
+ return value
52
+ }
53
+ function __pulse_numeric_charge_boolean(value: bool): bool {
54
+ __pulse_memory_charge(0, 0)
55
+ host_value_boolean(value ? 1 : 0)
56
+ return value
57
+ }
58
+ function __pulse_numeric_truthy(value: f64): bool { return value != 0.0 && !isNaN(value) }
59
+ function __pulse_numeric_box(value: f64): i32 {
60
+ __pulse_memory_charge(0, 0)
61
+ if (__pulse_numeric_cacheable(value)) return host_value_number(value)
62
+ const item = new __PulseFastlyValue(); item.kind = PULSE_VALUE_NUMBER; item.number = value
63
+ // Already charged at the original expression, never a general put bypass.
64
+ __pulse_fastly_values.push(item)
65
+ return __pulse_fastly_values.length
66
+ }
67
+ `;
68
+ }
69
+
70
+ function instrument(source, plan) {
71
+ if (!plan.callables?.length && !hasBoundedReadLoop(plan)) return source;
72
+ function insert(needle, extra) {
73
+ if (source.split(needle).length !== 2) throw new Error('Native value budget instrumentation mismatch: ' + needle);
74
+ source = source.replace(needle, needle + extra);
75
+ }
76
+ // Text assignments include lazy fetch bodies and response bodies as well as
77
+ // strings. Ropes reserve their eventual flat size before materialization.
78
+ insert('\n set text(value: string) {', '\n __pulse_memory_charge(0, i64(value.length) * 2)');
79
+ insert('\nfunction __pulse_fastly_put(value: __PulseFastlyValue): i32 {',
80
+ ` __pulse_memory_charge(1, ${policy.valueBytes} + (value.textLeft === null ? 0 : i64(value.textLength) * 2));`);
81
+ const helpers = { keys: 'key', values: 'value', headers: 'header' };
82
+ // Skip quoted data and comments: authored text can contain source-like text.
83
+ const tokens = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/[^\n]*|\/\*[\s\S]*?\*\/)|\b([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.(keys|values|headers)\.push\(/g;
84
+ source = source.replace(tokens, (match, quoted, object, field) => quoted || `__pulse_memory_${helpers[field]}_push(${object}.${field}, `);
85
+ const code = source.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, '');
86
+ if (/\.(keys|values|headers)\.push\(/.test(code)) throw new Error('Unmetered Native container growth.');
87
+ return source + runtimeSource();
88
+ }
89
+
90
+ module.exports = { policy, hasBoundedReadLoop, runtimeSource, numericRegionSource, instrument };
@@ -0,0 +1,114 @@
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 (!['pulse.entities', 'pulse.mcp'].includes(application.contractId)
16
+ || !['pulse.package-native-application.v1', 'pulse.package-native-application.v2'].includes(application.version)
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|f64|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 === 'package_http_version') return `function ${symbol}(): i32 { return 1 }`;
28
+ if (name === 'value_json') return `function ${symbol}(${parameters}): i32 {
29
+ // A failed request read must reach the provider's terminal error response,
30
+ // without trapping JSON.parse<string> in the guest first.
31
+ if (${args} <= 0) return changetype<i32>('""')
32
+ const text = __pulse_fastly_json(${args}, 0)
33
+ if (String.UTF8.byteLength(text) > ${CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES}) {
34
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 172, -1); return changetype<i32>('null')
35
+ }
36
+ return changetype<i32>(text)
37
+ }`;
38
+ const guard = name === 'log' ? `if (${args.split(',')[0]} <= 0 || ${args.split(',')[0]} > ${Number(plan.logging.reporting.level)}) return; ` : '';
39
+ return `function ${symbol}(${parameters}): ${result} { ${guard}${result === 'void' ? '' : 'return '}host_${name}(${args}) }`;
40
+ }
41
+ );
42
+ if (source.includes('@external("pulse_host",')) throw new Error('Unbound package Native host import.');
43
+ const codecs = (plan.schemas?.registry?.schemas || []).map((_schema, index) => `
44
+ function __pulse_schema_decode_${index}(input: string): string { return changetype<string>(pulse_schema_decode(${index}, changetype<i32>(input))) }
45
+ function __pulse_schema_encode_${index}(input: string): string { return changetype<string>(pulse_schema_encode(${index}, changetype<i32>(input))) }`).join('\n');
46
+ return { ...application, source: source + codecs + `\nconst __pulse_application_plan_hash: string = ${JSON.stringify(plan.planHash)}\n` };
47
+ }
48
+
49
+ function runtimeSource() {
50
+ return `
51
+ const __pulse_package_failed = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
52
+ function __pulse_package_take_effect_failure(): bool {
53
+ const code = __pulse_fastly_last_error
54
+ // Deadlines, invalid lifecycle transitions and request/body failures remain
55
+ // provider-terminal. Only an admitted effect's operational failure is framed.
56
+ if (__pulse_fastly_error_stage == 260 || __pulse_fastly_error_stage == 261) return false
57
+ if (code != PULSE_ERROR_HOSTCALL && code != PULSE_ERROR_TRANSPORT
58
+ && code != PULSE_ERROR_JSON && code != PULSE_ERROR_SCHEMA && code != PULSE_ERROR_VALUE) return false
59
+ __pulse_fastly_last_error = 0; __pulse_fastly_error_stage = 0; __pulse_fastly_error_effect = -1
60
+ return true
61
+ }
62
+ function host_effect_begin(effectIndex: i32, payload: i32): void {
63
+ if (__pulse_fastly_last_error != 0) return
64
+ __pulse_package_effect_begin(effectIndex, payload)
65
+ if (__pulse_package_take_effect_failure()) unchecked(__pulse_package_failed[effectIndex] = 1)
66
+ }
67
+ `;
68
+ }
69
+
70
+ function instrument(source) {
71
+ const signature = 'function host_effect_begin(effectIndex: i32, payload: i32): void {';
72
+ if (!source.includes(signature)) throw new Error('Fastly package effect admission boundary drift.');
73
+ return source.replace(signature, 'function __pulse_package_effect_begin(effectIndex: i32, payload: i32): void {') + runtimeSource();
74
+ }
75
+
76
+ function driverLoop() {
77
+ return ` while (runStatus == 1 && __pulse_fastly_last_error == 0) {
78
+ for (let effectIndex = 0; effectIndex < PULSE_FASTLY_EFFECT_COUNT; effectIndex += 1) {
79
+ const invocation = unchecked(__pulse_invocation_tickets[effectIndex])
80
+ if (invocation == 0) continue
81
+ let failed = unchecked(__pulse_package_failed[effectIndex]) != 0
82
+ unchecked(__pulse_package_failed[effectIndex] = 0)
83
+ let result = failed ? 0 : __pulse_fastly_resolve_effect(effectIndex)
84
+ if (__pulse_package_take_effect_failure()) { failed = true; result = 0 }
85
+ if (__pulse_fastly_last_error != 0) { __pulse_fastly_jwt_send_error(); return }
86
+ if (!failed && result <= 0) { __pulse_fastly_fail(PULSE_ERROR_STATE, 101, effectIndex); __pulse_fastly_jwt_send_error(); return }
87
+ if (__pulse_invocation_settle(effectIndex, invocation, result) != 1 || __pulse_fastly_last_error != 0) {
88
+ __pulse_fastly_fail(PULSE_ERROR_STATE, 101, effectIndex); __pulse_fastly_jwt_send_error(); return
89
+ }
90
+ }
91
+ runStatus = pulse_resume()
92
+ }`;
93
+ }
94
+
95
+ function terminalFailure(source, plan) {
96
+ if (plan.packages?.application?.terminalFailure !== 'package-http-v1') return source;
97
+ const helper = `
98
+ function __pulse_package_http_failure_send(kind: i32): void {
99
+ __request_error_response = true;
100
+ __pulse_fastly_last_error = 0; __pulse_fastly_error_stage = 0; __pulse_fastly_error_effect = -1;
101
+ const value = JSON.parse<JSON.Value>(changetype<string>(pulse_package_http_failure(kind)))
102
+ const response = host_response_custom(__pulse_package_value(value))
103
+ __pulse_fastly_send_result(response)
104
+ }
105
+ `;
106
+ const bodyMarker = ' output.status = __pulse_fastly_error_stage == 21 ? 413 : 400;';
107
+ if (!source.includes(bodyMarker)) throw new Error('Package HTTP body completion boundary drift');
108
+ source = source.replace(bodyMarker, ' const kind = __pulse_fastly_error_stage == 21 ? 2 : 3; __pulse_package_http_failure_send(kind); return true;\n' + bodyMarker);
109
+ const timeoutMarker = 'function __request_send_timeout(): void {';
110
+ if (!source.includes(timeoutMarker)) throw new Error('Package HTTP deadline completion boundary drift');
111
+ source = source.replace(timeoutMarker, timeoutMarker + '\n __pulse_package_http_failure_send(1); return;');
112
+ return source + helper;
113
+ }
114
+ module.exports = { enabled, portable, instrument, driverLoop, terminalFailure };