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

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.
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('node:crypto');
4
+ const conditionalKv = require('./kv-native.js');
4
5
  const fs = require('node:fs');
5
6
  const os = require('node:os');
6
7
  const path = require('node:path');
@@ -94,6 +95,8 @@ const FASTLY_NATIVE_PLATFORM_CAPABILITIES_BUFFER_BYTES = 65536;
94
95
  const FASTLY_NATIVE_SIZE_OPTIMIZATION = EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION;
95
96
 
96
97
  const FASTLY_NATIVE_PLATFORM_CAPABILITIES_IMPORTS = Object.freeze([
98
+ ...conditionalKv.KV_IMPORTS.map((key) => Object.freeze(key.split(':'))),
99
+ ...require('./s3-native.js').S3_IMPORTS.map((key) => Object.freeze(key.split(':'))),
97
100
  Object.freeze(['fastly_abi', 'init']),
98
101
  Object.freeze(['fastly_http_req', 'body_downstream_get']),
99
102
  Object.freeze(['fastly_http_req', 'method_get']),
@@ -127,7 +130,7 @@ const FASTLY_NATIVE_PLATFORM_CAPABILITIES_IMPORTS = Object.freeze([
127
130
  Object.freeze(['fastly_log', 'endpoint_get']),
128
131
  Object.freeze(['fastly_log', 'write']),
129
132
  Object.freeze(['wasi_snapshot_preview1', 'clock_time_get'])
130
- ]);
133
+ ].filter(([module, name], index, entries) => entries.findIndex((item) => item[0] === module && item[1] === name) === index));
131
134
 
