@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.
- package/README.md +7 -6
- package/dist/config-schema.json +4 -0
- package/package.json +11 -10
- package/src/build/kv-native.as.ts +229 -0
- package/src/build/kv-native.js +20 -0
- package/src/build/native-platform-capabilities.js +129 -42
- package/src/build/s3-native.as.ts +215 -0
- package/src/build/s3-native.js +30 -0
- package/src/config-schema.json +4 -0
- package/src/javascript/target-support-policy.js +4 -0
- package/src/provider-contract.js +9 -2
- package/src/runtime/canonical-api-runtime.js +4 -0
- package/src/testing/conditional-kv-host.js +109 -0
- package/src/testing/native-platform-capabilities-host.js +90 -13
- package/src/toolchain/config.js +3 -0
- package/src/toolchain/index.js +3 -1
- package/src/toolchain/s3.js +26 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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 >=
|
|
1280
|
-
c = this.source.charCodeAt(this.index);
|
|
1281
|
-
if (c == 34 || c == 92 || c == 47)
|
|
1282
|
-
else if (c == 98)
|
|
1283
|
-
else if (c == 102)
|
|
1284
|
-
else if (c == 110)
|
|
1285
|
-
else if (c == 114)
|
|
1286
|
-
else if (c == 116)
|
|
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 >
|
|
1289
|
-
|
|
1290
|
-
for (let
|
|
1291
|
-
const
|
|
1292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1339
|
+
output[count++] = u16(c);
|
|
1303
1340
|
}
|
|
1304
|
-
this.
|
|
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
|
|
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 {
|
|
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: '
|
|
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
|
|
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 };
|