@pulse-compute/provider-node 1.0.0-beta.5 → 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.
package/README.md CHANGED
@@ -2,20 +2,22 @@
2
2
 
3
3
  <!-- pulse-package-status:start -->
4
4
  > **Support tier:** Implementation/transitive surface<br>
5
- > **Audience:** Pulse canonical Node provider and compiler maintainers.<br>
6
- > **Install directly:** No for application projects; select provider: node through the CLI.<br>
7
- > **Supported entry points:** `@pulse-compute/provider-node/toolchain`<br>
8
- > **Stability:** The listed built-in toolchain entry is synchronized with the Beta bootstrap contract; other canonical Node provider interfaces remain internal.<br>
5
+ > **Audience:** Node deployment integrators and Pulse provider maintainers.<br>
6
+ > **Install directly:** Yes for the production Node launcher; application handlers remain provider-neutral.<br>
7
+ > **Supported entry points:** `@pulse-compute/provider-node/toolchain`, `@pulse-compute/provider-node/server`<br>
8
+ > **Stability:** The /server export is an explicit supported-entry-point exception within this implementation package: finite production HTTP for explicit Node Native and JavaScript builds. Forwarding, generated output, transforms, S3-body bindings and blob-specific production qualification remain separate. /toolchain serves the CLI; all other provider subpaths remain internal.<br>
9
9
  > **npm:** [`@pulse-compute/provider-node`](https://www.npmjs.com/package/@pulse-compute/provider-node)<br>
10
- > **Canonical replacement:** `provider: 'node' through @pulse-compute/cli`<br>
11
- > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/)
10
+ > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/)
12
11
  >
13
- > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.5` package policy.
12
+ > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.6` package policy.
14
13
  <!-- pulse-package-status:end -->
15
14
 
16
15
  Canonical Node provider implementation used internally when a project selects `provider: 'node'`.
17
16
 
18
- Application projects do not install or import this package directly. The supported entry is provider selection through `@pulse-compute/cli`.
17
+ Deployment integrators install this package and import `@pulse-compute/provider-node/server`
18
+ to launch a trusted `pulse build` directory. Application handlers remain provider-neutral.
19
+ See [Node build and execution](https://pulsecompute.io/v1.0.0-beta.6/guides/deploying-node/)
20
+ for configuration, readiness, drain, signals and restart.
19
21
 
20
22
  ## Responsibilities
21
23
 
@@ -60,4 +62,4 @@ The package exposes focused compiler and runtime modules for the synchronized
60
62
  repository, including `runtime/canonical-api-runtime`. Those exports are
61
63
  implementation interfaces, not application-author APIs.
62
64
 
63
- See [Contracts and providers](https://pulsecompute.io/v1.0.0-beta.5/concepts/contracts-and-providers/) and [Implementation packages](https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/).
65
+ See [Contracts and providers](https://pulsecompute.io/v1.0.0-beta.6/concepts/contracts-and-providers/) and [Implementation packages](https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-compute/provider-node",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.6",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": "^22.14.0 || ^24.0.0"
@@ -39,16 +39,20 @@
39
39
  "./javascript/package-effects": "./src/javascript/package-effects.js",
40
40
  "./javascript/jwt-verifier": "./src/javascript/jwt-verifier.js",
41
41
  "./javascript/grip-broadcast": "./src/javascript/grip-broadcast.js",
42
- "./javascript/source-package": "./src/javascript/source-package.js"
42
+ "./javascript/source-package": "./src/javascript/source-package.js",
43
+ "./server": {
44
+ "types": "./src/server/index.d.ts",
45
+ "default": "./src/server/index.js"
46
+ }
43
47
  },
44
48
  "dependencies": {
45
- "@pulse-compute/crypto": "1.0.0-beta.5",
46
- "@pulse-compute/jwt": "1.0.0-beta.5",
47
- "@pulse-compute/runtime": "1.0.0-beta.5",
48
- "@pulse-compute/s3": "1.0.0-beta.5",
49
- "@pulse-compute/wasm-build-support": "1.0.0-beta.5",
50
- "@pulse-compute/wasm-contracts": "1.0.0-beta.5",
51
- "@pulse-compute/wasm-host-runtime": "1.0.0-beta.5"
49
+ "@pulse-compute/crypto": "1.0.0-beta.6",
50
+ "@pulse-compute/jwt": "1.0.0-beta.6",
51
+ "@pulse-compute/runtime": "1.0.0-beta.6",
52
+ "@pulse-compute/s3": "1.0.0-beta.6",
53
+ "@pulse-compute/wasm-build-support": "1.0.0-beta.6",
54
+ "@pulse-compute/wasm-contracts": "1.0.0-beta.6",
55
+ "@pulse-compute/wasm-host-runtime": "1.0.0-beta.6"
52
56
  },
53
57
  "publishConfig": {
54
58
  "access": "public"
@@ -64,7 +68,7 @@
64
68
  "url": "git+https://github.com/pulse-compute/pulse.git",
65
69
  "directory": "packages/provider-node"
66
70
  },
67
- "homepage": "https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/",
71
+ "homepage": "https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/",
68
72
  "bugs": {
69
73
  "url": "https://github.com/pulse-compute/pulse/issues"
70
74
  }
@@ -3,8 +3,11 @@
3
3
  const NODE_JWT_VERIFY_CAPABILITY = 'jwt.verify';
4
4
 
5
5
  const NODE_CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
6
- 's3.head', 's3.getText', 's3.putText',
6
+ 's3.head', 's3.getText', 's3.putText', 's3.getBody',
7
7
  'request',
8
+ 'request.body.forward',
9
+ 'response.output',
10
+ 'request.body.transform',
8
11
  'response.json',
9
12
  'response.text',
10
13
  'response.custom',
@@ -21,14 +24,18 @@ const NODE_CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
21
24
  'assets.lookup',
22
25
  'grip.broadcast',
23
26
  NODE_JWT_VERIFY_CAPABILITY,
27
+ 'jwt.sign',
24
28
  'opaque.pass-through'
25
29
  ]);
