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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +7 -6
  2. package/dist/config-schema.json +30 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +11 -10
  8. package/src/build/canonical-target.js +41 -2
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +51 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/entities-native.js +6 -11
  13. package/src/build/host-extensions.js +254 -0
  14. package/src/build/kv-native.as.ts +331 -0
  15. package/src/build/kv-native.js +27 -0
  16. package/src/build/lifecycle.js +50 -0
  17. package/src/build/native-application-errors.js +159 -0
  18. package/src/build/native-binary-resources.js +30 -0
  19. package/src/build/native-body-forwarding.as.ts +325 -0
  20. package/src/build/native-body-forwarding.js +73 -0
  21. package/src/build/native-dynamic-backends.as.ts +115 -0
  22. package/src/build/native-dynamic-backends.js +52 -0
  23. package/src/build/native-fetch-accounting.as.ts +133 -0
  24. package/src/build/native-fetch-accounting.js +84 -0
  25. package/src/build/native-fetch-bounds.js +47 -0
  26. package/src/build/native-fetch-cohorts.as.ts +126 -0
  27. package/src/build/native-fetch-cohorts.js +65 -0
  28. package/src/build/native-fetch-deadlines.as.ts +169 -0
  29. package/src/build/native-fetch-deadlines.js +36 -0
  30. package/src/build/native-fetch-errors.js +38 -0
  31. package/src/build/native-fetch-headers.js +52 -0
  32. package/src/build/native-fetch-outcomes.js +87 -0
  33. package/src/build/native-fetch-timing.as.ts +74 -0
  34. package/src/build/native-fetch-timing.js +50 -0
  35. package/src/build/native-forwarding-accounting.as.ts +153 -0
  36. package/src/build/native-forwarding-accounting.js +47 -0
  37. package/src/build/native-handle-ownership.js +127 -0
  38. package/src/build/native-header-lookup.js +50 -0
  39. package/src/build/native-http-effects.js +164 -35
  40. package/src/build/native-platform-capabilities.js +815 -220
  41. package/src/build/native-request-body.js +45 -0
  42. package/src/build/native-request-headers.js +93 -0
  43. package/src/build/native-send-error-detail.as.ts +12 -0
  44. package/src/build/native-stdio.js +45 -0
  45. package/src/build/native-string-values.js +143 -0
  46. package/src/build/native-transport-details.as.ts +84 -0
  47. package/src/build/native-value-budget.js +90 -0
  48. package/src/build/package-native-application.js +114 -0
  49. package/src/build/request-budget-source.js +58 -0
  50. package/src/build/request-budget.js +46 -0
  51. package/src/build/response-authority.js +56 -0
  52. package/src/build/s3-body.as.ts +97 -0
  53. package/src/build/s3-native.as.ts +228 -0
  54. package/src/build/s3-native.js +30 -0
  55. package/src/build/schema-admission.js +83 -0
  56. package/src/build/schema-nested-json.js +93 -0
  57. package/src/build/schema-scalar-record.js +41 -0
  58. package/src/build/time-native.js +49 -0
  59. package/src/build/wasm-budget.js +28 -0
  60. package/src/config-api.js +6 -0
  61. package/src/config-schema.json +30 -0
  62. package/src/index.ts +25 -0
  63. package/src/javascript/fetch-adapter.js +3 -0
  64. package/src/javascript/jwt-verifier.js +14 -4
  65. package/src/javascript/logging.js +11 -2
  66. package/src/javascript/package-effects.js +5 -2
  67. package/src/javascript/runtime-host.js +8 -3
  68. package/src/javascript/source-package.js +4 -2
  69. package/src/javascript/target-support-policy.js +20 -3
  70. package/src/javascript/target.js +17 -1
  71. package/src/javascript/test-runtime.js +1 -0
  72. package/src/provider-contract.js +23 -6
  73. package/src/runtime/canonical-api-runtime.js +6 -0
  74. package/src/testing/conditional-kv-host.js +155 -0
  75. package/src/testing/native-dynamic-backends-host.js +39 -0
  76. package/src/testing/native-http-effects-host.js +18 -1
  77. package/src/testing/native-platform-capabilities-host.js +359 -24
  78. package/src/toolchain/body-forwarding.js +25 -0
  79. package/src/toolchain/config.js +16 -0
  80. package/src/toolchain/index.js +49 -5
  81. package/src/toolchain/logging.js +18 -0
  82. package/src/toolchain/native-execution.js +83 -0
  83. package/src/toolchain/s3.js +26 -0
