@pulse-compute/provider-node 0.0.0 → 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.
Files changed (42) hide show
  1. package/README.md +61 -1
  2. package/package.json +62 -6
  3. package/src/capabilities.js +65 -0
  4. package/src/compiler/assets-provider-proof.js +434 -0
  5. package/src/compiler/backend-json-request-body-proof.js +90 -0
  6. package/src/compiler/index.js +38 -0
  7. package/src/compiler/node-adapter.js +452 -0
  8. package/src/compiler/node-compiled-wasm-assets-lifecycle-proof.js +435 -0
  9. package/src/compiler/request-json-body-proof.js +181 -0
  10. package/src/compiler/route-handler-compiled-wasm-bridge.js +323 -0
  11. package/src/compiler/route-handler-effect-proof.js +215 -0
  12. package/src/compiler/route-handler-live-origin-proof.js +372 -0
  13. package/src/compiler/schema-decode-result-proof.js +180 -0
  14. package/src/compiler/schema-response-codec-proof.js +149 -0
  15. package/src/config/s3.js +20 -0
  16. package/src/events/reference-adapter.js +145 -0
  17. package/src/index.js +57 -0
  18. package/src/javascript/assets-adapter.js +68 -0
  19. package/src/javascript/bindings-adapter.js +90 -0
  20. package/src/javascript/fetch-adapter.js +187 -0
  21. package/src/javascript/grip-broadcast.js +243 -0
  22. package/src/javascript/jwt-verifier.js +137 -0
  23. package/src/javascript/lifecycle.js +69 -0
  24. package/src/javascript/node-adapter.js +284 -0
  25. package/src/javascript/package-effects.js +84 -0
  26. package/src/javascript/runtime-host.js +170 -0
  27. package/src/javascript/s3.js +21 -0
  28. package/src/javascript/source-package.js +439 -0
  29. package/src/javascript/support.js +205 -0
  30. package/src/javascript/target-support-policy.js +136 -0
  31. package/src/javascript/target.js +37 -0
  32. package/src/javascript/test-runtime.js +112 -0
  33. package/src/native/target.js +78 -0
  34. package/src/runtime/assets-provider.js +776 -0
  35. package/src/runtime/canonical-api-runtime.js +604 -0
  36. package/src/runtime/compiled-wasm-node-adapter-kv.js +15 -0
  37. package/src/runtime/conditional-kv.js +93 -0
  38. package/src/runtime/index.js +18 -0
  39. package/src/runtime/jwt-verifier.js +264 -0
  40. package/src/runtime/route-handler-effects.js +757 -0
  41. package/src/runtime/s3-reader.js +131 -0
  42. package/src/toolchain.js +269 -0
