@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.
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.4/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.4` 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.4/concepts/contracts-and-providers/) and [Implementation packages](https://pulsecompute.io/v1.0.0-beta.4/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.4",
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.4",
46
- "@pulse-compute/jwt": "1.0.0-beta.4",
47
- "@pulse-compute/runtime": "1.0.0-beta.4",
48
- "@pulse-compute/s3": "1.0.0-beta.4",
49
- "@pulse-compute/wasm-build-support": "1.0.0-beta.4",
50
- "@pulse-compute/wasm-contracts": "1.0.0-beta.4",
51
- "@pulse-compute/wasm-host-runtime": "1.0.0-beta.4"
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.4/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,12 +3,17 @@
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',
11
14
  'fetch',
15
+ 'time.wall-clock',
16
+ 'crypto.digestText',
12
17
  'config.get',
13
18
  'secret.get',
14
19
  'kv.get',
@@ -19,15 +24,21 @@ const NODE_CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
19
24
  'assets.lookup',
20
25
  'grip.broadcast',
21
26
  NODE_JWT_VERIFY_CAPABILITY,
27
+ 'jwt.sign',
22
28
  'opaque.pass-through'
23
29
  ]);
24
30
 
25
31
  const NODE_CANONICAL_PROVIDER_LOWERING = Object.freeze({
26
32
  request: 'node.http.request',
33
+ 'request.body.forward': 'node.http.incoming-request-v1',
27
34
  'response.json': 'node.http.response.json',
28
35
  'response.text': 'node.http.response.text',
29
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',
30
39
  fetch: 'node.fetch.dispatch',
40
+ 'time.wall-clock': 'node.runtime.time.now',
41
+ 'crypto.digestText': 'node.crypto.digest-text',
31
42
  'config.get': 'node.runtime.config.get',
32
43
  'secret.get': 'node.runtime.secret.get',
33
44
  'kv.get': 'node.runtime.kv.get',
@@ -40,21 +51,26 @@ const NODE_CANONICAL_PROVIDER_LOWERING = Object.freeze({
40
51
  's3.head': 'node.native.s3.head',
41
52
  's3.getText': 'node.native.s3.getText',
42
53
  's3.putText': 'node.native.s3.putText',
54
+ 's3.getBody': 'node.s3.opaque-body',
43
55
  'assets.lookup': 'node.runtime.assets.lookup',
44
56
  'grip.broadcast': 'node.http.grip.publish',
45
57
  [NODE_JWT_VERIFY_CAPABILITY]: 'node.native.jwt.verify',
58
+ 'jwt.sign': 'node.native.jwt.sign',
46
59
  'opaque.pass-through': 'node.http.response.stream'
47
60
  });
48
61
 
49
62
  const NODE_NATIVE_TARGET_CAPABILITIES = Object.freeze([
50
- 's3.head', 's3.getText', 's3.putText',
63
+ 's3.head', 's3.getText', 's3.putText', 's3.getBody',
51
64
  NODE_JWT_VERIFY_CAPABILITY,
65
+ 'jwt.sign',
52
66
  'jwt.verify.hs256',
53
67
  'jwt.verify.es256',
68
+ 'jwt.verify.rs256',
54
69
  'secret.get',
55
70
  'event.ingress',
56
71
  'event.emit',
57
- 'time.wall-clock'
72
+ 'time.wall-clock',
73
+ 'crypto.digestText'
58
74
  ]);
59
75
 
60
76
  module.exports = Object.freeze({
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,
@@ -29,6 +32,10 @@ function createNodeJavascriptFetchCapability(options = {}) {
29
32
  signal: execution.signal,
30
33
  redirect: 'follow'
31
34
  });
35
+ if (execution.signal?.aborted) {
36
+ if (response instanceof Response && response.body) void response.body.cancel(execution.signal.reason).catch(() => {});
37
+ throw execution.signal.reason;
38
+ }
32
39
  if (!(response instanceof Response) && !runtimeHost.isPulseFetchResponse(response)) {
33
40
  throw fetchError(
34
41
  'PULSE_FETCH_RESPONSE_INVALID',
@@ -132,6 +139,14 @@ function createNodeJavascriptFixtureFetch(fetches = {}, fallback, options = {})
132
139
  }
133
140
  await delayWithSignal(fixture.delayMs, init.signal);
134
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
+
135
150
  const status = Number(fixture.status || 200);
136
151
  let body = null;
137
152
  let defaultContentType;
@@ -165,7 +180,7 @@ function createNodeJavascriptFixtureFetch(fetches = {}, fallback, options = {})
165
180
  && status !== 304
166
181
  && !(status >= 100 && status < 200);
167
182
  const response = new Response(bodyAllowed ? body : null, { status, headers });
168
- if (options.rawResponse === true) return response;
183
+ if (options.rawResponse === true || init.duplex === 'half') return response;
169
184
  return runtimeHost.createOpaqueFetchResponse({ response, headers: headerPairs });
170
185
  };
171
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
@@ -6,6 +6,7 @@ const { createNodeJavascriptHandler } = require('./node-adapter.js');
6
6
  const NODE_JAVASCRIPT_LIFECYCLE_VERSION = 'pulse.node-javascript-lifecycle.v1';
7
7
 
8
8
  function statusForError(error) {
9
+ if (['PULSE_REQUEST_DEADLINE_EXCEEDED', 'PULSE_REQUEST_CLOCK_INVALID'].includes(error?.code)) return 504;
9
10
  if (error && error.code === 'PULSE_REQUEST_BODY_TOO_LARGE') return 413;
10
11
  if (error && error.code === 'PULSE_NODE_REQUEST_READ_FAILED') return 400;
11
12
  return 500;
@@ -13,12 +14,13 @@ function statusForError(error) {
13
14
 
14
15
  function writeSafeError(response, error) {
15
16
  if (response.headersSent) {
16
- if (!response.writableEnded) response.end();
17
+ if (!response.writableEnded) response.destroy(error);
17
18
  return;
18
19
  }
19
20
  response.statusCode = statusForError(error);
20
21
  response.setHeader('content-type', 'text/plain; charset=utf-8');
21
- response.end(response.statusCode === 413 ? 'Payload Too Large' : (response.statusCode === 400 ? 'Bad Request' : 'Internal Server Error'));
22
+ if (response.statusCode === 504) response.setHeader('connection', 'close');
23
+ response.end(response.statusCode === 504 ? 'Gateway Timeout' : response.statusCode === 413 ? 'Payload Too Large' : (response.statusCode === 400 ? 'Bad Request' : 'Internal Server Error'));
22
24
  }
23
25
 
24
26
  function createNodeJavascriptServer(application, options = {}) {