132
135
  const FASTLY_NATIVE_PLATFORM_CAPABILITIES_REQUIRED_IMPORTS = Object.freeze([
133
136
  Object.freeze(['fastly_abi', 'init']),
@@ -148,7 +151,13 @@ const FASTLY_NATIVE_PLATFORM_EFFECT_KIND = Object.freeze({
148
151
  'grip.publish': 8,
149
152
  'grip.broadcast': 9,
150
153
  'assets.lookup': 10,
151
- 'jwt.verify': 11
154
+ 'jwt.verify': 11,
155
+ 's3.head': 12,
156
+ 's3.getText': 13,
157
+ 's3.putText': 14,
158
+ 'kv.getVersioned': 15,
159
+ 'kv.insertIfAbsent': 16,
160
+ 'kv.compareAndSwap': 17
152
161
  });
153
162
 
154
163
  const FASTLY_NATIVE_PLATFORM_CAPABILITIES_ALLOWED_IMPORTS = new Set([
@@ -161,12 +170,16 @@ const FASTLY_NATIVE_PLATFORM_EFFECT_KINDS = Object.freeze([
161
170
  'secret.get',
162
171
  'kv.get',
163
172
  'kv.put',
173
+ ...conditionalKv.KV_CONDITIONAL_KINDS,
164
174
  'grip.channel',
165
175
  'grip.hold',
166
176
  'grip.publish',
167
177
  'grip.broadcast',
168
178
  'assets.lookup',
169
- 'jwt.verify'
179
+ 'jwt.verify',
180
+ 's3.head',
181
+ 's3.getText',
182
+ 's3.putText'
170
183
  ]);
171
184
 
172
185
  const FASTLY_NATIVE_PLATFORM_CAPABILITY_KINDS = new Set([
@@ -174,12 +187,16 @@ const FASTLY_NATIVE_PLATFORM_CAPABILITY_KINDS = new Set([
174
187
  'secret.get',
175
188
  'kv.get',
176
189
  'kv.put',
190
+ ...conditionalKv.KV_CONDITIONAL_KINDS,
177
191
  'assets.lookup',
178
192
  'grip.channel',
179
193
  'grip.hold',
180
194
  'grip.publish',
181
195
  'grip.broadcast',
182
- 'jwt.verify'
196
+ 'jwt.verify',
197
+ 's3.head',
198
+ 's3.getText',
199
+ 's3.putText'
183
200
  ]);
184
201
 
185
202
  class FastlyNativePlatformCapabilitiesError extends Error {
@@ -245,7 +262,7 @@ function selectedJwtCrypto(plan) {
245
262
  const hasJwt = (plan.effects || []).some((effect) => effect.kind === 'jwt.verify');
246
263
  if (!hasJwt) return undefined;
247
264
  const algorithms = plan.crypto && Array.isArray(plan.crypto.algorithms)
248
- ? plan.crypto.algorithms
265
+ ? plan.crypto.algorithms.filter((entry) => ['HS256', 'ES256'].includes(entry.algorithm))
249
266
  : [];
250
267
  if (algorithms.length !== 1) {
251
268
  fail(
@@ -572,7 +589,9 @@ function validatePlanBoundary(plan, options = {}) {
572
589
  && effect.kind === 'jwt.verify'
573
590
  && effect.operation === 'verify'
574
591
  && effect.capability === 'jwt.verify';
575
- return !grip && !assets && !jwt;
592
+ const s3 = effect.package === '@pulse-compute/s3' && effect.contractId === 'pulse.s3'
593
+ && ['head', 'getText', 'putText'].includes(effect.operation) && effect.kind === `s3.${effect.operation}` && effect.capability === effect.kind;
594
+ return !grip && !assets && !jwt && !s3;
576
595
  });
577
596
  if (unsupportedPackages.length > 0) {
578
597
  fail('Fastly native realization only accepts the trusted first-party Assets, GRIP, and JWT package contracts.', 'PULSE_FASTLY_NATIVE_PLATFORM_PACKAGE_UNSUPPORTED', {
@@ -592,7 +611,9 @@ function validatePlanBoundary(plan, options = {}) {
592
611
  && effect.kind === 'jwt.verify'
593
612
  && effect.operation === 'verify'
594
613
  && effect.capability === 'jwt.verify';
595
- return !grip && !assets && !jwt;
614
+ const s3 = effect.package === '@pulse-compute/s3' && effect.contractId === 'pulse.s3'
615
+ && ['head', 'getText', 'putText'].includes(effect.operation) && effect.kind === `s3.${effect.operation}` && effect.capability === effect.kind;
616
+ return !grip && !assets && !jwt && !s3;
596
617
  });
597
618
  if (unsupportedDeclarations.length > 0) {
598
619
  fail('Fastly native package realization is restricted to the Assets, GRIP, and JWT contracts.', 'PULSE_FASTLY_NATIVE_PLATFORM_PACKAGE_UNSUPPORTED', { effects: unsupportedDeclarations });
@@ -678,7 +699,7 @@ function resolveCapabilityBindings(plan, options = {}) {
678
699
  const effects = plan.effects || [];
679
700
  const hasConfig = effects.some((effect) => effect.kind === 'config.get');
680
701
  const hasSecret = effects.some((effect) => (
681
- effect.kind === 'secret.get'
702
+ effect.kind === 'secret.get' || ['s3.head', 's3.getText', 's3.putText'].includes(effect.kind)
682
703
  || (effect.kind === 'jwt.verify' && effect.resource && effect.resource.keyType === 'secret')
683
704
  ));
684
705
  const hasGripHold = effects.some((effect) => effect.kind === 'grip.hold');
@@ -703,7 +724,7 @@ function resolveCapabilityBindings(plan, options = {}) {
703
724
 
704
725
  const kv = [];
705
726
  for (const [index, effect] of effects.entries()) {
706
- if (effect.kind !== 'kv.get' && effect.kind !== 'kv.put' && effect.kind !== 'assets.lookup') continue;
727
+ if (effect.kind !== 'kv.get' && effect.kind !== 'kv.put' && !conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind) && effect.kind !== 'assets.lookup') continue;
707
728
  const packagePayload = inputExpression(effect, 'payload');
708
729
  const logical = effect.resource && effect.resource.kind === 'literal'
709
730
  && effect.kind !== 'assets.lookup'
@@ -715,6 +736,7 @@ function resolveCapabilityBindings(plan, options = {}) {
715
736
  resource: effect.resource
716
737
  });
717
738
  }
739
+ if (conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind) && Buffer.byteLength(logical, 'utf8') > conditionalKv.KV_CONDITIONAL_LIMITS.namespaceBytes) fail('Conditional KV namespace exceeds its portable bound.', 'PULSE_FASTLY_NATIVE_KV_NAMESPACE_DYNAMIC');
718
740
  const physical = stringBinding(
719
741
  kvMap[logical],
720
742
  `Fastly KV binding for ${logical}`,
@@ -801,6 +823,7 @@ function resolveCapabilityBindings(plan, options = {}) {
801
823
  return Object.freeze({
802
824
  configStore,
803
825
  secretStore,
826
+ s3: require('../toolchain/s3.js').resolveFastlyS3(effects.map((effect) => ({ ...effect, payload: { contentType: literalObjectField(inputExpression(effect, 'payload'), 'contentType') || undefined } })), source.s3),
804
827
  kv: Object.freeze(kv),
805
828
  fetch: Object.freeze(fetch),
806
829
  backends: Object.freeze(backends),
@@ -868,6 +891,7 @@ function requiredImportsForPlan(plan, bindings) {
868
891
  if (kinds.has('secret.get') || jwtSecretRequired || gripAuthenticationRequired) {
869
892
  for (const key of ['fastly_secret_store:open', 'fastly_secret_store:get', 'fastly_secret_store:plaintext']) keys.add(key);
870
893
  }
894
+ for (const kind of conditionalKv.KV_CONDITIONAL_KINDS) if (kinds.has(kind)) for (const key of conditionalKv.kvImports(kind)) keys.add(key);
871
895
  if (kinds.has('jwt.verify')) keys.add('wasi_snapshot_preview1:clock_time_get');
872
896
  if (kinds.has('kv.get') || kinds.has('kv.put') || kinds.has('assets.lookup')) keys.add('fastly_kv_store:open');
873
897
  if (kinds.has('kv.get') || kinds.has('assets.lookup')) for (const key of ['fastly_kv_store:lookup', 'fastly_kv_store:lookup_wait_v2', 'fastly_http_body:read']) keys.add(key);
@@ -877,6 +901,7 @@ function requiredImportsForPlan(plan, bindings) {
877
901
  keys.add('fastly_log:endpoint_get');
878
902
  keys.add('fastly_log:write');
879
903
  }
904
+ if (kinds.has('s3.head') || kinds.has('s3.getText') || kinds.has('s3.putText')) for (const key of require('./s3-native.js').S3_IMPORTS) keys.add(key);
880
905
  return Object.freeze([...keys].sort());
881
906
  }
882
907
 
@@ -1014,7 +1039,7 @@ function effectResultSource(plan, bindings) {
1014
1039
  for (const [index, effect] of (plan.effects || []).entries()) {
1015
1040
  const result = effect.result || {};
1016
1041
  const decoder = result.decoder;
1017
- const waiter = effect.kind === 'fetch'
1042
+ const waiter = conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind) ? '__pulse_fastly_kv_conditional_wait' : ['s3.head', 's3.getText', 's3.putText'].includes(effect.kind) ? '__pulse_fastly_s3_wait' : effect.kind === 'fetch'
1018
1043
  ? '__pulse_fastly_wait_fetch'
1019
1044
  : effect.kind === 'grip.publish'
1020
1045
  ? '__pulse_fastly_wait_grip_publish'
@@ -1054,8 +1079,12 @@ function effectDispatchSource(plan) {
1054
1079
  'config.get': '__pulse_fastly_config_begin',
1055
1080
  'secret.get': '__pulse_fastly_secret_begin',
1056
1081
  'jwt.verify': '__pulse_fastly_jwt_begin',
1082
+ 's3.head': '__pulse_fastly_s3_begin',
1083
+ 's3.getText': '__pulse_fastly_s3_begin',
1084
+ 's3.putText': '__pulse_fastly_s3_begin',
1057
1085
  'kv.get': '__pulse_fastly_kv_get_begin',
1058
1086
  'kv.put': '__pulse_fastly_kv_put_begin',
1087
+ ...Object.fromEntries(conditionalKv.KV_CONDITIONAL_KINDS.map(kind => [kind, '__pulse_fastly_kv_conditional_begin'])),
1059
1088
  'assets.lookup': '__pulse_fastly_assets_begin',
1060
1089
  'grip.channel': '__pulse_fastly_grip_channel_begin',
1061
1090
  'grip.hold': '__pulse_fastly_grip_hold_begin',
@@ -1255,11 +1284,14 @@ class __PulseJsonParser {
1255
1284
  source: string
1256
1285
  index: i32 = 0
1257
1286
  failed: bool = false
1258
- constructor(source: string) { this.source = source }
1287
+ tooLarge: bool = false
1288
+ entries: i32 = 0
1289
+ constructor(source: string, public conditionalKv: bool = false) { this.source = source }
1259
1290
  skip(): void { while (this.index < this.source.length) { const c = this.source.charCodeAt(this.index); if (c != 32 && c != 9 && c != 10 && c != 13) return; this.index += 1 } }
1260
1291
  parse(): i32 { this.skip(); const value = this.value(0); this.skip(); if (this.index != this.source.length) this.failed = true; return this.failed ? 0 : value }
1261
1292
  value(depth: i32): i32 {
1262
- this.skip(); if (this.index >= this.source.length || depth > 64) { this.failed = true; return 0 }
1293
+ this.skip(); if (this.conditionalKv && (depth > ${conditionalKv.KV_CONDITIONAL_LIMITS.depth + 1} || ++this.entries > ${conditionalKv.KV_CONDITIONAL_LIMITS.entries + 2})) { this.failed = true; this.tooLarge = true; return 0 }
1294
+ if (this.index >= this.source.length || (!this.conditionalKv && depth > ${plan.effects.some(effect => conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind)) ? 128 : 64})) { this.failed = true; return 0 }
1263
1295
  const c = this.source.charCodeAt(this.index)
1264
1296
  if (c == 34) return __pulse_fastly_string_value(this.string())
1265
1297
  if (c == 123) return this.object(depth + 1)
@@ -1271,37 +1303,43 @@ class __PulseJsonParser {
1271
1303
  this.failed = true; return 0
1272
1304
  }
1273
1305
  string(): string {
1274
- let output = ""; this.index += 1
1275
- while (this.index < this.source.length) {
1276
- let c = this.source.charCodeAt(this.index); this.index += 1
1277
- if (c == 34) return output
1306
+ // Find a bounded span, then decode each UTF-16 unit once. Concatenating one
1307
+ // character at a time exhausts stub/fixed memory on a valid maximum KV value.
1308
+ const start = this.index + 1; let end = start;
1309
+ while (end < this.source.length) {
1310
+ const c = this.source.charCodeAt(end);
1311
+ if (c == 34) break;
1312
+ if (c == 92) end += 1;
1313
+ end += 1;
1314
+ }
1315
+ if (end >= this.source.length) { this.failed = true; return "" }
1316
+ const output = new Uint16Array(end - start); let count = 0; this.index = start;
1317
+ while (this.index < end) {
1318
+ let c = this.source.charCodeAt(this.index++);
1278
1319
  if (c == 92) {
1279
- if (this.index >= this.source.length) { this.failed = true; return "" }
1280
- c = this.source.charCodeAt(this.index); this.index += 1
1281
- if (c == 34 || c == 92 || c == 47) output += String.fromCharCode(c)
1282
- else if (c == 98) output += String.fromCharCode(8)
1283
- else if (c == 102) output += String.fromCharCode(12)
1284
- else if (c == 110) output += "\n"
1285
- else if (c == 114) output += "\r"
1286
- else if (c == 116) output += "\t"
1320
+ if (this.index >= end) { this.failed = true; return "" }
1321
+ c = this.source.charCodeAt(this.index++);
1322
+ if (c == 34 || c == 92 || c == 47) {}
1323
+ else if (c == 98) c = 8;
1324
+ else if (c == 102) c = 12;
1325
+ else if (c == 110) c = 10;
1326
+ else if (c == 114) c = 13;
1327
+ else if (c == 116) c = 9;
1287
1328
  else if (c == 117) {
1288
- if (this.index + 4 > this.source.length) { this.failed = true; return "" }
1289
- let code = 0
1290
- for (let digitIndex = 0; digitIndex < 4; digitIndex += 1) {
1291
- const digitCode = this.source.charCodeAt(this.index + digitIndex)
1292
- let digit = -1
1293
- if (digitCode >= 48 && digitCode <= 57) digit = digitCode - 48
1294
- else if (digitCode >= 65 && digitCode <= 70) digit = digitCode - 55
1295
- else if (digitCode >= 97 && digitCode <= 102) digit = digitCode - 87
1329
+ if (this.index + 4 > end) { this.failed = true; return "" }
1330
+ c = 0;
1331
+ for (let i = 0; i < 4; i++) {
1332
+ const d = this.source.charCodeAt(this.index++);
1333
+ const digit = d >= 48 && d <= 57 ? d - 48 : d >= 65 && d <= 70 ? d - 55 : d >= 97 && d <= 102 ? d - 87 : -1;
1296
1334
  if (digit < 0) { this.failed = true; return "" }
1297
- code = (code << 4) | digit
1335
+ c = (c << 4) | digit;
1298
1336
  }
1299
- this.index += 4; output += String.fromCharCode(code)
1300
1337
  } else { this.failed = true; return "" }
1301
1338
  } else if (c < 32) { this.failed = true; return "" }
1302
- else output += String.fromCharCode(c)
1339
+ output[count++] = u16(c);
1303
1340
  }
1304
- this.failed = true; return ""
1341
+ this.index = end + 1;
1342
+ return String.UTF16.decodeUnsafe(output.dataStart, count * 2);
1305
1343
  }
1306
1344
  numberValue(): i32 {
1307
1345
  const start = this.index
@@ -1350,6 +1388,7 @@ class __PulseJsonParser {
1350
1388
  while (!this.failed) {
1351
1389
  this.skip(); if (this.index >= this.source.length || this.source.charCodeAt(this.index) != 34) { this.failed = true; break }
1352
1390
  const key = this.string(); this.skip()
1391
+ if (this.conditionalKv && __pulse_fastly_find(__pulse_fastly_value(output), key) >= 0) { this.failed = true; break }
1353
1392
  if (this.index >= this.source.length || this.source.charCodeAt(this.index) != 58) { this.failed = true; break }
1354
1393
  this.index += 1; host_value_object_set(output, __pulse_fastly_string_value(key), this.value(depth)); this.skip()
1355
1394
  if (this.index >= this.source.length) { this.failed = true; break }
@@ -1411,9 +1450,37 @@ function __pulse_fastly_redact(input: string): string { let output = input; for
1411
1450
  function host_log(level: i32, messageHandle: i32): void { let endpoint = __pulse_fastly_log_handle; if (endpoint == -2) return; if (endpoint == -1) { const name = __pulse_fastly_utf8("stdout"); const out = __pulse_fastly_out_i32(); if (fastly_log_endpoint_get(changetype<usize>(name), name.byteLength, changetype<usize>(out)) != FASTLY_STATUS_OK) { __pulse_fastly_log_handle = -2; return } endpoint = __pulse_fastly_out_value(out); __pulse_fastly_log_handle = endpoint } let label = "unknown"; if (level == 1) label = "error"; else if (level == 2) label = "warn"; else if (level == 3) label = "info"; else if (level == 4) label = "debug"; const bytes = __pulse_fastly_utf8("[pulse:" + label + "] " + __pulse_fastly_redact(__pulse_fastly_string(messageHandle)) + "\n"); const written = __pulse_fastly_out_i32(); fastly_log_write(endpoint, changetype<usize>(bytes), bytes.byteLength, changetype<usize>(written)) }
1412
1451
  function __pulse_fastly_number(handle: i32): f64 { const value = __pulse_fastly_value(handle); if (value.kind == PULSE_VALUE_NUMBER) return value.number; if (value.kind == PULSE_VALUE_BOOLEAN) return value.boolean != 0 ? 1.0 : 0.0; if (value.kind == PULSE_VALUE_STRING) return F64.parseFloat(value.text); return 0.0 }
1413
1452
  function __pulse_fastly_hex(value: i32): string { return String.fromCharCode(value < 10 ? 48 + value : 87 + value) }
1414
- function __pulse_fastly_quote(value: string): string { let out = "\""; for (let i = 0; i < value.length; i += 1) { const c = value.charCodeAt(i); if (c == 34) out += "\\\""; else if (c == 92) out += "\\\\"; else if (c == 8) out += "\\b"; else if (c == 9) out += "\\t"; else if (c == 10) out += "\\n"; else if (c == 12) out += "\\f"; else if (c == 13) out += "\\r"; else if (c < 32) out += "\\u00" + __pulse_fastly_hex((c >> 4) & 15) + __pulse_fastly_hex(c & 15); else out += String.fromCharCode(c) } return out + "\"" }
1453
+ function __pulse_fastly_is_unpaired(value: string, i: i32): bool {
1454
+ const c = value.charCodeAt(i);
1455
+ if (c >= 0xd800 && c <= 0xdbff) return i + 1 >= value.length || value.charCodeAt(i + 1) < 0xdc00 || value.charCodeAt(i + 1) > 0xdfff;
1456
+ if (c >= 0xdc00 && c <= 0xdfff) return i == 0 || value.charCodeAt(i - 1) < 0xd800 || value.charCodeAt(i - 1) > 0xdbff;
1457
+ return false;
1458
+ }
1459
+ function __pulse_fastly_quote(value: string): string {
1460
+ // Count once and write UTF-16 units once. Repeated concatenation can allocate
1461
+ // gigabytes for a valid bounded string when every byte needs JSON escaping.
1462
+ let size = 2;
1463
+ for (let i = 0; i < value.length; i++) {
1464
+ const c = value.charCodeAt(i);
1465
+ size += c == 34 || c == 92 || c == 8 || c == 9 || c == 10 || c == 12 || c == 13 ? 2 : (c < 32 || __pulse_fastly_is_unpaired(value, i)) ? 6 : 1;
1466
+ }
1467
+ const out = new Uint16Array(size); let cursor = 0; out[cursor++] = 34;
1468
+ for (let i = 0; i < value.length; i++) {
1469
+ const c = value.charCodeAt(i);
1470
+ if (c == 34 || c == 92) { out[cursor++] = 92; out[cursor++] = u16(c); }
1471
+ else if (c == 8 || c == 9 || c == 10 || c == 12 || c == 13) {
1472
+ out[cursor++] = 92; out[cursor++] = c == 8 ? 98 : c == 9 ? 116 : c == 10 ? 110 : c == 12 ? 102 : 114;
1473
+ } else if (c < 32 || __pulse_fastly_is_unpaired(value, i)) {
1474
+ out[cursor++] = 92; out[cursor++] = 117;
1475
+ out[cursor++] = u16('0123456789abcdef'.charCodeAt((c >> 12) & 15)); out[cursor++] = u16('0123456789abcdef'.charCodeAt((c >> 8) & 15));
1476
+ out[cursor++] = u16('0123456789abcdef'.charCodeAt((c >> 4) & 15)); out[cursor++] = u16('0123456789abcdef'.charCodeAt(c & 15));
1477
+ } else out[cursor++] = u16(c);
1478
+ }
1479
+ out[cursor] = 34;
1480
+ return String.UTF16.decodeUnsafe(out.dataStart, out.byteLength);
1481
+ }
1415
1482
  function __pulse_fastly_json(handle: i32, depth: i32): string {
1416
- if (depth > 64) { __pulse_fastly_fail(PULSE_ERROR_JSON, 2, -1); return "null" }
1483
+ if (depth > ${plan.effects.some(effect => conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind)) ? 128 : 64}) { __pulse_fastly_fail(PULSE_ERROR_JSON, 2, -1); return "null" }
1417
1484
  const value = __pulse_fastly_value(handle)
1418
1485
  if (value.kind == PULSE_VALUE_UNDEFINED || value.kind == PULSE_VALUE_NULL) return "null"
1419
1486
  if (value.kind == PULSE_VALUE_BOOLEAN) return value.boolean != 0 ? "true" : "false"
@@ -1426,7 +1493,24 @@ function __pulse_fastly_json(handle: i32, depth: i32): string {
1426
1493
  function __pulse_fastly_parse_json(text: string): i32 { const parser = new __PulseJsonParser(text); const value = parser.parse(); if (parser.failed || value <= 0) { __pulse_fastly_fail(PULSE_ERROR_JSON, 3, -1); return 0 } return value }
1427
1494
  function __pulse_fastly_path(uri: string): string { let start = 0; const scheme = uri.indexOf("://"); if (scheme >= 0) { const slash = uri.indexOf("/", scheme + 3); start = slash >= 0 ? slash : uri.length } let end = uri.length; const query = uri.indexOf("?", start); if (query >= 0 && query < end) end = query; const fragment = uri.indexOf("#", start); if (fragment >= 0 && fragment < end) end = fragment; return start >= end ? "/" : uri.substring(start, end) }
1428
1495
  function __pulse_fastly_read_req_string(kind: i32): string { const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES); const written = __pulse_fastly_out_i32(); const status = kind == 0 ? fastly_http_req_method_get(__pulse_fastly_request_handle, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(written)) : fastly_http_req_uri_get(__pulse_fastly_request_handle, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(written)); if (status != FASTLY_STATUS_OK) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 10 + kind, -1); return "" } return __pulse_fastly_decode(buffer, __pulse_fastly_out_value(written)) }
1429
- function __pulse_fastly_read_body(handle: i32, effectIndex: i32): string { let out = ""; let total = 0; while (true) { const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES); const read = __pulse_fastly_out_i32(); const status = fastly_http_body_read(handle, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(read)); if (status != FASTLY_STATUS_OK) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 20, effectIndex); return "" } const count = __pulse_fastly_out_value(read); if (count <= 0) return out; total += count; if (total > __PULSE_SCHEMA_MAX_BYTES && __PULSE_SCHEMA_MAX_BYTES > 0) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 21, effectIndex); return "" } out += __pulse_fastly_decode(buffer, count) } }
1496
+ function __pulse_fastly_read_body(handle: i32, effectIndex: i32): string {
1497
+ const chunks = new Array<Uint8Array>(); let total = 0;
1498
+ while (true) {
1499
+ const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES), read = __pulse_fastly_out_i32();
1500
+ const status = fastly_http_body_read(handle, buffer.dataStart, buffer.length, changetype<usize>(read));
1501
+ if (status != FASTLY_STATUS_OK) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 20, effectIndex); return "" }
1502
+ const count = __pulse_fastly_out_value(read);
1503
+ if (count < 0 || count > buffer.length) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 20, effectIndex); return "" }
1504
+ if (!count) break;
1505
+ total += count;
1506
+ if (total > __PULSE_SCHEMA_MAX_BYTES && __PULSE_SCHEMA_MAX_BYTES > 0) { __pulse_fastly_fail(PULSE_ERROR_SCHEMA, 21, effectIndex); return "" }
1507
+ chunks.push(buffer.subarray(0, count));
1508
+ }
1509
+ // UTF-8 scalars may straddle host read chunks. Decode the bounded body once.
1510
+ const bytes = new Uint8Array(total); let offset = 0;
1511
+ for (let i = 0; i < chunks.length; i++) { bytes.set(chunks[i], offset); offset += chunks[i].length; }
1512
+ return __pulse_fastly_decode(bytes, total);
1513
+ }
1430
1514
  function __pulse_fastly_request_header_text(name: string): string { const nameBytes = __pulse_fastly_utf8(name); const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES); const written = __pulse_fastly_out_i32(); const status = fastly_http_req_header_value_get(__pulse_fastly_request_handle, changetype<usize>(nameBytes), nameBytes.byteLength, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(written)); if (status == FASTLY_STATUS_NONE) return ""; if (status != FASTLY_STATUS_OK) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 22, -1); return "" } return __pulse_fastly_decode(buffer, __pulse_fastly_out_value(written)) }
1431
1515
  function __pulse_fastly_fetch_header_text(responseHandle: i32, name: string): string { const nameBytes = __pulse_fastly_utf8(name); const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES); const written = __pulse_fastly_out_i32(); const status = fastly_http_resp_header_value_get(responseHandle, changetype<usize>(nameBytes), nameBytes.byteLength, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(written)); if (status == FASTLY_STATUS_NONE) return ""; if (status != FASTLY_STATUS_OK) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 23, -1); return "" } return __pulse_fastly_decode(buffer, __pulse_fastly_out_value(written)) }
1432
1516
  function __pulse_fastly_fetch_body(value: __PulseFastlyValue): string { if (value.bodyLoaded == 0) { value.text = __pulse_fastly_read_body(value.bodyHandle, -1); value.bodyLoaded = 1 } return value.text }
@@ -1592,6 +1676,7 @@ function capabilityImports(effect) {
1592
1676
  'wasi_snapshot_preview1.clock_time_get'
1593
1677
  ];
1594
1678
  }
1679
+ if (conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind)) return conditionalKv.kvImports(effect.kind).map(key => key.replace(':', '.'));
1595
1680
  if (effect.kind === 'kv.get') return ['fastly_kv_store.open', 'fastly_kv_store.lookup', 'fastly_kv_store.lookup_wait_v2', 'fastly_http_body.read'];
1596
1681
  if (effect.kind === 'kv.put') return ['fastly_kv_store.open', 'fastly_kv_store.insert', 'fastly_kv_store.insert_wait', 'fastly_http_body.new', 'fastly_http_body.write'];
1597
1682
  if (effect.kind === 'assets.lookup') return ['fastly_kv_store.open', 'fastly_kv_store.lookup', 'fastly_kv_store.lookup_wait_v2', 'fastly_http_body.read'];
@@ -1649,6 +1734,8 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
1649
1734
  es256KeyRecords
1650
1735
  }),
1651
1736
  jwtSource ? jwtSource.source : '',
1737
+ require('./s3-native.js').s3NativeSource(plan, bindings),
1738
+ conditionalKv.kvNativeSource(plan),
1652
1739
  portableSource,
1653
1740
  driverSource(plan, { guestLinked: facts.guestUnits.length > 0 }),
1654
1741
  ''
@@ -1720,8 +1807,8 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
1720
1807
  providerNeutralInput: true,
1721
1808
  javascriptRuntime: false,
1722
1809
  jsComputeRuntime: false,
1723
- wasi: hasJwt ? 'clock_time_get only' : false,
1724
- effects: 'fetch, config.get, secret.get, kv.get, kv.put, assets.lookup, grip.channel, grip.hold, grip.publish, grip.broadcast, jwt.verify',
1810
+ wasi: hasJwt || bindings.s3.length || plan.effects.some(effect => conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind)) ? 'clock_time_get only' : false,
1811
+ effects: FASTLY_NATIVE_PLATFORM_EFFECT_KINDS.join(', '),
1725
1812
  continuations: true,
1726
1813
  groupedEffects: 'existing fetch start-before-wait ordering remains intact',
1727
1814
  config: 'Fastly Config Store open/get with undefined for missing values',
@@ -1729,7 +1816,7 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
1729
1816
  jwt: hasJwt
1730
1817
  ? `package-owned compact-JWS and registered-claim semantics composed with ${jwtCrypto.realization}; provider-owned request, clock, schema, and transport`
1731
1818
  : 'inactive',
1732
- kv: 'Fastly KV Store async lookup/insert with Pulse JSON envelope compatibility',
1819
+ kv: 'Fastly KV Store async lookup/insert; conditional operations preserve u64 generations, bounded readiness, strict Pulse envelopes and uncertain dispatch',
1733
1820
  assets: 'Fastly KV Store lookup with provider-owned content type, cache headers, and GET/HEAD body ownership',
1734
1821
  grip: 'canonical stateless framing plus bound, secret-referenced broadcast HTTP realization and stable acknowledgement',
1735
1822
  logging: 'best-effort redacted writes to the provider-owned stdout logging endpoint',
@@ -0,0 +1,215 @@
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
+ contentType: string = ''; endpoint: string = ''; bucket: string = ''; region: string = ''; backend: string = '';
14
+ id: string = ''; secret: string = ''; token: string = ''; max: i32 = 32768; timeout: i32 = 10000;
15
+ }
16
+ @lazy const __s3_dispatched = new StaticArray<bool>(PULSE_FASTLY_EFFECT_COUNT)
17
+ @lazy const __s3_sent_bytes = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
18
+ @lazy const __s3_digests = new Array<string>(PULSE_FASTLY_EFFECT_COUNT).fill('')
19
+ @lazy const __s3_deadlines = new StaticArray<i64>(PULSE_FASTLY_EFFECT_COUNT)
20
+ function __s3_clock(id: i32): i64 {
21
+ const out = new StaticArray<i64>(1);
22
+ return wasi_snapshot_preview1_clock_time_get(id, 1000000, changetype<usize>(out)) == 0 ? out[0] : -1;
23
+ }
24
+ function __s3_remaining(index: i32): i32 {
25
+ const now = __s3_clock(1);
26
+ if (now < 0) return 0;
27
+ const ns = __s3_deadlines[index] - now;
28
+ return ns <= 0 ? 0 : i32((ns + 999999) / 1000000);
29
+ }
30
+ function __s3_set(object: i32, name: string, value: i32): void { host_value_object_set(object, __pulse_fastly_string_value(name), value) }
31
+ function __s3_result(status: string, reason: string = '', http: i32 = 0): i32 {
32
+ const result = host_value_object(); __s3_set(result, 'status', __pulse_fastly_string_value(status));
33
+ if (reason.length) __s3_set(result, 'reason', __pulse_fastly_string_value(reason));
34
+ if (http > 0) { const value = new __PulseFastlyValue(); value.kind = PULSE_VALUE_NUMBER; value.number = f64(http); __s3_set(result, 'httpStatus', __pulse_fastly_put(value)); }
35
+ return result;
36
+ }
37
+ function __s3_failure(index: i32, reason: string, http: i32 = 0): i32 {
38
+ return __s3_result(__pulse_fastly_effect_kind(index) == 14 ? (__s3_dispatched[index] ? 'unknown' : 'not-stored') : 'failed', reason, http);
39
+ }
40
+ function __s3_fail(index: i32, reason: string): void { __pulse_fastly_ready_effect(index, __s3_failure(index, reason)) }
41
+ function __s3_secret(store: i32, name: string): string | null {
42
+ const key = __pulse_s3_bytes(name), out = new StaticArray<i32>(1), size = new StaticArray<i32>(1), bytes = new Uint8Array(4096);
43
+ if (fastly_secret_store_get(store, key.dataStart, key.length, changetype<usize>(out)) != 0) return null;
44
+ const status = fastly_secret_store_plaintext(out[0], bytes.dataStart, bytes.length, changetype<usize>(size));
45
+ let result: string | null = null;
46
+ if (status == 0 && size[0] > 0 && size[0] <= bytes.length) {
47
+ const raw = bytes.subarray(0, size[0]);
48
+ if (__pulse_s3_utf8_valid(raw)) result = String.UTF8.decodeUnsafe(raw.dataStart, raw.length, false);
49
+ }
50
+ bytes.fill(0);
51
+ if (result !== null) __pulse_fastly_remember_secret(result);
52
+ return result;
53
+ }
54
+ function __s3_header(request: i32, name: string, value: string): bool {
55
+ const n = __pulse_s3_bytes(name), v = __pulse_s3_bytes(value);
56
+ return fastly_http_req_header_insert(request, n.dataStart, n.length, v.dataStart, v.length) == 0;
57
+ }
58
+ function __pulse_fastly_s3_begin(index: i32, outer: __PulseFastlyValue): void {
59
+ __s3_dispatched[index] = false; __s3_sent_bytes[index] = 0; __s3_digests[index] = '';
60
+ const put = __pulse_fastly_effect_kind(index) == 14;
61
+ const payload = __pulse_fastly_value(__pulse_fastly_payload_field(outer, 'invocation'));
62
+ const input = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'key'));
63
+ if (input.kind != PULSE_VALUE_STRING) { __s3_fail(index, 'invalid-key'); return; }
64
+ const encoded = __pulse_s3_key(input.text);
65
+ if (encoded === null) { __s3_fail(index, 'invalid-key'); return; }
66
+ // Package effect payloads and signed paths must not enter provider logs.
67
+ __pulse_fastly_remember_secret(input.text);
68
+ const binding = __pulse_fastly_s3_binding(index), start = __s3_clock(1);
69
+ if (start < 0) { __s3_fail(index, 'configuration'); return; }
70
+ __s3_deadlines[index] = start + i64(binding.timeout) * 1000000;
71
+ let data = new Uint8Array(0);
72
+ if (put) {
73
+ 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; }
75
+ data = __pulse_s3_bytes(text.text);
76
+ if (data.length > binding.max) { __s3_fail(index, 'too-large'); return; }
77
+ __pulse_fastly_remember_secret(text.text);
78
+ }
79
+ const storeName = __pulse_s3_bytes(__pulse_fastly_secret_store(index)), store = new StaticArray<i32>(1);
80
+ if (fastly_secret_store_open(storeName.dataStart, storeName.length, changetype<usize>(store)) != 0) { __s3_fail(index, 'credentials'); return; }
81
+ const id = __s3_secret(store[0], binding.id), secret = __s3_secret(store[0], binding.secret);
82
+ const token = binding.token.length ? __s3_secret(store[0], binding.token) : '';
83
+ if (__s3_remaining(index) == 0) { __s3_fail(index, 'timeout'); return; }
84
+ 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;
86
+ const now = __s3_clock(0);
87
+ 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);
89
+ __s3_sent_bytes[index] = data.length; __s3_digests[index] = signature.digest;
90
+ __pulse_fastly_remember_secret(signature.digest);
91
+ __pulse_fastly_remember_secret(signature.authorization); __pulse_fastly_remember_secret(binding.endpoint + path);
92
+ if (__s3_remaining(index) == 0) { __s3_fail(index, 'timeout'); return; }
93
+ const request = new StaticArray<i32>(1), body = new StaticArray<i32>(1), pending = new StaticArray<i32>(1);
94
+ if (fastly_http_req_new(changetype<usize>(request)) != 0) { __s3_fail(index, 'transport'); return; }
95
+ const req = request[0], url = __pulse_s3_bytes(binding.endpoint + path), verb = __pulse_s3_bytes(method), backend = __pulse_s3_bytes(binding.backend);
96
+ let ok = fastly_http_req_uri_set(req, url.dataStart, url.length) == 0 && fastly_http_req_method_set(req, verb.dataStart, verb.length) == 0
97
+ && __s3_cache(req, 1, 0, 0) == 0 && __s3_decompress(req, 0) == 0
98
+ && __s3_header(req, 'host', binding.endpoint.slice(8)) && __s3_header(req, 'x-amz-date', signature.date)
99
+ && __s3_header(req, 'x-amz-content-sha256', signature.digest) && __s3_header(req, 'authorization', signature.authorization)
100
+ && __s3_header(req, 'accept-encoding', 'identity');
101
+ if (ok && put) ok = __s3_header(req, 'content-type', binding.contentType) && __s3_header(req, 'content-length', data.length.toString());
102
+ if (ok && token.length) ok = __s3_header(req, 'x-amz-security-token', token);
103
+ if (!ok || fastly_http_body_new(changetype<usize>(body)) != 0) { __s3_close_req(req); __s3_fail(index, 'transport'); return; }
104
+ let offset = 0; const written = new StaticArray<i32>(1);
105
+ while (offset < data.length) {
106
+ if (__s3_remaining(index) == 0) { __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'timeout'); return; }
107
+ 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) {
108
+ __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'transport'); return;
109
+ }
110
+ offset += written[0];
111
+ }
112
+ if (__s3_remaining(index) == 0) { __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'timeout'); return; }
113
+ __s3_dispatched[index] = true;
114
+ if (fastly_http_req_send_async(req, body[0], backend.dataStart, backend.length, changetype<usize>(pending)) != 0) {
115
+ __s3_close_req(req); __s3_close_body(body[0]); __s3_fail(index, 'transport'); return;
116
+ }
117
+ // send_async consumes request/body. Pending handles have no cancel hostcall.
118
+ __pulse_fastly_pending[index] = pending[0]; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_ASYNC;
119
+ }
120
+ function __s3_wait_ready(index: i32, handle: i32): i32 {
121
+ const timeout = __s3_remaining(index); if (timeout == 0) return 0;
122
+ const handles = new StaticArray<i32>(1), done = new StaticArray<i32>(1); handles[0] = handle;
123
+ if (__s3_select(changetype<usize>(handles), 1, timeout, changetype<usize>(done)) != 0) return -1;
124
+ return done[0] == -1 || __s3_remaining(index) == 0 ? 0 : 1;
125
+ }
126
+ function __s3_list(buffer: Uint8Array, size: i32): Array<string> | null {
127
+ if (size < 0 || size > buffer.length || (size > 0 && buffer[size - 1] != 0)) return null;
128
+ const result = new Array<string>(); let start = 0;
129
+ for (let i = 0; i < size; i++) if (buffer[i] == 0) {
130
+ const bytes = buffer.subarray(start, i); if (!__pulse_s3_utf8_valid(bytes)) return null;
131
+ result.push(String.UTF8.decodeUnsafe(bytes.dataStart, bytes.length, false)); start = i + 1;
132
+ }
133
+ return result;
134
+ }
135
+ function __s3_read_response(index: i32, response: i32, body: i32, status: i32): i32 {
136
+ const put = __pulse_fastly_effect_kind(index) == 14;
137
+ const rejected = put && status >= 400 && status <= 499 && status != 408;
138
+ if (!put && status == 404) return __s3_result('not-found');
139
+ if (status != 200 && !rejected) return __s3_failure(index, __pulse_s3_status_reason(status), status);
140
+ const buffer = new Uint8Array(16384), end = new StaticArray<i64>(1), size = new StaticArray<i32>(1);
141
+ 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);
142
+ const names = __s3_list(buffer, size[0]); if (names === null) return __s3_failure(index, 'protocol', status);
143
+ let total = 0, length: f64 = -1; const seen = new Set<string>();
144
+ const result = __s3_result(put ? 'stored' : 'found');
145
+ for (let i = 0; i < names.length; i++) {
146
+ const rawName = names[i], name = rawName.toLowerCase(), bytes = __pulse_s3_bytes(rawName);
147
+ 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);
148
+ const values = __s3_list(buffer, size[0]); if (values === null) return __s3_failure(index, 'protocol', status);
149
+ const selected = name == 'content-length' || name == 'content-type' || name == 'etag' || name == 'content-encoding';
150
+ if (selected && (seen.has(name) || values.length != 1)) return __s3_failure(index, 'protocol', status);
151
+ seen.add(name);
152
+ for (let v = 0; v < values.length; v++) {
153
+ const value = values[v], count = __pulse_s3_bytes(value).length;
154
+ total += bytes.length + count + 4; if (total > 16384) return __s3_failure(index, 'protocol', status);
155
+ if (!selected) continue;
156
+ if (count > 1024 || !__pulse_s3_scalar(value, true)) return __s3_failure(index, 'protocol', status);
157
+ if (name == 'content-encoding' && value.toLowerCase() != 'identity') return __s3_failure(index, 'protocol', status);
158
+ if (name == 'content-length') {
159
+ if (!value.length || (value.length > 1 && value.charCodeAt(0) == 48)) return __s3_failure(index, 'protocol', status);
160
+ length = 0;
161
+ 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); }
162
+ }
163
+ if (name == 'etag') { if (!value.length) return __s3_failure(index, 'protocol', status); __s3_set(result, 'etag', __pulse_fastly_string_value(value)); }
164
+ if (name == 'content-type' && !put) __s3_set(result, 'contentType', __pulse_fastly_string_value(value));
165
+ }
166
+ }
167
+ if (__s3_remaining(index) == 0) return __s3_failure(index, 'timeout', status);
168
+ const head = __pulse_fastly_effect_kind(index) == 12, binding = __pulse_fastly_s3_binding(index);
169
+ if (head && length < 0) return __s3_failure(index, 'protocol', status);
170
+ if (!head) {
171
+ const maximum = put && !rejected ? 0 : binding.max;
172
+ if (length > maximum) return __s3_failure(index, put ? 'protocol' : 'too-large', status);
173
+ const data = new Uint8Array(maximum + 1); let count = 0;
174
+ while (true) {
175
+ const ready = __s3_wait_ready(index, body);
176
+ if (ready <= 0) return __s3_failure(index, ready == 0 ? 'timeout' : 'transport', status);
177
+ if (fastly_http_body_read(body, data.dataStart + count, data.length - count, changetype<usize>(size)) != 0) return __s3_failure(index, 'protocol', status);
178
+ if (size[0] < 0 || size[0] > data.length - count) return __s3_failure(index, 'protocol', status);
179
+ if (size[0] == 0) break;
180
+ count += size[0]; if (count > maximum) return __s3_failure(index, put ? 'protocol' : 'too-large', status);
181
+ }
182
+ if (length >= 0 && length != count) return __s3_failure(index, 'protocol', status);
183
+ if (put) {
184
+ if (__s3_remaining(index) == 0) return __s3_failure(index, 'timeout', status);
185
+ if (rejected) return __s3_result('not-stored', status == 401 || status == 403 ? 'not-authorized' : status == 429 ? 'throttled' : 'rejected', status);
186
+ const sent = new __PulseFastlyValue(); sent.kind = PULSE_VALUE_NUMBER; sent.number = f64(__s3_sent_bytes[index]);
187
+ __s3_set(result, 'byteLength', __pulse_fastly_put(sent));
188
+ __s3_set(result, 'sha256', __pulse_fastly_string_value(__s3_digests[index]));
189
+ return result;
190
+ }
191
+ length = f64(count); const raw = data.subarray(0, count), digest = __pulse_s3_hex(__pulse_s3_sha(raw));
192
+ if (!__pulse_s3_utf8_valid(raw)) return __s3_failure(index, 'invalid-utf8', status);
193
+ __s3_set(result, 'text', __pulse_fastly_string_value(String.UTF8.decodeUnsafe(raw.dataStart, raw.length, false)));
194
+ __s3_set(result, 'sha256', __pulse_fastly_string_value(digest));
195
+ }
196
+ if (__s3_remaining(index) == 0) return __s3_failure(index, 'timeout', status);
197
+ const sizeValue = new __PulseFastlyValue(); sizeValue.kind = PULSE_VALUE_NUMBER; sizeValue.number = length;
198
+ __s3_set(result, 'byteLength', __pulse_fastly_put(sizeValue)); return result;
199
+ }
200
+ function __pulse_fastly_s3_wait(index: i32): i32 {
201
+ if (__pulse_fastly_pending_mode[index] == PULSE_FASTLY_PENDING_READY) return __pulse_fastly_wait_ready(index);
202
+ const pending = __pulse_fastly_pending[index], detail = new StaticArray<i32>(4), done = new StaticArray<i32>(1);
203
+ const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1), code = new StaticArray<i32>(1);
204
+ __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_NONE; __pulse_fastly_pending[index] = 0;
205
+ while (true) {
206
+ const ready = __s3_wait_ready(index, pending);
207
+ if (ready <= 0) return __s3_failure(index, ready == 0 ? 'timeout' : 'transport');
208
+ if (__s3_poll(pending, changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body)) != 0) return __s3_failure(index, 'transport');
209
+ if (done[0] != 0) break;
210
+ }
211
+ let result: i32;
212
+ if (fastly_http_resp_status_get(response[0], changetype<usize>(code)) != 0) result = __s3_failure(index, 'protocol');
213
+ 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;
215
+ }
@@ -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({ 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'), lines.join('\n')].join('\n');
29
+ }
30
+ module.exports = { S3_IMPORTS, s3NativeSource };