@@ -0,0 +1,604 @@
1
+ 'use strict';
2
+ const { createNodeKvReference } = require('./conditional-kv.js');
3
+
4
+ const path = require('node:path');
5
+ const { Readable } = require('node:stream');
6
+ const { setTimeout: delay } = require('node:timers/promises');
7
+ const routeHandlerEffects = require('./route-handler-effects.js');
8
+ const portableRuntimeHost = require('@pulse-compute/runtime/host');
9
+ const hostRuntime = require('@pulse-compute/wasm-host-runtime/runtime/canonical-api-runtime');
10
+ const nativeHost = require('@pulse-compute/wasm-host-runtime/runtime/canonical-native-host');
11
+ const handlerEffectContract = require('@pulse-compute/wasm-contracts/handler/effects');
12
+ const providerContract = require('@pulse-compute/wasm-contracts/provider/canonical-provider');
13
+ const { createNodeJavascriptGripBroadcast } = require('../javascript/grip-broadcast.js');
14
+ const { createNodeNativeJwtVerify } = require('./jwt-verifier.js');
15
+ const {
16
+ NODE_CANONICAL_PROVIDER_CAPABILITIES,
17
+ NODE_CANONICAL_PROVIDER_LOWERING
18
+ } = require('../capabilities.js');
19
+
20
+ const CANONICAL_NODE_RUNTIME_VERSION = 'pulse.canonical-node-runtime.v4';
21
+ const NODE_PROVIDER_DESCRIPTOR = providerContract.normalizeDescriptor({
22
+ id: 'node',
23
+ package: '@pulse-compute/provider-node',
24
+ runtime: CANONICAL_NODE_RUNTIME_VERSION,
25
+ buildTarget: 'node',
26
+ deployable: true,
27
+ capabilities: NODE_CANONICAL_PROVIDER_CAPABILITIES,
28
+ lowering: NODE_CANONICAL_PROVIDER_LOWERING
29
+ });
30
+
31
+ function normalizeFetchFixtures(fetches = {}) {
32
+ if (fetches instanceof Map) return new Map(fetches);
33
+ return new Map(Object.entries(fetches || {}));
34
+ }
35
+
36
+ function providerResponseSpec(spec = {}) {
37
+ if (spec.opaque || spec.kind === 'stream' || spec.bodyStream !== undefined || spec.bodyHandle !== undefined) {
38
+ return {
39
+ status: Number(Object.prototype.hasOwnProperty.call(spec, 'status') ? spec.status : 200),
40
+ kind: 'stream',
41
+ headers: hostRuntime.createCanonicalContext ? normalizeHeaders(spec.headers || [['content-type', 'application/octet-stream']]) : spec.headers,
42
+ bodyStream: spec.bodyStream || { chunks: Array.isArray(spec.chunks) ? [...spec.chunks] : [] },
43
+ bodyHandle: spec.bodyHandle,
44
+ responseRef: spec.responseRef,
45
+ streamRef: spec.streamRef
46
+ };
47
+ }
48
+ const body = Object.prototype.hasOwnProperty.call(spec, 'body') ? spec.body : JSON.stringify(Object.prototype.hasOwnProperty.call(spec, 'value') ? spec.value : {});
49
+ return {
50
+ status: Number(Object.prototype.hasOwnProperty.call(spec, 'status') ? spec.status : 200),
51
+ kind: spec.kind || 'text',
52
+ headers: normalizeHeaders(spec.headers || [['content-type', 'application/json; charset=utf-8']]),
53
+ body: typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body)
54
+ };
55
+ }
56
+
57
+ function normalizeHeaders(headers) {
58
+ if (!headers) return [];
59
+ if (Array.isArray(headers)) return headers.map(([name, value]) => [String(name), String(value)]);
60
+ if (typeof headers.entries === 'function') return [...headers.entries()].map(([name, value]) => [String(name), String(value)]);
61
+ return Object.entries(headers).flatMap(([name, value]) => Array.isArray(value) ? value.map((entry) => [String(name), String(entry)]) : [[String(name), String(value)]]);
62
+ }
63
+
64
+ function buildProviderBackends(fetchFixtures) {
65
+ const backends = {};
66
+ const specs = new Map();
67
+ for (const [fixtureKey, input] of fetchFixtures.entries()) {
68
+ const matched = /^([A-Za-z]+)\s+(\S.*)$/.exec(String(fixtureKey));
69
+ const url = matched ? matched[2] : String(fixtureKey);
70
+ const parts = hostRuntime.urlParts(url);
71
+ const spec = input && typeof input === 'object' ? input : { body: input };
72
+ const method = String(spec.method || (matched && matched[1]) || 'GET').toUpperCase();
73
+ if (!backends[parts.origin]) backends[parts.origin] = {};
74
+ backends[parts.origin][`${method} ${parts.path}`] = providerResponseSpec(spec);
75
+ if (method === 'GET') backends[parts.origin][parts.path] = providerResponseSpec(spec);
76
+ specs.set(`${method} ${parts.url}`, spec);
77
+ if (method === 'GET') specs.set(parts.url, spec);
78
+ }
79
+ return Object.freeze({ backends, specs });
80
+ }
81
+
82
+ function fixtureWebResponse(spec = {}, method = 'GET') {
83
+ const response = providerResponseSpec(spec);
84
+ const status = Number(response.status || 200);
85
+ const bodyAllowed = String(method).toUpperCase() !== 'HEAD'
86
+ && status !== 204
87
+ && status !== 205
88
+ && status !== 304
89
+ && !(status >= 100 && status < 200);
90
+ return new Response(bodyAllowed ? response.body : null, {
91
+ status,
92
+ headers: normalizeHeaders(response.headers)
93
+ });
94
+ }
95
+
96
+ function fixtureFetchImplementation(fetches) {
97
+ const providerData = buildProviderBackends(normalizeFetchFixtures(fetches));
98
+ return async function nodeCanonicalFixtureFetch(url, init = {}) {
99
+ const method = String(init.method || 'GET').toUpperCase();
100
+ const fixture = providerData.specs.get(`${method} ${String(url)}`)
101
+ || providerData.specs.get(String(url));
102
+ if (!fixture) throw new Error(`No Node provider fixture is configured for ${method} ${String(url)}.`);
103
+ if (fixture.kind === 'network-error') throw new Error('Node provider fixture produced a network failure.');
104
+ if (fixture.kind === 'timeout') {
105
+ await new Promise((_resolve, reject) => {
106
+ const signal = init.signal;
107
+ const onAbort = () => reject(signal.reason || new DOMException('Aborted', 'AbortError'));
108
+ if (signal && signal.aborted) onAbort();
109
+ else if (signal) signal.addEventListener('abort', onAbort, { once: true });
110
+ });
111
+ }
112
+ const delayMs = Math.max(0, Number(fixture.delayMs) || 0);
113
+ if (delayMs > 0) await delay(delayMs, undefined, { signal: init.signal });
114
+ return fixtureWebResponse(fixture, method);
115
+ };
116
+ }
117
+
118
+ function assetContentType(key) {
119
+ const extension = String(key).toLowerCase().split('.').pop();
120
+ return Object.freeze({
121
+ css: 'text/css; charset=utf-8',
122
+ html: 'text/html; charset=utf-8',
123
+ js: 'text/javascript; charset=utf-8',
124
+ json: 'application/json; charset=utf-8',
125
+ mjs: 'text/javascript; charset=utf-8',
126
+ svg: 'image/svg+xml',
127
+ txt: 'text/plain; charset=utf-8',
128
+ webp: 'image/webp',
129
+ png: 'image/png',
130
+ jpg: 'image/jpeg',
131
+ jpeg: 'image/jpeg',
132
+ gif: 'image/gif',
133
+ ico: 'image/x-icon'
134
+ })[extension] || 'application/octet-stream';
135
+ }
136
+
137
+ function assetResponse(payload, stores) {
138
+ const storeName = portableRuntimeHost.normalizeKvNamespace(payload.store, {});
139
+ const key = portableRuntimeHost.normalizeKvKey(payload.key, {});
140
+ const store = stores.get(storeName);
141
+ const found = store && store.has(key);
142
+ const status = found ? 200 : 404;
143
+ const headers = normalizeHeaders(payload.headers);
144
+ if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) {
145
+ headers.push(['content-type', found ? assetContentType(key) : 'text/plain; charset=utf-8']);
146
+ }
147
+ if (payload.cacheControl !== undefined) headers.push(['cache-control', String(payload.cacheControl)]);
148
+ const method = String(payload.method || 'GET').toUpperCase();
149
+ const value = found ? store.get(key) : '';
150
+ const body = method === 'HEAD' || !found
151
+ ? ''
152
+ : (typeof value === 'string' || Buffer.isBuffer(value) || value instanceof Uint8Array)
153
+ ? value
154
+ : JSON.stringify(value);
155
+ return Object.freeze({
156
+ status,
157
+ kind: 'text',
158
+ bodyClass: 'structured',
159
+ headers: Object.freeze(headers.map((entry) => Object.freeze(entry))),
160
+ body,
161
+ providerSource: 'node'
162
+ });
163
+ }
164
+
165
+ function responseHeaders(response) {
166
+ const headers = [];
167
+ if (response && response.headers && typeof response.headers.entries === 'function') {
168
+ for (const [name, value] of response.headers.entries()) {
169
+ if (String(name).toLowerCase() === 'set-cookie' && typeof response.headers.getSetCookie === 'function') continue;
170
+ headers.push([String(name), String(value)]);
171
+ }
172
+ if (typeof response.headers.getSetCookie === 'function') {
173
+ for (const value of response.headers.getSetCookie()) headers.push(['set-cookie', String(value)]);
174
+ }
175
+ }
176
+ return headers;
177
+ }
178
+
179
+ function headerValue(headers, name) {
180
+ const lower = String(name).toLowerCase();
181
+ const found = (headers || []).find(([key]) => String(key).toLowerCase() === lower);
182
+ return found && String(found[1]);
183
+ }
184
+
185
+ function responseStatusAllowsBody(status) {
186
+ const value = Number(status);
187
+ return value !== 204 && value !== 205 && value !== 304 && !(value >= 100 && value < 200);
188
+ }
189
+
190
+ function responseIsStructured(method, headers, status = 200) {
191
+ if (String(method).toUpperCase() === 'HEAD' || !responseStatusAllowsBody(status)) return true;
192
+ const contentType = String(headerValue(headers, 'content-type') || '').toLowerCase();
193
+ return contentType.startsWith('text/')
194
+ || contentType.includes('application/json')
195
+ || contentType.includes('+json')
196
+ || contentType.includes('application/xml')
197
+ || contentType.includes('+xml')
198
+ || contentType.includes('application/x-www-form-urlencoded');
199
+ }
200
+
201
+ async function readStructuredResponseText(response, maxBytes) {
202
+ const limit = Number.isSafeInteger(Number(maxBytes)) && Number(maxBytes) > 0 ? Number(maxBytes) : 65_536;
203
+ const declared = Number(response.headers && response.headers.get && response.headers.get('content-length'));
204
+ if (Number.isSafeInteger(declared) && declared > limit) {
205
+ throw new hostRuntime.CanonicalRuntimeError('BodyTooLargeError', 'PULSE_BODY_TOO_LARGE', `Fetched response exceeds the ${limit} byte structured-body limit.`, { bytes: declared, maxBytes: limit });
206
+ }
207
+ if (!response.body) return '';
208
+ const reader = response.body.getReader();
209
+ const chunks = [];
210
+ let bytes = 0;
211
+ try {
212
+ while (true) {
213
+ const result = await reader.read();
214
+ if (result.done) break;
215
+ const chunk = result.value instanceof Uint8Array ? result.value : new Uint8Array(result.value || 0);
216
+ bytes += chunk.byteLength;
217
+ if (bytes > limit) {
218
+ try { await reader.cancel(); } catch (_) { /* best effort */ }
219
+ throw new hostRuntime.CanonicalRuntimeError('BodyTooLargeError', 'PULSE_BODY_TOO_LARGE', `Fetched response exceeds the ${limit} byte structured-body limit.`, { bytes, maxBytes: limit });
220
+ }
221
+ chunks.push(chunk);
222
+ }
223
+ } finally {
224
+ try { reader.releaseLock(); } catch (_) { /* already released */ }
225
+ }
226
+ const output = new Uint8Array(bytes);
227
+ let offset = 0;
228
+ for (const chunk of chunks) {
229
+ output.set(chunk, offset);
230
+ offset += chunk.byteLength;
231
+ }
232
+ return new TextDecoder().decode(output);
233
+ }
234
+
235
+ async function executeLiveFetch(normalized, fetchImplementation, options = {}) {
236
+ const timeoutMs = Number(normalized.init.timeoutMs);
237
+ const controller = Number.isFinite(timeoutMs) && timeoutMs > 0 ? new AbortController() : undefined;
238
+ const timeout = controller ? setTimeout(() => controller.abort(new Error(`Fetch timed out after ${timeoutMs}ms.`)), timeoutMs) : undefined;
239
+ try {
240
+ const response = await fetchImplementation(normalized.parts.url, {
241
+ method: normalized.init.method,
242
+ headers: normalized.init.headers,
243
+ body: normalized.init.body,
244
+ signal: controller && controller.signal,
245
+ redirect: 'follow'
246
+ });
247
+ const headers = responseHeaders(response);
248
+ const responseMode = normalized.responseMode || 'auto';
249
+ const structured = responseMode === 'structured'
250
+ || (responseMode === 'auto' && responseIsStructured(normalized.init.method, headers, response.status));
251
+ if (structured) {
252
+ const body = normalized.init.method === 'HEAD' || !responseStatusAllowsBody(response.status)
253
+ ? ''
254
+ : await readStructuredResponseText(response, options.maxFetchBodyBytes || options.maxBodyBytes);
255
+ return { status: response.status, kind: 'text', headers, body };
256
+ }
257
+ const bodyStream = response.body && typeof Readable.fromWeb === 'function' ? Readable.fromWeb(response.body) : response.body;
258
+ return { status: response.status, kind: 'stream', headers, bodyStream };
259
+ } catch (error) {
260
+ if ((controller && controller.signal.aborted) || error && error.name === 'AbortError') {
261
+ throw new hostRuntime.CanonicalRuntimeError('FetchTimeoutError', 'PULSE_FETCH_TIMEOUT', 'Live Node fetch exceeded the configured timeout.', { effectId: normalized.id, url: normalized.parts.url, timeoutMs });
262
+ }
263
+ if (error instanceof hostRuntime.CanonicalRuntimeError) throw error;
264
+ throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', `Live Node fetch failed for ${normalized.init.method} ${normalized.parts.url}.`, { effectId: normalized.id, url: normalized.parts.url, message: error && error.message ? error.message : String(error) });
265
+ } finally {
266
+ if (timeout) clearTimeout(timeout);
267
+ }
268
+ }
269
+
270
+ function fixtureBodyChunks(snapshot) {
271
+ const stream = snapshot && snapshot.bodyStream;
272
+ if (stream && Array.isArray(stream.chunks)) return stream.chunks;
273
+ if (Array.isArray(snapshot && snapshot.chunks)) return snapshot.chunks;
274
+ if (typeof stream === 'string' || Buffer.isBuffer(stream) || stream instanceof Uint8Array) return [stream];
275
+ return undefined;
276
+ }
277
+
278
+ function structuredFixtureResponse(snapshot, normalized, options = {}) {
279
+ const status = Number(snapshot.status || 0);
280
+ if (normalized.init.method === 'HEAD' || !responseStatusAllowsBody(status)) {
281
+ return { ...snapshot, kind: 'text', bodyClass: 'structured', body: '', bodyStream: undefined, bodyHandle: undefined };
282
+ }
283
+ if (snapshot.kind !== 'stream' && snapshot.bodyClass !== 'opaque' && snapshot.bodyStream === undefined && snapshot.bodyHandle === undefined) {
284
+ return snapshot;
285
+ }
286
+ const chunks = fixtureBodyChunks(snapshot);
287
+ if (!chunks) {
288
+ throw new hostRuntime.CanonicalRuntimeError(
289
+ 'OpaqueBodyInspectionError',
290
+ 'PULSE_OPAQUE_BODY_INSPECTION',
291
+ 'The configured Node fetch fixture is pass-through only and cannot satisfy an explicit structured projection.',
292
+ { effectId: normalized.id, url: normalized.parts.url, bodyCopiedIntoWasm: false }
293
+ );
294
+ }
295
+ const buffers = chunks.map((chunk) => Buffer.isBuffer(chunk)
296
+ ? chunk
297
+ : chunk instanceof Uint8Array
298
+ ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)
299
+ : Buffer.from(String(chunk)));
300
+ const bytes = buffers.reduce((total, chunk) => total + chunk.byteLength, 0);
301
+ const configured = Number(options.maxFetchBodyBytes || options.maxBodyBytes);
302
+ const maxBytes = Number.isSafeInteger(configured) && configured > 0 ? configured : 65_536;
303
+ if (bytes > maxBytes) {
304
+ throw new hostRuntime.CanonicalRuntimeError(
305
+ 'BodyTooLargeError',
306
+ 'PULSE_BODY_TOO_LARGE',
307
+ `Fetched response exceeds the ${maxBytes} byte structured-body limit.`,
308
+ { effectId: normalized.id, bytes, maxBytes }
309
+ );
310
+ }
311
+ return {
312
+ ...snapshot,
313
+ kind: 'text',
314
+ bodyClass: 'structured',
315
+ body: Buffer.concat(buffers).toString('utf8'),
316
+ bodyStream: undefined,
317
+ bodyHandle: undefined
318
+ };
319
+ }
320
+
321
+ function opaqueFixtureResponse(snapshot, normalized) {
322
+ const status = Number(snapshot.status || 0);
323
+ if (snapshot.kind === 'stream' || snapshot.bodyClass === 'opaque' || snapshot.bodyStream !== undefined || snapshot.bodyHandle !== undefined) return snapshot;
324
+ const allowsBody = normalized.init.method !== 'HEAD' && responseStatusAllowsBody(status);
325
+ const body = allowsBody && snapshot.body !== undefined && snapshot.body !== null ? snapshot.body : undefined;
326
+ return {
327
+ ...snapshot,
328
+ kind: 'stream',
329
+ bodyClass: 'opaque',
330
+ body: undefined,
331
+ bodyStream: body === undefined ? undefined : { chunks: [body] }
332
+ };
333
+ }
334
+
335
+ function responseForMode(snapshot, normalized, options = {}) {
336
+ if (normalized.responseMode === 'structured') return structuredFixtureResponse(snapshot, normalized, options);
337
+ if (normalized.responseMode === 'opaque') return opaqueFixtureResponse(snapshot, normalized);
338
+ return snapshot;
339
+ }
340
+
341
+ function minimalEffectPlan(metadata = {}) {
342
+ return {
343
+ version: handlerEffectContract.ROUTE_HANDLER_EFFECT_PLAN_VERSION,
344
+ routes: [{ routeId: `canonical:${path.basename(metadata.file || 'app')}`, method: '*', path: '*', handlerName: '__pulse_handler', resolves: [] }],
345
+ continuations: (metadata.continuationSites || []).map((site) => ({ name: site.id, resultAccesses: [], responseMethodCalls: [] }))
346
+ };
347
+ }
348
+
349
+ function effectToProviderEffect(normalized) {
350
+ return Object.freeze({
351
+ kind: 'backend-fetch',
352
+ name: normalized.id,
353
+ backend: normalized.parts.origin,
354
+ request: Object.freeze({
355
+ method: normalized.init.method,
356
+ path: Object.freeze({ kind: 'literal', value: normalized.parts.path }),
357
+ headers: Object.freeze(normalized.init.headers.map(([name, value]) => Object.freeze({ name, value: Object.freeze({ kind: 'literal', value }) }))),
358
+ body: normalized.init.body
359
+ })
360
+ });
361
+ }
362
+
363
+ function createNodeProviderAdapter(baseOptions = {}) {
364
+ const kvExecutions = new Map();
365
+ const sharedKv = baseOptions.kvReference || createNodeKvReference(baseOptions);
366
+ const verifyNativeJwt = createNodeNativeJwtVerify({
367
+ captureWallClock: baseOptions.captureJwtWallClock,
368
+ secretLookup(name, executionOptions) {
369
+ return portableRuntimeHost.normalizeBindingValue(
370
+ 'secret.get',
371
+ name,
372
+ bindingValue(executionOptions, 'secrets', name),
373
+ bindingOptions(executionOptions)
374
+ );
375
+ }
376
+ });
377
+
378
+ function recordEntries(input, label) {
379
+ if (input === undefined || input === null) return [];
380
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
381
+ throw new hostRuntime.CanonicalRuntimeError(
382
+ 'BindingRecordError',
383
+ 'PULSE_BINDING_RECORD_INVALID',
384
+ `Pulse Node ${label} must be an object record.`,
385
+ { label, valueType: input === null ? 'null' : typeof input }
386
+ );
387
+ }
388
+ const prototype = Object.getPrototypeOf(input);
389
+ if (prototype !== Object.prototype && prototype !== null) {
390
+ throw new hostRuntime.CanonicalRuntimeError(
391
+ 'BindingRecordError',
392
+ 'PULSE_BINDING_RECORD_INVALID',
393
+ `Pulse Node ${label} must be an ordinary object record.`,
394
+ { label, prototype: prototype && prototype.constructor && prototype.constructor.name || 'null' }
395
+ );
396
+ }
397
+ const entries = [];
398
+ for (const key of Reflect.ownKeys(input)) {
399
+ if (typeof key !== 'string') {
400
+ throw new hostRuntime.CanonicalRuntimeError(
401
+ 'BindingRecordError',
402
+ 'PULSE_BINDING_RECORD_INVALID',
403
+ `Pulse Node ${label} may not contain symbol keys.`,
404
+ { label }
405
+ );
406
+ }
407
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
408
+ if (!descriptor || !descriptor.enumerable) continue;
409
+ if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
410
+ throw new hostRuntime.CanonicalRuntimeError(
411
+ 'BindingRecordError',
412
+ 'PULSE_BINDING_RECORD_INVALID',
413
+ `Pulse Node ${label} may only contain enumerable data properties.`,
414
+ { label, key }
415
+ );
416
+ }
417
+ entries.push([key, descriptor.value]);
418
+ }
419
+ return entries;
420
+ }
421
+
422
+ function valuesFor(executionOptions, key) {
423
+ const source = executionOptions[key] !== undefined ? executionOptions[key] : baseOptions[key];
424
+ return source && typeof source === 'object' ? source : {};
425
+ }
426
+
427
+ function bindingValue(executionOptions, key, name) {
428
+ const values = valuesFor(executionOptions, key);
429
+ const descriptor = Object.getOwnPropertyDescriptor(values, name);
430
+ if (!descriptor) return undefined;
431
+ if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
432
+ throw new hostRuntime.CanonicalRuntimeError(
433
+ 'BindingValueError',
434
+ 'PULSE_BINDING_VALUE_INVALID',
435
+ `Pulse ${key === 'secrets' ? 'secret' : 'config'} providers must expose data properties.`,
436
+ { bindingKind: key === 'secrets' ? 'secret' : 'config', valueType: 'accessor' }
437
+ );
438
+ }
439
+ return descriptor.value;
440
+ }
441
+
442
+ function bindingOptions(executionOptions) {
443
+ return {
444
+ maxBindingNameBytes: executionOptions.maxBindingNameBytes ?? baseOptions.maxBindingNameBytes,
445
+ maxBindingValueBytes: executionOptions.maxBindingValueBytes ?? baseOptions.maxBindingValueBytes,
446
+ maxKvNamespaceBytes: executionOptions.maxKvNamespaceBytes ?? baseOptions.maxKvNamespaceBytes,
447
+ maxKvKeyBytes: executionOptions.maxKvKeyBytes ?? baseOptions.maxKvKeyBytes,
448
+ maxKvValueBytes: executionOptions.maxKvValueBytes ?? baseOptions.maxKvValueBytes,
449
+ maxKvValueDepth: executionOptions.maxKvValueDepth ?? baseOptions.maxKvValueDepth,
450
+ maxKvValueEntries: executionOptions.maxKvValueEntries ?? baseOptions.maxKvValueEntries
451
+ };
452
+ }
453
+
454
+ function kvReferenceFor(executionOptions) {
455
+ if (executionOptions.kvReference) return executionOptions.kvReference;
456
+ if (!Object.prototype.hasOwnProperty.call(executionOptions, 'kv') || executionOptions.kv === baseOptions.kv) return sharedKv;
457
+ const executionId = String(executionOptions.executionId || 'default');
458
+ if (!kvExecutions.has(executionId)) kvExecutions.set(executionId, createNodeKvReference({ ...baseOptions, ...executionOptions, ...bindingOptions(executionOptions) }));
459
+ return kvExecutions.get(executionId);
460
+ }
461
+ function kvStoresFor(executionOptions) { return kvReferenceFor(executionOptions).legacyStores(); }
462
+
463
+ async function dispatchFetch(normalized, executionOptions = {}) {
464
+ const fetchFixtures = normalizeFetchFixtures(executionOptions.fetches || baseOptions.fetches);
465
+ const providerData = buildProviderBackends(fetchFixtures);
466
+ const fixture = providerData.specs.get(`${normalized.init.method} ${normalized.parts.url}`) || providerData.specs.get(normalized.parts.url);
467
+ const liveFetchEnabled = executionOptions.liveFetch === true || baseOptions.liveFetch === true || typeof executionOptions.fetchImplementation === 'function' || typeof baseOptions.fetchImplementation === 'function';
468
+ if (!fixture && liveFetchEnabled) {
469
+ const fetchImplementation = executionOptions.fetchImplementation || baseOptions.fetchImplementation || globalThis.fetch;
470
+ if (typeof fetchImplementation !== 'function') throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', 'Live Node fetch is unavailable in this runtime.', { effectId: normalized.id, url: normalized.parts.url });
471
+ return executeLiveFetch(normalized, fetchImplementation, executionOptions);
472
+ }
473
+ if (!fixture) throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', `No Node provider fixture is configured for ${normalized.init.method} ${normalized.parts.url}.`, { effectId: normalized.id, url: normalized.parts.url });
474
+ if (fixture.kind === 'network-error') throw new hostRuntime.CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', 'Node provider fixture produced a network failure.', { effectId: normalized.id, url: normalized.parts.url });
475
+ if (fixture.kind === 'timeout') throw new hostRuntime.CanonicalRuntimeError('FetchTimeoutError', 'PULSE_FETCH_TIMEOUT', 'Node provider fixture exceeded the fetch timeout.', { effectId: normalized.id, url: normalized.parts.url });
476
+ const delayMs = Math.max(0, Number(fixture.delayMs) || 0);
477
+ const timeoutMs = Number(normalized.init.timeoutMs);
478
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0 && delayMs > timeoutMs) {
479
+ await delay(timeoutMs);
480
+ throw new hostRuntime.CanonicalRuntimeError('FetchTimeoutError', 'PULSE_FETCH_TIMEOUT', 'Node provider fixture exceeded the fetch timeout.', { effectId: normalized.id, url: normalized.parts.url, timeoutMs, delayMs });
481
+ }
482
+ if (delayMs > 0) await delay(delayMs);
483
+ const provider = routeHandlerEffects.createNodeRouteHandlerEffectProvider({ plan: minimalEffectPlan(executionOptions.metadata), backends: providerData.backends });
484
+ const handle = provider.executeFetch(effectToProviderEffect(normalized), { route: provider.plan.routes[0] });
485
+ let snapshot = handle;
486
+ if (handle && typeof handle.toJSON === 'function') {
487
+ snapshot = { ...handle.toJSON() };
488
+ if (handle.bodyStream !== undefined) snapshot.bodyStream = handle.bodyStream;
489
+ if (handle.body !== undefined) snapshot.body = handle.body;
490
+ }
491
+ return responseForMode(snapshot, normalized, executionOptions);
492
+ }
493
+
494
+ return Object.freeze({
495
+ prepareConditionalKv(effect, execution) { return kvReferenceFor(execution).prepareConditionalKv(effect, execution); },
496
+ id: 'node',
497
+ version: CANONICAL_NODE_RUNTIME_VERSION,
498
+ createCapabilities() { return {}; },
499
+ resultMetadata({ metadata }) {
500
+ return providerContract.createProviderLoweringPlan(metadata, NODE_PROVIDER_DESCRIPTOR, {});
501
+ },
502
+ dispatchFetch,
503
+ async dispatchEffect(effect, executionOptions = {}) {
504
+ if (['s3.head', 's3.getText', 's3.putText'].includes(effect.kind)) {
505
+ return require('./s3-reader.js').readS3(effect, { ...baseOptions, ...executionOptions }, (name) => bindingValue(executionOptions, 'secrets', name));
506
+ }
507
+ if (effect.kind === 'fetch') return dispatchFetch(effect, executionOptions);
508
+ if (effect.kind === 'jwt.verify') return verifyNativeJwt(effect, executionOptions);
509
+ if (effect.kind === 'grip.broadcast') {
510
+ const grip = executionOptions.grip || baseOptions.grip || {};
511
+ const broadcast = createNodeJavascriptGripBroadcast({
512
+ ...grip,
513
+ fetchImplementation: executionOptions.fetchImplementation
514
+ || baseOptions.fetchImplementation
515
+ || fixtureFetchImplementation(executionOptions.fetches || baseOptions.fetches),
516
+ secretLookup: (name) => bindingValue(executionOptions, 'secrets', name)
517
+ });
518
+ return broadcast(effect.payload, { signal: executionOptions.signal });
519
+ }
520
+ if (effect.kind === 'config.get') {
521
+ return portableRuntimeHost.normalizeBindingValue(
522
+ 'config.get',
523
+ effect.name,
524
+ bindingValue(executionOptions, 'config', effect.name),
525
+ bindingOptions(executionOptions)
526
+ );
527
+ }
528
+ if (effect.kind === 'secret.get') {
529
+ return portableRuntimeHost.normalizeBindingValue(
530
+ 'secret.get',
531
+ effect.name,
532
+ bindingValue(executionOptions, 'secrets', effect.name),
533
+ bindingOptions(executionOptions)
534
+ );
535
+ }
536
+ if (effect.kind === 'kv.get' || effect.kind === 'kv.put') {
537
+ const namespace = portableRuntimeHost.normalizeKvNamespace(effect.store, bindingOptions(executionOptions));
538
+ const store = kvReferenceFor(executionOptions).kv(namespace);
539
+ return effect.kind === 'kv.get' ? store.get(effect.key) : store.put(effect.key, effect.value);
540
+ }
541
+ if (effect.kind === 'event.emit') {
542
+ const eventAdapter = executionOptions.eventAdapter || baseOptions.eventAdapter;
543
+ if (!eventAdapter || typeof eventAdapter.acceptOutbound !== 'function') {
544
+ throw new hostRuntime.CanonicalRuntimeError(
545
+ 'ProviderCapabilityError',
546
+ 'PULSE_PROVIDER_CAPABILITY_UNSUPPORTED',
547
+ 'Node provider event.emit requires an invocation-scoped event adapter.',
548
+ { provider: 'node', kind: effect.kind, effectId: effect.id }
549
+ );
550
+ }
551
+ return eventAdapter.acceptOutbound(effect.frame, executionOptions);
552
+ }
553
+ if (effect.kind === 'assets.lookup') {
554
+ return assetResponse(effect.payload || {}, kvStoresFor(executionOptions));
555
+ }
556
+ throw new hostRuntime.CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_PROVIDER_CAPABILITY_UNSUPPORTED', `Node provider does not implement ${effect.kind}.`, { provider: 'node', kind: effect.kind, effectId: effect.id });
557
+ },
558
+ disposeExecution({ executionId } = {}) {
559
+ kvExecutions.delete(String(executionId || 'default'));
560
+ }
561
+ });
562
+ }
563
+
564
+ function createCanonicalNodeRuntime(options = {}) {
565
+ return hostRuntime.createCanonicalHostRuntime({
566
+ ...options,
567
+ runtimeVersion: CANONICAL_NODE_RUNTIME_VERSION,
568
+ providerAdapter: createNodeProviderAdapter(options)
569
+ });
570
+ }
571
+
572
+ async function executeCanonicalProgram(programModule, options = {}) {
573
+ return createCanonicalNodeRuntime(options).execute(programModule, options);
574
+ }
575
+
576
+ async function executeNodeNativeEvent(compiled, frame, options = {}) {
577
+ if (options.providerAdapter !== undefined && options.eventAdapter !== undefined) {
578
+ throw new TypeError('Pulse Node Native event execution cannot combine providerAdapter and eventAdapter.');
579
+ }
580
+ return nativeHost.executeCanonicalNativeEvent(compiled, frame, {
581
+ ...options,
582
+ providerAdapter: options.providerAdapter || createNodeProviderAdapter(options)
583
+ });
584
+ }
585
+
586
+ module.exports = {
587
+ CANONICAL_NODE_RUNTIME_VERSION,
588
+ CANONICAL_RUNTIME_PROTOCOL_VERSION: hostRuntime.CANONICAL_RUNTIME_PROTOCOL_VERSION,
589
+ CANONICAL_EFFECT_PROTOCOL_VERSION: hostRuntime.CANONICAL_EFFECT_PROTOCOL_VERSION,
590
+ NODE_PROVIDER_DESCRIPTOR,
591
+ CanonicalRuntimeError: hostRuntime.CanonicalRuntimeError,
592
+ assertCanonicalProgramCompatibility: hostRuntime.assertCanonicalProgramCompatibility,
593
+ createCanonicalNodeRuntime,
594
+ executeCanonicalProgram,
595
+ executeNodeNativeEvent,
596
+ createNodeProviderAdapter,
597
+ createCanonicalContext: hostRuntime.createCanonicalContext,
598
+ normalizeFetchResponse: hostRuntime.normalizeFetchResponse,
599
+ finalResponse: hostRuntime.finalResponse,
600
+ redactedHeaders: hostRuntime.redactedHeaders,
601
+ executeLiveFetch,
602
+ responseIsStructured,
603
+ responseForMode
604
+ };
@@ -0,0 +1,15 @@
1
+
2
+ 'use strict';
3
+ const http = require('node:http');
4
+ const { createPulseWasmCompiledHostRuntimeWithKv } = require('@pulse-compute/wasm-host-runtime/runtime/compiled-wasm-host-runtime-kv');
5
+ function normalizeRawHeaders(rawHeaders) { if (!Array.isArray(rawHeaders)) return []; const pairs = []; for (let i = 0; i < rawHeaders.length; i += 2) if (rawHeaders[i] !== undefined && rawHeaders[i + 1] !== undefined) pairs.push([String(rawHeaders[i]), String(rawHeaders[i + 1])]); return pairs; }
6
+ function normalizeNodeHeaders(req) { if (Array.isArray(req && req.rawHeaders) && req.rawHeaders.length > 0) return normalizeRawHeaders(req.rawHeaders); const headers = req && req.headers ? req.headers : {}; const pairs = []; for (const [name, value] of Object.entries(headers)) Array.isArray(value) ? value.forEach((v) => pairs.push([name, String(v)])) : pairs.push([name, String(value)]); return pairs; }
7
+ function normalizeNodePath(req) { const rawUrl = req && req.url ? String(req.url) : '/'; try { return new URL(rawUrl, 'http://pulsewasm.local').pathname || '/'; } catch (_) { return rawUrl.split('?')[0] || '/'; } }
8
+ function nodeRequestToHostRequest(req, extra = {}) { return { method: String((req && req.method) || 'GET').toUpperCase(), path: normalizeNodePath(req), headers: normalizeNodeHeaders(req), bodyText: extra.bodyText }; }
9
+ function groupHeaders(headers) { const order = []; const groups = new Map(); for (const [name, value] of headers || []) { const display = String(name); const lower = display.toLowerCase(); if (!groups.has(lower)) { groups.set(lower, { name: display, values: [] }); order.push(lower); } groups.get(lower).values.push(String(value)); } return order.map((key) => groups.get(key)); }
10
+ function writeNodeResponse(res, hostResult) { const result = hostResult || { status: 500, kind: 'empty', headers: [] }; res.statusCode = result.status || 500; for (const group of groupHeaders(result.headers || [])) res.setHeader(group.name, group.values.length === 1 ? group.values[0] : group.values.slice()); if (result.kind === 'stream' && result.bodyStream) { if (typeof result.bodyStream.pipe === 'function') { result.bodyStream.pipe(res); return result; } if (Array.isArray(result.bodyStream.chunks)) { if (typeof res.write === 'function') { for (const chunk of result.bodyStream.chunks) res.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))); res.end(); } else { res.end(Buffer.concat(result.bodyStream.chunks.map((chunk) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))))); } return result; } if (typeof result.bodyStream === 'string' || Buffer.isBuffer(result.bodyStream)) { res.end(result.bodyStream); return result; } } res.end(result.body === undefined || result.body === null ? '' : String(result.body)); return result; }
11
+ function createNodeHandler(runtime, options = {}) { if (!runtime || typeof runtime.executeRequest !== 'function') throw new Error('createNodeHandler requires a compiled PulseWasm host runtime.'); return function compiledPulseWasmNodeHandler(req, res) { const result = runtime.executeRequest(nodeRequestToHostRequest(req, options)); writeNodeResponse(res, result); return result; }; }
12
+ function createNodeServer(runtime, options = {}) { const handler = createNodeHandler(runtime, options); return http.createServer((req, res) => { Promise.resolve(handler(req, res)).catch((error) => { res.statusCode = 500; res.setHeader('Content-Type', 'text/plain'); res.end(error && error.message ? error.message : String(error)); }); }); }
13
+ function createPulseWasmCompiledNodeAdapterWithKv(options = {}) { const runtime = options.runtime || createPulseWasmCompiledHostRuntimeWithKv(options); return { version: 'pulsewasm.compiled-wasm-node-adapter-kv.v1', runtime, provider: runtime.provider, kv(name){ return runtime.kv(name); }, executeRequest(input){ return runtime.executeRequest(input); }, executeConnect(input){ return runtime.executeConnect(input); }, executeDisconnect(input){ return runtime.executeDisconnect(input); }, handleNodeRequest: createNodeHandler(runtime, options), createNodeHandler(){ return createNodeHandler(runtime, options); }, createNodeServer(serverOptions = {}) { return createNodeServer(runtime, { ...options, ...serverOptions }); }, close(){ if (runtime.close) return runtime.close(); } };
14
+ }
15
+ module.exports = { createPulseWasmCompiledNodeAdapterWithKv, createNodeHandler, createNodeServer, nodeRequestToHostRequest, normalizeNodeHeaders, normalizeRawHeaders, normalizeNodePath, writeNodeResponse, groupHeaders };