@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.
- package/README.md +60 -1
- package/package.json +61 -6
- package/src/capabilities.js +56 -0
- package/src/compiler/assets-provider-proof.js +434 -0
- package/src/compiler/backend-json-request-body-proof.js +90 -0
- package/src/compiler/index.js +38 -0
- package/src/compiler/node-adapter.js +452 -0
- package/src/compiler/node-compiled-wasm-assets-lifecycle-proof.js +435 -0
- package/src/compiler/request-json-body-proof.js +181 -0
- package/src/compiler/route-handler-compiled-wasm-bridge.js +323 -0
- package/src/compiler/route-handler-effect-proof.js +215 -0
- package/src/compiler/route-handler-live-origin-proof.js +372 -0
- package/src/compiler/schema-decode-result-proof.js +180 -0
- package/src/compiler/schema-response-codec-proof.js +149 -0
- package/src/events/reference-adapter.js +145 -0
- package/src/index.js +57 -0
- package/src/javascript/assets-adapter.js +68 -0
- package/src/javascript/bindings-adapter.js +120 -0
- package/src/javascript/fetch-adapter.js +187 -0
- package/src/javascript/grip-broadcast.js +243 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +69 -0
- package/src/javascript/node-adapter.js +281 -0
- package/src/javascript/package-effects.js +78 -0
- package/src/javascript/runtime-host.js +165 -0
- package/src/javascript/source-package.js +439 -0
- package/src/javascript/support.js +205 -0
- package/src/javascript/target-support-policy.js +134 -0
- package/src/javascript/target.js +37 -0
- package/src/javascript/test-runtime.js +111 -0
- package/src/native/target.js +78 -0
- package/src/runtime/assets-provider.js +776 -0
- package/src/runtime/canonical-api-runtime.js +632 -0
- package/src/runtime/compiled-wasm-node-adapter-kv.js +15 -0
- package/src/runtime/index.js +17 -0
- package/src/runtime/jwt-verifier.js +264 -0
- package/src/runtime/route-handler-effects.js +757 -0
- package/src/toolchain.js +258 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
|
|
5
|
+
const NODE_JAVASCRIPT_FETCH_ADAPTER_VERSION = 'pulse.node-javascript-fetch-adapter.v1';
|
|
6
|
+
|
|
7
|
+
function fetchError(code, message, detail, cause) {
|
|
8
|
+
const error = new runtimeHost.PulseRuntimeContractError(code, message, { cause });
|
|
9
|
+
if (detail !== undefined) Object.defineProperty(error, 'detail', { enumerable: false, value: Object.freeze({ ...detail }) });
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createNodeJavascriptFetchCapability(options = {}) {
|
|
14
|
+
const implementation = options.fetchImplementation;
|
|
15
|
+
return async function nodeJavascriptFetch(url, init = {}, execution = {}) {
|
|
16
|
+
if (typeof implementation !== 'function') {
|
|
17
|
+
throw fetchError(
|
|
18
|
+
'PULSE_FETCH_IMPLEMENTATION_UNAVAILABLE',
|
|
19
|
+
'Node JavaScript fetch is disabled or no provider-owned implementation was supplied.',
|
|
20
|
+
{ url: String(url), method: String(init.method || 'GET') }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await implementation(String(url), {
|
|
26
|
+
method: init.method,
|
|
27
|
+
headers: init.headers,
|
|
28
|
+
body: init.body,
|
|
29
|
+
signal: execution.signal,
|
|
30
|
+
redirect: 'follow'
|
|
31
|
+
});
|
|
32
|
+
if (!(response instanceof Response) && !runtimeHost.isPulseFetchResponse(response)) {
|
|
33
|
+
throw fetchError(
|
|
34
|
+
'PULSE_FETCH_RESPONSE_INVALID',
|
|
35
|
+
'Node JavaScript fetch implementations must resolve to a Web Response or Pulse fetch response.',
|
|
36
|
+
{ url: String(url), type: typeof response }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return response;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof runtimeHost.PulseRuntimeContractError) throw error;
|
|
42
|
+
if (execution.signal && execution.signal.aborted) {
|
|
43
|
+
const reason = execution.signal.reason;
|
|
44
|
+
if (reason instanceof runtimeHost.PulseRuntimeContractError) throw reason;
|
|
45
|
+
throw reason || error;
|
|
46
|
+
}
|
|
47
|
+
throw fetchError(
|
|
48
|
+
'PULSE_FETCH_NETWORK',
|
|
49
|
+
`Node JavaScript fetch failed for ${String(init.method || 'GET')} ${String(url)}.`,
|
|
50
|
+
{ url: String(url), method: String(init.method || 'GET') },
|
|
51
|
+
error
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function delayWithSignal(ms, signal) {
|
|
58
|
+
const delay = Math.max(0, Number(ms) || 0);
|
|
59
|
+
if (delay === 0) return Promise.resolve();
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
let timer;
|
|
62
|
+
const finish = (callback, value) => {
|
|
63
|
+
if (timer) clearTimeout(timer);
|
|
64
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
65
|
+
callback(value);
|
|
66
|
+
};
|
|
67
|
+
const onAbort = () => finish(reject, signal.reason || new DOMException('Aborted', 'AbortError'));
|
|
68
|
+
if (signal && signal.aborted) return onAbort();
|
|
69
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
70
|
+
timer = setTimeout(() => finish(resolve), delay);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fixtureHeaderPairs(input, defaultContentType) {
|
|
75
|
+
const pairs = [];
|
|
76
|
+
const hasContentType = () => pairs.some(([name]) => String(name).toLowerCase() === 'content-type');
|
|
77
|
+
if (Array.isArray(input)) {
|
|
78
|
+
for (const pair of input) {
|
|
79
|
+
if (!Array.isArray(pair) || pair.length !== 2) throw new TypeError('Node JavaScript fetch fixture headers must be [name, value] pairs.');
|
|
80
|
+
pairs.push([String(pair[0]), String(pair[1])]);
|
|
81
|
+
}
|
|
82
|
+
} else if (input instanceof Headers) {
|
|
83
|
+
for (const [name, value] of input.entries()) pairs.push([name, value]);
|
|
84
|
+
if (typeof input.getSetCookie === 'function') {
|
|
85
|
+
const cookies = input.getSetCookie();
|
|
86
|
+
if (cookies.length > 1) {
|
|
87
|
+
for (let index = pairs.length - 1; index >= 0; index -= 1) {
|
|
88
|
+
if (pairs[index][0].toLowerCase() === 'set-cookie') pairs.splice(index, 1);
|
|
89
|
+
}
|
|
90
|
+
for (const cookie of cookies) pairs.push(['set-cookie', cookie]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} else if (input && typeof input === 'object') {
|
|
94
|
+
for (const [name, value] of Object.entries(input)) pairs.push([name, String(value)]);
|
|
95
|
+
}
|
|
96
|
+
if (defaultContentType && !hasContentType()) pairs.push(['content-type', defaultContentType]);
|
|
97
|
+
return Object.freeze(pairs.map(([name, value]) => Object.freeze([name, value])));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fixtureHeaders(pairs) {
|
|
101
|
+
const headers = new Headers();
|
|
102
|
+
for (const [name, value] of pairs) headers.append(name, value);
|
|
103
|
+
return headers;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fixtureKey(fetches, url, method) {
|
|
107
|
+
if (!fetches || typeof fetches !== 'object') return undefined;
|
|
108
|
+
const exact = `${String(method || 'GET').toUpperCase()} ${String(url)}`;
|
|
109
|
+
return Object.prototype.hasOwnProperty.call(fetches, exact) ? exact
|
|
110
|
+
: Object.prototype.hasOwnProperty.call(fetches, String(url)) ? String(url)
|
|
111
|
+
: undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createNodeJavascriptFixtureFetch(fetches = {}, fallback, options = {}) {
|
|
115
|
+
return async function nodeJavascriptFixtureFetch(url, init = {}) {
|
|
116
|
+
const key = fixtureKey(fetches, url, init.method);
|
|
117
|
+
if (key === undefined) {
|
|
118
|
+
if (typeof fallback === 'function') return fallback(url, init);
|
|
119
|
+
throw new Error(`No Node JavaScript fetch fixture is configured for ${String(init.method || 'GET')} ${String(url)}.`);
|
|
120
|
+
}
|
|
121
|
+
const fixture = fetches[key] || {};
|
|
122
|
+
if (fixture.kind === 'network-error' || fixture.networkError === true) {
|
|
123
|
+
throw new Error(fixture.message || 'Configured Node JavaScript fetch network failure.');
|
|
124
|
+
}
|
|
125
|
+
if (fixture.kind === 'timeout' || fixture.timeout === true) {
|
|
126
|
+
await new Promise((_resolve, reject) => {
|
|
127
|
+
const signal = init.signal;
|
|
128
|
+
const onAbort = () => reject(signal.reason || new DOMException('Aborted', 'AbortError'));
|
|
129
|
+
if (signal && signal.aborted) onAbort();
|
|
130
|
+
else if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
await delayWithSignal(fixture.delayMs, init.signal);
|
|
134
|
+
|
|
135
|
+
const status = Number(fixture.status || 200);
|
|
136
|
+
let body = null;
|
|
137
|
+
let defaultContentType;
|
|
138
|
+
if (fixture.opaque === true || Array.isArray(fixture.chunks)) {
|
|
139
|
+
const chunks = Array.isArray(fixture.chunks) ? fixture.chunks : [];
|
|
140
|
+
body = new ReadableStream({
|
|
141
|
+
start(controller) {
|
|
142
|
+
for (const chunk of chunks) controller.enqueue(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk);
|
|
143
|
+
controller.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
defaultContentType = 'application/octet-stream';
|
|
147
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'value')) {
|
|
148
|
+
body = JSON.stringify(fixture.value);
|
|
149
|
+
defaultContentType = 'application/json; charset=utf-8';
|
|
150
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'json')) {
|
|
151
|
+
body = JSON.stringify(fixture.json);
|
|
152
|
+
defaultContentType = 'application/json; charset=utf-8';
|
|
153
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'text')) {
|
|
154
|
+
body = String(fixture.text);
|
|
155
|
+
defaultContentType = 'text/plain; charset=utf-8';
|
|
156
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'body')) {
|
|
157
|
+
body = fixture.body == null ? null : fixture.body;
|
|
158
|
+
}
|
|
159
|
+
const headerPairs = fixtureHeaderPairs(fixture.headers, defaultContentType);
|
|
160
|
+
const headers = fixtureHeaders(headerPairs);
|
|
161
|
+
const method = String(init.method || 'GET').toUpperCase();
|
|
162
|
+
const bodyAllowed = method !== 'HEAD'
|
|
163
|
+
&& status !== 204
|
|
164
|
+
&& status !== 205
|
|
165
|
+
&& status !== 304
|
|
166
|
+
&& !(status >= 100 && status < 200);
|
|
167
|
+
const response = new Response(bodyAllowed ? body : null, { status, headers });
|
|
168
|
+
if (options.rawResponse === true) return response;
|
|
169
|
+
return runtimeHost.createOpaqueFetchResponse({ response, headers: headerPairs });
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function withNodeFetchCapability(capabilities, options = {}) {
|
|
174
|
+
const value = capabilities && typeof capabilities === 'object' ? capabilities : {};
|
|
175
|
+
if (typeof value.fetch === 'function') return value;
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
...value,
|
|
178
|
+
fetch: createNodeJavascriptFetchCapability(options)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = Object.freeze({
|
|
183
|
+
NODE_JAVASCRIPT_FETCH_ADAPTER_VERSION,
|
|
184
|
+
createNodeJavascriptFetchCapability,
|
|
185
|
+
createNodeJavascriptFixtureFetch,
|
|
186
|
+
withNodeFetchCapability
|
|
187
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PulseNodeJavascriptPackageEffectError } = require('./package-effects.js');
|
|
4
|
+
|
|
5
|
+
const NODE_JAVASCRIPT_GRIP_BROADCAST_VERSION = 'pulse.node-javascript-grip-broadcast.v1';
|
|
6
|
+
const DEFAULT_MAX_REQUEST_BYTES = 65536;
|
|
7
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
8
|
+
|
|
9
|
+
function fail(code, message, detail = {}) {
|
|
10
|
+
throw new PulseNodeJavascriptPackageEffectError(code, message, detail);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function positiveSafeInteger(value, fallback, field) {
|
|
14
|
+
if (value === undefined) return fallback;
|
|
15
|
+
const number = Number(value);
|
|
16
|
+
if (!Number.isSafeInteger(number) || number <= 0) {
|
|
17
|
+
throw new TypeError(`Pulse Node JavaScript GRIP ${field} must be a positive safe integer.`);
|
|
18
|
+
}
|
|
19
|
+
return number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeEndpoint(input) {
|
|
23
|
+
if (typeof input !== 'string' || input.trim().length === 0) {
|
|
24
|
+
fail(
|
|
25
|
+
'PULSE_GRIP_BROADCAST_CAPABILITY_REQUIRED',
|
|
26
|
+
'GRIP broadcast requires an explicitly configured provider publish endpoint.',
|
|
27
|
+
{ operation: 'broadcast', configuration: 'grip.publishEndpoint' }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
let endpoint;
|
|
31
|
+
try {
|
|
32
|
+
endpoint = new URL(input);
|
|
33
|
+
} catch {
|
|
34
|
+
fail(
|
|
35
|
+
'PULSE_GRIP_BROADCAST_ENDPOINT_INVALID',
|
|
36
|
+
'GRIP broadcast publishEndpoint must be an absolute HTTP or HTTPS URL.',
|
|
37
|
+
{ operation: 'broadcast', configuration: 'grip.publishEndpoint' }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)) {
|
|
41
|
+
fail(
|
|
42
|
+
'PULSE_GRIP_BROADCAST_ENDPOINT_INVALID',
|
|
43
|
+
'GRIP broadcast publishEndpoint must use HTTP or HTTPS.',
|
|
44
|
+
{ operation: 'broadcast', configuration: 'grip.publishEndpoint', protocol: endpoint.protocol }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return endpoint.href;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeAuthentication(input) {
|
|
51
|
+
if (input === undefined) return undefined;
|
|
52
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
53
|
+
throw new TypeError('Pulse Node JavaScript GRIP authentication must be an object.');
|
|
54
|
+
}
|
|
55
|
+
const scheme = input.scheme === undefined ? 'bearer' : String(input.scheme).toLowerCase();
|
|
56
|
+
if (scheme !== 'bearer') {
|
|
57
|
+
fail(
|
|
58
|
+
'PULSE_GRIP_BROADCAST_AUTH_SCHEME_UNSUPPORTED',
|
|
59
|
+
'GRIP broadcast currently supports only bearer authentication.',
|
|
60
|
+
{ operation: 'broadcast', scheme }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const secretRef = typeof input.secretRef === 'string' ? input.secretRef.trim() : '';
|
|
64
|
+
if (!secretRef) {
|
|
65
|
+
fail(
|
|
66
|
+
'PULSE_GRIP_BROADCAST_SECRET_REFERENCE_REQUIRED',
|
|
67
|
+
'GRIP bearer authentication requires a named secret reference.',
|
|
68
|
+
{ operation: 'broadcast', configuration: 'grip.authentication.secretRef' }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return Object.freeze({ scheme, secretRef });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requestBody(payload) {
|
|
75
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
76
|
+
fail('PULSE_GRIP_BROADCAST_MESSAGE_INVALID', 'GRIP broadcast requires a structured message.', { operation: 'broadcast' });
|
|
77
|
+
}
|
|
78
|
+
const channel = typeof payload.channel === 'string' ? payload.channel.trim() : '';
|
|
79
|
+
if (!channel || /[,\r\n]/.test(channel)) {
|
|
80
|
+
fail(
|
|
81
|
+
'PULSE_GRIP_BROADCAST_CHANNEL_INVALID',
|
|
82
|
+
'GRIP broadcast channel must be a non-empty string without commas or line breaks.',
|
|
83
|
+
{ operation: 'broadcast' }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'data') || payload.data === undefined) {
|
|
87
|
+
fail('PULSE_GRIP_BROADCAST_MESSAGE_INVALID', 'GRIP broadcast requires a JSON-compatible data value.', { operation: 'broadcast' });
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const body = JSON.stringify({
|
|
91
|
+
channel,
|
|
92
|
+
data: payload.data,
|
|
93
|
+
...(payload.event === undefined ? {} : { event: String(payload.event) }),
|
|
94
|
+
...(payload.id === undefined ? {} : { id: String(payload.id) })
|
|
95
|
+
});
|
|
96
|
+
if (typeof body !== 'string') throw new TypeError('not JSON serializable');
|
|
97
|
+
return body;
|
|
98
|
+
} catch {
|
|
99
|
+
fail('PULSE_GRIP_BROADCAST_MESSAGE_INVALID', 'GRIP broadcast message must be JSON-compatible.', { operation: 'broadcast' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readBoundedResponseText(response, maxBytes) {
|
|
104
|
+
const contentLength = Number(response.headers && response.headers.get && response.headers.get('content-length'));
|
|
105
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
106
|
+
fail(
|
|
107
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_TOO_LARGE',
|
|
108
|
+
'GRIP broadcast provider response exceeded the configured byte limit.',
|
|
109
|
+
{ operation: 'broadcast', maxResponseBytes: maxBytes }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (!response.body) return '';
|
|
113
|
+
const reader = response.body.getReader();
|
|
114
|
+
const decoder = new TextDecoder();
|
|
115
|
+
let size = 0;
|
|
116
|
+
let output = '';
|
|
117
|
+
try {
|
|
118
|
+
for (;;) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done) break;
|
|
121
|
+
size += value.byteLength;
|
|
122
|
+
if (size > maxBytes) {
|
|
123
|
+
await reader.cancel();
|
|
124
|
+
fail(
|
|
125
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_TOO_LARGE',
|
|
126
|
+
'GRIP broadcast provider response exceeded the configured byte limit.',
|
|
127
|
+
{ operation: 'broadcast', maxResponseBytes: maxBytes }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
output += decoder.decode(value, { stream: true });
|
|
131
|
+
}
|
|
132
|
+
return output + decoder.decode();
|
|
133
|
+
} finally {
|
|
134
|
+
reader.releaseLock();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeAck(status, text) {
|
|
139
|
+
if (!text.trim()) return Object.freeze({ accepted: true, status });
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(text);
|
|
143
|
+
} catch {
|
|
144
|
+
fail(
|
|
145
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_INVALID',
|
|
146
|
+
'GRIP broadcast provider returned an invalid acknowledgement.',
|
|
147
|
+
{ operation: 'broadcast', status }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
151
|
+
fail(
|
|
152
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_INVALID',
|
|
153
|
+
'GRIP broadcast provider acknowledgement must be an object.',
|
|
154
|
+
{ operation: 'broadcast', status }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const messageId = parsed.messageId === undefined ? undefined : String(parsed.messageId);
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
accepted: parsed.accepted !== false,
|
|
160
|
+
status,
|
|
161
|
+
...(messageId === undefined ? {} : { messageId })
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createNodeJavascriptGripBroadcast(options = {}) {
|
|
166
|
+
const endpoint = normalizeEndpoint(options.publishEndpoint);
|
|
167
|
+
const authentication = normalizeAuthentication(options.authentication);
|
|
168
|
+
const maxRequestBytes = positiveSafeInteger(options.maxRequestBytes, DEFAULT_MAX_REQUEST_BYTES, 'maxRequestBytes');
|
|
169
|
+
const maxResponseBytes = positiveSafeInteger(options.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES, 'maxResponseBytes');
|
|
170
|
+
const fetchImplementation = options.fetchImplementation;
|
|
171
|
+
const secretLookup = options.secretLookup;
|
|
172
|
+
if (typeof fetchImplementation !== 'function') {
|
|
173
|
+
fail(
|
|
174
|
+
'PULSE_GRIP_BROADCAST_CAPABILITY_REQUIRED',
|
|
175
|
+
'GRIP broadcast requires an explicitly supplied provider fetch implementation.',
|
|
176
|
+
{ operation: 'broadcast', configuration: 'fetchImplementation' }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return async function nodeJavascriptGripBroadcast(payload, execution = {}) {
|
|
181
|
+
const body = requestBody(payload);
|
|
182
|
+
const requestBytes = new TextEncoder().encode(body).byteLength;
|
|
183
|
+
if (requestBytes > maxRequestBytes) {
|
|
184
|
+
fail(
|
|
185
|
+
'PULSE_GRIP_BROADCAST_REQUEST_TOO_LARGE',
|
|
186
|
+
'GRIP broadcast request exceeded the configured byte limit.',
|
|
187
|
+
{ operation: 'broadcast', maxRequestBytes }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const headers = new Headers({
|
|
191
|
+
accept: 'application/json',
|
|
192
|
+
'content-type': 'application/json; charset=utf-8'
|
|
193
|
+
});
|
|
194
|
+
if (authentication) {
|
|
195
|
+
const secret = typeof secretLookup === 'function' ? await secretLookup(authentication.secretRef) : undefined;
|
|
196
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
197
|
+
fail(
|
|
198
|
+
'PULSE_GRIP_BROADCAST_SECRET_REQUIRED',
|
|
199
|
+
'GRIP broadcast authentication secret is unavailable.',
|
|
200
|
+
{ operation: 'broadcast', secretRef: authentication.secretRef }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
headers.set('authorization', `Bearer ${secret}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let response;
|
|
207
|
+
let text;
|
|
208
|
+
try {
|
|
209
|
+
response = await fetchImplementation(endpoint, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers,
|
|
212
|
+
body,
|
|
213
|
+
signal: execution.signal,
|
|
214
|
+
redirect: 'error'
|
|
215
|
+
});
|
|
216
|
+
if (!(response instanceof Response)) {
|
|
217
|
+
fail('PULSE_GRIP_BROADCAST_RESPONSE_INVALID', 'GRIP broadcast provider must return a Web Response.', { operation: 'broadcast' });
|
|
218
|
+
}
|
|
219
|
+
text = await readBoundedResponseText(response, maxResponseBytes);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof PulseNodeJavascriptPackageEffectError) throw error;
|
|
222
|
+
if (execution.signal && execution.signal.aborted) {
|
|
223
|
+
fail('PULSE_GRIP_BROADCAST_ABORTED', 'GRIP broadcast was cancelled with its owning request.', { operation: 'broadcast' });
|
|
224
|
+
}
|
|
225
|
+
fail('PULSE_GRIP_BROADCAST_NETWORK', 'GRIP broadcast provider request failed.', { operation: 'broadcast' });
|
|
226
|
+
}
|
|
227
|
+
if (response.status < 200 || response.status > 299) {
|
|
228
|
+
fail(
|
|
229
|
+
'PULSE_GRIP_BROADCAST_REJECTED',
|
|
230
|
+
'GRIP broadcast provider rejected the publication.',
|
|
231
|
+
{ operation: 'broadcast', status: response.status }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return normalizeAck(response.status, text);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = Object.freeze({
|
|
239
|
+
NODE_JAVASCRIPT_GRIP_BROADCAST_VERSION,
|
|
240
|
+
DEFAULT_MAX_REQUEST_BYTES,
|
|
241
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
242
|
+
createNodeJavascriptGripBroadcast
|
|
243
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
normalizeJwtVerifyEffect
|
|
5
|
+
} = require('@pulse-compute/wasm-contracts/jwt/contracts');
|
|
6
|
+
const {
|
|
7
|
+
CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
8
|
+
CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION
|
|
9
|
+
} = require('@pulse-compute/wasm-contracts/crypto/contracts');
|
|
10
|
+
|
|
11
|
+
const NODE_JAVASCRIPT_JWT_VERIFIER_VERSION = 'pulse.node-javascript-jwt-verifier.v2';
|
|
12
|
+
const NODE_JAVASCRIPT_JWT_REALIZATIONS = Object.freeze({
|
|
13
|
+
HS256: Object.freeze({
|
|
14
|
+
algorithm: 'HS256',
|
|
15
|
+
realization: 'runtime-builtin',
|
|
16
|
+
implementation: CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
17
|
+
automaticFallback: false
|
|
18
|
+
}),
|
|
19
|
+
ES256: Object.freeze({
|
|
20
|
+
algorithm: 'ES256',
|
|
21
|
+
realization: 'runtime-builtin',
|
|
22
|
+
implementation: CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
23
|
+
automaticFallback: false
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
const NODE_JAVASCRIPT_JWT_REALIZATION =
|
|
27
|
+
NODE_JAVASCRIPT_JWT_REALIZATIONS.HS256;
|
|
28
|
+
|
|
29
|
+
let runtimeModules;
|
|
30
|
+
|
|
31
|
+
function loadRuntimeModules() {
|
|
32
|
+
if (!runtimeModules) {
|
|
33
|
+
runtimeModules = Promise.all([
|
|
34
|
+
import('@pulse-compute/jwt/provider'),
|
|
35
|
+
import('@pulse-compute/crypto')
|
|
36
|
+
]).then(([jwtProvider, cryptoProvider]) => Object.freeze({
|
|
37
|
+
jwtProvider,
|
|
38
|
+
cryptoProvider
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
return runtimeModules;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nodeWallClock() {
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
unixEpochSeconds: Date.now() / 1000,
|
|
47
|
+
trusted: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertRequestActive(execution, jwtProvider) {
|
|
52
|
+
if (execution && execution.signal && execution.signal.aborted) {
|
|
53
|
+
throw jwtProvider.jwtError('PULSE_JWT_OPERATION_FAILED', {
|
|
54
|
+
category: 'request-cancelled',
|
|
55
|
+
automaticFallback: false
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertRuntimeBuiltin(cryptoProvider, jwtProvider) {
|
|
61
|
+
if (
|
|
62
|
+
!cryptoProvider
|
|
63
|
+
|| cryptoProvider.CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION !== CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION
|
|
64
|
+
|| !cryptoProvider.crypto
|
|
65
|
+
|| !cryptoProvider.crypto.mac
|
|
66
|
+
|| typeof cryptoProvider.crypto.mac.verify !== 'function'
|
|
67
|
+
|| !cryptoProvider.crypto.signature
|
|
68
|
+
|| typeof cryptoProvider.crypto.signature.verify !== 'function'
|
|
69
|
+
) {
|
|
70
|
+
throw jwtProvider.jwtError('PULSE_JWT_TARGET_UNSUPPORTED', {
|
|
71
|
+
category: 'crypto-realization',
|
|
72
|
+
realization: NODE_JAVASCRIPT_JWT_REALIZATION.realization,
|
|
73
|
+
implementation: NODE_JAVASCRIPT_JWT_REALIZATION.implementation,
|
|
74
|
+
automaticFallback: false
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return cryptoProvider.crypto;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createNodeJavascriptJwtVerify(options = {}) {
|
|
81
|
+
const captureWallClock = options.captureWallClock || nodeWallClock;
|
|
82
|
+
const secretLookup = options.secretLookup;
|
|
83
|
+
return async function verifyNodeJavascriptJwt(effect, execution) {
|
|
84
|
+
const { jwtProvider, cryptoProvider } = await loadRuntimeModules();
|
|
85
|
+
assertRequestActive(execution, jwtProvider);
|
|
86
|
+
const input = normalizeJwtVerifyEffect(effect);
|
|
87
|
+
if (execution && typeof execution.registerRedactionValue === 'function') {
|
|
88
|
+
execution.registerRedactionValue(input.token);
|
|
89
|
+
}
|
|
90
|
+
assertRequestActive(execution, jwtProvider);
|
|
91
|
+
const selectedCrypto = assertRuntimeBuiltin(cryptoProvider, jwtProvider);
|
|
92
|
+
const host = {
|
|
93
|
+
async captureWallClock() {
|
|
94
|
+
assertRequestActive(execution, jwtProvider);
|
|
95
|
+
const captured = await captureWallClock();
|
|
96
|
+
assertRequestActive(execution, jwtProvider);
|
|
97
|
+
return captured;
|
|
98
|
+
},
|
|
99
|
+
resolveSecret: typeof secretLookup === 'function'
|
|
100
|
+
? async (binding) => {
|
|
101
|
+
assertRequestActive(execution, jwtProvider);
|
|
102
|
+
const secret = await secretLookup(binding, execution);
|
|
103
|
+
assertRequestActive(execution, jwtProvider);
|
|
104
|
+
return secret;
|
|
105
|
+
}
|
|
106
|
+
: undefined,
|
|
107
|
+
registerSensitiveValue(value) {
|
|
108
|
+
if (execution && typeof execution.registerRedactionValue === 'function') {
|
|
109
|
+
execution.registerRedactionValue(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (execution && typeof execution.validateSchemaValue === 'function') {
|
|
114
|
+
host.validateClaims = async (schemaId, claims, context) => {
|
|
115
|
+
assertRequestActive(execution, jwtProvider);
|
|
116
|
+
const result = await execution.validateSchemaValue(schemaId, claims, context);
|
|
117
|
+
assertRequestActive(execution, jwtProvider);
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const result = await jwtProvider.verifyJwtWithCrypto(
|
|
122
|
+
input,
|
|
123
|
+
Object.freeze(host),
|
|
124
|
+
selectedCrypto
|
|
125
|
+
);
|
|
126
|
+
assertRequestActive(execution, jwtProvider);
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = Object.freeze({
|
|
132
|
+
NODE_JAVASCRIPT_JWT_VERIFIER_VERSION,
|
|
133
|
+
NODE_JAVASCRIPT_JWT_REALIZATION,
|
|
134
|
+
NODE_JAVASCRIPT_JWT_REALIZATIONS,
|
|
135
|
+
createNodeJavascriptJwtVerify,
|
|
136
|
+
nodeWallClock
|
|
137
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const { createNodeJavascriptHandler } = require('./node-adapter.js');
|
|
5
|
+
|
|
6
|
+
const NODE_JAVASCRIPT_LIFECYCLE_VERSION = 'pulse.node-javascript-lifecycle.v1';
|
|
7
|
+
|
|
8
|
+
function statusForError(error) {
|
|
9
|
+
if (error && error.code === 'PULSE_REQUEST_BODY_TOO_LARGE') return 413;
|
|
10
|
+
if (error && error.code === 'PULSE_NODE_REQUEST_READ_FAILED') return 400;
|
|
11
|
+
return 500;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeSafeError(response, error) {
|
|
15
|
+
if (response.headersSent) {
|
|
16
|
+
if (!response.writableEnded) response.end();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
response.statusCode = statusForError(error);
|
|
20
|
+
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
|
+
}
|
|
23
|
+
|
|
24
|
+
function createNodeJavascriptServer(application, options = {}) {
|
|
25
|
+
const handler = createNodeJavascriptHandler(application, options);
|
|
26
|
+
return http.createServer((request, response) => {
|
|
27
|
+
Promise.resolve(handler(request, response)).catch((error) => {
|
|
28
|
+
writeSafeError(response, error);
|
|
29
|
+
if (typeof options.onError === 'function') options.onError(error, Object.freeze({ request, response }));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function listenNodeJavascriptApplication(application, options = {}) {
|
|
35
|
+
const server = createNodeJavascriptServer(application, options);
|
|
36
|
+
const host = options.host || options.hostname || '127.0.0.1';
|
|
37
|
+
const port = options.port === undefined ? 0 : Number(options.port);
|
|
38
|
+
await new Promise((resolve, reject) => {
|
|
39
|
+
function cleanup() {
|
|
40
|
+
server.removeListener('listening', onListening);
|
|
41
|
+
server.removeListener('error', onError);
|
|
42
|
+
}
|
|
43
|
+
function onListening() {
|
|
44
|
+
cleanup();
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
47
|
+
function onError(error) {
|
|
48
|
+
cleanup();
|
|
49
|
+
reject(error);
|
|
50
|
+
}
|
|
51
|
+
server.once('listening', onListening);
|
|
52
|
+
server.once('error', onError);
|
|
53
|
+
server.listen(port, host);
|
|
54
|
+
});
|
|
55
|
+
return server;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function closeNodeJavascriptServer(server) {
|
|
59
|
+
if (!server || typeof server.close !== 'function' || !server.listening) return Promise.resolve();
|
|
60
|
+
return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = Object.freeze({
|
|
64
|
+
NODE_JAVASCRIPT_LIFECYCLE_VERSION,
|
|
65
|
+
closeNodeJavascriptServer,
|
|
66
|
+
createNodeJavascriptServer,
|
|
67
|
+
listenNodeJavascriptApplication,
|
|
68
|
+
statusForError
|
|
69
|
+
});
|