@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,50 @@
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
+ const __request_clock_result = new StaticArray<i64>(1);
9
+ function __request_clock(): i64 {
10
+ const out = __request_clock_result;
11
+ if (wasi_snapshot_preview1_clock_time_get(1, 1, changetype<usize>(out)) != 0 || out[0] < __request_last_clock) {
12
+ __request_expired = true;
13
+ return -1;
14
+ }
15
+ __request_last_clock = out[0];
16
+ return out[0];
17
+ }
18
+ function __request_remaining(): i32 {
19
+ if (__request_expired) return 0;
20
+ const now = __request_clock();
21
+ if (now < 0 || now >= __request_deadline) { __request_expired = true; return 0; }
22
+ return i32((__request_deadline - now + 999999) / 1000000);
23
+ }
24
+ function __request_check(index: i32 = -1): bool {
25
+ if (__request_error_response) return true;
26
+ if (__request_remaining() > 0) return true;
27
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 260, index);
28
+ return false;
29
+ }
30
+ function __request_wait(handle: i32, index: i32): bool {
31
+ const remaining = __request_remaining();
32
+ if (remaining <= 0) return __request_check(index);
33
+ const handles = new StaticArray<i32>(1), ready = new StaticArray<i32>(1);
34
+ handles[0] = handle;
35
+ const status = __request_select(changetype<usize>(handles), 1, remaining, changetype<usize>(ready));
36
+ if (status != 0) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 261, index); return false; }
37
+ if (ready[0] == -1) __request_expired = true;
38
+ return __request_check(index);
39
+ }
40
+ function __request_send_timeout(): void {
41
+ // A timeout says nothing about remote acceptance of dispatched writes.
42
+ __request_error_response = true;
43
+ __pulse_fastly_last_error = 0;
44
+ const options = host_value_object();
45
+ host_value_object_set(options, __pulse_fastly_string_value('status'), host_value_number(504.0));
46
+ const response = host_response_text(__pulse_fastly_string_value('Gateway Timeout'), options);
47
+ __pulse_fastly_last_error = 0;
48
+ const status = __pulse_fastly_send_result(response);
49
+ if (status != 0) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 262, -1);
50
+ }
@@ -0,0 +1,47 @@
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 == 1 && __pulse_fetch_has_deadlines()) && kind != 12 && kind != 13 && kind != 14 && kind != 21 && ${plan.effects.filter(e=>e.kind.startsWith('kv.')&&['kv.getVersioned','kv.insertIfAbsent','kv.compareAndSwap','kv.list','kv.delete'].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
+ replace('function __pulse_fastly_jwt_send_error(): void {', 'function __pulse_fastly_jwt_send_error(): void { if (__request_expired || !__request_check()) { __request_send_timeout(); return; }');
37
+ source = source.replaceAll('runStatus = pulse_resume()', 'if (!__request_check()) { __request_send_timeout(); return; }\n runStatus = pulse_resume()');
38
+ replace(' const result = pulse_result_handle()', ' if (!__request_check()) { __request_send_timeout(); return; }\n const result = pulse_result_handle()');
39
+ 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);');
40
+ 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);');
41
+ 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;');
42
+ replace('function __pulse_fetch_request_check(index: i32): bool { return true }', 'function __pulse_fetch_request_check(index: i32): bool { return __request_check(index) }');
43
+ replace('function __pulse_fetch_request_remaining(): i32 { return 2147483647 }', 'function __pulse_fetch_request_remaining(): i32 { return __request_remaining() }');
44
+ if (plan.callables?.length || plan.binaryResources || plan.numericWorkspaces?.length) replace('function __pulse_resource_request_check(): bool { return true }', 'function __pulse_resource_request_check(): bool { return __request_check() }');
45
+ return source + '\n' + fs.readFileSync(path.join(__dirname,'request-budget.as.ts'),'utf8');
46
+ }
47
+ 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
+ }
@@ -0,0 +1,228 @@
1
+ // Provider-owned lifecycle and host ABI. Protocol algorithms are S3-owned.
2
+ @external('fastly_async_io', 'select') declare function __s3_select(handles: usize, count: i32, timeout: i32, done: usize): i32
3
+ @external('fastly_http_req', 'pending_req_poll_v2') declare function __s3_poll(pending: i32, detail: usize, done: usize, response: usize, body: usize): i32
4
+ @external('fastly_http_req', 'cache_override_set') declare function __s3_cache(request: i32, tag: i32, ttl: i32, swr: i32): i32
5
+ @external('fastly_http_req', 'auto_decompress_response_set') declare function __s3_decompress(request: i32, encodings: i32): i32
6
+ @external('fastly_http_req', 'close') declare function __s3_close_req(request: i32): i32
7
+ @external('fastly_http_resp', 'close') declare function __s3_close_resp(response: i32): i32
8
+ @external('fastly_http_body', 'close') declare function __s3_close_body(body: i32): i32
9
+ @external('fastly_http_resp', 'header_names_get') declare function __s3_header_names(response: i32, buffer: usize, size: i32, cursor: i32, end: usize, written: usize): i32
10
+ @external('fastly_http_resp', 'header_values_get') declare function __s3_header_values(response: i32, name: usize, length: i32, buffer: usize, size: i32, cursor: i32, end: usize, written: usize): i32
11
+
12
+ class __PulseS3Binding {
13
+ method: string = ''; range: string = ''; ifNoneMatch: string = '';
14
+ contentType: string = ''; endpoint: string = ''; bucket: string = ''; region: string = ''; backend: string = '';
15
+ id: string = ''; secret: string = ''; token: string = ''; max: i32 = 32768; timeout: i32 = 10000;
16
+ }
17
+ @lazy const __s3_dispatched = new StaticArray<bool>(PULSE_FASTLY_EFFECT_COUNT)
18
+ @lazy const __s3_sent_bytes = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
19
+ @lazy const __s3_digests = new Array<string>(PULSE_FASTLY_EFFECT_COUNT).fill('')
20
+ @lazy const __s3_deadlines = new StaticArray<i64>(PULSE_FASTLY_EFFECT_COUNT)
21
+ function __s3_clock(id: i32): i64 {
22
+ const out = new StaticArray<i64>(1);
23
+ return wasi_snapshot_preview1_clock_time_get(id, 1000000, changetype<usize>(out)) == 0 ? out[0] : -1;
24
+ }
25
+ function __s3_remaining(index: i32): i32 {
26
+ const now = __s3_clock(1);
27
+ if (now < 0) return 0;
28
+ const ns = __s3_deadlines[index] - now;
29
+ return ns <= 0 ? 0 : i32((ns + 999999) / 1000000);
30
+ }
31
+ function __s3_set(object: i32, name: string, value: i32): void { host_value_object_set(object, __pulse_fastly_string_value(name), value) }
32
+ function __s3_result(status: string, reason: string = '', http: i32 = 0): i32 {
33
+ const result = host_value_object(); __s3_set(result, 'status', __pulse_fastly_string_value(status));
34
+ if (reason.length) __s3_set(result, 'reason', __pulse_fastly_string_value(reason));
35
+ if (http > 0) { const value = new __PulseFastlyValue(); value.kind = PULSE_VALUE_NUMBER; value.number = f64(http); __s3_set(result, 'httpStatus', __pulse_fastly_put(value)); }
36
+ return result;
37
+ }
38
+ function __s3_failure(index: i32, reason: string, http: i32 = 0): i32 {
39
+ if (__pulse_fastly_effect_kind(index) == 21) return __s3_body_failure(reason);
40
+ return __s3_result(__pulse_fastly_effect_kind(index) == 14 ? (__s3_dispatched[index] ? 'unknown' : 'not-stored') : 'failed', reason, http);
41
+ }
42
+ function __s3_fail(index: i32, reason: string): void { __pulse_fastly_ready_effect(index, __s3_failure(index, reason)) }
43
+ function __s3_secret(store: i32, name: string): string | null {
44
+ const key = __pulse_s3_bytes(name), out = new StaticArray<i32>(1), size = new StaticArray<i32>(1), bytes = new Uint8Array(4096);
45
+ if (fastly_secret_store_get(store, key.dataStart, key.length, changetype<usize>(out)) != 0) return null;
46
+ const status = fastly_secret_store_plaintext(out[0], bytes.dataStart, bytes.length, changetype<usize>(size));
47
+ let result: string | null = null;
48
+ if (status == 0 && size[0] > 0 && size[0] <= bytes.length) {
49
+ const raw = bytes.subarray(0, size[0]);
50
+ if (__pulse_s3_utf8_valid(raw)) result = String.UTF8.decodeUnsafe(raw.dataStart, raw.length, false);
51
+ }
52
+ bytes.fill(0);
53
+ if (result !== null) __pulse_fastly_remember_secret(result);
54
+ return result;
55
+ }
56
+ function __s3_header(request: i32, name: string, value: string): bool {
57
+ const n = __pulse_s3_bytes(name), v = __pulse_s3_bytes(value);
58
+ return fastly_http_req_header_insert(request, n.dataStart, n.length, v.dataStart, v.length) == 0;
59
+ }
60
+ function __pulse_fastly_s3_begin(index: i32, outer: __PulseFastlyValue): void {
61
+ __pulse_transport_slots[index] = null;
62
+ __s3_dispatched[index] = false; __s3_sent_bytes[index] = 0; __s3_digests[index] = '';
63
+ const put = __pulse_fastly_effect_kind(index) == 14;
64
+ const payload = __pulse_fastly_value(__pulse_fastly_payload_field(outer, 'invocation'));
65
+ const input = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'key'));
66
+ if (input.kind != PULSE_VALUE_STRING) { __s3_fail(index, 'invalid-key'); return; }
67
+ const encoded = __pulse_s3_key(input.text);
68
+ if (encoded === null) { __s3_fail(index, 'invalid-key'); return; }
69
+ // Package effect payloads and signed paths must not enter provider logs.
70
+ __pulse_fastly_remember_secret(input.text);
71
+ const binding = __pulse_fastly_s3_binding(index), start = __s3_clock(1);
72
+ if (start < 0) { __s3_fail(index, 'configuration'); return; }
73
+ __s3_deadlines[index] = start + i64(binding.timeout) * 1000000;
74
+ let data = new Uint8Array(0);
75
+ if (put) {
76
+ const text = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'text'));
77
+ if (text.kind != PULSE_VALUE_STRING) { __s3_fail(index, 'invalid-text'); return; }
78
+ if (text.text.length > binding.max) { __s3_fail(index, 'too-large'); return; }
79
+ if (!__pulse_s3_scalar(text.text)) { __s3_fail(index, 'invalid-text'); return; }
80
+ data = __pulse_s3_bytes(text.text);
81
+ if (data.length > binding.max) { __s3_fail(index, 'too-large'); return; }
82
+ __pulse_fastly_remember_secret(text.text);
83
+ }
84
+ const storeName = __pulse_s3_bytes(__pulse_fastly_secret_store(index)), store = new StaticArray<i32>(1);
85
+ if (fastly_secret_store_open(storeName.dataStart, storeName.length, changetype<usize>(store)) != 0) { __s3_fail(index, 'credentials'); return; }
86
+ const id = __s3_secret(store[0], binding.id), secret = __s3_secret(store[0], binding.secret);
87
+ const token = binding.token.length ? __s3_secret(store[0], binding.token) : '';
88
+ if (__s3_remaining(index) == 0) { __s3_fail(index, 'timeout'); return; }
89
+ if (id === null || secret === null || token === null || !__pulse_s3_credentials(id, secret, token)) { __s3_fail(index, 'credentials'); return; }
90
+ const method = binding.method.length ? binding.method : put ? 'PUT' : __pulse_fastly_effect_kind(index) == 12 ? 'HEAD' : 'GET', path = '/' + binding.bucket + '/' + encoded;
91
+ const now = __s3_clock(0);
92
+ if (now < 0 || !__pulse_s3_date(now / 1000000000).length) { __s3_fail(index, 'configuration'); return; }
93
+ const signature = __pulse_s3_sign(method, path, binding.endpoint.slice(8), binding.region, id, secret, token, now / 1000000000, data, binding.contentType, binding.range, binding.ifNoneMatch);
94
+ __s3_sent_bytes[index] = data.length; __s3_digests[index] = signature.digest;
95
+ __pulse_fastly_remember_secret(signature.digest);
96
+ __pulse_fastly_remember_secret(signature.authorization); __pulse_fastly_remember_secret(binding.endpoint + path);
97
+ if (__s3_remaining(index) == 0) { __s3_fail(index, 'timeout'); return; }
98
+ const request = new StaticArray<i32>(1), body = new StaticArray<i32>(1), pending = new StaticArray<i32>(1);
99
+ if (fastly_http_req_new(changetype<usize>(request)) != 0) { __s3_fail(index, 'transport'); return; }
100
+ const req = request[0], url = __pulse_s3_bytes(binding.endpoint + path), verb = __pulse_s3_bytes(method), backend = __pulse_s3_bytes(binding.backend);
101
+ let ok = fastly_http_req_uri_set(req, url.dataStart, url.length) == 0 && fastly_http_req_method_set(req, verb.dataStart, verb.length) == 0
102
+ && __s3_cache(req, 1, 0, 0) == 0 && __s3_decompress(req, 0) == 0
103
+ && __s3_header(req, 'host', binding.endpoint.slice(8)) && __s3_header(req, 'x-amz-date', signature.date)
104
+ && __s3_header(req, 'x-amz-content-sha256', signature.digest) && __s3_header(req, 'authorization', signature.authorization)
105
+ && __s3_header(req, 'accept-encoding', 'identity');
106
+ if (ok && binding.range.length) ok = __s3_header(req, 'range', binding.range);
107
+ if (ok && binding.ifNoneMatch.length) ok = __s3_header(req, 'if-none-match', binding.ifNoneMatch);
108
+ if (ok && put) ok = __s3_header(req, 'content-type', binding.contentType) && __s3_header(req, 'content-length', data.length.toString());
109
+ if (ok && token.length) ok = __s3_header(req, 'x-amz-security-token', token);
110
+ if (!ok || fastly_http_body_new(changetype<usize>(body)) != 0) { __s3_close_req(req); __s3_fail(index, 'transport'); return; }
111
+ let offset = 0; const written = new StaticArray<i32>(1);
112
+ while (offset < data.length) {
113
+ if (__s3_remaining(index) == 0) { __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'timeout'); return; }
114
+ if (fastly_http_body_write(body[0], data.dataStart + offset, data.length - offset, 0, changetype<usize>(written)) != 0 || written[0] <= 0 || written[0] > data.length - offset) {
115
+ __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'transport'); return;
116
+ }
117
+ offset += written[0];
118
+ }
119
+ if (__s3_remaining(index) == 0) { __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'timeout'); return; }
120
+ __s3_dispatched[index] = true;
121
+ const sent = fastly_http_req_send_async(req, body[0], backend.dataStart, backend.length, changetype<usize>(pending));
122
+ if (sent != 0) {
123
+ __pulse_transport_capture(index, sent, 'send');
124
+ __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'transport'); return;
125
+ }
126
+ // send_async consumes request/body. Pending handles have no cancel hostcall.
127
+ __pulse_fastly_pending[index] = pending[0]; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_ASYNC;
128
+ }
129
+ function __s3_wait_ready(index: i32, handle: i32): i32 {
130
+ const timeout = __s3_remaining(index); if (timeout == 0) return 0;
131
+ const handles = new StaticArray<i32>(1), done = new StaticArray<i32>(1); handles[0] = handle;
132
+ if (__s3_select(changetype<usize>(handles), 1, timeout, changetype<usize>(done)) != 0) return -1;
133
+ return done[0] == -1 || __s3_remaining(index) == 0 ? 0 : 1;
134
+ }
135
+ function __s3_list(buffer: Uint8Array, size: i32): Array<string> | null {
136
+ if (size < 0 || size > buffer.length || (size > 0 && buffer[size - 1] != 0)) return null;
137
+ const result = new Array<string>(); let start = 0;
138
+ for (let i = 0; i < size; i++) if (buffer[i] == 0) {
139
+ const bytes = buffer.subarray(start, i); if (!__pulse_s3_utf8_valid(bytes)) return null;
140
+ result.push(String.UTF8.decodeUnsafe(bytes.dataStart, bytes.length, false)); start = i + 1;
141
+ }
142
+ return result;
143
+ }
144
+ function __s3_read_response(index: i32, response: i32, body: i32, status: i32): i32 {
145
+ if (__pulse_fastly_effect_kind(index) == 21) return __s3_body_response(index, response, body, status);
146
+ const put = __pulse_fastly_effect_kind(index) == 14;
147
+ const rejected = put && status >= 400 && status <= 499 && status != 408;
148
+ if (!put && status == 404) return __s3_result('not-found');
149
+ if (status != 200 && !rejected) return __s3_failure(index, __pulse_s3_status_reason(status), status);
150
+ const buffer = new Uint8Array(16384), end = new StaticArray<i64>(1), size = new StaticArray<i32>(1);
151
+ if (__s3_header_names(response, buffer.dataStart, buffer.length, 0, changetype<usize>(end), changetype<usize>(size)) != 0 || end[0] >= 0) return __s3_failure(index, 'protocol', status);
152
+ const names = __s3_list(buffer, size[0]); if (names === null) return __s3_failure(index, 'protocol', status);
153
+ let total = 0, length: f64 = -1; const seen = new Set<string>();
154
+ const result = __s3_result(put ? 'stored' : 'found');
155
+ for (let i = 0; i < names.length; i++) {
156
+ const rawName = names[i], name = rawName.toLowerCase(), bytes = __pulse_s3_bytes(rawName);
157
+ 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_failure(index, 'protocol', status);
158
+ const values = __s3_list(buffer, size[0]); if (values === null) return __s3_failure(index, 'protocol', status);
159
+ const selected = name == 'content-length' || name == 'content-type' || name == 'etag' || name == 'content-encoding';
160
+ if (selected && (seen.has(name) || values.length != 1)) return __s3_failure(index, 'protocol', status);
161
+ seen.add(name);
162
+ for (let v = 0; v < values.length; v++) {
163
+ const value = values[v], count = __pulse_s3_bytes(value).length;
164
+ total += bytes.length + count + 4; if (total > 16384) return __s3_failure(index, 'protocol', status);
165
+ if (!selected) continue;
166
+ if (count > 1024 || !__pulse_s3_scalar(value, true)) return __s3_failure(index, 'protocol', status);
167
+ if (name == 'content-encoding' && value.toLowerCase() != 'identity') return __s3_failure(index, 'protocol', status);
168
+ if (name == 'content-length') {
169
+ if (!value.length || (value.length > 1 && value.charCodeAt(0) == 48)) return __s3_failure(index, 'protocol', status);
170
+ length = 0;
171
+ for (let c = 0; c < value.length; c++) { const digit = value.charCodeAt(c) - 48; if (digit < 0 || digit > 9) return __s3_failure(index, 'protocol', status); length = length * 10 + digit; if (length > 9007199254740991) return __s3_failure(index, 'protocol', status); }
172
+ }
173
+ if (name == 'etag') { if (!value.length) return __s3_failure(index, 'protocol', status); __s3_set(result, 'etag', __pulse_fastly_string_value(value)); }
174
+ if (name == 'content-type' && !put) __s3_set(result, 'contentType', __pulse_fastly_string_value(value));
175
+ }
176
+ }
177
+ if (__s3_remaining(index) == 0) return __s3_failure(index, 'timeout', status);
178
+ const head = __pulse_fastly_effect_kind(index) == 12, binding = __pulse_fastly_s3_binding(index);
179
+ if (head && length < 0) return __s3_failure(index, 'protocol', status);
180
+ if (!head) {
181
+ const maximum = put && !rejected ? 0 : binding.max;
182
+ if (length > maximum) return __s3_failure(index, put ? 'protocol' : 'too-large', status);
183
+ const data = new Uint8Array(maximum + 1); let count = 0;
184
+ while (true) {
185
+ const ready = __s3_wait_ready(index, body);
186
+ if (ready <= 0) return __s3_failure(index, ready == 0 ? 'timeout' : 'transport', status);
187
+ if (fastly_http_body_read(body, data.dataStart + count, data.length - count, changetype<usize>(size)) != 0) return __s3_failure(index, 'protocol', status);
188
+ if (size[0] < 0 || size[0] > data.length - count) return __s3_failure(index, 'protocol', status);
189
+ if (size[0] == 0) break;
190
+ count += size[0]; if (count > maximum) return __s3_failure(index, put ? 'protocol' : 'too-large', status);
191
+ }
192
+ if (length >= 0 && length != count) return __s3_failure(index, 'protocol', status);
193
+ if (put) {
194
+ if (__s3_remaining(index) == 0) return __s3_failure(index, 'timeout', status);
195
+ if (rejected) return __s3_result('not-stored', status == 401 || status == 403 ? 'not-authorized' : status == 429 ? 'throttled' : 'rejected', status);
196
+ const sent = new __PulseFastlyValue(); sent.kind = PULSE_VALUE_NUMBER; sent.number = f64(__s3_sent_bytes[index]);
197
+ __s3_set(result, 'byteLength', __pulse_fastly_put(sent));
198
+ __s3_set(result, 'sha256', __pulse_fastly_string_value(__s3_digests[index]));
199
+ return result;
200
+ }
201
+ length = f64(count); const raw = data.subarray(0, count), digest = __pulse_s3_hex(__pulse_s3_sha(raw));
202
+ if (!__pulse_s3_utf8_valid(raw)) return __s3_failure(index, 'invalid-utf8', status);
203
+ __s3_set(result, 'text', __pulse_fastly_string_value(String.UTF8.decodeUnsafe(raw.dataStart, raw.length, false)));
204
+ __s3_set(result, 'sha256', __pulse_fastly_string_value(digest));
205
+ }
206
+ if (__s3_remaining(index) == 0) return __s3_failure(index, 'timeout', status);
207
+ const sizeValue = new __PulseFastlyValue(); sizeValue.kind = PULSE_VALUE_NUMBER; sizeValue.number = length;
208
+ __s3_set(result, 'byteLength', __pulse_fastly_put(sizeValue)); return result;
209
+ }
210
+ function __pulse_fastly_s3_wait(index: i32): i32 {
211
+ if (__pulse_fastly_pending_mode[index] == PULSE_FASTLY_PENDING_READY) return __pulse_fastly_wait_ready(index);
212
+ const pending = __pulse_fastly_pending[index], detail = new StaticArray<i32>(__PULSE_SEND_ERROR_DETAIL_WORDS), done = new StaticArray<i32>(1);
213
+ const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1), code = new StaticArray<i32>(1);
214
+ __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_NONE; __pulse_fastly_pending[index] = 0;
215
+ while (true) {
216
+ const ready = __s3_wait_ready(index, pending);
217
+ if (ready <= 0) return __s3_failure(index, ready == 0 ? 'timeout' : 'transport');
218
+ done[0] = 0; __pulse_send_error_detail_reset(changetype<usize>(detail));
219
+ const status = __s3_poll(pending, changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body));
220
+ if (status != 0) { __pulse_transport_capture(index, status, 'poll', changetype<usize>(detail)); return __s3_failure(index, 'transport'); }
221
+ if (done[0] != 0) break;
222
+ }
223
+ let result: i32;
224
+ if (fastly_http_resp_status_get(response[0], changetype<usize>(code)) != 0) result = __s3_failure(index, 'protocol');
225
+ else result = __s3_remaining(index) == 0 ? __s3_failure(index, 'timeout', code[0]) : __s3_read_response(index, response[0], body[0], code[0]);
226
+ if (!__pulse_fastly_value(result).s3Body) __s3_close_body(body[0]);
227
+ __s3_close_resp(response[0]); return result;
228
+ }
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { pulseS3AssemblyScriptSource } = require('@pulse-compute/s3/pulsewasm-native');
5
+ const S3_IMPORTS = Object.freeze([
6
+ 'fastly_async_io:select', 'fastly_http_req:pending_req_poll_v2', 'fastly_http_req:cache_override_set',
7
+ 'fastly_http_req:auto_decompress_response_set', 'fastly_http_req:close', 'fastly_http_resp:close', 'fastly_http_body:close',
8
+ 'fastly_http_resp:header_names_get', 'fastly_http_resp:header_values_get', 'fastly_http_resp:status_get',
9
+ 'fastly_http_req:new', 'fastly_http_req:method_set', 'fastly_http_req:uri_set', 'fastly_http_req:header_insert', 'fastly_http_req:send_async',
10
+ 'fastly_http_body:new', 'fastly_http_body:read', 'fastly_http_body:write', 'fastly_secret_store:open', 'fastly_secret_store:get', 'fastly_secret_store:plaintext',
11
+ 'wasi_snapshot_preview1:clock_time_get'
12
+ ]);
13
+ function s3NativeSource(plan, bindings) {
14
+ if (!bindings.s3.length) return '';
15
+ const contribution = pulseS3AssemblyScriptSource();
16
+ for (const name of contribution.cryptoRequirements) {
17
+ const selected = plan.crypto && plan.crypto.algorithms.find((entry) => entry.algorithm === name);
18
+ if (!selected || selected.realization !== 'guest-source:pulse-hmac-as' || selected.implementation !== 'pulse-hmac-as.v1'
19
+ || selected.kind !== 'guest-source' || selected.targetImplemented !== true || selected.automaticFallback !== false) throw new TypeError('Fastly S3 requires its exact selected Native Crypto algorithms.');
20
+ }
21
+ const lines = ['function __pulse_fastly_s3_binding(index: i32): __PulseS3Binding {', 'const value = new __PulseS3Binding();', 'switch (index) {'];
22
+ for (const binding of bindings.s3) {
23
+ lines.push(`case ${binding.index}:`);
24
+ for (const [field, input] of Object.entries({ method: 'method', range: 'range', ifNoneMatch: 'ifNoneMatch', contentType: 'contentType', endpoint: 'endpoint', bucket: 'bucket', region: 'region', backend: 'backend', id: 'accessKeyIdSecret', secret: 'secretAccessKeySecret', token: 'sessionTokenSecret' })) lines.push(`value.${field} = ${JSON.stringify(binding[input] || '')};`);
25
+ lines.push(`value.max = ${binding.maxTextBytes}; value.timeout = ${binding.timeoutMs}; break;`);
26
+ }
27
+ lines.push('default: unreachable();', '}', 'return value;', '}');
28
+ return [contribution.source, fs.readFileSync(path.join(__dirname, 's3-native.as.ts'), 'utf8'), fs.readFileSync(path.join(__dirname, 's3-body.as.ts'), 'utf8'), lines.join('\n')].join('\n');
29
+ }
30
+ module.exports = { S3_IMPORTS, s3NativeSource };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const { schemaAdmission } = require('@pulse-compute/wasm-runtime-core-as/compiler');
4
+
5
+ function fastlyJsonValueAdmissionSource() {
6
+ return `
7
+ class __PulseFastlyAdmissionFrame {
8
+ index: i32 = 0
9
+ constructor(public handle: i32) {}
10
+ }
11
+ function __pulse_fastly_admission_enter(handle: i32, budget: __PulseJsonAdmissionBudget,
12
+ stack: Array<__PulseFastlyAdmissionFrame>, active: Set<i32>): bool {
13
+ if (!__pulse_fastly_has_value(handle)) return budget.fail(12)
14
+ const value = __pulse_fastly_value(handle)
15
+ const array = value.kind == PULSE_VALUE_ARRAY
16
+ const object = value.kind == PULSE_VALUE_OBJECT
17
+ if (!budget.node(stack.length + (array || object ? 1 : 0))) return false
18
+ if (value.kind == PULSE_VALUE_NULL) return budget.addBytes(4)
19
+ if (value.kind == PULSE_VALUE_BOOLEAN) return budget.addBytes(value.boolean != 0 ? 4 : 5)
20
+ if (value.kind == PULSE_VALUE_NUMBER) return isFinite(value.number) ? budget.addBytes(24) : budget.fail(11)
21
+ if (value.kind == PULSE_VALUE_STRING) return budget.stringBytes(value.text, false)
22
+ if (!array && !object) return budget.fail(12)
23
+ if (active.has(handle)) return budget.fail(13)
24
+ if (array && value.values.length > budget.limits.maxArrayItems) return budget.fail(6)
25
+ if (object && value.keys.length > budget.limits.maxObjectMembers) return budget.fail(5)
26
+ if (object && value.keys.length != value.values.length) return budget.fail(12)
27
+ if (!budget.addBytes(2)) return false
28
+ active.add(handle)
29
+ stack.push(new __PulseFastlyAdmissionFrame(handle))
30
+ return true
31
+ }
32
+ function __pulse_fastly_admit_bounded_value(handle: i32, limits: __PulseJsonAdmissionLimits): __PulseJsonAdmissionBudget {
33
+ const budget = new __PulseJsonAdmissionBudget(limits)
34
+ const stack = new Array<__PulseFastlyAdmissionFrame>()
35
+ const active = new Set<i32>()
36
+ if (!__pulse_fastly_admission_enter(handle, budget, stack, active)) return budget
37
+ while (stack.length > 0) {
38
+ const frame = stack[stack.length - 1]
39
+ const value = __pulse_fastly_value(frame.handle)
40
+ if (frame.index == value.values.length) { active.delete(frame.handle); stack.pop(); continue }
41
+ const index = frame.index++
42
+ if (index > 0 && !budget.addBytes(1)) return budget
43
+ if (value.kind == PULSE_VALUE_OBJECT) {
44
+ if (!budget.stringBytes(value.keys[index], true) || !budget.addBytes(1)) return budget
45
+ }
46
+ if (!__pulse_fastly_admission_enter(value.values[index], budget, stack, active)) return budget
47
+ }
48
+ return budget
49
+ }
50
+ `;
51
+ }
52
+
53
+ // Standalone composition used by focused admission fixtures.
54
+ function generateFastlyJsonAdmission(limits, parserMaxDepth = 64) {
55
+ const generated = schemaAdmission.generateJsonAdmission(limits);
56
+ if (!Number.isInteger(parserMaxDepth) || parserMaxDepth < 1 || parserMaxDepth > 0x7fffffff
57
+ || generated.identity.limits.maxDepth > parserMaxDepth) {
58
+ throw new TypeError('JSON admission depth exceeds the selected Fastly parser capacity.');
59
+ }
60
+ return Object.freeze({ identity: generated.identity, source: `${generated.source}
61
+ ${fastlyJsonValueAdmissionSource()}
62
+ function __pulse_fastly_admit_value(handle: i32): __PulseJsonAdmissionBudget {
63
+ return __pulse_fastly_admit_bounded_value(handle, __pulse_json_limits())
64
+ }
65
+ function __pulse_fastly_admitted_json(text: string, duplicateMode: i32 = 0): i32 {
66
+ const admission = __pulse_json_admit(text, duplicateMode)
67
+ if (admission.failure != 0) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 57, -1); return 0 }
68
+ return __pulse_fastly_parse_json(text)
69
+ }
70
+ function __pulse_fastly_admitted_json_text(handle: i32): string {
71
+ const admission = __pulse_fastly_admit_value(handle)
72
+ if (admission.failure != 0) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 57, -1); return '' }
73
+ const text = __pulse_fastly_json(handle, 0)
74
+ if (String.UTF8.byteLength(text) > admission.limits.maxTextBytes) {
75
+ __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 57, -1)
76
+ return ''
77
+ }
78
+ return text
79
+ }
80
+ ` });
81
+ }
82
+
83
+ module.exports = { generateFastlyJsonAdmission, fastlyJsonValueAdmissionSource };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const { schemaHasNestedJson, schemaHasScalarRecord } = require('@pulse-compute/wasm-contracts/schema-json/registry');
4
+ const { fastlyJsonValueAdmissionSource } = require('./schema-admission.js');
5
+
6
+ function hasJsonAdmission(plan) { return (plan.schemas?.registry?.schemas || []).some(schema => schema.jsonLimits); }
7
+ function tracksJsonDuplicates(plan) {
8
+ return (plan.schemas?.registry?.schemas || []).some(schema => schemaHasNestedJson(schema.root) || schemaHasScalarRecord(schema.root));
9
+ }
10
+ function schemaParserDepth(plan, baseline = 64) {
11
+ return Math.max(baseline, ...(plan.schemas?.registry?.schemas || []).map(schema => schema.jsonLimits?.maxDepth || 0));
12
+ }
13
+
14
+ function nestedJsonProjectionLines(node) {
15
+ return [
16
+ ...(node.kind === 'json-object' ? [' if (input.kind != PULSE_VALUE_OBJECT) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 52, -1); return 0 }'] : []),
17
+ ' return __pulse_fastly_copy_json(valueHandle, checkDuplicates)'
18
+ ];
19
+ }
20
+
21
+ function schemaFetchJsonSource() {
22
+ return `
23
+ function __pulse_fastly_fetch_json_headers(input: __PulseFastlyValue): __PulseFastlyValue {
24
+ for (let index = 0; index < input.keys.length; index++) if (input.keys[index].toLowerCase() == "content-type") return input
25
+ const output = new __PulseFastlyValue()
26
+ output.kind = PULSE_VALUE_OBJECT
27
+ for (let index = 0; index < input.keys.length; index++) {
28
+ output.keys.push(input.keys[index]); output.values.push(input.values[index])
29
+ }
30
+ output.keys.push("content-type")
31
+ output.values.push(__pulse_fastly_string_value("application/json; charset=utf-8"))
32
+ return output
33
+ }
34
+ function __pulse_fastly_fetch_json_body(init: __PulseFastlyValue, valueHandle: i32): string {
35
+ const index = __pulse_fastly_find(init, "schema")
36
+ let value = valueHandle
37
+ if (index >= 0) {
38
+ const schema = __pulse_fastly_value(init.values[index])
39
+ if (schema.kind != PULSE_VALUE_STRING || schema.text.length == 0) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 55, -1); return '' }
40
+ value = __pulse_fastly_schema_apply(schema.text, valueHandle, true)
41
+ if (value <= 0 || __pulse_fastly_last_error != 0) return ''
42
+ }
43
+ return __pulse_fastly_json(value, 0)
44
+ }
45
+ `;
46
+ }
47
+
48
+ function schemaJsonAdmissionSource(plan) {
49
+ const schemas = plan.schemas?.registry?.schemas || [];
50
+ if (!hasJsonAdmission(plan)) return '';
51
+ const nested = schemas.some(schema => schemaHasNestedJson(schema.root));
52
+ return `${fastlyJsonValueAdmissionSource()}
53
+ function __pulse_fastly_schema_parse_json(text: string, schemaHandle: i32): i32 {
54
+ const schema = __pulse_fastly_value(schemaHandle)
55
+ if (schema.kind == PULSE_VALUE_STRING) {
56
+ ${schemas.map((schema, index) => schema.jsonLimits ? ` if (schema.text == ${JSON.stringify(schema.id)} && __pulse_schema_json_scan_${index}(text, 0).failure != 0) {
57
+ __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 57, -1); return 0
58
+ }` : '').join('\n')}
59
+ }
60
+ return __pulse_fastly_parse_json(text)
61
+ }
62
+ ${nested ? `
63
+ class __PulseFastlyJsonCopyFrame {
64
+ index: i32 = 0
65
+ constructor(public input: i32, public output: i32) {}
66
+ }
67
+ function __pulse_fastly_json_copy_enter(handle: i32, duplicates: bool, frames: Array<__PulseFastlyJsonCopyFrame>): i32 {
68
+ const value = __pulse_fastly_value(handle)
69
+ if (value.kind != PULSE_VALUE_ARRAY && value.kind != PULSE_VALUE_OBJECT) return handle
70
+ if (duplicates && value.duplicateJsonKeys) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 57, -1); return 0 }
71
+ const copy = value.kind == PULSE_VALUE_ARRAY ? host_value_array() : host_value_object()
72
+ frames.push(new __PulseFastlyJsonCopyFrame(handle, copy))
73
+ return copy
74
+ }
75
+ function __pulse_fastly_copy_json(handle: i32, duplicates: bool): i32 {
76
+ const frames = new Array<__PulseFastlyJsonCopyFrame>()
77
+ const output = __pulse_fastly_json_copy_enter(handle, duplicates, frames)
78
+ while (frames.length > 0) {
79
+ const frame = frames[frames.length - 1]
80
+ const value = __pulse_fastly_value(frame.input)
81
+ if (frame.index == value.values.length) { frames.pop(); continue }
82
+ const index = frame.index++
83
+ const copy = __pulse_fastly_json_copy_enter(value.values[index], duplicates, frames)
84
+ if (copy <= 0) return 0
85
+ if (value.kind == PULSE_VALUE_ARRAY) host_value_array_push(frame.output, copy)
86
+ else host_value_object_set(frame.output, __pulse_fastly_string_value(value.keys[index]), copy)
87
+ }
88
+ return output
89
+ }
90
+ ` : ''}`;
91
+ }
92
+
93
+ module.exports = { hasJsonAdmission, tracksJsonDuplicates, schemaParserDepth, nestedJsonProjectionLines, schemaJsonAdmissionSource, schemaFetchJsonSource };