26
30
 
27
31
  const NODE_CANONICAL_PROVIDER_LOWERING = Object.freeze({
28
32
  request: 'node.http.request',
33
+ 'request.body.forward': 'node.http.incoming-request-v1',
29
34
  'response.json': 'node.http.response.json',
30
35
  'response.text': 'node.http.response.text',
31
36
  'response.custom': 'node.http.response.custom',
37
+ 'request.body.transform': 'node.http.bounded-text-transform-v1',
38
+ 'response.output': 'node.http.generated-output-v1',
32
39
  fetch: 'node.fetch.dispatch',
33
40
  'time.wall-clock': 'node.runtime.time.now',
34
41
  'crypto.digestText': 'node.crypto.digest-text',
@@ -44,17 +51,21 @@ const NODE_CANONICAL_PROVIDER_LOWERING = Object.freeze({
44
51
  's3.head': 'node.native.s3.head',
45
52
  's3.getText': 'node.native.s3.getText',
46
53
  's3.putText': 'node.native.s3.putText',
54
+ 's3.getBody': 'node.s3.opaque-body',
47
55
  'assets.lookup': 'node.runtime.assets.lookup',
48
56
  'grip.broadcast': 'node.http.grip.publish',
49
57
  [NODE_JWT_VERIFY_CAPABILITY]: 'node.native.jwt.verify',
58
+ 'jwt.sign': 'node.native.jwt.sign',
50
59
  'opaque.pass-through': 'node.http.response.stream'
51
60
  });
52
61
 
53
62
  const NODE_NATIVE_TARGET_CAPABILITIES = Object.freeze([
54
- 's3.head', 's3.getText', 's3.putText',
63
+ 's3.head', 's3.getText', 's3.putText', 's3.getBody',
55
64
  NODE_JWT_VERIFY_CAPABILITY,
65
+ 'jwt.sign',
56
66
  'jwt.verify.hs256',
57
67
  'jwt.verify.es256',
68
+ 'jwt.verify.rs256',
58
69
  'secret.get',
59
70
  'event.ingress',
60
71
  'event.emit',
package/src/config/s3.js CHANGED
@@ -13,7 +13,7 @@ function normalizeNodeS3(input = {}) {
13
13
  }
14
14
  function validateNodeS3Operations(metadata, bindings) {
15
15
  for (const effect of metadata.providerOperations || []) {
16
- if (!['s3.head', 's3.getText', 's3.putText'].includes(effect.kind)) continue;
16
+ if (!['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(effect.kind)) continue;
17
17
  if (!bindings.s3 || !Object.hasOwn(bindings.s3, effect.resource && effect.resource.binding)) throw new TypeError('The required Node S3 binding is missing.');
18
18
  }
19
19
  }
@@ -22,6 +22,9 @@ function createNodeJavascriptFetchCapability(options = {}) {
22
22
  }
23
23
 
24
24
  try {
25
+ if (init.body && typeof init.body === 'object') {
26
+ return await runtimeHost.forwardIncomingBody(init.body, String(url), init, execution);
27
+ }
25
28
  const response = await implementation(String(url), {
26
29
  method: init.method,
27
30
  headers: init.headers,
@@ -136,6 +139,14 @@ function createNodeJavascriptFixtureFetch(fetches = {}, fallback, options = {})
136
139
  }
137
140
  await delayWithSignal(fixture.delayMs, init.signal);
138
141
 
142
+ // Incoming forwarding fixture transport consumes the already bounded
143
+ // provider stream without accumulating bytes or replaying it.
144
+ if (init.duplex === 'half' && init.body?.getReader) {
145
+ const reader = init.body.getReader();
146
+ try { while (!(await reader.read()).done) init.signal?.throwIfAborted(); }
147
+ finally { reader.releaseLock(); }
148
+ }
149
+
139
150
  const status = Number(fixture.status || 200);
140
151
  let body = null;
141
152
  let defaultContentType;
@@ -169,7 +180,7 @@ function createNodeJavascriptFixtureFetch(fetches = {}, fallback, options = {})
169
180
  && status !== 304
170
181
  && !(status >= 100 && status < 200);
171
182
  const response = new Response(bodyAllowed ? body : null, { status, headers });
172
- if (options.rawResponse === true) return response;
183
+ if (options.rawResponse === true || init.duplex === 'half') return response;
173
184
  return runtimeHost.createOpaqueFetchResponse({ response, headers: headerPairs });
174
185
  };
175
186
  }
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ const host = require('@pulse-compute/runtime/host');
4
+ const CHUNK_BYTES = 16 * 1024;
5
+ const QUEUE_BYTES = 64 * 1024;
6
+ const error = (code, message) => new host.PulseRuntimeContractError(code, message);
7
+
8
+ function normalizeBodyForwarding(value, maxDurationMs) {
9
+ if (value === undefined) return undefined;
10
+ if (!value || typeof value !== 'object' || Object.keys(value).some(key => key !== 'maxBytes')
11
+ || !Number.isSafeInteger(value.maxBytes) || value.maxBytes <= 0
12
+ || !Number.isSafeInteger(maxDurationMs) || maxDurationMs < 1 || maxDurationMs > 30000) {
13
+ throw error('PULSE_REQUEST_FORWARDING_CONFIG_INVALID', 'node.bodyForwarding requires a positive safe integer maxBytes and node.maxDurationMs (1–30000).');
14
+ }
15
+ return Object.freeze({ maxBytes: value.maxBytes });
16
+ }
17
+
18
+ // One retained host chunk, one pending read, no whole-body accumulation. The
19
+ // host chunk contract is checked before retaining it; slices are copied so a
20
+ // small outgoing chunk cannot keep an oversized backing allocation alive.
21
+ function boundedBody(source, maxBytes, signal, observe, onClose) {
22
+ let reader, controller, pending, failure, offset = 0, ended = false, stopped = false;
23
+ const metrics = { bytes: 0, maxSourceChunkBytes: 0, maxChunkBytes: 0, maxQueuedBytes: 0 };
24
+ const release = () => { try { reader?.releaseLock(); } catch (_) {} };
25
+ function stop(reason) {
26
+ if (stopped || ended) return;
27
+ failure ||= reason;
28
+ stopped = true; pending = undefined;
29
+ try { Promise.resolve(reader ? reader.cancel(reason) : source?.cancel(reason)).catch(() => {}).finally(release); } catch (_) {}
30
+ try { reason ? controller.error(reason) : controller.close(); } catch (_) {}
31
+ signal?.removeEventListener('abort', abort);
32
+ onClose?.();
33
+ }
34
+ const abort = () => stop(signal.reason);
35
+ const stream = new ReadableStream({
36
+ start(value) { controller = value; signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted) abort(); },
37
+ async pull(value) {
38
+ if (stopped || ended) return;
39
+ try {
40
+ signal?.throwIfAborted();
41
+ if (!pending) {
42
+ if (!source) { ended = true; value.close(); signal?.removeEventListener('abort', abort); onClose?.(); return; }
43
+ reader ||= source.getReader();
44
+ const next = await reader.read();
45
+ if (stopped) return;
46
+ signal?.throwIfAborted();
47
+ if (next.done) { ended = true; value.close(); release(); signal?.removeEventListener('abort', abort); onClose?.(); return; }
48
+ if (!(next.value instanceof Uint8Array) || next.value.byteLength > QUEUE_BYTES
49
+ || next.value.buffer.byteLength > QUEUE_BYTES) {
50
+ throw error('PULSE_REQUEST_FORWARDING_CHUNK_INVALID', 'Provider body chunks and their backing allocations must not exceed 65536 bytes.');
51
+ }
52
+ metrics.bytes += next.value.byteLength;
53
+ if (metrics.bytes > maxBytes) throw error('PULSE_REQUEST_BODY_TOO_LARGE', 'Forwarded body exceeds the configured transfer limit.');
54
+ pending = next.value; offset = 0;
55
+ metrics.maxSourceChunkBytes = Math.max(metrics.maxSourceChunkBytes, pending.byteLength);
56
+ metrics.maxQueuedBytes = Math.max(metrics.maxQueuedBytes, pending.byteLength);
57
+ }
58
+ const count = Math.min(CHUNK_BYTES, pending.byteLength - offset);
59
+ if (count) {
60
+ value.enqueue(Uint8Array.from(pending.subarray(offset, offset + count)));
61
+ metrics.maxChunkBytes = Math.max(metrics.maxChunkBytes, count);
62
+ }
63
+ offset += count;
64
+ if (offset === pending.byteLength) pending = undefined;
65
+ observe?.(Object.freeze({ ...metrics }));
66
+ } catch (cause) { stop(cause); }
67
+ },
68
+ cancel(reason) { stop(reason); }
69
+ }, { highWaterMark: 0 });
70
+ return { stream, stop, metrics, get failure() { return failure; } };
71
+ }
72
+
73
+ function createIncomingBody(request, options) {
74
+ if (options.bodyTransform !== undefined) return require('../runtime/body-transform.js').createTransformInput(request, options);
75
+ const policy = normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs);
76
+ if (!policy) return undefined;
77
+ let upload, timeoutMs, timer, responseSignal, responseTransferred = false;
78
+ const clearTimer = () => { if (timer !== undefined) clearTimeout(timer); timer = undefined; };
79
+ function validate() {
80
+ const length = request.headers.get('content-length');
81
+ if (length !== null && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length)))) {
82
+ throw error('PULSE_REQUEST_FORWARDING_HEADERS_INVALID', 'Invalid incoming content length.');
83
+ }
84
+ if (length !== null && Number(length) > policy.maxBytes) throw error('PULSE_REQUEST_BODY_TOO_LARGE', 'Incoming body exceeds the configured transfer limit.');
85
+ }
86
+ validate();
87
+ return host.createIncomingBodyOwnership({
88
+ get responseSignal() { return responseSignal; },
89
+ validate(init) {
90
+ validate();
91
+ if (init.method !== 'POST') throw error('PULSE_FETCH_BODY_METHOD_UNSUPPORTED', 'Incoming forwarding requires an outbound POST.');
92
+ for (const [name] of init.headers) if (/^(host|content-length|transfer-encoding|connection|keep-alive|te|trailer|upgrade|proxy-connection)$/i.test(name)) {
93
+ throw error('PULSE_REQUEST_FORWARDING_HEADERS_INVALID', 'The provider owns forwarding framing and hop-by-hop headers.');
94
+ }
95
+ if (typeof options.fetchImplementation !== 'function') throw error('PULSE_FETCH_IMPLEMENTATION_UNAVAILABLE', 'Incoming forwarding requires provider-owned network transport.');
96
+ timeoutMs = init.timeoutMs;
97
+ },
98
+ async forward(url, init, execution) {
99
+ // The ordinary fetch effect settles at headers. Keep its shorter budget
100
+ // through response-body completion for this single-use transfer.
101
+ const timeout = new AbortController();
102
+ if (timeoutMs !== undefined) timer = setTimeout(() => timeout.abort(error('PULSE_FETCH_TIMEOUT', 'Incoming forwarding exceeded its fetch timeout.')), Math.min(timeoutMs, options.maxDurationMs));
103
+ const signal = AbortSignal.any([execution.signal, options.signal, timeout.signal].filter(Boolean));
104
+ responseSignal = signal;
105
+ upload = boundedBody(request.body, policy.maxBytes, signal, options.onBodyForwardingObservation);
106
+ let response;
107
+ try {
108
+ response = await options.fetchImplementation(url, {
109
+ method: 'POST', headers: init.headers, body: upload.stream,
110
+ signal, duplex: 'half', redirect: 'manual'
111
+ });
112
+ } catch (cause) { clearTimer(); throw upload.failure || cause; }
113
+ finally { upload.stop(); }
114
+ if (signal.aborted) {
115
+ clearTimer();
116
+ try { void response?.body?.cancel(signal.reason).catch(() => {}); } catch (_) {}
117
+ signal.throwIfAborted();
118
+ }
119
+ if (!(response instanceof Response)) throw error('PULSE_FETCH_RESPONSE_INVALID', 'Forwarding transport must return a Web Response.');
120
+ if (response.status >= 300 && response.status < 400) {
121
+ try { void response.body?.cancel().catch(() => {}); } catch (_) {}
122
+ throw error('PULSE_REQUEST_FORWARDING_REDIRECT', 'Single-use incoming bodies cannot be replayed on redirects.');
123
+ }
124
+ const responseBody = response.body && boundedBody(response.body, policy.maxBytes, signal, options.onBodyForwardingObservation,
125
+ () => { if (!options.responseWriterOwnsCompletion) clearTimer(); });
126
+ responseTransferred = Boolean(responseBody);
127
+ if (!responseBody && !options.responseWriterOwnsCompletion) clearTimer();
128
+ // Headers and status remain provider-owned. No incoming request headers
129
+ // are copied implicitly; the response bytes retain their ordinary form.
130
+ return new Response(responseBody ? responseBody.stream : null, { status: response.status, statusText: response.statusText, headers: response.headers });
131
+ },
132
+ cancel(reason) {
133
+ if (!responseTransferred || options.responseWriterOwnsCompletion) clearTimer();
134
+ upload?.stop(reason);
135
+ if (!upload && request.body && !request.body.locked) void request.body.cancel(reason).catch(() => {});
136
+ // Response ownership has transferred separately to the HTTP writer.
137
+ }
138
+ });
139
+ }
140
+
141
+ // Adapt IncomingMessage without switching it into flowing mode. Node's own
142
+ // transport buffer is separate from the Pulse queue and stays at most 64 KiB.
143
+ function nodeIncomingStream(request) {
144
+ if (request.readableHighWaterMark > QUEUE_BYTES) throw error('PULSE_REQUEST_FORWARDING_CHUNK_INVALID', 'Incoming Node transport high-water mark exceeds 65536 bytes.');
145
+ let stopped = false, wake;
146
+ const notify = () => { wake?.(); wake = undefined; };
147
+ for (const event of ['readable', 'end', 'error', 'aborted']) request.on(event, notify);
148
+ function cleanup() { for (const event of ['readable', 'end', 'error', 'aborted']) request.removeListener(event, notify); }
149
+ return new ReadableStream({
150
+ async pull(controller) {
151
+ while (!stopped) {
152
+ if (request.errored || request.aborted) { cleanup(); throw request.errored || error('PULSE_NODE_REQUEST_READ_FAILED', 'Incoming request was aborted.'); }
153
+ const chunk = request.read(Math.min(request.readableLength || CHUNK_BYTES, CHUNK_BYTES));
154
+ if (chunk) { controller.enqueue(Uint8Array.from(chunk)); return; }
155
+ if (request.readableEnded) { cleanup(); controller.close(); return; }
156
+ await new Promise(resolve => { wake = resolve; });
157
+ }
158
+ },
159
+ cancel() { stopped = true; request.pause(); cleanup(); notify(); }
160
+ }, { highWaterMark: 0 });
161
+ }
162
+
163
+ module.exports = { normalizeBodyForwarding, boundedBody, createIncomingBody, nodeIncomingStream };
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
+ normalizeJwtSignEffect,
4
5
  normalizeJwtVerifyEffect
5
6
  } = require('@pulse-compute/wasm-contracts/jwt/contracts');
