@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
@@ -10,6 +10,7 @@
10
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
11
 
12
12
  class __PulseS3Binding {
13
+ method: string = ''; range: string = ''; ifNoneMatch: string = '';
13
14
  contentType: string = ''; endpoint: string = ''; bucket: string = ''; region: string = ''; backend: string = '';
14
15
  id: string = ''; secret: string = ''; token: string = ''; max: i32 = 32768; timeout: i32 = 10000;
15
16
  }
@@ -35,6 +36,7 @@ function __s3_result(status: string, reason: string = '', http: i32 = 0): i32 {
35
36
  return result;
36
37
  }
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);
38
40
  return __s3_result(__pulse_fastly_effect_kind(index) == 14 ? (__s3_dispatched[index] ? 'unknown' : 'not-stored') : 'failed', reason, http);
39
41
  }
40
42
  function __s3_fail(index: i32, reason: string): void { __pulse_fastly_ready_effect(index, __s3_failure(index, reason)) }
@@ -71,7 +73,9 @@ function __pulse_fastly_s3_begin(index: i32, outer: __PulseFastlyValue): void {
71
73
  let data = new Uint8Array(0);
72
74
  if (put) {
73
75
  const text = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'text'));
74
- if (text.kind != PULSE_VALUE_STRING || !__pulse_s3_scalar(text.text)) { __s3_fail(index, 'invalid-text'); return; }
76
+ if (text.kind != PULSE_VALUE_STRING) { __s3_fail(index, 'invalid-text'); return; }
77
+ if (text.text.length > binding.max) { __s3_fail(index, 'too-large'); return; }
78
+ if (!__pulse_s3_scalar(text.text)) { __s3_fail(index, 'invalid-text'); return; }
75
79
  data = __pulse_s3_bytes(text.text);
76
80
  if (data.length > binding.max) { __s3_fail(index, 'too-large'); return; }
77
81
  __pulse_fastly_remember_secret(text.text);
@@ -82,10 +86,10 @@ function __pulse_fastly_s3_begin(index: i32, outer: __PulseFastlyValue): void {
82
86
  const token = binding.token.length ? __s3_secret(store[0], binding.token) : '';
83
87
  if (__s3_remaining(index) == 0) { __s3_fail(index, 'timeout'); return; }
84
88
  if (id === null || secret === null || token === null || !__pulse_s3_credentials(id, secret, token)) { __s3_fail(index, 'credentials'); return; }
85
- const method = put ? 'PUT' : __pulse_fastly_effect_kind(index) == 12 ? 'HEAD' : 'GET', path = '/' + binding.bucket + '/' + encoded;
89
+ const method = binding.method.length ? binding.method : put ? 'PUT' : __pulse_fastly_effect_kind(index) == 12 ? 'HEAD' : 'GET', path = '/' + binding.bucket + '/' + encoded;
86
90
  const now = __s3_clock(0);
87
91
  if (now < 0 || !__pulse_s3_date(now / 1000000000).length) { __s3_fail(index, 'configuration'); return; }
88
- const signature = __pulse_s3_sign(method, path, binding.endpoint.slice(8), binding.region, id, secret, token, now / 1000000000, data, binding.contentType);
92
+ 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);
89
93
  __s3_sent_bytes[index] = data.length; __s3_digests[index] = signature.digest;
90
94
  __pulse_fastly_remember_secret(signature.digest);
91
95
  __pulse_fastly_remember_secret(signature.authorization); __pulse_fastly_remember_secret(binding.endpoint + path);
