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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +7 -6
  2. package/dist/config-schema.json +30 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +11 -10
  8. package/src/build/canonical-target.js +35 -2
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +49 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/entities-native.js +6 -11
  13. package/src/build/kv-native.as.ts +331 -0
  14. package/src/build/kv-native.js +27 -0
  15. package/src/build/native-application-errors.js +159 -0
  16. package/src/build/native-binary-resources.js +30 -0
  17. package/src/build/native-body-forwarding.as.ts +341 -0
  18. package/src/build/native-body-forwarding.js +69 -0
  19. package/src/build/native-dynamic-backends.as.ts +115 -0
  20. package/src/build/native-dynamic-backends.js +52 -0
  21. package/src/build/native-fetch-deadlines.as.ts +164 -0
  22. package/src/build/native-fetch-deadlines.js +27 -0
  23. package/src/build/native-fetch-errors.js +38 -0
  24. package/src/build/native-handle-ownership.js +127 -0
  25. package/src/build/native-header-lookup.js +50 -0
  26. package/src/build/native-http-effects.js +155 -32
  27. package/src/build/native-platform-capabilities.js +755 -210
  28. package/src/build/native-request-body.js +44 -0
  29. package/src/build/native-request-headers.js +93 -0
  30. package/src/build/native-send-error-detail.as.ts +12 -0
  31. package/src/build/native-stdio.js +45 -0
  32. package/src/build/native-string-values.js +143 -0
  33. package/src/build/native-transport-details.as.ts +84 -0
  34. package/src/build/native-value-budget.js +90 -0
  35. package/src/build/package-native-application.js +114 -0
  36. package/src/build/request-budget.as.ts +50 -0
  37. package/src/build/request-budget.js +47 -0
  38. package/src/build/s3-body.as.ts +97 -0
  39. package/src/build/s3-native.as.ts +228 -0
  40. package/src/build/s3-native.js +30 -0
  41. package/src/build/schema-admission.js +83 -0
  42. package/src/build/schema-nested-json.js +93 -0
  43. package/src/build/schema-scalar-record.js +41 -0
  44. package/src/build/time-native.js +49 -0
  45. package/src/build/wasm-budget.js +28 -0
  46. package/src/config-api.js +6 -0
  47. package/src/config-schema.json +30 -0
  48. package/src/index.ts +25 -0
  49. package/src/javascript/jwt-verifier.js +14 -4
  50. package/src/javascript/logging.js +11 -2
  51. package/src/javascript/package-effects.js +5 -2
  52. package/src/javascript/runtime-host.js +7 -3
  53. package/src/javascript/source-package.js +4 -2
  54. package/src/javascript/target-support-policy.js +20 -3
  55. package/src/javascript/target.js +17 -1
  56. package/src/javascript/test-runtime.js +1 -0
  57. package/src/provider-contract.js +22 -6
  58. package/src/runtime/canonical-api-runtime.js +6 -0
  59. package/src/testing/conditional-kv-host.js +155 -0
  60. package/src/testing/native-dynamic-backends-host.js +39 -0
  61. package/src/testing/native-http-effects-host.js +9 -1
  62. package/src/testing/native-platform-capabilities-host.js +349 -24
  63. package/src/toolchain/body-forwarding.js +25 -0
  64. package/src/toolchain/config.js +16 -0
  65. package/src/toolchain/index.js +48 -5
  66. package/src/toolchain/logging.js +18 -0
  67. package/src/toolchain/native-execution.js +78 -0
  68. package/src/toolchain/s3.js +26 -0
@@ -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);
@@ -224,13 +228,16 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
224
228
  const body = bodies.get(Number(bodyHandle));
225
229
  if (!request || !body) return FASTLY_STATUS_BADF;
226
230
  const backend = readUtf8(backendPointer, backendLength);
231
+ dynamicBackends.route(backend, request);
227
232
  const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
228
233
  if (!fixture) {
229
234
  trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), backend, method: request.method, url: request.url, missingFixture: true });
230
235
  return FASTLY_STATUS_ERROR;
231
236
  }
232
237
  const pendingHandle = alloc('pending');
233
- pending.set(pendingHandle, { request: { ...request, headers: request.headers.map((entry) => [...entry]), body: bodyBytes(bodyHandle) }, backend, fixture });
238
+ const outbound = { ...request, headers: request.headers.map((entry) => [...entry]), body: bodyBytes(bodyHandle) };
239
+ outboundRequests.push(outbound);
240
+ pending.set(pendingHandle, { request: outbound, backend, fixture });
234
241
  writeU32(pendingOut, pendingHandle);
235
242
  trace.push({ module: 'fastly_http_req', name: 'send_async', handle: Number(handle), bodyHandle: Number(bodyHandle), pendingHandle, backend, method: request.method, url: request.url });
236
243
  return FASTLY_STATUS_OK;
@@ -373,6 +380,7 @@ function executeFastlyNativeHttpEffects(input, options = {}) {
373
380
  bodyHandle: downstream.bodyHandle
374
381
  }),
375
382
  trace: Object.freeze(trace.map((entry) => Object.freeze({ ...entry }))),
383
+ outboundRequests: Object.freeze(outboundRequests.map(entry => Object.freeze(entry))),
376
384
  instance
377
385
  });
378
386
  }