@pulse-compute/provider-node 0.0.0 → 1.0.0-beta.1

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