@@ -98,6 +102,8 @@ function __pulse_fastly_s3_begin(index: i32, outer: __PulseFastlyValue): void {
98
102
  && __s3_header(req, 'host', binding.endpoint.slice(8)) && __s3_header(req, 'x-amz-date', signature.date)
99
103
  && __s3_header(req, 'x-amz-content-sha256', signature.digest) && __s3_header(req, 'authorization', signature.authorization)
100
104
  && __s3_header(req, 'accept-encoding', 'identity');
105
+ if (ok && binding.range.length) ok = __s3_header(req, 'range', binding.range);
106
+ if (ok && binding.ifNoneMatch.length) ok = __s3_header(req, 'if-none-match', binding.ifNoneMatch);
101
107
  if (ok && put) ok = __s3_header(req, 'content-type', binding.contentType) && __s3_header(req, 'content-length', data.length.toString());
102
108
  if (ok && token.length) ok = __s3_header(req, 'x-amz-security-token', token);
103
109
  if (!ok || fastly_http_body_new(changetype<usize>(body)) != 0) { __s3_close_req(req); __s3_fail(index, 'transport'); return; }
@@ -133,6 +139,7 @@ function __s3_list(buffer: Uint8Array, size: i32): Array<string> | null {
133
139
  return result;
134
140
  }
135
141
  function __s3_read_response(index: i32, response: i32, body: i32, status: i32): i32 {
142
+ if (__pulse_fastly_effect_kind(index) == 21) return __s3_body_response(index, response, body, status);
136
143
  const put = __pulse_fastly_effect_kind(index) == 14;
137
144
  const rejected = put && status >= 400 && status <= 499 && status != 408;
138
145
  if (!put && status == 404) return __s3_result('not-found');
@@ -211,5 +218,6 @@ function __pulse_fastly_s3_wait(index: i32): i32 {
211
218
  let result: i32;
212
219
  if (fastly_http_resp_status_get(response[0], changetype<usize>(code)) != 0) result = __s3_failure(index, 'protocol');
213
220
  else result = __s3_remaining(index) == 0 ? __s3_failure(index, 'timeout', code[0]) : __s3_read_response(index, response[0], body[0], code[0]);
214
- __s3_close_body(body[0]); __s3_close_resp(response[0]); return result;
221
+ if (!__pulse_fastly_value(result).s3Body) __s3_close_body(body[0]);
222
+ __s3_close_resp(response[0]); return result;
215
223
  }
@@ -21,10 +21,10 @@ function s3NativeSource(plan, bindings) {
21
21
  const lines = ['function __pulse_fastly_s3_binding(index: i32): __PulseS3Binding {', 'const value = new __PulseS3Binding();', 'switch (index) {'];
22
22
  for (const binding of bindings.s3) {
23
23
  lines.push(`case ${binding.index}:`);
24
- for (const [field, input] of Object.entries({ contentType: 'contentType', endpoint: 'endpoint', bucket: 'bucket', region: 'region', backend: 'backend', id: 'accessKeyIdSecret', secret: 'secretAccessKeySecret', token: 'sessionTokenSecret' })) lines.push(`value.${field} = ${JSON.stringify(binding[input] || '')};`);
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
25
  lines.push(`value.max = ${binding.maxTextBytes}; value.timeout = ${binding.timeoutMs}; break;`);
26
26
  }
27
27
  lines.push('default: unreachable();', '}', 'return value;', '}');
28
- return [contribution.source, fs.readFileSync(path.join(__dirname, 's3-native.as.ts'), 'utf8'), lines.join('\n')].join('\n');
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
29
  }
30
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 (handle <= 0 || handle > __pulse_fastly_values.length) return budget.fail(12)
14
+ const value = __pulse_fastly_values[handle - 1]
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_values[frame.handle - 1]
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 };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { schemaHasScalarRecord } = require('@pulse-compute/wasm-contracts/schema-json/registry');
4
+
5
+ function hasScalarRecords(plan) {
6
+ return (plan.schemas?.registry?.schemas || []).some(schema => schemaHasScalarRecord(schema.root));
7
+ }
8
+
9
+ // Both Fastly Native realizations validate handles before json-as sees them.
10
+ function scalarRecordProjectionLines(node) {
11
+ const limits = node.limits;
12
+ return [
13
+ ' if (input.kind != PULSE_VALUE_OBJECT || (checkDuplicates && input.duplicateJsonKeys)) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }',
14
+ ` if (input.keys.length > ${limits.maxKeys}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
15
+ ' const keys = input.keys.slice().sort()',
16
+ ' const output = host_value_object()',
17
+ ' let bytes = 2',
18
+ ' for (let i = 0; i < keys.length; i++) {',
19
+ ' const key = keys[i]',
20
+ ` if (key.length > ${limits.maxKeyLength}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
21
+ ' const keyHandle = __pulse_fastly_string_value(key)',
22
+ ' const itemHandle = host_value_property(valueHandle, keyHandle)',
23
+ ' const item = __pulse_fastly_value(itemHandle)',
24
+ ' let size = 0',
25
+ ' if (item.kind == PULSE_VALUE_STRING) {',
26
+ ` if (item.text.length > ${limits.maxStringLength}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
27
+ ' size = __pulse_scalar_record_string_bytes(item.text)',
28
+ ' } else if (item.kind == PULSE_VALUE_NUMBER && isFinite(item.number)) {',
29
+ ` size = ${limits.numberBytes}`,
30
+ ' } else if (item.kind == PULSE_VALUE_BOOLEAN) size = item.boolean != 0 ? 4 : 5',
31
+ ' else if (item.kind == PULSE_VALUE_NULL) size = 4',
32
+ ' else { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }',
33
+ ' bytes += (i == 0 ? 0 : 1) + __pulse_scalar_record_string_bytes(key) + 1 + size',
34
+ ` if (bytes > ${limits.maxBytes}) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 56, -1); return 0 }`,
35
+ ' host_value_object_set(output, keyHandle, itemHandle)',
36
+ ' }',
37
+ ' return output'
38
+ ];
39
+ }
40
+
41
+ module.exports = { hasScalarRecords, scalarRecordProjectionLines };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ // Provider-owned realtime sampling. The portable guest still suspends through
4
+ // effect_begin; no clock import is exposed to application-authored code.
5
+ function timeNativeSource(plan) {
6
+ if (!(plan.effects || []).some(effect => effect.kind === 'time.now')) return '';
7
+ return `
8
+ function __pulse_time_pad(value: i32, width: i32): string {
9
+ let text = value.toString()
10
+ while (text.length < width) text = "0" + text
11
+ return text
12
+ }
13
+ function __pulse_time_iso(ms: u64): string {
14
+ // Gregorian civil date from whole days since the Unix epoch.
15
+ const z = <i32>(ms / 86400000) + 719468
16
+ const era = z / 146097
17
+ const doe = z - era * 146097
18
+ const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
19
+ let year = yoe + era * 400
20
+ const doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
21
+ const mp = (5 * doy + 2) / 153
22
+ const day = doy - (153 * mp + 2) / 5 + 1
23
+ const month = mp + (mp < 10 ? 3 : -9)
24
+ if (month <= 2) year += 1
25
+ const rest = <i32>(ms % 86400000)
26
+ return __pulse_time_pad(year, 4) + "-" + __pulse_time_pad(month, 2) + "-" + __pulse_time_pad(day, 2)
27
+ + "T" + __pulse_time_pad(rest / 3600000, 2) + ":" + __pulse_time_pad((rest / 60000) % 60, 2)
28
+ + ":" + __pulse_time_pad((rest / 1000) % 60, 2) + "." + __pulse_time_pad(rest % 1000, 3) + "Z"
29
+ }
30
+ function __pulse_fastly_time_begin(effectIndex: i32, payload: __PulseFastlyValue): void {
31
+ const out = new StaticArray<u64>(1)
32
+ const status = wasi_snapshot_preview1_clock_time_get(0, 1000000, changetype<usize>(out))
33
+ const result = host_value_object()
34
+ if (status != 0) {
35
+ host_value_object_set(result, __pulse_fastly_string_value("status"), __pulse_fastly_string_value("failed"))
36
+ host_value_object_set(result, __pulse_fastly_string_value("reason"), __pulse_fastly_string_value("unavailable"))
37
+ } else {
38
+ // Divide integer nanoseconds before conversion to f64 to retain millisecond precision.
39
+ const ms = unchecked(out[0]) / 1000000
40
+ host_value_object_set(result, __pulse_fastly_string_value("status"), __pulse_fastly_string_value("ok"))
41
+ host_value_object_set(result, __pulse_fastly_string_value("unixEpochMs"), host_value_number(<f64>ms))
42
+ host_value_object_set(result, __pulse_fastly_string_value("iso8601"), __pulse_fastly_string_value(__pulse_time_iso(ms)))
43
+ }
44
+ __pulse_fastly_ready_effect(effectIndex, result)
45
+ }
46
+ `;
47
+ }
48
+
49
+ module.exports = Object.freeze({ timeNativeSource });
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { fastlyConfigDefault } = require('../config-schema.js');
4
+ const DEFAULT_FASTLY_MAX_WASM_BYTES = fastlyConfigDefault('fastly.build.maxWasmBytes');
5
+
6
+ function normalizeFastlyMaxWasmBytes(value) {
7
+ if (value === undefined) return DEFAULT_FASTLY_MAX_WASM_BYTES;
8
+ if (!Number.isSafeInteger(value) || value <= 0) {
9
+ const error = new TypeError('fastly.build.maxWasmBytes must be a positive safe integer byte count.');
10
+ error.code = 'PULSE_FASTLY_MAX_WASM_BYTES_INVALID';
11
+ throw error;
12
+ }
13
+ return value;
14
+ }
15
+
16
+ function assertFastlyWasmBudget(bytes, value) {
17
+ const maxBytes = normalizeFastlyMaxWasmBytes(value);
18
+ if (bytes > maxBytes) {
19
+ const error = new Error('Generated Fastly Native module exceeds the configured Pulse compilation budget.');
20
+ error.name = 'FastlyNativePlatformCapabilitiesError';
21
+ error.code = 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_WASM_TOO_LARGE';
22
+ error.detail = Object.freeze({ bytes, maxBytes });
23
+ throw error;
24
+ }
25
+ return maxBytes;
26
+ }
27
+
28
+ module.exports = Object.freeze({ DEFAULT_FASTLY_MAX_WASM_BYTES, normalizeFastlyMaxWasmBytes, assertFastlyWasmBudget });
package/src/config-api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeFastlyMaxWasmBytes } = require('./build/wasm-budget.js');
3
4
  const { fastlyConfigDefault } = require('./config-schema.js');
4
5
 
5
6
  const FASTLY_PROVIDER_API_VERSION = 'pulse.provider-fastly-api.v2';
@@ -38,6 +39,7 @@ function fastly(options = {}) {
38
39
  const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
39
40
  return Object.freeze({
40
41
  kind: 'fastly',
42
+ ...(options.maxDurationMs === undefined ? {} : { maxDurationMs: require('@pulse-compute/runtime/host').normalizeRequestDuration(options.maxDurationMs) }),
41
43
  version: FASTLY_PROVIDER_API_VERSION,
42
44
  bindings: Object.freeze({
43
45
  configStore: String(options.configStore ?? fastlyConfigDefault('fastly.configStore')),
@@ -48,6 +50,7 @@ function fastly(options = {}) {
48
50
  dynamicBackends: options.dynamicBackends === undefined ? fastlyConfigDefault('fastly.dynamicBackends') : options.dynamicBackends === true
49
51
  }),
50
52
  build: Object.freeze({
53
+ maxWasmBytes: normalizeFastlyMaxWasmBytes(options.maxWasmBytes),
51
54
  name: String(options.name ?? fastlyConfigDefault('fastly.name')),
52
55
  description: String(options.description ?? fastlyConfigDefault('fastly.description')),
53
56
  authors: Object.freeze((Array.isArray(options.authors) ? options.authors : fastlyConfigDefault('fastly.authors')).map(String)),
@@ -72,6 +72,16 @@
72
72
  "fastly.local.networkFetch": {
73
73
  "kind": "boolean",
74
74
  "default": false
75
+ },
76
+ "fastly.maxDurationMs": {
77
+ "kind": "optional-number"
78
+ },
79
+ "fastly.build.maxWasmBytes": {
80
+ "kind": "positive-safe-integer",
81
+ "default": 4194304
82
+ },
83
+ "fastly.maxWasmBytes": {
84
+ "kind": "positive-safe-integer"
75
85
  }
76
86
  }
77
87
  }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ const { normalizeFastlyMaxWasmBytes } = require('../src/build/wasm-budget.js') as { normalizeFastlyMaxWasmBytes(value?: number): number };
2
+ const { normalizeRequestDuration } = require('@pulse-compute/runtime/host') as { normalizeRequestDuration(value?: number): number | undefined };
1
3
  import FASTLY_CONFIG_SCHEMA from './config-schema.json';
2
4
 
3
5
  type FastlySchemaRule = Readonly<{ kind: string; default?: unknown }>;
@@ -23,6 +25,9 @@ export interface PulseFastlyLocalOptions {
23
25
  }
24
26
 
25
27
  export interface PulseFastlyProviderOptions {
28
+ /** Inclusive final Native Wasm byte budget; default 4 MiB. */
29
+ readonly maxWasmBytes?: number;
30
+ readonly maxDurationMs?: number;
26
31
  readonly configStore?: string;
27
32
  readonly secretStore?: string;
28
33
  readonly kv?: Readonly<Record<string, string>>;
@@ -36,6 +41,7 @@ export interface PulseFastlyProviderOptions {
36
41
  }
37
42
 
38
43
  export interface PulseFastlyProviderConfig {
44
+ readonly maxDurationMs?: number;
39
45
  readonly kind: 'fastly';
40
46
  readonly version: typeof FASTLY_PROVIDER_API_VERSION;
41
47
  readonly bindings: {
@@ -57,6 +63,7 @@ export interface PulseFastlyProviderConfig {
57
63
  readonly dynamicBackends: boolean;
58
64
  };
59
65
  readonly build: {
66
+ readonly maxWasmBytes: number;
60
67
  readonly name: string;
61
68
  readonly description: string;
62
69
  readonly authors: readonly string[];
@@ -97,6 +104,7 @@ export function fastly(options: PulseFastlyProviderOptions = {}): PulseFastlyPro
97
104
  const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
98
105
  return Object.freeze({
99
106
  kind: 'fastly',
107
+ maxDurationMs: normalizeRequestDuration(options.maxDurationMs),
100
108
  version: FASTLY_PROVIDER_API_VERSION,
101
109
  bindings: Object.freeze({
102
110
  configStore: String(options.configStore ?? fastlyDefaults['fastly.configStore']),
@@ -107,6 +115,7 @@ export function fastly(options: PulseFastlyProviderOptions = {}): PulseFastlyPro
107
115
  dynamicBackends: options.dynamicBackends === undefined ? Boolean(fastlyDefaults['fastly.dynamicBackends']) : options.dynamicBackends === true
108
116
  }),
109
117
  build: Object.freeze({
118
+ maxWasmBytes: normalizeFastlyMaxWasmBytes(options.maxWasmBytes),
110
119
  name: String(options.name ?? fastlyDefaults['fastly.name']),
111
120
  description: String(options.description ?? fastlyDefaults['fastly.description']),
112
121
  authors: Object.freeze((options.authors ?? (Array.isArray(fastlyDefaults['fastly.authors']) ? fastlyDefaults['fastly.authors'] : [])).map(String)),
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
+ normalizeJwtSignEffect,
4
5
  normalizeJwtVerifyEffect
5
6
  } = require('@pulse-compute/wasm-contracts/jwt/contracts');
6
7
  const {
7
8
  CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
9
+ CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION,
8
10
  CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION
9
11
  } = require('@pulse-compute/wasm-contracts/crypto/contracts');
10
12
 
@@ -16,6 +18,7 @@ const FASTLY_JAVASCRIPT_JWT_REALIZATIONS = Object.freeze({
16
18
  implementation: CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION,
17
19
  automaticFallback: false
18
20
  }),
21
+ RS256: Object.freeze({ algorithm: 'RS256', realization: 'runtime-builtin', implementation: CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION, automaticFallback: false }),
19
22
  ES256: Object.freeze({
20
23
  algorithm: 'ES256',
21
24
  realization: 'runtime-builtin',
@@ -83,12 +86,19 @@ function createFastlyJavascriptJwtVerify(options = {}) {
83
86
  return async function verifyFastlyJavascriptJwt(effect, execution) {
84
87
  const { jwtProvider, cryptoProvider } = await loadRuntimeModules();
85
88
  assertRequestActive(execution, jwtProvider);
86
- const input = normalizeJwtVerifyEffect(effect);
87
- if (execution && typeof execution.registerRedactionValue === 'function') {
89
+ const signing = effect.operation === 'sign';
90
+ const input = signing ? normalizeJwtSignEffect(effect) : normalizeJwtVerifyEffect(effect);
91
+ if (!signing && execution && typeof execution.registerRedactionValue === 'function') {
88
92
  execution.registerRedactionValue(input.token);
89
93
  }
90
94
  assertRequestActive(execution, jwtProvider);
91
- const selectedCrypto = assertRuntimeBuiltin(cryptoProvider, jwtProvider);
95
+ const selectedCrypto = signing
96
+ ? (input.options.algorithm === 'RS256'
97
+ ? require('@pulse-compute/crypto/provider').bindJavascriptRs256Signer()
98
+ : input.options.algorithm === 'ES256'
99
+ ? require('@pulse-compute/crypto/provider').bindJavascriptEs256Signer()
100
+ : require('@pulse-compute/crypto/provider').bindJavascriptDigestMac(['HMAC-SHA256']))
101
+ : assertRuntimeBuiltin(cryptoProvider, jwtProvider);
92
102
  const host = {
93
103
  async captureWallClock() {
94
104
  assertRequestActive(execution, jwtProvider);
@@ -118,7 +128,7 @@ function createFastlyJavascriptJwtVerify(options = {}) {
118
128
  return result;
119
129
  };
120
130
  }
121
- const result = await jwtProvider.verifyJwtWithCrypto(
131
+ const result = await (signing ? jwtProvider.signJwtWithCrypto : jwtProvider.verifyJwtWithCrypto)(
122
132
  input,
123
133
  Object.freeze(host),
124
134
  selectedCrypto
@@ -15,10 +15,12 @@ function unavailable(effect = {}) {
15
15
  function createFastlyJavascriptPackageEffectCapabilities(options = {}) {
16
16
  const assetsLookup = options.assetsLookup;
17
17
  const gripBroadcast = options.gripBroadcast;
18
+ const digestText = require('@pulse-compute/crypto/provider').createJavascriptTextDigest(options);
18
19
  const jwtVerify = options.jwtVerify;
19
20
  return Object.freeze({
20
21
  version: FASTLY_JAVASCRIPT_PACKAGE_EFFECTS_VERSION,
21
22
  effect(effect, execution) {
23
+ if (effect && effect.contractId === 'pulse.crypto' && effect.operation === 'digestText') return digestText(effect, execution);
22
24
  if (effect && effect.contractId === 'pulse.assets' && effect.operation === 'lookup') {
23
25
  if (typeof assetsLookup !== 'function') return unavailable(effect);
24
26
  return assetsLookup(effect.payload, execution);
@@ -33,7 +35,7 @@ function createFastlyJavascriptPackageEffectCapabilities(options = {}) {
33
35
  }
34
36
  return gripBroadcast(effect.payload, execution);
35
37
  }
36
- if (effect && effect.contractId === 'pulse.jwt' && effect.operation === 'verify') {
38
+ if (effect && effect.contractId === 'pulse.jwt' && ['verify', 'sign'].includes(effect.operation)) {
37
39
  if (typeof jwtVerify !== 'function') return unavailable(effect);
38
40
  return jwtVerify(effect, execution);
39
41
  }
@@ -49,7 +51,8 @@ function withFastlyPackageEffectCapabilities(capabilities, options = {}) {
49
51
  ...base,
50
52
  effect(effect, execution) {
51
53
  if (effect && (
52
- effect.contractId === 'pulse.assets'
54
+ effect.contractId === 'pulse.crypto'
55
+ || effect.contractId === 'pulse.assets'
53
56
  || effect.contractId === 'pulse.grip'
54
57
  || effect.contractId === 'pulse.jwt'
55
58
  )) {
@@ -45,10 +45,10 @@ function createFastlyJavascriptCapabilities(options = {}) {
45
45
  : undefined;
46
46
  return withFastlyPackageEffectCapabilities(
47
47
  withFastlyFetchCapability(
48
- withFastlyBindingCapabilities(options.capabilities, { ...options, bindingCapabilities }),
48
+ withFastlyBindingCapabilities({ time: () => runtimeHost.readWallTime(options.wallClock === undefined ? () => Date.now() : options.wallClock), ...options.capabilities }, { ...options, bindingCapabilities }),
49
49
  { ...options, fetchImplementation: options.fetchImplementation }
50
50
  ),
51
- { assetsLookup, gripBroadcast, jwtVerify }
51
+ { assetsLookup, gripBroadcast, jwtVerify, digestSubtle: options.digestSubtle }
52
52
  );
53
53
  }
54
54
 
@@ -57,6 +57,9 @@ function requestHeaderPairs(request) {
57
57
  }
58
58
 
59
59
  async function executeFastlyJavascriptApplication(application, request, options = {}) {
60
+ if (options.maxDurationMs !== undefined || options.requestBudget !== undefined) {
61
+ throw new runtimeHost.PulseRuntimeContractError('PULSE_REQUEST_DURATION_UNSUPPORTED', 'Fastly JavaScript does not implement the request deadline; select Native.');
62
+ }
60
63
  const capabilities = options.effectAdapter === undefined
61
64
  ? createFastlyJavascriptCapabilities(options)
62
65
  : undefined;
@@ -16,8 +16,11 @@ function blockedRestriction(id, restriction) {
16
16
 
17
17
  function classifyFastlyJavascriptCapability(id, restrictions = {}) {
18
18
  const capability = String(id);
19
+ if (capability === 'request.body.transform') return decision(capability, 'blocked', 'fastly-body-transform-unavailable', 'provider-fastly');
20
+ if (capability === 'request.body.forward' || capability === 'req.body') return decision(capability, 'blocked', 'fastly-incoming-body-forwarding-unavailable', 'provider-fastly');
21
+ if (capability === 'crypto.digestText') return decision(capability, 'eligible', 'fastly-javascript-crypto-sha256-runtime-builtin', 'provider-fastly');
19
22
  if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap'].includes(capability)) return decision(capability, 'blocked', 'fastly-conditional-kv-incomplete', 'provider-fastly');
20
- if (['s3.head', 's3.getText', 's3.putText'].includes(capability)) return decision(capability, 'blocked', 'fastly-javascript-s3-raw-headers-unavailable', 'provider-fastly');
23
+ if (['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(capability)) return decision(capability, 'blocked', 'fastly-javascript-s3-raw-headers-unavailable', 'provider-fastly');
21
24
  if (isJavascriptCoreCapability(capability)) {
22
25
  return decision(capability, 'eligible', 'fastly-request-router-runtime', 'provider-fastly');
23
26
  }
@@ -81,9 +84,11 @@ function classifyFastlyJavascriptCapability(id, restrictions = {}) {
81
84
  return decision(capability, 'eligible', 'fastly-javascript-provider-wall-clock-authority', 'provider-fastly');
82
85
  }
83
86
  if (
84
- capability === 'jwt.verify'
87
+ capability === 'jwt.sign'
88
+ || capability === 'jwt.verify'
85
89
  || capability === 'jwt.verify.hs256'
86
90
  || capability === 'jwt.verify.es256'
91
+ || capability === 'jwt.verify.rs256'
87
92
  ) {
88
93
  return blockedRestriction(capability, restrictions.jwt)
89
94
  || decision(capability, 'eligible', 'fastly-javascript-jwt-crypto-runtime-builtin', 'provider-fastly');
@@ -102,8 +107,9 @@ function classifyFastlyJavascriptCapability(id, restrictions = {}) {
102
107
 
103
108
  function classifyFastlyJavascriptProviderRequirement(id, restrictions = {}) {
104
109
  const requirement = String(id);
110
+ if (requirement === 'crypto.digestText') return decision(requirement, 'eligible', 'fastly-javascript-crypto-sha256-runtime-builtin', 'provider-fastly');
105
111
  if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap'].includes(requirement)) return decision(requirement, 'blocked', 'fastly-conditional-kv-incomplete', 'provider-fastly');
106
- if (['s3.head', 's3.getText', 's3.putText'].includes(requirement)) return decision(requirement, 'blocked', 'fastly-javascript-s3-raw-headers-unavailable', 'provider-fastly');
112
+ if (['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(requirement)) return decision(requirement, 'blocked', 'fastly-javascript-s3-raw-headers-unavailable', 'provider-fastly');
107
113
  if (requirement === 'request' || ['response.json', 'response.text', 'response.custom'].includes(requirement)) {
108
114
  return decision(requirement, 'eligible', 'fastly-request-response-lifecycle', 'provider-fastly');
109
115
  }
@@ -145,9 +151,11 @@ function classifyFastlyJavascriptProviderRequirement(id, restrictions = {}) {
145
151
  return decision(requirement, 'eligible', 'fastly-javascript-provider-wall-clock-authority', 'provider-fastly');
146
152
  }
147
153
  if (
148
- requirement === 'jwt.verify'
154
+ requirement === 'jwt.sign'
155
+ || requirement === 'jwt.verify'
149
156
  || requirement === 'jwt.verify.hs256'
150
157
  || requirement === 'jwt.verify.es256'
158
+ || requirement === 'jwt.verify.rs256'
151
159
  ) {
152
160
  return blockedRestriction(requirement, restrictions.jwt)
153
161
  || decision(requirement, 'eligible', 'fastly-javascript-jwt-crypto-runtime-builtin', 'provider-fastly');
@@ -169,6 +177,11 @@ function restriction(reasonId, configuration) {
169
177
  }
170
178
 
171
179
  function fastlyJavascriptProjectRestrictions(compiled, project) {
180
+ if (project.providerConfig?.maxDurationMs !== undefined) {
181
+ const error = new TypeError('fastly.maxDurationMs requires the Native target; Fastly JavaScript does not implement the request deadline.');
182
+ error.code = 'PULSE_REQUEST_DURATION_UNSUPPORTED';
183
+ throw error;
184
+ }
172
185
  const bindings = project.providerConfig && project.providerConfig.bindings || {};
173
186
  const backends = bindings.backends && typeof bindings.backends === 'object' ? bindings.backends : {};
174
187
  const kv = bindings.kv && typeof bindings.kv === 'object' ? bindings.kv : {};
@@ -181,7 +194,7 @@ function fastlyJavascriptProjectRestrictions(compiled, project) {
181
194
  const secretSites = sites.filter((entry) => entry.kind === 'secret.get');
182
195
  const jwtSites = sites.filter((entry) => (
183
196
  entry.contractId === 'pulse.jwt'
184
- && (entry.operation === 'verify' || entry.kind === 'jwt.verify')
197
+ && (['verify', 'sign'].includes(entry.operation) || ['jwt.verify', 'jwt.sign'].includes(entry.kind))
185
198
  ));
186
199
  const output = {};
187
200