@pulse-compute/provider-node 1.0.0-beta.4 → 1.0.0-beta.6

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.
@@ -67,33 +67,31 @@ function bodyAllowed(method) {
67
67
  return normalized !== 'GET' && normalized !== 'HEAD';
68
68
  }
69
69
 
70
- async function readNodeRequestBody(request, maxBytes = DEFAULT_MAX_BODY_BYTES) {
70
+ async function readNodeRequestBody(request, maxBytes = DEFAULT_MAX_BODY_BYTES, signal) {
71
71
  const limit = Number(maxBytes);
72
72
  if (!Number.isSafeInteger(limit) || limit <= 0) throw new TypeError('Pulse Node maxBodyBytes must be a positive safe integer.');
73
- const chunks = [];
74
- let bytes = 0;
75
- try {
76
- for await (const chunk of request) {
73
+ return new Promise((resolve, reject) => {
74
+ const chunks = [];
75
+ let bytes = 0;
76
+ function cleanup() {
77
+ for (const [event, fn] of [['data', data], ['end', end], ['error', error], ['aborted', aborted]]) request.removeListener(event, fn);
78
+ signal?.removeEventListener('abort', cancel);
79
+ }
80
+ function fail(cause) { cleanup(); chunks.length = 0; request.once('error', () => {}); request.resume(); reject(cause); }
81
+ function cancel() { fail(signal.reason); }
82
+ function error(cause) { fail(new PulseNodeJavascriptRequestError('PULSE_NODE_REQUEST_READ_FAILED', 'Pulse could not read the Node request body.', { causeName: cause?.name })); }
83
+ function aborted() { error(new Error('Request aborted')); }
84
+ function data(chunk) {
77
85
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
78
86
  bytes += buffer.byteLength;
79
- if (bytes > limit) {
80
- throw new PulseNodeJavascriptRequestError(
81
- 'PULSE_REQUEST_BODY_TOO_LARGE',
82
- `Request body exceeds ${limit} bytes.`,
83
- { maxBytes: limit, bytes }
84
- );
85
- }
87
+ if (bytes > limit) return fail(new PulseNodeJavascriptRequestError('PULSE_REQUEST_BODY_TOO_LARGE', `Request body exceeds ${limit} bytes.`, { maxBytes: limit, bytes }));
86
88
  chunks.push(buffer);
87
89
  }
88
- } catch (error) {
89
- if (error instanceof PulseNodeJavascriptRequestError) throw error;
90
- throw new PulseNodeJavascriptRequestError(
91
- 'PULSE_NODE_REQUEST_READ_FAILED',
92
- 'Pulse could not read the Node request body.',
93
- { causeName: error && error.name }
94
- );
95
- }
96
- return Buffer.concat(chunks);
90
+ function end() { cleanup(); resolve(Buffer.concat(chunks)); }
91
+ if (signal?.aborted) return cancel();
92
+ signal?.addEventListener('abort', cancel, { once: true });
93
+ request.on('data', data); request.once('end', end); request.once('error', error); request.once('aborted', aborted);
94
+ });
97
95
  }
98
96
 
99
97
  async function nodeRequestToWebRequest(request, options = {}) {
@@ -104,10 +102,15 @@ async function nodeRequestToWebRequest(request, options = {}) {
104
102
  for (const [name, value] of headerPairs) headers.append(name, value);
105
103
  const init = { method, headers };
106
104
  if (bodyAllowed(method)) {
107
- const body = await readNodeRequestBody(request, options.maxRequestBodyBytes ?? options.maxBodyBytes);
108
- if (body.byteLength > 0) {
109
- init.body = body;
105
+ if (options.bodyForwarding || options.bodyTransform) {
106
+ init.body = require('./incoming-body.js').nodeIncomingStream(request);
110
107
  init.duplex = 'half';
108
+ } else {
109
+ const body = await readNodeRequestBody(request, options.maxRequestBodyBytes ?? options.maxBodyBytes, options.signal);
110
+ if (body.byteLength > 0) {
111
+ init.body = body;
112
+ init.duplex = 'half';
113
+ }
111
114
  }
112
115
  }
113
116
  return Object.freeze({
@@ -144,25 +147,40 @@ async function writeWebResponseToNode(response, nodeResponse, options = {}) {
144
147
  if (!(response instanceof Response)) throw new TypeError('Pulse Node response adaptation requires a Web Response.');
145
148
  if (!nodeResponse || typeof nodeResponse.end !== 'function') throw new TypeError('Pulse Node response adaptation requires a ServerResponse-like object.');
146
149
 
147
- nodeResponse.statusCode = response.status;
148
- if (response.statusText && typeof nodeResponse.statusMessage === 'string') nodeResponse.statusMessage = response.statusText;
149
- for (const group of groupHeaderPairs(runtimeHost.responseHeaderPairs(response))) {
150
- nodeResponse.setHeader(group.name, group.values.length === 1 ? group.values[0] : [...group.values]);
151
- }
150
+ const check = () => { options.requestBudget?.check(); options.signal?.throwIfAborted(); };
151
+ try {
152
+ check();
153
+ nodeResponse.statusCode = response.status;
154
+ if (response.statusText && typeof nodeResponse.statusMessage === 'string') nodeResponse.statusMessage = response.statusText;
155
+ for (const group of groupHeaderPairs(runtimeHost.responseHeaderPairs(response))) {
156
+ nodeResponse.setHeader(group.name, group.values.length === 1 ? group.values[0] : [...group.values]);
157
+ }
158
+ if (options.closeConnection) nodeResponse.setHeader('connection', 'close');
152
159
 
153
- const method = String(options.requestMethod || 'GET').toUpperCase();
154
- if (method === 'HEAD' || !statusAllowsBody(response.status) || response.body == null) {
155
- nodeResponse.end();
156
- return response;
157
- }
160
+ const method = String(options.requestMethod || 'GET').toUpperCase();
161
+ if (method === 'HEAD' || !statusAllowsBody(response.status) || response.body == null) {
162
+ check();
163
+ nodeResponse.end();
164
+ return response;
165
+ }
166
+
167
+ if (typeof Readable.fromWeb === 'function') {
168
+ check();
169
+ await pipeline(Readable.fromWeb(response.body), nodeResponse, { signal: options.signal });
170
+ check();
171
+ return response;
172
+ }
158
173
 
159
- if (typeof Readable.fromWeb === 'function') {
160
- await pipeline(Readable.fromWeb(response.body), nodeResponse);
174
+ const body = Buffer.from(await response.arrayBuffer());
175
+ check();
176
+ nodeResponse.end(body);
161
177
  return response;
178
+ } catch (error) {
179
+ if (response.body && !response.body.locked) void response.body.cancel(error).catch(() => {});
180
+ // Preserve the deadline diagnostic instead of pipeline's generic AbortError.
181
+ check();
182
+ throw error;
162
183
  }
163
-
164
- nodeResponse.end(Buffer.from(await response.arrayBuffer()));
165
- return response;
166
184
  }
167
185
 
168
186
  async function resolveRequestOption(value, context) {
@@ -188,86 +206,120 @@ function createNodeJavascriptHandler(application, options = {}) {
188
206
  maxKvValueDepth: options.maxKvValueDepth,
189
207
  maxKvValueEntries: options.maxKvValueEntries
190
208
  });
191
- return async function pulseNodeJavascriptHandler(request, response) {
192
- const adapted = await nodeRequestToWebRequest(request, options);
193
- const requestContext = Object.freeze({
194
- request,
195
- response,
196
- webRequest: adapted.request,
197
- headerPairs: adapted.headerPairs
198
- });
199
- const requestedCapabilities = await resolveRequestOption(options.capabilities, requestContext);
200
- const effectAdapter = await resolveRequestOption(options.effectAdapter, requestContext);
201
- const schemaCodecs = await resolveRequestOption(options.schemaCodecs, requestContext);
202
- const config = hasDynamicBindings ? await resolveRequestOption(options.config, requestContext) : options.config;
203
- const secrets = hasDynamicBindings ? await resolveRequestOption(options.secrets, requestContext) : options.secrets;
204
- const kv = hasDynamicBindings ? await resolveRequestOption(options.kv, requestContext) : options.kv;
205
- const bindings = staticBindings || createNodeJavascriptBindingCapabilities({
206
- config, secrets, kv,
207
- kvReference: options.kvReference,
208
- maxBindingNameBytes: options.maxBindingNameBytes,
209
- maxBindingValueBytes: options.maxBindingValueBytes,
210
- maxKvNamespaceBytes: options.maxKvNamespaceBytes,
211
- maxKvKeyBytes: options.maxKvKeyBytes,
212
- maxKvValueBytes: options.maxKvValueBytes,
213
- maxKvValueDepth: options.maxKvValueDepth,
214
- maxKvValueEntries: options.maxKvValueEntries
215
- });
216
- const capabilities = effectAdapter === undefined
217
- ? withNodeBindingCapabilities(requestedCapabilities, { bindings })
218
- : requestedCapabilities;
219
- const started = process.hrtime.bigint();
220
- const activeApplication = options.getApplication
221
- ? runtimeHost.normalizeApplication(options.getApplication())
222
- : normalizedApplication;
223
- const webResponse = await executeNodeJavascriptApplication(activeApplication, adapted.request, {
224
- capabilities,
225
- effectAdapter,
226
- config,
227
- secrets,
228
- kv,
229
- application: options.application,
230
- requestHeaders: adapted.headerPairs,
231
- signal: options.signal || adapted.request.signal,
232
- fetchImplementation: options.fetchImplementation,
233
- s3: options.s3 || options.bindings && options.bindings.s3,
234
- s3FetchImplementation: options.s3FetchImplementation,
235
- assetsLookup: options.assetsLookup,
236
- gripBroadcast: options.gripBroadcast,
237
- jwtCaptureWallClock: options.jwtCaptureWallClock,
238
- assets: options.assets,
239
- grip: options.grip,
240
- maxEffects: options.maxEffects,
241
- kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
242
- maxRequestBodyBytes: options.maxRequestBodyBytes ?? options.maxBodyBytes,
243
- maxFetchBodyBytes: options.maxFetchBodyBytes,
244
- maxStructuredBodyBytes: options.maxStructuredBodyBytes,
245
- maxBindingNameBytes: options.maxBindingNameBytes,
246
- maxBindingValueBytes: options.maxBindingValueBytes,
247
- maxKvNamespaceBytes: options.maxKvNamespaceBytes,
248
- maxKvKeyBytes: options.maxKvKeyBytes,
249
- maxKvValueBytes: options.maxKvValueBytes,
250
- maxKvValueDepth: options.maxKvValueDepth,
251
- maxKvValueEntries: options.maxKvValueEntries,
252
- schemaCodecs,
253
- strict: options.strict === true,
254
- provider: options.provider || 'node',
255
- redactionValues: options.redactionValues,
256
- onJsonTrace: options.onJsonTrace,
257
- onEffectObservation: options.onEffectObservation,
258
- onEffectSummary: options.onEffectSummary
259
- });
260
- await writeWebResponseToNode(webResponse, response, { requestMethod: adapted.method });
261
- if (typeof options.onRequest === 'function') {
262
- options.onRequest(Object.freeze({
263
- method: adapted.method,
264
- url: adapted.request.url,
265
- path: new URL(adapted.request.url).pathname,
266
- status: webResponse.status,
267
- durationMs: Number(process.hrtime.bigint() - started) / 1e6
268
- }));
209
+ return async function pulseNodeJavascriptHandler(request, response, requestSignal) {
210
+ const connection = new AbortController();
211
+ const aborted = () => connection.abort(new Error('Node request disconnected.'));
212
+ const closed = () => { if (!response.writableFinished) aborted(); };
213
+ request.once('aborted', aborted); response.once?.('close', closed);
214
+ const signal = AbortSignal.any([options.signal, requestSignal, connection.signal].filter(Boolean));
215
+ const budget = runtimeHost.createRequestBudget({ ...options, signal });
216
+ let incomingBody, adapted, outputExecution;
217
+ try {
218
+ budget.check();
219
+ adapted = await budget.race(nodeRequestToWebRequest(request, { ...options, signal: budget.signal }));
220
+ if ((options.bodyForwarding || options.bodyTransform) && bodyAllowed(adapted.method)) response.setHeader('connection', 'close');
221
+ incomingBody = require('./incoming-body.js').createIncomingBody(adapted.request, {
222
+ ...options, signal: budget.signal, responseWriterOwnsCompletion: true
223
+ });
224
+ const requestContext = Object.freeze({
225
+ request,
226
+ response,
227
+ webRequest: adapted.request,
228
+ signal: budget.signal,
229
+ headerPairs: adapted.headerPairs
230
+ });
231
+ const resolve = value => { budget.check(); return budget.race(resolveRequestOption(value, requestContext)); };
232
+ const requestedCapabilities = await resolve(options.capabilities);
233
+ const effectAdapter = await resolve(options.effectAdapter);
234
+ const schemaCodecs = await resolve(options.schemaCodecs);
235
+ const config = hasDynamicBindings ? await resolve(options.config) : options.config;
236
+ const secrets = hasDynamicBindings ? await resolve(options.secrets) : options.secrets;
237
+ const kv = hasDynamicBindings ? await resolve(options.kv) : options.kv;
238
+ const bindings = staticBindings || createNodeJavascriptBindingCapabilities({
239
+ config, secrets, kv,
240
+ kvReference: options.kvReference,
241
+ maxBindingNameBytes: options.maxBindingNameBytes,
242
+ maxBindingValueBytes: options.maxBindingValueBytes,
243
+ maxKvNamespaceBytes: options.maxKvNamespaceBytes,
244
+ maxKvKeyBytes: options.maxKvKeyBytes,
245
+ maxKvValueBytes: options.maxKvValueBytes,
246
+ maxKvValueDepth: options.maxKvValueDepth,
247
+ maxKvValueEntries: options.maxKvValueEntries
248
+ });
249
+ const capabilities = effectAdapter === undefined
250
+ ? withNodeBindingCapabilities(requestedCapabilities, { bindings })
251
+ : requestedCapabilities;
252
+ const started = process.hrtime.bigint();
253
+ const activeApplication = options.getApplication
254
+ ? runtimeHost.normalizeApplication(options.getApplication())
255
+ : normalizedApplication;
256
+ outputExecution = require('../runtime/generated-output.js').createGeneratedOutput(response, { ...options, requestBudget: budget, requestMethod: adapted.method });
257
+ const webResponse = await executeNodeJavascriptApplication(activeApplication, adapted.request, {
258
+ outputExecution,
259
+ incomingBody,
260
+ capabilities,
261
+ effectAdapter,
262
+ config,
263
+ secrets,
264
+ kv,
265
+ application: options.application,
266
+ requestHeaders: adapted.headerPairs,
267
+ signal: budget.signal,
268
+ requestBudget: budget, requestClock: options.requestClock,
269
+ fetchImplementation: options.fetchImplementation,
270
+ s3: options.s3 || options.bindings && options.bindings.s3,
271
+ s3FetchImplementation: options.s3FetchImplementation,
272
+ assetsLookup: options.assetsLookup,
273
+ gripBroadcast: options.gripBroadcast,
274
+ jwtCaptureWallClock: options.jwtCaptureWallClock,
275
+ assets: options.assets,
276
+ grip: options.grip,
277
+ maxEffects: options.maxEffects,
278
+ kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
279
+ maxRequestBodyBytes: options.maxRequestBodyBytes ?? options.maxBodyBytes,
280
+ maxFetchBodyBytes: options.maxFetchBodyBytes,
281
+ maxStructuredBodyBytes: options.maxStructuredBodyBytes,
282
+ maxBindingNameBytes: options.maxBindingNameBytes,
283
+ maxBindingValueBytes: options.maxBindingValueBytes,
284
+ maxKvNamespaceBytes: options.maxKvNamespaceBytes,
285
+ maxKvKeyBytes: options.maxKvKeyBytes,
286
+ maxKvValueBytes: options.maxKvValueBytes,
287
+ maxKvValueDepth: options.maxKvValueDepth,
288
+ maxKvValueEntries: options.maxKvValueEntries,
289
+ schemaCodecs,
290
+ strict: options.strict === true,
291
+ provider: options.provider || 'node',
292
+ redactionValues: options.redactionValues,
293
+ onJsonTrace: options.onJsonTrace,
294
+ onEffectObservation: options.onEffectObservation,
295
+ onEffectSummary: options.onEffectSummary
296
+ });
297
+ if (incomingBody?.failure) throw incomingBody.failure;
298
+ if (outputExecution?.started) await outputExecution.finish();
299
+ else await writeWebResponseToNode(webResponse, response, {
300
+ requestMethod: adapted.method,
301
+ signal: incomingBody?.responseSignal || budget.signal, requestBudget: budget,
302
+ closeConnection: Boolean((options.bodyForwarding || options.bodyTransform) && bodyAllowed(adapted.method))
303
+ });
304
+ if (typeof options.onRequest === 'function') {
305
+ options.onRequest(Object.freeze({
306
+ method: adapted.method,
307
+ url: adapted.request.url,
308
+ path: new URL(adapted.request.url).pathname,
309
+ status: webResponse.status,
310
+ durationMs: Number(process.hrtime.bigint() - started) / 1e6
311
+ }));
312
+ }
313
+ return webResponse;
314
+ } finally {
315
+ outputExecution?.dispose();
316
+ await incomingBody?.close();
317
+ if ((options.bodyForwarding || options.bodyTransform) && !incomingBody && adapted?.request.body && !adapted.request.body.locked) {
318
+ void adapted.request.body.cancel().catch(() => {});
319
+ }
320
+ request.removeListener('aborted', aborted); response.removeListener?.('close', closed);
321
+ if (!options.requestBudget) budget.close();
269
322
  }
270
- return webResponse;
271
323
  };
272
324
  }
273
325
 
@@ -30,6 +30,7 @@ function unavailable(effect) {
30
30
  function createNodeJavascriptPackageEffectCapabilities(options = {}) {
31
31
  const assetsLookup = options.assetsLookup || (options.assets && options.assets.lookup);
32
32
  const gripBroadcast = options.gripBroadcast || (options.grip && options.grip.broadcast);
33
+ const digestText = require('@pulse-compute/crypto/provider').createJavascriptTextDigest(options);
33
34
  const jwtVerify = options.jwtVerify || (options.jwt && options.jwt.verify);
34
35
  const s3 = options.s3;
35
36
  return Object.freeze({
@@ -39,6 +40,7 @@ function createNodeJavascriptPackageEffectCapabilities(options = {}) {
39
40
  if (typeof s3 !== 'function') return unavailable(effect);
40
41
  return s3(effect, execution);
41
42
  }
43
+ if (effect && effect.contractId === 'pulse.crypto' && effect.operation === 'digestText') return digestText(effect, execution);
42
44
  if (effect && effect.contractId === 'pulse.assets' && effect.operation === 'lookup') {
43
45
  if (typeof assetsLookup !== 'function') return unavailable(effect);
44
46
  return assetsLookup(effect.payload, execution);
@@ -47,7 +49,7 @@ function createNodeJavascriptPackageEffectCapabilities(options = {}) {
47
49
  if (typeof gripBroadcast !== 'function') return unavailableGrip();
48
50
  return gripBroadcast(effect.payload, execution);
49
51
  }
50
- if (effect && effect.contractId === 'pulse.jwt' && effect.operation === 'verify') {
52
+ if (effect && effect.contractId === 'pulse.jwt' && ['verify', 'sign'].includes(effect.operation)) {
51
53
  if (typeof jwtVerify !== 'function') return unavailable(effect);
52
54
  return jwtVerify(effect, execution);
53
55
  }
@@ -63,7 +65,8 @@ function withNodePackageEffectCapabilities(capabilities, options = {}) {
63
65
  ...base,
64
66
  effect(effect, execution) {
65
67
  if (effect && (
66
- effect.contractId === 'pulse.assets'
68
+ effect.contractId === 'pulse.crypto'
69
+ || effect.contractId === 'pulse.assets'
67
70
  || effect.contractId === 'pulse.grip'
68
71
  || effect.contractId === 'pulse.jwt'
69
72
  || effect.contractId === 'pulse.s3'
@@ -44,7 +44,10 @@ function nodeJavascriptProviderCapabilities(options) {
44
44
  maxKvValueDepth: options.maxKvValueDepth,
45
45
  maxKvValueEntries: options.maxKvValueEntries
46
46
  };
47
- const bindingCapabilities = withNodeBindingCapabilities(options.capabilities, bindingOptions);
47
+ const bindingCapabilities = Object.freeze({
48
+ time: () => runtimeHost.readWallTime(options.wallClock === undefined ? () => Date.now() : options.wallClock),
49
+ ...withNodeBindingCapabilities(options.capabilities, bindingOptions)
50
+ });
48
51
  const configuredGripBroadcast = options.gripBroadcast
49
52
  || (options.grip && options.grip.broadcast)
50
53
  || (options.grip && options.grip.publishEndpoint
@@ -67,6 +70,7 @@ function nodeJavascriptProviderCapabilities(options) {
67
70
  assetsLookup: configuredAssetsLookup,
68
71
  gripBroadcast: configuredGripBroadcast,
69
72
  jwtVerify: configuredJwtVerify,
73
+ digestSubtle: options.digestSubtle,
70
74
  s3: createNodeJavascriptS3(options, bindingCapabilities.secret),
71
75
  assets: options.assets,
72
76
  grip: options.grip
@@ -84,12 +88,18 @@ function nodeJavascriptProviderCapabilities(options) {
84
88
 
85
89
  async function executeNodeJavascriptApplication(application, request, options = {}) {
86
90
  const capabilities = nodeJavascriptProviderCapabilities(options);
87
- return runtimeHost.executeApplication(assertNodeJavascriptApplication(application), request, {
91
+ const incomingBody = options.incomingBody || require('./incoming-body.js').createIncomingBody(request, options);
92
+ try {
93
+ options.outputExecution?.bindInput(incomingBody);
94
+ return await runtimeHost.executeApplication(assertNodeJavascriptApplication(application), request, {
95
+ outputExecution: options.outputExecution,
96
+ incomingBody,
88
97
  capabilities,
89
98
  effectAdapter: options.effectAdapter,
90
99
  application: options.application,
91
100
  requestHeaders: options.requestHeaders,
92
101
  signal: options.signal,
102
+ maxDurationMs: options.maxDurationMs, requestBudget: options.requestBudget, requestClock: options.requestClock,
93
103
  kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
94
104
  maxEffects: options.maxEffects,
95
105
  maxRequestBodyBytes: options.maxRequestBodyBytes,
@@ -113,7 +123,7 @@ async function executeNodeJavascriptApplication(application, request, options =
113
123
  onJsonTrace: options.onJsonTrace,
114
124
  onEffectObservation: options.onEffectObservation,
115
125
  onEffectSummary: options.onEffectSummary
116
- });
126
+ }); } finally { if (!options.incomingBody) await incomingBody?.close(); }
117
127
  }
118
128
 
119
129
  async function executeNodeJavascriptEvent(application, frame, options = {}) {
@@ -5,17 +5,18 @@ const { readS3 } = require('../runtime/s3-reader.js');
5
5
 
6
6
  function createNodeJavascriptS3(options, secretLookup) {
7
7
  const s3 = normalizeNodeS3(options.s3 || options.bindings && options.bindings.s3);
8
- return (effect, execution = {}) => {
8
+ return async (effect, execution = {}) => {
9
9
  // This provider target explicitly selects Crypto's Web Crypto realization.
10
10
  // Native execution supplies its own Wasm exports to the same Node transport.
11
11
  let selected;
12
12
  try { selected = bindJavascriptDigestMac(['SHA-256', 'HMAC-SHA256']); }
13
13
  catch { selected = null; }
14
- return readS3(effect, {
15
- s3, signal: execution.signal, registerRedactionValue: execution.registerRedactionValue,
14
+ const result = await readS3(effect, {
15
+ s3, signal: execution.signal, responseSignal: options.signal, requestBudget: execution.requestBudget, registerRedactionValue: execution.registerRedactionValue,
16
16
  fetchImplementation: options.s3FetchImplementation || options.fetchImplementation, cryptoTarget: 'javascript',
17
17
  cryptoVerifier: selected, cryptoRealization: selected && selected.realization
18
18
  }, (name) => secretLookup(name, execution));
19
+ return effect.operation === 'getBody' ? require('@pulse-compute/runtime/host').markOpaqueResponse(result) : result;
19
20
  };
20
21
  }
21
22
  module.exports = { createNodeJavascriptS3 };
@@ -10,7 +10,12 @@ function decision(id, status, reasonId, owner, required = true) {
10
10
 
11
11
  function classifyNodeJavascriptCapability(id, options = {}) {
12
12
  const capability = String(id);
13
- if (['s3.head', 's3.getText', 's3.putText'].includes(capability)) return decision(capability, 'eligible', 'node-s3-bounded-origin-transport', 'provider-node');
13
+ if (capability === 'request.body.transform') return decision(capability, options.bodyTransform ? 'eligible' : 'blocked', options.bodyTransform ? 'node-body-transform-experimental' : 'node-body-transform-not-configured', 'provider-node');
14
+ if (capability === 'response.output') return decision(capability, options.generatedOutput ? 'eligible' : 'blocked', options.generatedOutput ? 'node-generated-output-experimental' : 'node-generated-output-not-configured', 'provider-node');
15
+ if (capability === 'request.body.forward' || capability === 'req.body') return decision(capability,
16
+ options.bodyForwarding ? 'eligible' : 'blocked', options.bodyForwarding ? 'node-incoming-body-forwarding' : 'node-incoming-body-forwarding-not-configured', 'provider-node');
17
+ if (capability === 'crypto.digestText') return decision(capability, 'eligible', 'node-javascript-crypto-sha256-runtime-builtin', 'provider-node');
18
+ if (['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(capability)) return decision(capability, 'eligible', 'node-s3-bounded-origin-transport', 'provider-node');
14
19
  if (isJavascriptCoreCapability(capability)) {
15
20
  return decision(capability, 'eligible', 'node-router-context-parity', 'provider-node');
16
21
  }
@@ -40,9 +45,11 @@ function classifyNodeJavascriptCapability(id, options = {}) {
40
45
  return decision(capability, 'eligible', 'node-provider-wall-clock-authority', 'provider-node');
41
46
  }
42
47
  if (
43
- capability === 'jwt.verify'
48
+ capability === 'jwt.sign'
49
+ || capability === 'jwt.verify'
44
50
  || capability === 'jwt.verify.hs256'
45
51
  || capability === 'jwt.verify.es256'
52
+ || capability === 'jwt.verify.rs256'
46
53
  ) {
47
54
  return decision(capability, 'eligible', 'node-javascript-jwt-crypto-runtime-builtin', 'provider-node');
48
55
  }
@@ -76,7 +83,11 @@ function classifyNodeJavascriptCapability(id, options = {}) {
76
83
 
77
84
  function classifyNodeJavascriptProviderRequirement(id, compilerCapabilities, options = {}) {
78
85
  const requirement = String(id);
79
- if (['s3.head', 's3.getText', 's3.putText'].includes(requirement)) return decision(requirement, 'eligible', 'node-s3-bounded-origin-transport', 'provider-node');
86
+ if (requirement === 'request.body.transform') return classifyNodeJavascriptCapability(requirement, options);
87
+ if (requirement === 'response.output') return classifyNodeJavascriptCapability(requirement, options);
88
+ if (requirement === 'request.body.forward') return classifyNodeJavascriptCapability(requirement, options);
89
+ if (requirement === 'crypto.digestText') return decision(requirement, 'eligible', 'node-javascript-crypto-sha256-runtime-builtin', 'provider-node');
90
+ if (['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(requirement)) return decision(requirement, 'eligible', 'node-s3-bounded-origin-transport', 'provider-node');
80
91
  if (requirement === 'request') {
81
92
  const bodyRequired = compilerCapabilities.some((entry) => entry === 'request.json' || entry.startsWith('request.body'));
82
93
  return bodyRequired
@@ -104,9 +115,11 @@ function classifyNodeJavascriptProviderRequirement(id, compilerCapabilities, opt
104
115
  return decision(requirement, 'eligible', 'node-provider-wall-clock-authority', 'provider-node');
105
116
  }
106
117
  if (
107
- requirement === 'jwt.verify'
118
+ requirement === 'jwt.sign'
119
+ || requirement === 'jwt.verify'
108
120
  || requirement === 'jwt.verify.hs256'
109
121
  || requirement === 'jwt.verify.es256'
122
+ || requirement === 'jwt.verify.rs256'
110
123
  ) {
111
124
  return decision(requirement, 'eligible', 'node-javascript-jwt-crypto-runtime-builtin', 'provider-node');
112
125
  }
@@ -27,6 +27,11 @@ const NODE_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor({
27
27
  realization: 'runtime-builtin',
28
28
  implemented: true,
29
29
  status: 'implemented-g3'
30
+ }, {
31
+ algorithm: 'RS256',
32
+ realization: 'runtime-builtin',
33
+ implemented: true,
34
+ status: 'implemented-rs256'
30
35
  }]
31
36
  }),
32
37
  commands: { compile: true, inspect: true, doctor: true, build: true, test: true, dev: true }
@@ -42,7 +42,10 @@ async function executeNodeJavascriptTestCase(application, testCase, options = {}
42
42
  options.networkFetch === true ? globalThis.fetch : undefined,
43
43
  { rawResponse: true }
44
44
  );
45
+ const collected = options.generatedOutput ? require('../runtime/generated-output-test.js').collector({ ...options, requestMethod: adapted.request.method }) : undefined;
46
+ try {
45
47
  const response = await executeNodeJavascriptApplication(application, adapted.request, {
48
+ outputExecution: collected?.outputExecution,
46
49
  capabilities: options.capabilities,
47
50
  effectAdapter: options.effectAdapter,
48
51
  fetchImplementation: fixtureFetch,
@@ -60,6 +63,9 @@ async function executeNodeJavascriptTestCase(application, testCase, options = {}
60
63
  kv: testCase.kv || {},
61
64
  application: options.application,
62
65
  requestHeaders: adapted.headerPairs,
66
+ maxDurationMs: options.maxDurationMs, requestClock: options.requestClock, requestBudget: collected?.budget || options.requestBudget, signal: options.signal,
67
+ bodyForwarding: options.bodyForwarding,
68
+ bodyTransform: options.bodyTransform,
63
69
  maxEffects: options.maxEffects,
64
70
  maxRequestBodyBytes: options.maxRequestBodyBytes ?? options.maxBodyBytes ?? testCase.maxBodyBytes,
65
71
  maxFetchBodyBytes: options.maxFetchBodyBytes ?? testCase.maxBodyBytes,
@@ -85,7 +91,8 @@ async function executeNodeJavascriptTestCase(application, testCase, options = {}
85
91
  }
86
92
  });
87
93
  const method = String(testCase.request && testCase.request.method || 'GET').toUpperCase();
88
- const body = method === 'HEAD' ? '' : Buffer.from(await response.arrayBuffer()).toString('utf8');
94
+ if (collected?.outputExecution.started) await collected.outputExecution.finish();
95
+ const body = collected?.outputExecution.started ? collected.body() : method === 'HEAD' ? '' : Buffer.from(await response.arrayBuffer()).toString('utf8');
89
96
  return Object.freeze({
90
97
  version: NODE_JAVASCRIPT_TEST_RUNTIME_VERSION,
91
98
  response: Object.freeze({
@@ -106,6 +113,7 @@ async function executeNodeJavascriptTestCase(application, testCase, options = {}
106
113
  }))),
107
114
  resolutionOrder: effectSummary ? effectSummary.resolutionOrder : Object.freeze([])
108
115
  });
116
+ } finally { collected?.dispose(); }
109
117
  }
110
118
 
111
119
  module.exports = Object.freeze({
@@ -30,6 +30,13 @@ const NODE_NATIVE_TARGET_DESCRIPTOR = Object.freeze({
30
30
  capabilities: NODE_NATIVE_TARGET_CAPABILITIES,
31
31
  keyTypes: Object.freeze(['secret', 'jwk', 'jwks']),
32
32
  realizations: Object.freeze([
33
+ Object.freeze({
34
+ kind: 'package-native-application',
35
+ contractId: 'pulse.entities',
36
+ implementation: 'pulse.package-native-application.v1',
37
+ implemented: true,
38
+ automaticFallback: false
39
+ }),
33
40
  Object.freeze({
34
41
  kind: 'crypto-composed',
35
42
  realization: 'guest-source:pulse-hmac-as',
@@ -55,6 +62,19 @@ const NODE_NATIVE_TARGET_DESCRIPTOR = Object.freeze({
55
62
  guestUnitRequired: true,
56
63
  portable: true,
57
64
  automaticFallback: false
65
+ }),
66
+ Object.freeze({
67
+ kind: 'crypto-composed',
68
+ realization: 'guest-linked:pulse-rs256-bearssl-i31',
69
+ implementation: 'bearssl.0.6.rsa-i31.sha256.v1',
70
+ algorithms: Object.freeze(['RS256']),
71
+ keyTypes: Object.freeze(['jwk', 'jwks']),
72
+ implemented: true,
73
+ status: 'implemented-rs256',
74
+ semanticOwner: '@pulse-compute/crypto',
75
+ guestUnitRequired: true,
76
+ portable: true,
77
+ automaticFallback: false
58
78
  })
59
79
  ]),
60
80
  automaticFallback: false,
@@ -70,6 +90,11 @@ const NODE_NATIVE_TARGET_DESCRIPTOR = Object.freeze({
70
90
  realization: 'guest-linked:pulse-es256-rustcrypto-p256',
71
91
  implemented: true,
72
92
  status: 'implemented-g3'
93
+ }, {
94
+ algorithm: 'RS256',
95
+ realization: 'guest-linked:pulse-rs256-bearssl-i31',
96
+ implemented: true,
97
+ status: 'implemented-rs256'
73
98
  }]
74
99
  }),
75
100
  finalWasmPolicy: nodeFinalWasmPolicy