6
7
  const {
7
8
  CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
9
+ CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION,
8
10
  CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION
9
11
  } = require('@pulse-compute/wasm-contracts/crypto/contracts');
10
12
 
@@ -16,6 +18,7 @@ const NODE_JAVASCRIPT_JWT_REALIZATIONS = Object.freeze({
16
18
  implementation: CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION,
17
19
  automaticFallback: false
18
20
  }),
21
+ RS256: Object.freeze({ algorithm: 'RS256', realization: 'runtime-builtin', implementation: CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION, automaticFallback: false }),
19
22
  ES256: Object.freeze({
20
23
  algorithm: 'ES256',
21
24
  realization: 'runtime-builtin',
@@ -83,12 +86,19 @@ function createNodeJavascriptJwtVerify(options = {}) {
83
86
  return async function verifyNodeJavascriptJwt(effect, execution) {
84
87
  const { jwtProvider, cryptoProvider } = await loadRuntimeModules();
85
88
  assertRequestActive(execution, jwtProvider);
86
- const input = normalizeJwtVerifyEffect(effect);
87
- if (execution && typeof execution.registerRedactionValue === 'function') {
89
+ const signing = effect.operation === 'sign';
90
+ const input = signing ? normalizeJwtSignEffect(effect) : normalizeJwtVerifyEffect(effect);
91
+ if (!signing && execution && typeof execution.registerRedactionValue === 'function') {
88
92
  execution.registerRedactionValue(input.token);
89
93
  }
90
94
  assertRequestActive(execution, jwtProvider);
91
- const selectedCrypto = assertRuntimeBuiltin(cryptoProvider, jwtProvider);
95
+ const selectedCrypto = signing
96
+ ? (input.options.algorithm === 'RS256'
97
+ ? require('@pulse-compute/crypto/provider').bindJavascriptRs256Signer()
98
+ : input.options.algorithm === 'ES256'
99
+ ? require('@pulse-compute/crypto/provider').bindJavascriptEs256Signer()
100
+ : require('@pulse-compute/crypto/provider').bindJavascriptDigestMac(['HMAC-SHA256']))
101
+ : assertRuntimeBuiltin(cryptoProvider, jwtProvider);
92
102
  const host = {
93
103
  async captureWallClock() {
94
104
  assertRequestActive(execution, jwtProvider);
@@ -118,7 +128,7 @@ function createNodeJavascriptJwtVerify(options = {}) {
118
128
  return result;
119
129
  };
120
130
  }
121
- const result = await jwtProvider.verifyJwtWithCrypto(
131
+ const result = await (signing ? jwtProvider.signJwtWithCrypto : jwtProvider.verifyJwtWithCrypto)(
122
132
  input,
123
133
  Object.freeze(host),
124
134
  selectedCrypto
@@ -14,7 +14,7 @@ function statusForError(error) {
14
14
 
15
15
  function writeSafeError(response, error) {
16
16
  if (response.headersSent) {
17
- if (!response.writableEnded) response.end();
17
+ if (!response.writableEnded) response.destroy(error);
18
18
  return;
19
19
  }
20
20
  response.statusCode = statusForError(error);
@@ -102,10 +102,15 @@ async function nodeRequestToWebRequest(request, options = {}) {
102
102
  for (const [name, value] of headerPairs) headers.append(name, value);
103
103
  const init = { method, headers };
104
104
  if (bodyAllowed(method)) {
105
- const body = await readNodeRequestBody(request, options.maxRequestBodyBytes ?? options.maxBodyBytes, options.signal);
106
- if (body.byteLength > 0) {
107
- init.body = body;
105
+ if (options.bodyForwarding || options.bodyTransform) {
106
+ init.body = require('./incoming-body.js').nodeIncomingStream(request);
108
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
+ }
109
114
  }
110
115
  }
111
116
  return Object.freeze({
@@ -150,6 +155,7 @@ async function writeWebResponseToNode(response, nodeResponse, options = {}) {
150
155
  for (const group of groupHeaderPairs(runtimeHost.responseHeaderPairs(response))) {
151
156
  nodeResponse.setHeader(group.name, group.values.length === 1 ? group.values[0] : [...group.values]);
152
157
  }
158
+ if (options.closeConnection) nodeResponse.setHeader('connection', 'close');
153
159
 
154
160
  const method = String(options.requestMethod || 'GET').toUpperCase();
155
161
  if (method === 'HEAD' || !statusAllowsBody(response.status) || response.body == null) {
@@ -200,11 +206,21 @@ function createNodeJavascriptHandler(application, options = {}) {
200
206
  maxKvValueDepth: options.maxKvValueDepth,
201
207
  maxKvValueEntries: options.maxKvValueEntries
202
208
  });
203
- return async function pulseNodeJavascriptHandler(request, response) {
204
- const budget = runtimeHost.createRequestBudget(options);
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;
205
217
  try {
206
218
  budget.check();
207
- const adapted = await budget.race(nodeRequestToWebRequest(request, { ...options, signal: budget.signal }));
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
+ });
208
224
  const requestContext = Object.freeze({
209
225
  request,
210
226
  response,
@@ -237,7 +253,10 @@ function createNodeJavascriptHandler(application, options = {}) {
237
253
  const activeApplication = options.getApplication
238
254
  ? runtimeHost.normalizeApplication(options.getApplication())
239
255
  : normalizedApplication;
256
+ outputExecution = require('../runtime/generated-output.js').createGeneratedOutput(response, { ...options, requestBudget: budget, requestMethod: adapted.method });
240
257
  const webResponse = await executeNodeJavascriptApplication(activeApplication, adapted.request, {
258
+ outputExecution,
259
+ incomingBody,
241
260
  capabilities,
242
261
  effectAdapter,
243
262
  config,
@@ -275,7 +294,13 @@ function createNodeJavascriptHandler(application, options = {}) {
275
294
  onEffectObservation: options.onEffectObservation,
276
295
  onEffectSummary: options.onEffectSummary
277
296
  });
278
- await writeWebResponseToNode(webResponse, response, { requestMethod: adapted.method, signal: budget.signal, requestBudget: budget });
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
+ });
279
304
  if (typeof options.onRequest === 'function') {
280
305
  options.onRequest(Object.freeze({
281
306
  method: adapted.method,
@@ -286,7 +311,15 @@ function createNodeJavascriptHandler(application, options = {}) {
286
311
  }));
287
312
  }
288
313
  return webResponse;
289
- } finally { if (!options.requestBudget) budget.close(); }
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();
322
+ }
290
323
  };
291
324
  }
292
325
 
@@ -49,7 +49,7 @@ function createNodeJavascriptPackageEffectCapabilities(options = {}) {
49
49
  if (typeof gripBroadcast !== 'function') return unavailableGrip();
50
50
  return gripBroadcast(effect.payload, execution);
51
51
  }
52
- if (effect && effect.contractId === 'pulse.jwt' && effect.operation === 'verify') {
52
+ if (effect && effect.contractId === 'pulse.jwt' && ['verify', 'sign'].includes(effect.operation)) {
53
53
  if (typeof jwtVerify !== 'function') return unavailable(effect);
54
54
  return jwtVerify(effect, execution);
55
55
  }
@@ -88,7 +88,12 @@ function nodeJavascriptProviderCapabilities(options) {
88
88
 
89
89
  async function executeNodeJavascriptApplication(application, request, options = {}) {
90
90
  const capabilities = nodeJavascriptProviderCapabilities(options);
91
- 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,
92
97
  capabilities,
93
98
  effectAdapter: options.effectAdapter,
94
99
  application: options.application,
@@ -118,7 +123,7 @@ async function executeNodeJavascriptApplication(application, request, options =
118
123
  onJsonTrace: options.onJsonTrace,
119
124
  onEffectObservation: options.onEffectObservation,
120
125
  onEffectSummary: options.onEffectSummary
121
- });
126
+ }); } finally { if (!options.incomingBody) await incomingBody?.close(); }
122
127
  }
123
128
 
124
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, requestBudget: execution.requestBudget, 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,8 +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 (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');
13
17
  if (capability === 'crypto.digestText') return decision(capability, 'eligible', 'node-javascript-crypto-sha256-runtime-builtin', 'provider-node');
14
- if (['s3.head', 's3.getText', 's3.putText'].includes(capability)) return decision(capability, 'eligible', 'node-s3-bounded-origin-transport', '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');
15
19
  if (isJavascriptCoreCapability(capability)) {
16
20
  return decision(capability, 'eligible', 'node-router-context-parity', 'provider-node');
17
21
  }
@@ -41,9 +45,11 @@ function classifyNodeJavascriptCapability(id, options = {}) {
41
45
  return decision(capability, 'eligible', 'node-provider-wall-clock-authority', 'provider-node');
42
46
  }
43
47
  if (
44
- capability === 'jwt.verify'
48
+ capability === 'jwt.sign'
49
+ || capability === 'jwt.verify'
45
50
  || capability === 'jwt.verify.hs256'
46
51
  || capability === 'jwt.verify.es256'
52
+ || capability === 'jwt.verify.rs256'
47
53
  ) {
48
54
  return decision(capability, 'eligible', 'node-javascript-jwt-crypto-runtime-builtin', 'provider-node');
49
55
  }
@@ -77,8 +83,11 @@ function classifyNodeJavascriptCapability(id, options = {}) {
77
83
 
78
84
  function classifyNodeJavascriptProviderRequirement(id, compilerCapabilities, options = {}) {
79
85
  const requirement = String(id);
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);
80
89
  if (requirement === 'crypto.digestText') return decision(requirement, 'eligible', 'node-javascript-crypto-sha256-runtime-builtin', 'provider-node');
81
- if (['s3.head', 's3.getText', 's3.putText'].includes(requirement)) return decision(requirement, 'eligible', 'node-s3-bounded-origin-transport', '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');
82
91
  if (requirement === 'request') {
83
92
  const bodyRequired = compilerCapabilities.some((entry) => entry === 'request.json' || entry.startsWith('request.body'));
84
93
  return bodyRequired
@@ -106,9 +115,11 @@ function classifyNodeJavascriptProviderRequirement(id, compilerCapabilities, opt
106
115
  return decision(requirement, 'eligible', 'node-provider-wall-clock-authority', 'provider-node');
107
116
  }
108
117
  if (
109
- requirement === 'jwt.verify'
118
+ requirement === 'jwt.sign'
119
+ || requirement === 'jwt.verify'
110
120
  || requirement === 'jwt.verify.hs256'
111
121
  || requirement === 'jwt.verify.es256'
122
+ || requirement === 'jwt.verify.rs256'
112
123
  ) {
113
124
  return decision(requirement, 'eligible', 'node-javascript-jwt-crypto-runtime-builtin', 'provider-node');
114
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 }