@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 +11 -9
- package/package.json +14 -10
- package/src/capabilities.js +19 -3
- package/src/config/s3.js +1 -1
- package/src/javascript/fetch-adapter.js +16 -1
- package/src/javascript/incoming-body.js +163 -0
- package/src/javascript/jwt-verifier.js +14 -4
- package/src/javascript/lifecycle.js +4 -2
- package/src/javascript/node-adapter.js +170 -118
- package/src/javascript/package-effects.js +5 -2
- package/src/javascript/runtime-host.js +13 -3
- package/src/javascript/s3.js +4 -3
- package/src/javascript/target-support-policy.js +17 -4
- package/src/javascript/target.js +5 -0
- package/src/javascript/test-runtime.js +9 -1
- package/src/native/target.js +25 -0
- package/src/runtime/body-transform.js +87 -0
- package/src/runtime/canonical-api-runtime.js +53 -11
- package/src/runtime/generated-output-test.js +35 -0
- package/src/runtime/generated-output.js +167 -0
- package/src/runtime/incoming-body.js +63 -0
- package/src/runtime/jwt-verifier.js +26 -5
- package/src/runtime/s3-reader.js +76 -12
- package/src/server/build.js +53 -0
- package/src/server/config.js +54 -0
- package/src/server/index.d.ts +53 -0
- package/src/server/index.js +146 -0
- package/src/toolchain.js +24 -6
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
nodeResponse.
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
?
|
|
218
|
-
:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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 = {}) {
|
package/src/javascript/s3.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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.
|
|
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 (
|
|
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.
|
|
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
|
}
|
package/src/javascript/target.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
package/src/native/target.js
CHANGED
|
@@ -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
|