@@ -14,6 +14,16 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
14
14
  crypto: defineCryptoTargetCapabilities({
15
15
  target: 'javascript',
16
16
  algorithms: [{
17
+ algorithm: 'HMAC-SHA256',
18
+ realization: 'runtime-builtin',
19
+ implemented: true,
20
+ status: 'implemented-jwt-sign'
21
+ }, {
22
+ algorithm: 'SHA-256',
23
+ realization: 'runtime-builtin',
24
+ implemented: true,
25
+ status: 'implemented-d1'
26
+ }, {
17
27
  algorithm: 'HS256',
18
28
  realization: 'runtime-builtin',
19
29
  implemented: true,
@@ -23,6 +33,11 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
23
33
  realization: 'runtime-builtin',
24
34
  implemented: true,
25
35
  status: 'implemented-h4'
36
+ }, {
37
+ algorithm: 'RS256',
38
+ realization: 'runtime-builtin',
39
+ implemented: true,
40
+ status: 'implemented-h4'
26
41
  }]
27
42
  }),
28
43
  capabilities: {
@@ -45,7 +60,7 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
45
60
  kv: true,
46
61
  parallel: true
47
62
  },
48
- packages: ['pulse.assets', 'pulse.grip', 'pulse.jwt'],
63
+ packages: ['pulse.assets', 'pulse.crypto', 'pulse.grip', 'pulse.jwt'],
49
64
  restrictions: {
50
65
  fetch: 'origin-backend-map-or-dynamic-opt-in',
51
66
  kv: 'logical-store-map-required',
@@ -57,6 +72,7 @@ const FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor(
57
72
  supported: true,
58
73
  default: 'info',
59
74
  levels: ['off', 'error', 'warn', 'info', 'debug'],
75
+ destinations: ['stdout', 'configured-named-endpoint'],
60
76
  destination: 'provider-owned',
61
77
  betaDefault: 'stdio-log-tail'
62
78
  },
@@ -331,6 +331,7 @@ async function executeFastlyJavascriptLocalRequest(application, request, options
331
331
  maxRequestBodyBytes: options.maxRequestBodyBytes ?? options.maxBodyBytes,
332
332
  maxFetchBodyBytes: options.maxFetchBodyBytes ?? options.maxBodyBytes,
333
333
  maxStructuredBodyBytes: options.maxStructuredBodyBytes,
334
+ binaryResources: options.binaryResources,
334
335
  schemaCodecs: options.schemaCodecs,
335
336
  strict: options.strict === true,
336
337
  reporting: options.reporting,
@@ -12,27 +12,41 @@ const FASTLY_PROVIDER_DESCRIPTOR = canonical.normalizeDescriptor({
12
12
  deployable: true,
13
13
  localExecution: true,
14
14
  capabilities: [
15
- 'request', 'response.json', 'response.text', 'response.custom', 'fetch',
16
- 'config.get', 'secret.get', 'kv.get', 'kv.put', 'assets.lookup',
17
- 'grip.channel', 'grip.hold', 'grip.publish', 'grip.broadcast', 'jwt.verify',
18
- 'opaque.pass-through'
15
+ 'host.extension', 'request', 'request.body.forward', 'response.json', 'response.text', 'response.custom', 'fetch',
16
+ 'time.wall-clock', 'crypto.digestText', 'config.get', 'secret.get', 'kv.get', 'kv.put', 'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'kv.list', 'kv.delete', 'assets.lookup',
17
+ 'grip.channel', 'grip.hold', 'grip.publish', 'grip.broadcast', 'jwt.verify', 'jwt.sign',
18
+ 'opaque.pass-through', 's3.head', 's3.getText', 's3.putText', 's3.getBody'
19
19
  ],
20
20
  lowering: {
21
+ 'host.extension': 'fastly.native.terminal-request-backend.v1',
21
22
  request: 'fastly.compute.request',
23
+ 'request.body.forward': 'fastly.native.body-forwarding',
22
24
  'response.json': 'fastly.compute.response.json',
23
25
  'response.text': 'fastly.compute.response.text',
24
26
  'response.custom': 'fastly.compute.response.custom',
25
27
  fetch: 'fastly.backend.fetch',
28
+ 'time.wall-clock': 'wasi.clock_time_get.realtime',
29
+ 'crypto.digestText': 'pulse.crypto.guest-source.sha256',
26
30
  'config.get': 'fastly.config-store.get',
27
31
  'secret.get': 'fastly.secret-store.get',
28
32
  'kv.get': 'fastly.kv-store.get',
29
33
  'kv.put': 'fastly.kv-store.put',
34
+ 'kv.list': 'fastly.native.kv-store.list',
35
+ 'kv.delete': 'fastly.native.kv-store.delete',
36
+ 'kv.getVersioned': 'fastly.native.kv-store.getVersioned',
37
+ 'kv.insertIfAbsent': 'fastly.native.kv-store.insertIfAbsent',
38
+ 'kv.compareAndSwap': 'fastly.native.kv-store.compareAndSwap',
30
39
  'assets.lookup': 'fastly.kv-store.assets.lookup',
31
40
  'grip.channel': 'fastly.fanout.channel',
32
41
  'grip.hold': 'fastly.fanout.hold',
33
42
  'grip.publish': 'fastly.fanout.publish',
34
43
  'grip.broadcast': 'fastly.grip.publish-control',
35
44
  'jwt.verify': 'fastly.native.jwt.verify',
45
+ 'jwt.sign': 'fastly.native.jwt.sign',
46
+ 's3.head': 'fastly.native.s3.head',
47
+ 's3.getText': 'fastly.native.s3.getText',
48
+ 's3.getBody': 'fastly.native.s3.getBody',
49
+ 's3.putText': 'fastly.native.s3.putText',
36
50
  'opaque.pass-through': 'fastly.response-body.stream'
37
51
  }
38
52
  });
@@ -52,9 +66,12 @@ function validateStaticBackendBindings(metadata = {}, bindings = {}) {
52
66
  }
53
67
  }
54
68
 
55
- function createFastlyLoweringPlan(metadata, bindings = {}) {
69
+ function createFastlyLoweringPlan(metadata, bindings = {}, options = {}) {
70
+ const forwarding = require('./toolchain/body-forwarding.js').normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs);
71
+ const descriptor = forwarding ? FASTLY_PROVIDER_DESCRIPTOR : {...FASTLY_PROVIDER_DESCRIPTOR, capabilities: FASTLY_PROVIDER_DESCRIPTOR.capabilities.filter(id => id !== 'request.body.forward')};
72
+ require('./toolchain/s3.js').resolveFastlyS3(metadata.providerOperations || [], bindings.s3);
56
73
  validateStaticBackendBindings(metadata, bindings);
57
- return canonical.createProviderLoweringPlan(metadata, FASTLY_PROVIDER_DESCRIPTOR, {
74
+ return canonical.createProviderLoweringPlan(metadata, descriptor, {
58
75
  ...bindings,
59
76
  grip: bindings.grip || 'fastly-fanout'
60
77
  });
@@ -337,9 +337,15 @@ function createFastlyProviderAdapter(baseOptions = {}) {
337
337
  return Object.freeze({
338
338
  id: 'fastly',
339
339
  version: CANONICAL_FASTLY_RUNTIME_VERSION,
340
+ // This legacy JavaScript fixture surface has no Native KV host ABI. Keep
341
+ // that absence pre-dispatch/configuration, rather than pretending a failed
342
+ // fixture dispatch might have mutated storage. Real K3 evidence runs Wasm.
343
+ prepareConditionalKv() { return undefined; },
340
344
  async dispatchEffect(effect, executionOptions = {}) {
341
345
  const state = stateFor(executionOptions);
342
346
  if (effect.kind === 'fetch') return dispatchFetch(effect, executionOptions);
347
+ if (effect.kind === 'crypto.digestText') return require('@pulse-compute/crypto/provider').createJavascriptTextDigest(baseOptions)(effect, executionOptions);
348
+ if (effect.kind === 'time.now') return require('@pulse-compute/runtime/host').readWallTime(baseOptions.wallClock === undefined ? () => Date.now() : baseOptions.wallClock);
343
349
  if (effect.kind === 'config.get') return state.surface.config.adapter.get(effect.name);
344
350
  if (effect.kind === 'secret.get') {
345
351
  const secret = state.surface.secret.adapter.get(effect.name);
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ // Injected ABI authority for real generated Wasm evidence, never a service adapter.
4
+ // Raw persisted bytes and u64 generations remain independent of the Pulse codec.
5
+ function createConditionalKvAuthority() {
6
+ const stores = new Map(); let next = 1n;
7
+ return {
8
+ stores,
9
+ seed(store, key, wire, generation = next++) {
10
+ if (!stores.has(store)) stores.set(store, new Map());
11
+ stores.get(store).set(key, { wire: Buffer.from(wire), generation: BigInt(generation) });
12
+ },
13
+ get(store, key) { return stores.get(store)?.get(key); },
14
+ delete(store, key) { return stores.get(store).delete(key) ? 1 : 3; },
15
+ list(store, { limit, prefix = '', cursor }) {
16
+ // Test authority models a live ordered page independently of Pulse's parser.
17
+ let after;
18
+ if (cursor !== undefined) {
19
+ if (!/^abi-kv:/.test(cursor)) return { code: 2 };
20
+ try { const token = JSON.parse(Buffer.from(cursor.slice(7), 'base64url')); if (token.store !== store || token.prefix !== prefix || typeof token.after !== 'string') return { code: 2 }; after = token.after; }
21
+ catch (_) { return { code: 2 }; }
22
+ }
23
+ const all = [...stores.get(store).keys()].sort((a,b) => Buffer.compare(Buffer.from(a),Buffer.from(b))).filter(key => key.startsWith(prefix) && (after === undefined || Buffer.compare(Buffer.from(key), Buffer.from(after)) > 0));
24
+ const data = all.slice(0, limit), meta = { limit, ...(prefix ? { prefix } : {}) };
25
+ if (all.length > limit) meta.next_cursor = 'abi-kv:' + Buffer.from(JSON.stringify({ store, prefix, after: data[data.length - 1] })).toString('base64url');
26
+ return { code: 1, wire: Buffer.from(JSON.stringify({ data, meta })) };
27
+ },
28
+ apply(store, key, wire, mode, condition) {
29
+ const values = stores.get(store), current = values.get(key);
30
+ if (mode === 1 && current || condition !== undefined && (!current || current.generation !== condition)) return 4;
31
+ while (next === current?.generation) next++;
32
+ this.seed(store, key, wire, next++); return 1;
33
+ }
34
+ };
35
+ }
36
+
37
+ function attachConditionalKvHost(imports, host, options) {
38
+ const { view, writeU32, writeU64, readBytes, readUtf8, alloc, bodies, bodyBytes, trace, now, advance } = host;
39
+ const authority = options.authority, stores = new Map(), pending = new Map(), bodyFixtures = new Map();
40
+ let sendingResponse = false;
41
+ const newResponse = imports.fastly_http_resp.new;
42
+ imports.fastly_http_resp.new = (...args) => { sendingResponse = true; return newResponse(...args); };
43
+ const call = (stage, detail = {}) => {
44
+ const fault = typeof options.onCall === 'function' ? options.onCall(stage, detail) || {} : {};
45
+ advance(Number(fault.delayMs || 0));
46
+ trace.push({ module: 'pulse_kv_evidence', name: stage, hostStatus: fault.status ?? 0, ...(fault.kvError === undefined ? {} : { kvError: fault.kvError }) });
47
+ return fault;
48
+ };
49
+ imports.fastly_kv_store = {
50
+ open(ptr, len, out) {
51
+ const name = readUtf8(ptr, len), fault = call('open', { store: name });
52
+ if (fault.status) return fault.status;
53
+ if (!authority.stores.has(name)) return 10;
54
+ const handle = alloc('kvStore'); stores.set(handle, name); writeU32(out, handle); return 0;
55
+ },
56
+ lookup(store, ptr, len, mask, config, out) {
57
+ const name = stores.get(store), key = readUtf8(ptr, len), fault = call('lookup', { store: name, key, mask, config: readBytes(config, 4) });
58
+ if (fault.status) return fault.status;
59
+ const item = fault.observation || authority.get(name, key);
60
+ const handle = alloc('kvLookup'); pending.set(handle, { kind: 'lookup', item, readyAt: now() + Number(fault.readyDelayMs || 0), fault });
61
+ writeU32(out, handle); return 0;
62
+ },
63
+ lookup_wait_v2(handle, body, meta, capacity, written, generation, error) {
64
+ const item = pending.get(handle); if (!item || item.readyAt > now()) throw new Error('KV lookup wait called before readiness.');
65
+ pending.delete(handle); const fault = call('lookup_wait_v2', { capacity, observation: item.item });
66
+ writeU32(error, fault.kvError ?? (item.item ? 1 : 3)); writeU64(generation, fault.generation ?? item.item?.generation ?? 0n);
67
+ writeU32(written, fault.metadataLength ?? 0);
68
+ if (fault.status) return fault.status;
69
+ if ((fault.kvError ?? (item.item ? 1 : 3)) !== 1) return 0;
70
+ const handleOut = alloc('body'); const bytes = Buffer.from(fault.wire || item.item.wire);
71
+ bodies.set(handleOut, { bytes, readOffset: 0, writes: [], readyAt: now() + Number(fault.bodyDelayMs || 0) });
72
+ bodyFixtures.set(handleOut, fault); writeU32(body, handleOut); return 0;
73
+ },
74
+ insert(store, ptr, len, body, mask, config, out) {
75
+ const name = stores.get(store), key = readUtf8(ptr, len), wire = Buffer.from(bodyBytes(body));
76
+ const mode = view().getUint32(config, true), condition = mask & 32 ? view().getBigUint64(config + 24, true) : undefined;
77
+ const fault = call('insert', { store: name, key, wire, mode, mask, condition, config: readBytes(config, 32) });
78
+ let code = fault.kvError;
79
+ if (code === undefined && (!fault.status || fault.commit)) code = authority.apply(name, key, wire, mode, condition);
80
+ bodies.delete(body); // Host takes ownership of the body.
81
+ if (fault.status) return fault.status;
82
+ const handle = alloc('kvInsert'); pending.set(handle, { kind: 'insert', code, readyAt: now() + Number(fault.readyDelayMs || 0) });
83
+ writeU32(out, handle); return 0;
84
+ },
85
+ delete(store, ptr, len, mask, config, out) {
86
+ if (mask !== 0 || view().getUint32(config, true) !== 0) throw new Error('KV delete config must be empty.');
87
+ const name = stores.get(store), key = readUtf8(ptr, len), fault = call('delete', { store: name, key, mask });
88
+ const code = fault.kvError ?? ((!fault.status || fault.commit) ? authority.delete(name, key) : undefined);
89
+ if (fault.status) return fault.status;
90
+ const handle = alloc('kvDelete'); pending.set(handle, { kind: 'delete', code, readyAt: now() + Number(fault.readyDelayMs || 0) });
91
+ writeU32(out, handle); return 0;
92
+ },
93
+ delete_wait(handle, error) {
94
+ const item = pending.get(handle); if (!item || item.kind !== 'delete' || item.readyAt > now()) throw new Error('KV delete wait called before readiness.');
95
+ pending.delete(handle); const fault = call('delete_wait'); writeU32(error, fault.kvError ?? item.code ?? 0); return fault.status ?? 0;
96
+ },
97
+ list(store, mask, config, out) {
98
+ if ((mask & ~14) !== 0 || !(mask & 4) || view().getUint32(config, true) !== 0) throw new Error('KV list requires bounded strong mode.');
99
+ const cursor = mask & 2 ? readUtf8(view().getUint32(config + 4, true), view().getUint32(config + 8, true)) : undefined;
100
+ const limit = view().getUint32(config + 12, true);
101
+ const prefix = mask & 8 ? readUtf8(view().getUint32(config + 16, true), view().getUint32(config + 20, true)) : '';
102
+ if (limit < 1 || limit > 100 || cursor === '' || mask & 8 && !prefix) throw new Error('KV list config is invalid.');
103
+ const name = stores.get(store), fault = call('list', { store: name, mask, limit, prefix, cursor });
104
+ if (fault.status) return fault.status;
105
+ const result = authority.list(name, { limit, prefix, cursor });
106
+ const handle = alloc('kvList'); pending.set(handle, { kind: 'list', ...result, readyAt: now() + Number(fault.readyDelayMs || 0) });
107
+ writeU32(out, handle); return 0;
108
+ },
109
+ list_wait(handle, body, error) {
110
+ const item = pending.get(handle); if (!item || item.kind !== 'list' || item.readyAt > now()) throw new Error('KV list wait called before readiness.');
111
+ pending.delete(handle); const fault = call('list_wait'); writeU32(error, fault.kvError ?? item.code);
112
+ if (fault.status) return fault.status;
113
+ if ((fault.kvError ?? item.code) !== 1) return 0;
114
+ const bodyHandle = alloc('body'); bodies.set(bodyHandle, { bytes: Buffer.from(fault.wire ?? item.wire), readOffset: 0, writes: [], readyAt: now() + Number(fault.bodyDelayMs || 0) });
115
+ bodyFixtures.set(bodyHandle, fault); writeU32(body, bodyHandle); return 0;
116
+ },
117
+ insert_wait(handle, error) {
118
+ const item = pending.get(handle); if (!item || item.readyAt > now()) throw new Error('KV insert wait called before readiness.');
119
+ pending.delete(handle); const fault = call('insert_wait'); writeU32(error, fault.kvError ?? item.code ?? 0); return fault.status ?? 0;
120
+ }
121
+ };
122
+ const select = imports.fastly_async_io.select;
123
+ imports.fastly_async_io.select = (handles, count, timeout, done) => {
124
+ const handle = view().getUint32(handles, true), item = pending.get(handle) || (bodyFixtures.has(handle) && bodies.get(handle));
125
+ if (!item) return select(handles, count, timeout, done);
126
+ if (count !== 1 || timeout <= 0) throw new Error('KV select must be bounded.');
127
+ const fault = call('select', { kind: item.kind || 'body', timeout });
128
+ if (fault.status) return fault.status;
129
+ const wait = Math.max(0, item.readyAt - now()); advance(Math.min(wait, timeout));
130
+ writeU32(done, fault.readyIndex ?? (wait >= timeout ? 0xffffffff : 0)); return 0;
131
+ };
132
+ for (const name of ['new', 'write', 'read', 'close']) {
133
+ const original = imports.fastly_http_body[name];
134
+ imports.fastly_http_body[name] = (...args) => {
135
+ const [handle] = args;
136
+ if (sendingResponse) return original(...args);
137
+ if (name === 'read' && !bodyFixtures.has(handle)) return original(...args);
138
+ const fault = call('body_' + name, { handle });
139
+ if (fault.status) return fault.status;
140
+ const result = original(...args);
141
+ if (name === 'read') {
142
+ const fixture = bodyFixtures.get(handle), body = bodies.get(handle);
143
+ if (body) body.readyAt = now() + Number(fixture.chunkDelayMs || 0);
144
+ if (fault.written !== undefined) writeU32(args[3], fault.written);
145
+ }
146
+ if (name === 'write' && fault.written !== undefined) writeU32(args[4], fault.written);
147
+ if (name === 'close') bodyFixtures.delete(handle);
148
+ return result;
149
+ };
150
+ }
151
+ const clock = imports.wasi_snapshot_preview1.clock_time_get;
152
+ imports.wasi_snapshot_preview1.clock_time_get = (...args) => { const fault = call('clock'); return fault.status || clock(...args); };
153
+ return { pending, bodyFixtures };
154
+ }
155
+ module.exports = { createConditionalKvAuthority, attachConditionalKvHost };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ // Independent ABI oracle shared by the two injected Fastly hosts. This is test
4
+ // infrastructure, not a network implementation or a hosted capability probe.
5
+ const assert = require('node:assert/strict');
6
+ function createDynamicBackendHost({ view, readUtf8, trace, options, beforeRegister = () => {} }) {
7
+ const backends = new Map();
8
+ function register(np, nl, tp, tl, mask, config) {
9
+ beforeRegister();
10
+ assert.equal(config % 4, 0);
11
+ const words = Array.from({ length: 27 }, (_, i) => view().getUint32(config + i * 4, true));
12
+ const text = i => readUtf8(words[i], words[i + 1]);
13
+ const name = readUtf8(np, nl), target = readUtf8(tp, tl), host = text(0);
14
+ const tls = (mask & 32) !== 0;
15
+ assert.equal(mask, tls ? 2338 : 2, 'Only Host and URL-derived TLS settings are enabled');
16
+ const used = new Set(tls ? [0, 1, 7, 8, 13, 14] : [0, 1]);
17
+ words.forEach((word, i) => { if (!used.has(i)) assert.equal(word, 0, 'Unused ABI field ' + i); });
18
+ const record = { name, target, host, tls, certificateHostname: tls ? text(7) : '', sni: tls ? text(13) : '' };
19
+ if (tls) assert.equal(record.certificateHostname, record.sni);
20
+ let status = Number(options.dynamicBackendStatus || 0);
21
+ if (typeof options.onDynamicBackendRegistration === 'function') status = options.onDynamicBackendRegistration(record) ?? status;
22
+ if (backends.has(name) || (options.staticBackendNames || []).includes(name)) status = 1;
23
+ if (!status) backends.set(name, record);
24
+ trace.push({ ...record, module: 'fastly_http_req', backendName: name, name: 'register_dynamic_backend', status });
25
+ return status;
26
+ }
27
+ function route(name, request) {
28
+ const backend = backends.get(name);
29
+ if (!backend) return; // Statically configured names retain fixture routing.
30
+ const url = new URL(request.url);
31
+ assert.equal(backend.target, url.hostname + ':' + (url.port || (url.protocol === 'https:' ? '443' : '80')));
32
+ assert.equal(backend.tls, url.protocol === 'https:');
33
+ if (backend.tls) assert.equal(backend.sni, url.hostname);
34
+ request.headers = request.headers.filter(([key]) => key.toLowerCase() !== 'host');
35
+ request.headers.push(['host', backend.host]);
36
+ }
37
+ return { register, route };
38
+ }
39
+ module.exports = { createDynamicBackendHost };
@@ -80,6 +80,7 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
80
80
  const bodies = new Map();
81
81
  const responses = new Map();
82
82
  const pending = new Map();
83
+ const outboundRequests = [];
83
84
  const downstreamRequestHandle = 1;
84
85
  const downstreamBodyHandle = 2;
85
86
  requests.set(downstreamRequestHandle, { method, url, headers: requestHeaders.map((entry) => [...entry]) });
@@ -111,6 +112,8 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
111
112
  function readBytes(pointer, length) { return Buffer.from(new Uint8Array(memory().buffer, Number(pointer), Number(length))); }
112
113
  function readUtf8(pointer, length) { return readBytes(pointer, length).toString('utf8'); }
113
114
 
115
+ const dynamicBackends = require('./native-dynamic-backends-host.js').createDynamicBackendHost({ view, readUtf8, trace, options });
116
+
114
117
  function writeUtf8(value, pointer, maxLength, writtenOut) {
115
118
  const bytes = Buffer.from(String(value), 'utf8');
116
119
  writeU32(writtenOut, bytes.length);
@@ -164,6 +167,7 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
164
167
  }
165
168
  },
166
169
  fastly_http_req: {
170
+ register_dynamic_backend: dynamicBackends.register,
167
171
  body_downstream_get(requestOut, bodyOut) {
168
172
  trace.push({ module: 'fastly_http_req', name: 'body_downstream_get' });
169
173
  writeU32(requestOut, downstreamRequestHandle);
@@ -219,18 +223,30 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
219
223
  trace.push({ module: 'fastly_http_req', name: 'header_insert', handle: Number(handle), header: [name, value] });
220
224
  return FASTLY_STATUS_OK;
221
225
  },
226
+ header_append(handle, namePointer, nameLength, valuePointer, valueLength) {
227
+ const request = requests.get(Number(handle));
228
+ if (!request) return FASTLY_STATUS_BADF;
229
+ const name = readUtf8(namePointer, nameLength);
230
+ const value = readUtf8(valuePointer, valueLength);
231
+ request.headers.push([name, value]);
232
+ trace.push({ module: 'fastly_http_req', name: 'header_append', handle: Number(handle), header: [name, value] });
233
+ return FASTLY_STATUS_OK;
234
+ },
222
235
  send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
223
236
  const request = requests.get(Number(handle));
224
237
  const body = bodies.get(Number(bodyHandle));
225
238
  if (!request || !body) return FASTLY_STATUS_BADF;
226
239
  const backend = readUtf8(backendPointer, backendLength);
240
+ dynamicBackends.route(backend, request);
227
241
  const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
228
242
  if (!fixture) {
229
243
  trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: request.url, missingFixture: true });
230
244
  return FASTLY_STATUS_ERROR;
231
245
  }
232
246
  const pendingHandle = alloc('pending');
233
- pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: bodyBytes(bodyHandle) }, backend, fixture });
247
+ const outbound = { ...request, headers: request.headers.map((entry) => [...entry]), body: bodyBytes(bodyHandle) };
248
+ outboundRequests.push(outbound);
249
+ pending.set(pendingHandle, { request: outbound, backend, fixture });
234
250
  writeU32(pendingOut, pendingHandle);
235
251
  trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), bodyHandle: Number(bodyHandle), pendingHandle, backend, method: request.method, url: request.url });
236
252
  return FASTLY_STATUS_OK;
@@ -373,6 +389,7 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
373
389
  bodyHandle: downstream.bodyHandle
374
390
  }),
375
391
  trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
392
+ outboundRequests: Object.freeze(outboundRequests.map(entry => Object.freeze(entry))),
376
393
  instance
377
394
  });
378
395
  }