@pulse-compute/wasm-host-runtime 0.0.0 → 1.0.0-beta.2
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 +47 -1
- package/package.json +46 -6
- package/src/compiler/backend-capabilities.js +372 -0
- package/src/compiler/channel-broadcaster.js +194 -0
- package/src/compiler/compiled-wasm-runtime.js +612 -0
- package/src/compiler/host-runtime-kernel.js +690 -0
- package/src/compiler/index.js +33 -0
- package/src/compiler/json-body.js +454 -0
- package/src/compiler/request-result-headers.js +153 -0
- package/src/compiler/streaming-passthrough.js +212 -0
- package/src/compiler/wasm-host-abi.js +314 -0
- package/src/compiler/wasm-host-bridge.js +984 -0
- package/src/diagnostics.js +3 -0
- package/src/index.js +11 -0
- package/src/runtime/canonical-api-runtime.js +1569 -0
- package/src/runtime/canonical-native-host.js +1187 -0
- package/src/runtime/compiled-wasm-host-runtime-kv.js +32 -0
- package/src/runtime/continuation-registry.js +181 -0
- package/src/runtime/index.js +22 -0
- package/src/runtime/kv-provider.js +56 -0
- package/src/runtime/native-crypto-verifier.js +340 -0
|
@@ -0,0 +1,1569 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const portableKv = require('@pulse-compute/runtime/host');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function loadRuntimeApi() {
|
|
6
|
+
try { return require('@pulse-compute/wasm-contracts/handler/runtime-api'); }
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/handler/runtime-api.js');
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadCanonicalRuntimeContract() {
|
|
14
|
+
try { return require('@pulse-compute/wasm-contracts/handler/canonical-runtime'); }
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/handler/canonical-runtime.js');
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadStreamingContract() {
|
|
22
|
+
try { return require('@pulse-compute/wasm-contracts/host/streaming-passthrough'); }
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/host/streaming-passthrough.js');
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadContinuationRegistry() {
|
|
30
|
+
try { return require('@pulse-compute/wasm-host-runtime/runtime/continuation-registry'); }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../host-runtime/src/runtime/continuation-registry.js');
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadJsonSemanticTraceContract() {
|
|
38
|
+
try { return require('@pulse-compute/wasm-contracts/schema-json/semantic-trace'); }
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/schema-json/semantic-trace.js');
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadLoggingContract() {
|
|
46
|
+
try { return require('@pulse-compute/wasm-contracts/logging'); }
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/logging.js');
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadEventContract() {
|
|
54
|
+
try { return require('@pulse-compute/wasm-contracts/events'); }
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/events/contracts.js');
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const runtimeApi = loadRuntimeApi();
|
|
62
|
+
const canonicalRuntimeContract = loadCanonicalRuntimeContract();
|
|
63
|
+
const streamingContract = loadStreamingContract();
|
|
64
|
+
const { createContinuationRegistry } = loadContinuationRegistry();
|
|
65
|
+
const jsonSemanticTrace = loadJsonSemanticTraceContract();
|
|
66
|
+
const loggingContract = loadLoggingContract();
|
|
67
|
+
const eventContract = loadEventContract();
|
|
68
|
+
|
|
69
|
+
const CANONICAL_HOST_RUNTIME_VERSION = 'pulse.canonical-host-runtime.v1';
|
|
70
|
+
const REDACTED_VALUE = '<redacted>';
|
|
71
|
+
const CIRCULAR_VALUE = '<cycle>';
|
|
72
|
+
const BINARY_VALUE = '<binary>';
|
|
73
|
+
const BOUNDED_VALUE = '<bounded>';
|
|
74
|
+
const ACCESSOR_VALUE = '<accessor>';
|
|
75
|
+
const MAX_REDACTION_DEPTH = 32;
|
|
76
|
+
const MAX_REDACTION_ENTRIES = 1024;
|
|
77
|
+
const SENSITIVE_HEADERS = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'cookie', 'set-cookie']);
|
|
78
|
+
const SENSITIVE_FIELDS = new Set([
|
|
79
|
+
...SENSITIVE_HEADERS,
|
|
80
|
+
'api-key', 'apikey', 'credential', 'credentials', 'password', 'secret', 'token'
|
|
81
|
+
]);
|
|
82
|
+
let executionSequence = 0;
|
|
83
|
+
|
|
84
|
+
class CanonicalRuntimeError extends Error {
|
|
85
|
+
constructor(name, code, message, detail = {}) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = name;
|
|
88
|
+
this.code = code;
|
|
89
|
+
this.detail = Object.freeze({ ...detail });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertCanonicalProgramCompatibility(programModule) {
|
|
94
|
+
const metadata = programModule && programModule.metadata;
|
|
95
|
+
const actualProgramVersion = programModule && programModule.version;
|
|
96
|
+
const actualMetadataVersion = metadata && metadata.version;
|
|
97
|
+
const actualProtocolVersion = metadata && metadata.runtimeProtocolVersion;
|
|
98
|
+
const expectedProgramVersion = canonicalRuntimeContract.CANONICAL_PROGRAM_VERSION;
|
|
99
|
+
const expectedProtocolVersion = canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION;
|
|
100
|
+
if (actualProgramVersion !== expectedProgramVersion || actualMetadataVersion !== expectedProgramVersion || actualProtocolVersion !== expectedProtocolVersion) {
|
|
101
|
+
throw new CanonicalRuntimeError(
|
|
102
|
+
'CanonicalProgramCompatibilityError',
|
|
103
|
+
'PULSE_CANONICAL_PROGRAM_INCOMPATIBLE',
|
|
104
|
+
'Canonical program and runtime protocol versions are incompatible.',
|
|
105
|
+
{
|
|
106
|
+
expectedProgramVersion,
|
|
107
|
+
actualProgramVersion,
|
|
108
|
+
actualMetadataVersion,
|
|
109
|
+
expectedProtocolVersion,
|
|
110
|
+
actualProtocolVersion
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const schemaIds = metadata && Array.isArray(metadata.schemaIds) ? metadata.schemaIds : [];
|
|
115
|
+
const schemaCodecs = programModule && programModule.schemaCodecs;
|
|
116
|
+
if (schemaIds.length > 0 && (!schemaCodecs || typeof schemaCodecs.decode !== 'function' || typeof schemaCodecs.encode !== 'function')) {
|
|
117
|
+
throw new CanonicalRuntimeError(
|
|
118
|
+
'CanonicalSchemaCompatibilityError',
|
|
119
|
+
'PULSE_CANONICAL_SCHEMA_CODECS_MISSING',
|
|
120
|
+
'Canonical program declares schemas but does not export its compiled schema codecs.',
|
|
121
|
+
{ schemaIds }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return metadata;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeHeaders(headers) {
|
|
128
|
+
return runtimeApi.body.normalizeHeaders(headers).map(([name, value]) => [String(name), String(value)]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function headerValue(headers, name) {
|
|
132
|
+
return runtimeApi.body.headerValue(headers, name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isSensitiveField(value) {
|
|
136
|
+
const key = String(value || '').toLowerCase();
|
|
137
|
+
if (SENSITIVE_FIELDS.has(key)) return true;
|
|
138
|
+
const compact = key.replace(/[^a-z0-9]/g, '');
|
|
139
|
+
return compact.endsWith('token')
|
|
140
|
+
|| compact.endsWith('secret')
|
|
141
|
+
|| compact.endsWith('password')
|
|
142
|
+
|| compact.endsWith('credential')
|
|
143
|
+
|| compact.endsWith('credentials')
|
|
144
|
+
|| compact.endsWith('apikey');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ownDataValue(object, key) {
|
|
148
|
+
if (!object || (typeof object !== 'object' && typeof object !== 'function')) return undefined;
|
|
149
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
150
|
+
return descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
151
|
+
? descriptor.value
|
|
152
|
+
: undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function initialSensitiveValues(options = {}) {
|
|
156
|
+
const output = new Set();
|
|
157
|
+
const secrets = options.secrets && typeof options.secrets === 'object' ? options.secrets : {};
|
|
158
|
+
for (const key of Reflect.ownKeys(secrets)) {
|
|
159
|
+
if (typeof key !== 'string') continue;
|
|
160
|
+
const value = ownDataValue(secrets, key);
|
|
161
|
+
if (typeof value === 'string' && value.length > 0) output.add(value);
|
|
162
|
+
}
|
|
163
|
+
const redactionValues = Array.isArray(options.redactionValues) ? options.redactionValues : [];
|
|
164
|
+
for (let index = 0; index < redactionValues.length; index += 1) {
|
|
165
|
+
const value = ownDataValue(redactionValues, String(index));
|
|
166
|
+
if (typeof value === 'string' && value.length > 0) output.add(value);
|
|
167
|
+
}
|
|
168
|
+
return output;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function redactString(value, sensitiveValues = new Set()) {
|
|
172
|
+
let output = String(value);
|
|
173
|
+
const tokens = [...sensitiveValues].filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
174
|
+
.sort((left, right) => right.length - left.length);
|
|
175
|
+
for (const secret of tokens) output = output.split(secret).join(REDACTED_VALUE);
|
|
176
|
+
return output;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function defineRedactedProperty(target, key, value, enumerable = true) {
|
|
180
|
+
try {
|
|
181
|
+
Object.defineProperty(target, key, {
|
|
182
|
+
enumerable,
|
|
183
|
+
configurable: true,
|
|
184
|
+
writable: false,
|
|
185
|
+
value
|
|
186
|
+
});
|
|
187
|
+
} catch (_) {
|
|
188
|
+
// Redaction must contain hostile evidence rather than execute or preserve it.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function redactRuntimeValue(value, sensitiveValues = new Set(), seen = new WeakSet(), keyName = '', depth = 0) {
|
|
193
|
+
if (isSensitiveField(keyName)) return REDACTED_VALUE;
|
|
194
|
+
if (typeof value === 'string') return redactString(value, sensitiveValues);
|
|
195
|
+
if (value === null || value === undefined || typeof value !== 'object') return value;
|
|
196
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return BINARY_VALUE;
|
|
197
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) return BINARY_VALUE;
|
|
198
|
+
if (depth >= MAX_REDACTION_DEPTH) return BOUNDED_VALUE;
|
|
199
|
+
if (value instanceof Error) return redactRuntimeError(value, sensitiveValues, seen, depth);
|
|
200
|
+
if (seen.has(value)) return CIRCULAR_VALUE;
|
|
201
|
+
seen.add(value);
|
|
202
|
+
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
const output = [];
|
|
205
|
+
const length = Math.min(value.length, MAX_REDACTION_ENTRIES);
|
|
206
|
+
for (let index = 0; index < length; index += 1) {
|
|
207
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
208
|
+
const entry = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
209
|
+
? descriptor.value
|
|
210
|
+
: ACCESSOR_VALUE;
|
|
211
|
+
if (Array.isArray(entry)) {
|
|
212
|
+
const name = ownDataValue(entry, '0');
|
|
213
|
+
if (entry.length >= 2 && isSensitiveField(name)) {
|
|
214
|
+
const pair = [redactString(name, sensitiveValues), REDACTED_VALUE];
|
|
215
|
+
const pairLength = Math.min(entry.length, MAX_REDACTION_ENTRIES);
|
|
216
|
+
for (let pairIndex = 2; pairIndex < pairLength; pairIndex += 1) {
|
|
217
|
+
const pairDescriptor = Object.getOwnPropertyDescriptor(entry, String(pairIndex));
|
|
218
|
+
pair.push(pairDescriptor && Object.prototype.hasOwnProperty.call(pairDescriptor, 'value')
|
|
219
|
+
? redactRuntimeValue(pairDescriptor.value, sensitiveValues, seen, '', depth + 1)
|
|
220
|
+
: ACCESSOR_VALUE);
|
|
221
|
+
}
|
|
222
|
+
output.push(Object.freeze(pair));
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
output.push(redactRuntimeValue(entry, sensitiveValues, seen, '', depth + 1));
|
|
227
|
+
}
|
|
228
|
+
return Object.freeze(output);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const output = {};
|
|
232
|
+
for (const key of Reflect.ownKeys(value).slice(0, MAX_REDACTION_ENTRIES)) {
|
|
233
|
+
if (typeof key !== 'string') continue;
|
|
234
|
+
const outputKey = redactString(key, sensitiveValues);
|
|
235
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
236
|
+
const entry = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
237
|
+
? redactRuntimeValue(descriptor.value, sensitiveValues, seen, key, depth + 1)
|
|
238
|
+
: (isSensitiveField(key) ? REDACTED_VALUE : ACCESSOR_VALUE);
|
|
239
|
+
defineRedactedProperty(output, outputKey, entry);
|
|
240
|
+
}
|
|
241
|
+
return Object.freeze(output);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function redactRuntimeError(error, sensitiveValues = new Set(), seen = new WeakSet(), depth = 0) {
|
|
245
|
+
if (typeof error === 'string') return redactString(error, sensitiveValues);
|
|
246
|
+
if (!error || typeof error !== 'object') return error;
|
|
247
|
+
if (depth >= MAX_REDACTION_DEPTH) {
|
|
248
|
+
const bounded = new Error(BOUNDED_VALUE);
|
|
249
|
+
bounded.name = 'PulseRedactedError';
|
|
250
|
+
return bounded;
|
|
251
|
+
}
|
|
252
|
+
if (seen.has(error)) {
|
|
253
|
+
const circular = new Error(CIRCULAR_VALUE);
|
|
254
|
+
circular.name = 'PulseRedactedError';
|
|
255
|
+
return circular;
|
|
256
|
+
}
|
|
257
|
+
seen.add(error);
|
|
258
|
+
|
|
259
|
+
const rawMessage = ownDataValue(error, 'message');
|
|
260
|
+
const rawName = ownDataValue(error, 'name');
|
|
261
|
+
const rawCode = ownDataValue(error, 'code');
|
|
262
|
+
const rawDetail = ownDataValue(error, 'detail');
|
|
263
|
+
const message = redactString(typeof rawMessage === 'string' ? rawMessage : 'Pulse runtime failure.', sensitiveValues);
|
|
264
|
+
const name = redactString(typeof rawName === 'string' ? rawName : 'Error', sensitiveValues);
|
|
265
|
+
const code = typeof rawCode === 'string' ? redactString(rawCode, sensitiveValues) : undefined;
|
|
266
|
+
const detail = rawDetail === undefined ? undefined : redactRuntimeValue(rawDetail, sensitiveValues, seen, 'detail', depth + 1);
|
|
267
|
+
|
|
268
|
+
const safe = code === undefined
|
|
269
|
+
? Object.assign(new Error(message), { name })
|
|
270
|
+
: new CanonicalRuntimeError(name, code, message, detail === undefined ? {} : detail);
|
|
271
|
+
|
|
272
|
+
const rawCause = ownDataValue(error, 'cause');
|
|
273
|
+
if (rawCause !== undefined) {
|
|
274
|
+
defineRedactedProperty(safe, 'cause', redactRuntimeError(rawCause, sensitiveValues, seen, depth + 1), false);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const reserved = new Set(['name', 'message', 'stack', 'code', 'detail', 'cause']);
|
|
278
|
+
for (const key of Reflect.ownKeys(error).slice(0, MAX_REDACTION_ENTRIES)) {
|
|
279
|
+
if (typeof key !== 'string' || reserved.has(key)) continue;
|
|
280
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
281
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) continue;
|
|
282
|
+
defineRedactedProperty(
|
|
283
|
+
safe,
|
|
284
|
+
redactString(key, sensitiveValues),
|
|
285
|
+
redactRuntimeValue(descriptor.value, sensitiveValues, seen, key, depth + 1),
|
|
286
|
+
descriptor.enumerable
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const rawStack = ownDataValue(error, 'stack');
|
|
291
|
+
if (typeof rawStack === 'string') {
|
|
292
|
+
try { Object.defineProperty(safe, 'stack', { configurable: true, writable: true, value: redactString(rawStack, sensitiveValues) }); }
|
|
293
|
+
catch (_) { /* best effort */ }
|
|
294
|
+
}
|
|
295
|
+
return safe;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function redactedHeaders(headers, sensitiveValues = new Set()) {
|
|
299
|
+
return normalizeHeaders(headers).map(([name, value]) => [
|
|
300
|
+
redactString(name, sensitiveValues),
|
|
301
|
+
isSensitiveField(name) ? REDACTED_VALUE : redactString(value, sensitiveValues)
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function jsonTrace(trace, input, value) {
|
|
306
|
+
if (trace && typeof trace.json === 'function') trace.json(input, value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function jsonTraceErrorDetail(error) {
|
|
310
|
+
const detail = error && (error.detail || error.details) || {};
|
|
311
|
+
return {
|
|
312
|
+
errorCode: error && error.code || 'PULSE_SCHEMA_OPERATION_FAILED',
|
|
313
|
+
errorPath: detail.path || detail.field || null,
|
|
314
|
+
expected: detail.expected || null,
|
|
315
|
+
actualKind: detail.actualKind || detail.actual || null
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function strictSchemaPolicy(options, schemaCodecs) {
|
|
320
|
+
return options && options.strict === true
|
|
321
|
+
&& Boolean(schemaCodecs && Array.isArray(schemaCodecs.ids) && schemaCodecs.ids.length > 0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function requireSchemaCodecs(schemaCodecs, schemaId, source) {
|
|
325
|
+
if (
|
|
326
|
+
!schemaCodecs
|
|
327
|
+
|| typeof schemaCodecs.has !== 'function'
|
|
328
|
+
|| typeof schemaCodecs.decodeJsonText !== 'function'
|
|
329
|
+
|| typeof schemaCodecs.encodeJsonText !== 'function'
|
|
330
|
+
) {
|
|
331
|
+
throw new CanonicalRuntimeError(
|
|
332
|
+
'SchemaReferenceError',
|
|
333
|
+
'PULSE_SCHEMA_CODECS_UNAVAILABLE',
|
|
334
|
+
`Pulse cannot use schema ${JSON.stringify(String(schemaId))} because the project codec registry is unavailable.`,
|
|
335
|
+
{ schemaId: String(schemaId), source }
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return schemaCodecs;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function requireSchemaId(schemaCodecs, schemaId, source) {
|
|
342
|
+
if (typeof schemaId !== 'string' || schemaId.length === 0) {
|
|
343
|
+
throw new CanonicalRuntimeError(
|
|
344
|
+
'SchemaReferenceError',
|
|
345
|
+
'PULSE_SCHEMA_ID_INVALID',
|
|
346
|
+
'Pulse schema IDs must be non-empty strings.',
|
|
347
|
+
{ schemaIdType: typeof schemaId, source }
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const codecs = requireSchemaCodecs(schemaCodecs, schemaId, source);
|
|
351
|
+
if (!codecs.has(schemaId)) {
|
|
352
|
+
throw new CanonicalRuntimeError(
|
|
353
|
+
'SchemaReferenceError',
|
|
354
|
+
'PULSE_SCHEMA_REFERENCE',
|
|
355
|
+
`Unknown compiled schema ${schemaId}.`,
|
|
356
|
+
{ schemaId, source }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return schemaId;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function assertSchemaJsonContentType(schemaCodecs, headers, schemaId, source) {
|
|
363
|
+
const policy = schemaCodecs && schemaCodecs.registry && schemaCodecs.registry.contentTypePolicy || 'accept-json-or-missing';
|
|
364
|
+
const contentType = String(headerValue(headers, 'content-type') || '').trim().toLowerCase();
|
|
365
|
+
const mediaType = contentType.split(';')[0].trim();
|
|
366
|
+
const json = mediaType === 'application/json' || mediaType.endsWith('+json');
|
|
367
|
+
const accepted = json || (!contentType && policy === 'accept-json-or-missing');
|
|
368
|
+
if (accepted) return;
|
|
369
|
+
throw new CanonicalRuntimeError(
|
|
370
|
+
'SchemaDecodeError',
|
|
371
|
+
'PULSE_SCHEMA_CONTENT_TYPE',
|
|
372
|
+
`Schema ${String(schemaId)} requires a JSON content type.`,
|
|
373
|
+
{ schemaId: String(schemaId), source, contentType, contentTypePolicy: policy }
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function assertSchemaBodySize(schemaCodecs, text, schemaId, source) {
|
|
378
|
+
const configured = Number(schemaCodecs && schemaCodecs.registry && schemaCodecs.registry.maxBytes);
|
|
379
|
+
const maxBytes = Number.isSafeInteger(configured) && configured > 0 ? configured : 65536;
|
|
380
|
+
const bytes = Buffer.byteLength(String(text || ''), 'utf8');
|
|
381
|
+
if (bytes <= maxBytes) return;
|
|
382
|
+
throw new CanonicalRuntimeError(
|
|
383
|
+
'BodyTooLargeError',
|
|
384
|
+
'PULSE_BODY_TOO_LARGE',
|
|
385
|
+
`Structured schema body exceeds the ${maxBytes} byte limit.`,
|
|
386
|
+
{ schemaId: String(schemaId), source, bytes, maxBytes }
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const runtimeValueErrorCodes = Object.freeze({
|
|
391
|
+
BodyTooLargeError: 'PULSE_BODY_TOO_LARGE',
|
|
392
|
+
BodyDecodeError: 'PULSE_BODY_DECODE',
|
|
393
|
+
BodyUnavailableError: 'PULSE_BODY_UNAVAILABLE',
|
|
394
|
+
OpaqueBodyInspectionError: 'PULSE_OPAQUE_BODY_INSPECTION',
|
|
395
|
+
ResponseEncodeError: 'PULSE_RESPONSE_ENCODE',
|
|
396
|
+
FetchNetworkError: 'PULSE_FETCH_NETWORK',
|
|
397
|
+
FetchTimeoutError: 'PULSE_FETCH_TIMEOUT',
|
|
398
|
+
ContinuationExpiredError: 'PULSE_CONTINUATION_EXPIRED',
|
|
399
|
+
ContinuationDoubleResumeError: 'PULSE_CONTINUATION_DOUBLE_RESUME',
|
|
400
|
+
ProviderCapabilityMissingError: 'PULSE_PROVIDER_CAPABILITY_MISSING'
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
function unwrap(result) {
|
|
404
|
+
if (result && result.ok === false) {
|
|
405
|
+
const name = result.error || 'CanonicalRuntimeError';
|
|
406
|
+
const code = runtimeValueErrorCodes[name]
|
|
407
|
+
|| `PULSE_${String(name || 'RUNTIME').replace(/Error$/i, '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[^A-Z0-9]+/gi, '_').toUpperCase()}`;
|
|
408
|
+
const error = new CanonicalRuntimeError(name, code, result.message || name || 'Canonical runtime operation failed.', result.detail || {});
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
return result && Object.prototype.hasOwnProperty.call(result, 'value') ? result.value : result;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeRequest(options = {}) {
|
|
415
|
+
const input = options.request || {};
|
|
416
|
+
const pathValue = String(input.path || (() => {
|
|
417
|
+
try { return new URL(String(input.url || 'https://app.example.test/')).pathname; }
|
|
418
|
+
catch (_) { return '/'; }
|
|
419
|
+
})());
|
|
420
|
+
const url = String(input.url || `https://app.example.test${pathValue}`);
|
|
421
|
+
const headers = normalizeHeaders(input.headers || (input.contentType ? [['content-type', input.contentType]] : []));
|
|
422
|
+
return Object.freeze({
|
|
423
|
+
method: String(input.method || 'GET').toUpperCase(),
|
|
424
|
+
url,
|
|
425
|
+
path: pathValue,
|
|
426
|
+
headers,
|
|
427
|
+
body: Object.prototype.hasOwnProperty.call(input, 'body') ? input.body : undefined
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function createRequestSurface(request, options = {}, schemaCodecs, trace) {
|
|
432
|
+
const body = runtimeApi.body.createValue(
|
|
433
|
+
{ body: request.body, headers: request.headers },
|
|
434
|
+
{ maxBytes: options.maxRequestBodyBytes || options.maxBodyBytes || options.maxStructuredBodyBytes }
|
|
435
|
+
);
|
|
436
|
+
const schemaMemo = new Map();
|
|
437
|
+
return Object.freeze({
|
|
438
|
+
surface: Object.freeze({
|
|
439
|
+
method: request.method,
|
|
440
|
+
url: request.url,
|
|
441
|
+
path: request.path,
|
|
442
|
+
headers: request.headers,
|
|
443
|
+
header(name) { return headerValue(request.headers, name); },
|
|
444
|
+
text() { return unwrap(body.text()); },
|
|
445
|
+
json(schemaId) {
|
|
446
|
+
if (schemaId === undefined) {
|
|
447
|
+
if (strictSchemaPolicy(options, schemaCodecs)) {
|
|
448
|
+
throw new CanonicalRuntimeError(
|
|
449
|
+
'SchemaReferenceError',
|
|
450
|
+
'PULSE_SCHEMA_REQUIRED',
|
|
451
|
+
'Strict Pulse request JSON requires a registered schema ID.',
|
|
452
|
+
{ source: 'request', strict: true }
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return unwrap(body.json());
|
|
456
|
+
}
|
|
457
|
+
const key = requireSchemaId(schemaCodecs, schemaId, 'request');
|
|
458
|
+
if (!schemaMemo.has(key)) {
|
|
459
|
+
const event = {
|
|
460
|
+
boundary: 'request',
|
|
461
|
+
schemaId: key,
|
|
462
|
+
responseCaseId: null,
|
|
463
|
+
operationId: `request:${key}`,
|
|
464
|
+
status: null,
|
|
465
|
+
contentType: headerValue(request.headers, 'content-type') || null,
|
|
466
|
+
headers: request.headers,
|
|
467
|
+
bodyOwnership: 'request-snapshot',
|
|
468
|
+
target: options.target || null,
|
|
469
|
+
provider: options.provider || null,
|
|
470
|
+
effectId: null,
|
|
471
|
+
groupId: null
|
|
472
|
+
};
|
|
473
|
+
try {
|
|
474
|
+
assertSchemaJsonContentType(schemaCodecs, request.headers, key, 'request');
|
|
475
|
+
const text = unwrap(body.text());
|
|
476
|
+
assertSchemaBodySize(schemaCodecs, text, key, 'request');
|
|
477
|
+
const decoded = schemaCodecs.decodeJsonText(key, text, 'request');
|
|
478
|
+
schemaMemo.set(key, decoded);
|
|
479
|
+
jsonTrace(trace, { ...event, kind: 'json.decode.request' }, decoded);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
jsonTrace(trace, { ...event, kind: 'json.decode.error', ...jsonTraceErrorDetail(error) });
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return schemaMemo.get(key);
|
|
486
|
+
}
|
|
487
|
+
}),
|
|
488
|
+
body
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function createResponseBuilders(schemaCodecs, options = {}, trace) {
|
|
493
|
+
const builders = runtimeApi.body.createResponseBuilders();
|
|
494
|
+
function checked(result) {
|
|
495
|
+
if (result && result.ok === false) return unwrap(result);
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
return Object.freeze({
|
|
499
|
+
json(value, descriptor) {
|
|
500
|
+
let schemaId;
|
|
501
|
+
let responseCaseId;
|
|
502
|
+
let responseOptions;
|
|
503
|
+
if (typeof descriptor === 'string') {
|
|
504
|
+
const codecs = requireSchemaCodecs(schemaCodecs, descriptor, 'application-response');
|
|
505
|
+
const responseCase = typeof codecs.responseCase === 'function'
|
|
506
|
+
? codecs.responseCase(descriptor)
|
|
507
|
+
: undefined;
|
|
508
|
+
if (!responseCase) {
|
|
509
|
+
throw new CanonicalRuntimeError(
|
|
510
|
+
'SchemaReferenceError',
|
|
511
|
+
'PULSE_RESPONSE_CASE_REFERENCE',
|
|
512
|
+
`Unknown compiled response case ${descriptor}.`,
|
|
513
|
+
{ responseCaseId: descriptor, source: 'application-response' }
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
responseCaseId = descriptor;
|
|
517
|
+
schemaId = requireSchemaId(schemaCodecs, responseCase.schemaId, 'application-response');
|
|
518
|
+
responseOptions = { status: responseCase.status };
|
|
519
|
+
} else {
|
|
520
|
+
if (descriptor !== undefined && (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor))) {
|
|
521
|
+
throw new CanonicalRuntimeError(
|
|
522
|
+
'SchemaReferenceError',
|
|
523
|
+
'PULSE_RESPONSE_DESCRIPTOR_INVALID',
|
|
524
|
+
'Pulse JSON response metadata must be an object or registered response-case ID.',
|
|
525
|
+
{ source: 'application-response', valueType: Array.isArray(descriptor) ? 'array' : typeof descriptor }
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const valueOptions = descriptor || {};
|
|
529
|
+
schemaId = Object.prototype.hasOwnProperty.call(valueOptions, 'schema')
|
|
530
|
+
? requireSchemaId(schemaCodecs, valueOptions.schema, 'application-response')
|
|
531
|
+
: undefined;
|
|
532
|
+
const { schema: _schema, ...rest } = valueOptions;
|
|
533
|
+
responseOptions = rest;
|
|
534
|
+
}
|
|
535
|
+
if (schemaId === undefined) {
|
|
536
|
+
if (strictSchemaPolicy(options, schemaCodecs)) {
|
|
537
|
+
throw new CanonicalRuntimeError(
|
|
538
|
+
'SchemaReferenceError',
|
|
539
|
+
'PULSE_SCHEMA_REQUIRED',
|
|
540
|
+
'Strict Pulse JSON responses require a schema descriptor or registered response-case ID.',
|
|
541
|
+
{ source: 'application-response', strict: true }
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return checked(builders.json(value, responseOptions));
|
|
545
|
+
}
|
|
546
|
+
const key = String(schemaId);
|
|
547
|
+
const status = responseOptions.status === undefined ? 200 : Number(responseOptions.status);
|
|
548
|
+
const traceStatus = Number.isSafeInteger(status) && status >= 100 && status <= 599 ? status : null;
|
|
549
|
+
const headers = normalizeHeaders(responseOptions.headers);
|
|
550
|
+
if (!headerValue(headers, 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
551
|
+
const event = {
|
|
552
|
+
boundary: 'application-response',
|
|
553
|
+
schemaId: key,
|
|
554
|
+
responseCaseId: responseCaseId || null,
|
|
555
|
+
operationId: `application-response:${responseCaseId || key}`,
|
|
556
|
+
status: traceStatus,
|
|
557
|
+
contentType: headerValue(headers, 'content-type') || null,
|
|
558
|
+
headers,
|
|
559
|
+
bodyOwnership: 'application-owned',
|
|
560
|
+
target: options.target || null,
|
|
561
|
+
provider: options.provider || null,
|
|
562
|
+
effectId: null,
|
|
563
|
+
groupId: null
|
|
564
|
+
};
|
|
565
|
+
try {
|
|
566
|
+
const text = schemaCodecs.encodeJsonText(key, value, 'application-response');
|
|
567
|
+
const normalized = schemaCodecs.decodeJsonText(key, text, 'application-response-trace');
|
|
568
|
+
const result = checked(builders.jsonText(text, responseOptions, normalized));
|
|
569
|
+
jsonTrace(trace, { ...event, kind: 'json.encode.response' }, normalized);
|
|
570
|
+
return result;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
jsonTrace(trace, { ...event, kind: 'json.encode.error', ...jsonTraceErrorDetail(error) });
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
text(value, options) { return checked(builders.text(value, options)); },
|
|
577
|
+
response(value) { return checked(builders.response(value)); }
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function createCanonicalContext(options, trace) {
|
|
582
|
+
const request = normalizeRequest(options);
|
|
583
|
+
const schemaCodecs = options.schemaCodecs;
|
|
584
|
+
const requestSurface = createRequestSurface(request, options, schemaCodecs, trace);
|
|
585
|
+
const builders = createResponseBuilders(schemaCodecs, options, trace);
|
|
586
|
+
const configValues = Object.freeze({ ...(options.config || {}) });
|
|
587
|
+
const secretValues = Object.freeze({ ...(options.secrets || {}) });
|
|
588
|
+
const kvStores = new Map(Object.entries(options.kv || {}).map(([name, values]) => [String(name), new Map(Object.entries(values || {}))]));
|
|
589
|
+
const requestState = new Map();
|
|
590
|
+
const capabilities = options.capabilities && typeof options.capabilities === 'object' ? options.capabilities : {};
|
|
591
|
+
const configCapability = capabilities.config && typeof capabilities.config.get === 'function' ? capabilities.config : undefined;
|
|
592
|
+
const secretCapability = capabilities.secret && typeof capabilities.secret.get === 'function' ? capabilities.secret : undefined;
|
|
593
|
+
const kvCapability = typeof capabilities.kv === 'function' ? capabilities.kv : undefined;
|
|
594
|
+
const reporting = loggingContract.reportingDescriptor(options.reporting);
|
|
595
|
+
const providerLog = typeof options.log === 'function' ? options.log : undefined;
|
|
596
|
+
const redactLogMessage = typeof options.redactLogMessage === 'function'
|
|
597
|
+
? options.redactLogMessage
|
|
598
|
+
: String;
|
|
599
|
+
const log = {};
|
|
600
|
+
for (const [name, level] of Object.entries(loggingContract.LOG_METHOD_LEVELS)) {
|
|
601
|
+
log[name] = (message) => {
|
|
602
|
+
if (typeof message !== 'string') throw new TypeError(`ctx.log.${name} requires one string message.`);
|
|
603
|
+
if (!loggingContract.logStatementEnabled(level, reporting.level)) return;
|
|
604
|
+
let safeMessage;
|
|
605
|
+
try { safeMessage = String(redactLogMessage(message)); }
|
|
606
|
+
catch (_) { safeMessage = REDACTED_VALUE; }
|
|
607
|
+
const event = Object.freeze({
|
|
608
|
+
version: 'pulse.log-event.v1',
|
|
609
|
+
type: 'log',
|
|
610
|
+
level,
|
|
611
|
+
name,
|
|
612
|
+
message: safeMessage,
|
|
613
|
+
target: options.target || null,
|
|
614
|
+
provider: options.provider || null
|
|
615
|
+
});
|
|
616
|
+
try { trace.push(event); }
|
|
617
|
+
catch (_) { /* Logging evidence is best effort. */ }
|
|
618
|
+
if (providerLog) {
|
|
619
|
+
try { providerLog(level, safeMessage, event); }
|
|
620
|
+
catch (_) { /* Provider logging failures never fail the request. */ }
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
trace.push(Object.freeze({
|
|
626
|
+
version: 'pulse.log-event.v1',
|
|
627
|
+
type: 'logging-config',
|
|
628
|
+
reporting: Object.freeze({ name: reporting.name, level: reporting.level }),
|
|
629
|
+
target: options.target || null,
|
|
630
|
+
provider: options.provider || null
|
|
631
|
+
}));
|
|
632
|
+
} catch (_) {
|
|
633
|
+
// Logging evidence is best effort.
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const ctx = Object.freeze({
|
|
637
|
+
req: requestSurface.surface,
|
|
638
|
+
log: Object.freeze(log),
|
|
639
|
+
fetch() {
|
|
640
|
+
throw new CanonicalRuntimeError('CanonicalUnloweredFetchError', 'PULSE_CANONICAL_UNLOWERED_FETCH', 'ctx.fetch reached the runtime without compiler lowering.');
|
|
641
|
+
},
|
|
642
|
+
json: builders.json,
|
|
643
|
+
text: builders.text,
|
|
644
|
+
response: builders.response,
|
|
645
|
+
state: Object.freeze({
|
|
646
|
+
get(key) {
|
|
647
|
+
if (typeof key !== 'string') {
|
|
648
|
+
throw new CanonicalRuntimeError('CanonicalStateKeyError', 'PULSE_STATE_KEY_STRING_REQUIRED', 'ctx.state.get requires a string key.', { keyType: typeof key });
|
|
649
|
+
}
|
|
650
|
+
return requestState.has(key) ? requestState.get(key) : undefined;
|
|
651
|
+
},
|
|
652
|
+
set(key, value) {
|
|
653
|
+
if (typeof key !== 'string') {
|
|
654
|
+
throw new CanonicalRuntimeError('CanonicalStateKeyError', 'PULSE_STATE_KEY_STRING_REQUIRED', 'ctx.state.set requires a string key.', { keyType: typeof key });
|
|
655
|
+
}
|
|
656
|
+
if (typeof value !== 'string') {
|
|
657
|
+
throw new CanonicalRuntimeError('CanonicalStateValueError', 'PULSE_STATE_VALUE_STRING_REQUIRED', 'ctx.state.set requires a string value.', { key, valueType: typeof value });
|
|
658
|
+
}
|
|
659
|
+
requestState.set(key, value);
|
|
660
|
+
}
|
|
661
|
+
}),
|
|
662
|
+
config: Object.freeze({
|
|
663
|
+
get(name) {
|
|
664
|
+
const key = String(name);
|
|
665
|
+
trace.push(Object.freeze({ type: 'config-read', name: key, source: configCapability ? 'provider-capability' : 'runtime-values' }));
|
|
666
|
+
return configCapability ? configCapability.get(key) : configValues[key];
|
|
667
|
+
}
|
|
668
|
+
}),
|
|
669
|
+
secret: Object.freeze({
|
|
670
|
+
get(name) {
|
|
671
|
+
const key = String(name);
|
|
672
|
+
trace.push(Object.freeze({ type: 'secret-read', name: key, value: '<redacted>', source: secretCapability ? 'provider-capability' : 'runtime-values' }));
|
|
673
|
+
return secretCapability ? secretCapability.get(key) : secretValues[key];
|
|
674
|
+
}
|
|
675
|
+
}),
|
|
676
|
+
kv(name) {
|
|
677
|
+
const storeName = String(name);
|
|
678
|
+
const externalStore = kvCapability ? kvCapability(storeName) : undefined;
|
|
679
|
+
if (externalStore !== undefined && (!externalStore || typeof externalStore.get !== 'function' || typeof externalStore.put !== 'function')) {
|
|
680
|
+
throw new CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_PROVIDER_CAPABILITY_INVALID', `Provider KV capability ${storeName} must expose get and put operations.`, { capability: 'kv', store: storeName });
|
|
681
|
+
}
|
|
682
|
+
if (!externalStore && !kvStores.has(storeName)) kvStores.set(storeName, new Map());
|
|
683
|
+
const store = externalStore || kvStores.get(storeName);
|
|
684
|
+
return Object.freeze({
|
|
685
|
+
get(key) {
|
|
686
|
+
const normalized = String(key);
|
|
687
|
+
trace.push(Object.freeze({ type: 'kv-get', store: storeName, key: normalized, source: externalStore ? 'provider-capability' : 'runtime-values' }));
|
|
688
|
+
return store.get(normalized);
|
|
689
|
+
},
|
|
690
|
+
put(key, value) {
|
|
691
|
+
const normalized = String(key);
|
|
692
|
+
const result = store.put ? store.put(normalized, value) : store.set(normalized, value);
|
|
693
|
+
trace.push(Object.freeze({ type: 'kv-put', store: storeName, key: normalized, source: externalStore ? 'provider-capability' : 'runtime-values' }));
|
|
694
|
+
return externalStore ? result : true;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
return Object.freeze({ ctx, request, requestBody: requestSurface.body, kvStores, requestState });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function normalizedInit(init = {}, schemaCodecs, options = {}) {
|
|
704
|
+
if (init !== undefined && (!init || typeof init !== 'object' || Array.isArray(init))) {
|
|
705
|
+
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_INIT_INVALID', 'Canonical fetch init must be an object.');
|
|
706
|
+
}
|
|
707
|
+
const value = init || {};
|
|
708
|
+
const supportedFields = new Set(['method', 'headers', 'body', 'json', 'schema', 'timeoutMs']);
|
|
709
|
+
for (const field of Object.keys(value)) {
|
|
710
|
+
if (!supportedFields.has(field)) {
|
|
711
|
+
throw new CanonicalRuntimeError(
|
|
712
|
+
'FetchRequestError',
|
|
713
|
+
'PULSE_FETCH_INIT_FIELD_UNSUPPORTED',
|
|
714
|
+
`Canonical fetch init field ${JSON.stringify(field)} is outside the portable contract.`,
|
|
715
|
+
{ field }
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const method = String(value.method || 'GET').toUpperCase();
|
|
720
|
+
if (!['GET', 'HEAD', 'POST'].includes(method)) {
|
|
721
|
+
throw new CanonicalRuntimeError(
|
|
722
|
+
'FetchRequestError',
|
|
723
|
+
'PULSE_FETCH_METHOD_UNSUPPORTED',
|
|
724
|
+
`Canonical fetch method ${method} is outside the portable GET, HEAD, and POST contract.`,
|
|
725
|
+
{ method }
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const headers = normalizeHeaders(value.headers);
|
|
729
|
+
const hasBody = Object.prototype.hasOwnProperty.call(value, 'body');
|
|
730
|
+
const hasJson = Object.prototype.hasOwnProperty.call(value, 'json');
|
|
731
|
+
const hasSchema = Object.prototype.hasOwnProperty.call(value, 'schema');
|
|
732
|
+
if (hasBody && hasJson) {
|
|
733
|
+
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_BODY_AMBIGUOUS', 'Canonical fetch init accepts either body or json, not both.');
|
|
734
|
+
}
|
|
735
|
+
if (hasSchema && !hasJson) {
|
|
736
|
+
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_SCHEMA_WITHOUT_JSON', 'Canonical fetch schema is only valid together with the semantic json field.');
|
|
737
|
+
}
|
|
738
|
+
const schemaId = hasSchema
|
|
739
|
+
? requireSchemaId(schemaCodecs, value.schema, 'fetch-request')
|
|
740
|
+
: undefined;
|
|
741
|
+
let body;
|
|
742
|
+
let bodyMode = 'none';
|
|
743
|
+
if (hasBody) {
|
|
744
|
+
if (typeof value.body !== 'string') {
|
|
745
|
+
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_BODY_INVALID', 'Canonical fetch body must be a string.', { valueType: typeof value.body });
|
|
746
|
+
}
|
|
747
|
+
body = value.body;
|
|
748
|
+
bodyMode = 'text';
|
|
749
|
+
} else if (hasJson) {
|
|
750
|
+
if (!headerValue(headers, 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
751
|
+
if (hasSchema) {
|
|
752
|
+
const event = {
|
|
753
|
+
boundary: 'fetch-request',
|
|
754
|
+
schemaId,
|
|
755
|
+
responseCaseId: null,
|
|
756
|
+
operationId: `fetch-request:${options.effectId || schemaId}`,
|
|
757
|
+
status: null,
|
|
758
|
+
contentType: headerValue(headers, 'content-type') || 'application/json; charset=utf-8',
|
|
759
|
+
headers,
|
|
760
|
+
bodyOwnership: 'application-owned',
|
|
761
|
+
target: options.target || null,
|
|
762
|
+
provider: options.provider || null,
|
|
763
|
+
effectId: options.effectId || null,
|
|
764
|
+
groupId: options.groupId || null
|
|
765
|
+
};
|
|
766
|
+
try {
|
|
767
|
+
body = schemaCodecs.encodeJsonText(schemaId, value.json, 'fetch-request');
|
|
768
|
+
const normalized = schemaCodecs.decodeJsonText(schemaId, body, 'fetch-request-trace');
|
|
769
|
+
jsonTrace(options.trace, { ...event, kind: 'json.encode.fetch' }, normalized);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
jsonTrace(options.trace, { ...event, kind: 'json.encode.error', ...jsonTraceErrorDetail(error) });
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
if (strictSchemaPolicy(options, schemaCodecs)) {
|
|
776
|
+
throw new CanonicalRuntimeError(
|
|
777
|
+
'SchemaReferenceError',
|
|
778
|
+
'PULSE_SCHEMA_REQUIRED',
|
|
779
|
+
'Strict outbound fetch JSON requires a registered schema ID.',
|
|
780
|
+
{ source: 'fetch-request', strict: true }
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
try { body = JSON.stringify(value.json); }
|
|
784
|
+
catch (error) {
|
|
785
|
+
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_JSON_ENCODE', 'Canonical fetch json could not be serialized.', { message: error.message });
|
|
786
|
+
}
|
|
787
|
+
if (body === undefined) {
|
|
788
|
+
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_JSON_ENCODE', 'Canonical fetch json must serialize to a JSON value.');
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
bodyMode = 'json';
|
|
792
|
+
}
|
|
793
|
+
if ((method === 'GET' || method === 'HEAD') && bodyMode !== 'none') {
|
|
794
|
+
throw new CanonicalRuntimeError(
|
|
795
|
+
'FetchRequestError',
|
|
796
|
+
'PULSE_FETCH_BODY_METHOD_UNSUPPORTED',
|
|
797
|
+
`Canonical ${method} fetch operations cannot include a body.`,
|
|
798
|
+
{ method, bodyMode }
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
let timeoutMs;
|
|
802
|
+
if (value.timeoutMs !== undefined) {
|
|
803
|
+
timeoutMs = Number(value.timeoutMs);
|
|
804
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
805
|
+
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_TIMEOUT_INVALID', 'Canonical fetch timeoutMs must be a positive safe integer.', { timeoutMs: value.timeoutMs });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return Object.freeze({
|
|
809
|
+
method,
|
|
810
|
+
headers,
|
|
811
|
+
body,
|
|
812
|
+
bodyMode,
|
|
813
|
+
...(hasSchema ? { schema: schemaId } : {}),
|
|
814
|
+
timeoutMs
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function urlParts(value) {
|
|
819
|
+
if (typeof value !== 'string') {
|
|
820
|
+
throw new CanonicalRuntimeError(
|
|
821
|
+
'FetchRequestError',
|
|
822
|
+
'PULSE_FETCH_URL_INVALID',
|
|
823
|
+
'Canonical fetch URLs must be absolute HTTP or HTTPS strings.',
|
|
824
|
+
{ valueType: typeof value }
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
let parsed;
|
|
828
|
+
try { parsed = new URL(value); }
|
|
829
|
+
catch (error) { throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_URL_INVALID', `Invalid fetch URL ${value}.`, { url: value, message: error.message }); }
|
|
830
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
831
|
+
throw new CanonicalRuntimeError(
|
|
832
|
+
'FetchRequestError',
|
|
833
|
+
'PULSE_FETCH_URL_INVALID',
|
|
834
|
+
`Canonical fetch only supports absolute HTTP and HTTPS URLs; received ${parsed.protocol || '<none>'}.`,
|
|
835
|
+
{ url: parsed.toString(), protocol: parsed.protocol }
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return Object.freeze({ url: parsed.toString(), origin: parsed.origin, path: `${parsed.pathname}${parsed.search}` || '/' });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function normalizeFetchEffect(effect, schemaCodecs, options = {}) {
|
|
842
|
+
const responseMode = effect.responseMode === undefined ? 'auto' : String(effect.responseMode);
|
|
843
|
+
if (!['auto', 'structured', 'opaque'].includes(responseMode)) {
|
|
844
|
+
throw new CanonicalRuntimeError(
|
|
845
|
+
'CanonicalEffectProtocolError',
|
|
846
|
+
'PULSE_FETCH_RESPONSE_MODE_INVALID',
|
|
847
|
+
`Canonical fetch response mode ${JSON.stringify(responseMode)} is unsupported.`,
|
|
848
|
+
{ effectId: String(effect.id), responseMode }
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const projection = String(effect.projection || (responseMode === 'structured' ? 'text' : 'response'));
|
|
852
|
+
if (!['text', 'json', 'response'].includes(projection)) {
|
|
853
|
+
throw new CanonicalRuntimeError(
|
|
854
|
+
'CanonicalEffectProtocolError',
|
|
855
|
+
'PULSE_FETCH_PROJECTION_INVALID',
|
|
856
|
+
`Canonical fetch projection ${JSON.stringify(projection)} is unsupported.`,
|
|
857
|
+
{ effectId: String(effect.id), responseMode, projection }
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
if ((responseMode === 'structured' && projection === 'response')
|
|
861
|
+
|| (responseMode === 'opaque' && projection !== 'response')) {
|
|
862
|
+
throw new CanonicalRuntimeError(
|
|
863
|
+
'CanonicalEffectProtocolError',
|
|
864
|
+
'PULSE_FETCH_PROJECTION_MODE_CONFLICT',
|
|
865
|
+
`Canonical fetch projection ${JSON.stringify(projection)} conflicts with response mode ${JSON.stringify(responseMode)}.`,
|
|
866
|
+
{ effectId: String(effect.id), responseMode, projection }
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return Object.freeze({
|
|
870
|
+
id: String(effect.id),
|
|
871
|
+
kind: 'fetch',
|
|
872
|
+
parts: urlParts(effect.url),
|
|
873
|
+
init: normalizedInit(effect.init, schemaCodecs, {
|
|
874
|
+
...options,
|
|
875
|
+
effectId: String(effect.id),
|
|
876
|
+
groupId: effect.groupKey === undefined ? null : String(effect.groupKey)
|
|
877
|
+
}),
|
|
878
|
+
responseMode,
|
|
879
|
+
projection,
|
|
880
|
+
...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }),
|
|
881
|
+
source: effect.source
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
function normalizeProviderEffect(effect, schemaCodecs, options = {}) {
|
|
887
|
+
if (!effect || typeof effect !== 'object') {
|
|
888
|
+
throw new CanonicalRuntimeError('CanonicalEffectProtocolError', 'PULSE_CANONICAL_EFFECT_PROTOCOL', 'Canonical provider effect must be an object.', { effect });
|
|
889
|
+
}
|
|
890
|
+
if (portableKv.isConditionalKv(effect.kind)) return Object.freeze({ ...portableKv.admitConditionalKv(effect, options), id: effect.id, groupKey: effect.groupKey, source: effect.source });
|
|
891
|
+
if (effect.kind === 'fetch') return normalizeFetchEffect(effect, schemaCodecs, options);
|
|
892
|
+
const id = String(effect.id || '');
|
|
893
|
+
if (!id) throw new CanonicalRuntimeError('CanonicalEffectProtocolError', 'PULSE_CANONICAL_EFFECT_PROTOCOL', 'Canonical provider effect requires an id.', { effect });
|
|
894
|
+
if (effect.kind === 'config.get' || effect.kind === 'secret.get') {
|
|
895
|
+
return Object.freeze({ id, kind: effect.kind, name: String(effect.name), ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }), source: effect.source });
|
|
896
|
+
}
|
|
897
|
+
if (effect.kind === 'kv.get' || effect.kind === 'kv.put') {
|
|
898
|
+
return Object.freeze({ id, kind: effect.kind, store: String(effect.store), key: String(effect.key), value: effect.kind === 'kv.put' ? effect.value : undefined, ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }), source: effect.source });
|
|
899
|
+
}
|
|
900
|
+
if (effect.kind === 'event.emit') {
|
|
901
|
+
const emission = effect.emission;
|
|
902
|
+
if (!emission || typeof emission !== 'object' || Array.isArray(emission)) {
|
|
903
|
+
throw new CanonicalRuntimeError(
|
|
904
|
+
'CanonicalEffectProtocolError',
|
|
905
|
+
'PULSE_RUNTIME_EVENT_EMIT_INPUT_INVALID',
|
|
906
|
+
'Canonical event.emit requires a data emission descriptor.',
|
|
907
|
+
{ effectId: id }
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const prototype = Object.getPrototypeOf(emission);
|
|
911
|
+
const symbols = Object.getOwnPropertySymbols(emission);
|
|
912
|
+
const unknown = Object.keys(emission).filter((key) => key !== 'schema' && key !== 'payload').sort();
|
|
913
|
+
if ((prototype !== Object.prototype && prototype !== null)
|
|
914
|
+
|| symbols.length > 0
|
|
915
|
+
|| unknown.length > 0
|
|
916
|
+
|| !Object.prototype.hasOwnProperty.call(emission, 'schema')
|
|
917
|
+
|| Object.keys(emission).some((key) => {
|
|
918
|
+
const descriptor = Object.getOwnPropertyDescriptor(emission, key);
|
|
919
|
+
return !descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value');
|
|
920
|
+
})) {
|
|
921
|
+
throw new CanonicalRuntimeError(
|
|
922
|
+
'CanonicalEffectProtocolError',
|
|
923
|
+
'PULSE_RUNTIME_EVENT_EMIT_INPUT_INVALID',
|
|
924
|
+
'Canonical event.emit requires exactly schema and the schema-dependent payload field.',
|
|
925
|
+
{ effectId: id, unknown }
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
let frame = eventContract.normalizeEventFrame({
|
|
929
|
+
version: eventContract.EVENT_FRAME_VERSION,
|
|
930
|
+
type: effect.type,
|
|
931
|
+
schemaId: Object.getOwnPropertyDescriptor(emission, 'schema').value,
|
|
932
|
+
...(Object.prototype.hasOwnProperty.call(emission, 'payload')
|
|
933
|
+
? { payload: Object.getOwnPropertyDescriptor(emission, 'payload').value }
|
|
934
|
+
: {})
|
|
935
|
+
}, options.eventLimits);
|
|
936
|
+
if (frame.schemaId !== null) {
|
|
937
|
+
const payload = requireSchemaCodecs(schemaCodecs, frame.schemaId, 'event-emit')
|
|
938
|
+
.decode(frame.schemaId, frame.payload, 'event-emit');
|
|
939
|
+
frame = eventContract.normalizeEventFrame({ ...frame, payload }, options.eventLimits);
|
|
940
|
+
}
|
|
941
|
+
return Object.freeze({
|
|
942
|
+
id,
|
|
943
|
+
kind: 'event.emit',
|
|
944
|
+
providerKind: 'event',
|
|
945
|
+
operation: 'emit',
|
|
946
|
+
capability: 'event.emit',
|
|
947
|
+
frame,
|
|
948
|
+
...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }),
|
|
949
|
+
source: effect.source
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (effect.package && effect.contractId && effect.operation) {
|
|
953
|
+
const invocation = effect.invocation && typeof effect.invocation === 'object'
|
|
954
|
+
? effect.invocation
|
|
955
|
+
: {};
|
|
956
|
+
return Object.freeze({
|
|
957
|
+
id,
|
|
958
|
+
kind: String(effect.kind),
|
|
959
|
+
package: String(effect.package),
|
|
960
|
+
contractId: String(effect.contractId),
|
|
961
|
+
providerKind: String(effect.providerKind || 'package'),
|
|
962
|
+
operation: String(effect.operation),
|
|
963
|
+
capability: String(effect.capability || effect.kind),
|
|
964
|
+
result: String(effect.result || 'value'),
|
|
965
|
+
resource: Object.freeze({ ...(effect.resource || {}) }),
|
|
966
|
+
payload: Object.freeze({ ...(effect.payload || {}), ...invocation }),
|
|
967
|
+
...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }),
|
|
968
|
+
source: effect.source
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
throw new CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_PROVIDER_CAPABILITY_UNSUPPORTED', `Canonical provider effect ${String(effect.kind)} is unsupported.`, { effectId: id, kind: effect.kind });
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function effectTraceStart(adapterId, executionId, normalized, sensitiveValues) {
|
|
975
|
+
const base = { type: 'effect-start', provider: adapterId, executionId, effectId: normalized.id, kind: normalized.kind, ...(normalized.groupKey === undefined ? {} : { groupKey: normalized.groupKey }) };
|
|
976
|
+
if (normalized.kind === 'fetch') return Object.freeze({
|
|
977
|
+
...base,
|
|
978
|
+
url: redactString(normalized.parts.url, sensitiveValues),
|
|
979
|
+
method: normalized.init.method,
|
|
980
|
+
responseMode: normalized.responseMode,
|
|
981
|
+
projection: normalized.projection,
|
|
982
|
+
headers: redactedHeaders(normalized.init.headers, sensitiveValues)
|
|
983
|
+
});
|
|
984
|
+
if (normalized.kind === 'config.get' || normalized.kind === 'secret.get') return Object.freeze({ ...base, name: redactString(normalized.name, sensitiveValues) });
|
|
985
|
+
if (portableKv.isConditionalKv(normalized.kind)) return Object.freeze({ ...base, key: '<redacted>', generation: '<redacted>', value: '<redacted>' });
|
|
986
|
+
if (normalized.kind === 'kv.get' || normalized.kind === 'kv.put') return Object.freeze({ ...base, store: redactString(normalized.store, sensitiveValues), key: redactString(normalized.key, sensitiveValues) });
|
|
987
|
+
if (normalized.kind === 'event.emit') return Object.freeze({ ...base, eventType: redactString(normalized.frame.type, sensitiveValues), schemaId: normalized.frame.schemaId });
|
|
988
|
+
if (normalized.package) return Object.freeze({ ...base, package: normalized.package, contractId: normalized.contractId, operation: normalized.operation });
|
|
989
|
+
return Object.freeze(base);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function effectTraceResolved(adapterId, executionId, normalized, value) {
|
|
993
|
+
const base = { type: 'effect-resolved', provider: adapterId, executionId, effectId: normalized.id, kind: normalized.kind, ...(normalized.groupKey === undefined ? {} : { groupKey: normalized.groupKey }) };
|
|
994
|
+
if (normalized.kind === 'fetch') return Object.freeze({ ...base, status: value.status, bodyClass: value.bodyClass });
|
|
995
|
+
if (normalized.kind === 'secret.get') return Object.freeze({ ...base, hit: value !== undefined, value: '<redacted>' });
|
|
996
|
+
if (normalized.kind === 'config.get') return Object.freeze({ ...base, hit: value !== undefined });
|
|
997
|
+
if (portableKv.isConditionalKv(normalized.kind)) return Object.freeze({ ...base, status: value.status });
|
|
998
|
+
if (normalized.kind === 'kv.get') return Object.freeze({ ...base, hit: value !== undefined });
|
|
999
|
+
if (normalized.kind === 'kv.put') return Object.freeze({ ...base, stored: Boolean(value) });
|
|
1000
|
+
if (normalized.kind === 'event.emit') return Object.freeze({ ...base, accepted: value === undefined });
|
|
1001
|
+
if (normalized.package) return Object.freeze({ ...base, result: normalized.result, status: value && value.status, bodyClass: value && value.bodyClass });
|
|
1002
|
+
return Object.freeze(base);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function createStructuredFetchResponse(snapshot, schemaCodecs, options = {}) {
|
|
1006
|
+
const schemaMemo = new Map();
|
|
1007
|
+
const headers = normalizeHeaders(snapshot.headers);
|
|
1008
|
+
const body = runtimeApi.body.createValue(
|
|
1009
|
+
{ body: snapshot.body, headers },
|
|
1010
|
+
{ maxBytes: options.maxBytes, forceStructured: true }
|
|
1011
|
+
);
|
|
1012
|
+
function text() { return unwrap(body.text()); }
|
|
1013
|
+
const responseStatus = Number(snapshot.status);
|
|
1014
|
+
const traceStatus = Number.isSafeInteger(responseStatus) && responseStatus >= 100 && responseStatus <= 599
|
|
1015
|
+
? responseStatus
|
|
1016
|
+
: null;
|
|
1017
|
+
return Object.freeze({
|
|
1018
|
+
status: Number(snapshot.status || 0),
|
|
1019
|
+
ok: Number(snapshot.status || 0) >= 200 && Number(snapshot.status || 0) < 300,
|
|
1020
|
+
kind: String(snapshot.kind || 'text'),
|
|
1021
|
+
bodyClass: 'structured',
|
|
1022
|
+
get body() { return text(); },
|
|
1023
|
+
headers,
|
|
1024
|
+
header(name) { return headerValue(headers, name); },
|
|
1025
|
+
text,
|
|
1026
|
+
json(schemaId) {
|
|
1027
|
+
if (schemaId !== undefined) {
|
|
1028
|
+
const key = requireSchemaId(schemaCodecs, schemaId, 'fetch-response');
|
|
1029
|
+
if (!schemaMemo.has(key)) {
|
|
1030
|
+
const event = {
|
|
1031
|
+
boundary: 'fetch-response',
|
|
1032
|
+
schemaId: key,
|
|
1033
|
+
responseCaseId: null,
|
|
1034
|
+
operationId: `fetch-response:${options.effectId || key}`,
|
|
1035
|
+
status: traceStatus,
|
|
1036
|
+
contentType: headerValue(headers, 'content-type') || null,
|
|
1037
|
+
headers,
|
|
1038
|
+
bodyOwnership: 'fetched-response-snapshot',
|
|
1039
|
+
target: options.target || null,
|
|
1040
|
+
provider: options.provider || null,
|
|
1041
|
+
effectId: options.effectId || null,
|
|
1042
|
+
groupId: options.groupId || null
|
|
1043
|
+
};
|
|
1044
|
+
try {
|
|
1045
|
+
const value = text();
|
|
1046
|
+
assertSchemaJsonContentType(schemaCodecs, headers, key, 'fetch-response');
|
|
1047
|
+
assertSchemaBodySize(schemaCodecs, value, key, 'fetch-response');
|
|
1048
|
+
const decoded = schemaCodecs.decodeJsonText(key, value, 'fetch-response');
|
|
1049
|
+
schemaMemo.set(key, decoded);
|
|
1050
|
+
jsonTrace(options.trace, { ...event, kind: 'json.decode.fetch' }, decoded);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
jsonTrace(options.trace, { ...event, kind: 'json.decode.error', ...jsonTraceErrorDetail(error) });
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return schemaMemo.get(key);
|
|
1057
|
+
}
|
|
1058
|
+
if (strictSchemaPolicy(options, schemaCodecs)) {
|
|
1059
|
+
throw new CanonicalRuntimeError(
|
|
1060
|
+
'SchemaReferenceError',
|
|
1061
|
+
'PULSE_SCHEMA_REQUIRED',
|
|
1062
|
+
'Strict fetched-response JSON requires a registered schema ID.',
|
|
1063
|
+
{ source: 'fetch-response', strict: true }
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
return unwrap(body.json());
|
|
1067
|
+
},
|
|
1068
|
+
bodyStats() { return body.stats(); },
|
|
1069
|
+
toJSON() { return { status: Number(snapshot.status || 0), ok: this.ok, kind: this.kind, bodyClass: 'structured', headers, body: text() }; }
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function opaqueInspectionError(handle, helper) {
|
|
1074
|
+
return new CanonicalRuntimeError('OpaqueBodyInspectionError', 'PULSE_OPAQUE_BODY_INSPECTION', `Opaque ${handle.bodyKind || 'stream'} response bodies are pass-through only and cannot be inspected with ${helper}().`, { helper, handleId: handle.handleId, bodyKind: handle.bodyKind, bodyCopiedIntoWasm: false });
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function createOpaqueFetchResponse(snapshot, effectId, providerSource = 'canonical-provider') {
|
|
1078
|
+
const bodyHandle = runtimeApi.opaque.createBodyHandle({
|
|
1079
|
+
id: snapshot.bodyHandle && snapshot.bodyHandle.handleId || `${effectId}-body`,
|
|
1080
|
+
bodyKind: snapshot.bodyHandle && snapshot.bodyHandle.bodyKind || 'stream',
|
|
1081
|
+
headers: snapshot.headers,
|
|
1082
|
+
status: snapshot.status,
|
|
1083
|
+
source: String(snapshot.providerSource || snapshot.source || providerSource),
|
|
1084
|
+
responseRef: snapshot.responseRef,
|
|
1085
|
+
streamRef: snapshot.streamRef
|
|
1086
|
+
});
|
|
1087
|
+
const headers = normalizeHeaders(snapshot.headers);
|
|
1088
|
+
return Object.freeze({
|
|
1089
|
+
status: Number(snapshot.status || 0),
|
|
1090
|
+
ok: Number(snapshot.status || 0) >= 200 && Number(snapshot.status || 0) < 300,
|
|
1091
|
+
kind: 'stream',
|
|
1092
|
+
bodyClass: 'opaque',
|
|
1093
|
+
headers,
|
|
1094
|
+
header(name) { return headerValue(headers, name); },
|
|
1095
|
+
bodyHandle,
|
|
1096
|
+
bodyStream: snapshot.bodyStream,
|
|
1097
|
+
responseRef: snapshot.responseRef,
|
|
1098
|
+
streamRef: snapshot.streamRef,
|
|
1099
|
+
hostOwnsStream: true,
|
|
1100
|
+
wasmOwnsBytes: false,
|
|
1101
|
+
streamResultAbiVersion: streamingContract.STREAM_RESULT_ABI_VERSION,
|
|
1102
|
+
json() { throw opaqueInspectionError(bodyHandle, 'json'); },
|
|
1103
|
+
text() { throw opaqueInspectionError(bodyHandle, 'text'); },
|
|
1104
|
+
bytes() { throw opaqueInspectionError(bodyHandle, 'bytes'); },
|
|
1105
|
+
toJSON() { return { status: this.status, ok: this.ok, kind: 'stream', bodyClass: 'opaque', headers, bodyHandle, responseRef: this.responseRef, streamRef: this.streamRef, hostOwnsStream: true, wasmOwnsBytes: false }; }
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function responseSnapshot(handle) {
|
|
1110
|
+
if (!handle || typeof handle !== 'object') return handle;
|
|
1111
|
+
if (typeof handle.toJSON !== 'function') return handle;
|
|
1112
|
+
const snapshot = handle.toJSON();
|
|
1113
|
+
return Object.freeze({
|
|
1114
|
+
...snapshot,
|
|
1115
|
+
bodyStream: handle.bodyStream === undefined ? snapshot.bodyStream : handle.bodyStream,
|
|
1116
|
+
body: handle.body === undefined ? snapshot.body : handle.body,
|
|
1117
|
+
responseRef: handle.responseRef === undefined ? snapshot.responseRef : handle.responseRef,
|
|
1118
|
+
streamRef: handle.streamRef === undefined ? snapshot.streamRef : handle.streamRef
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function opaqueSnapshotFromStructured(snapshot) {
|
|
1123
|
+
const status = Number(snapshot.status || 0);
|
|
1124
|
+
const allowsBody = status !== 204 && status !== 205 && status !== 304 && !(status >= 100 && status < 200);
|
|
1125
|
+
const body = allowsBody && snapshot.body !== undefined && snapshot.body !== null ? snapshot.body : undefined;
|
|
1126
|
+
return Object.freeze({
|
|
1127
|
+
...snapshot,
|
|
1128
|
+
kind: 'stream',
|
|
1129
|
+
bodyClass: 'opaque',
|
|
1130
|
+
body: undefined,
|
|
1131
|
+
bodyStream: snapshot.bodyStream === undefined
|
|
1132
|
+
? (body === undefined ? undefined : Object.freeze({ chunks: Object.freeze([body]) }))
|
|
1133
|
+
: snapshot.bodyStream
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function bodyStreamChunks(snapshot) {
|
|
1138
|
+
const stream = snapshot && snapshot.bodyStream;
|
|
1139
|
+
if (stream && Array.isArray(stream.chunks)) return stream.chunks;
|
|
1140
|
+
if (Array.isArray(snapshot && snapshot.chunks)) return snapshot.chunks;
|
|
1141
|
+
if (typeof stream === 'string' || Buffer.isBuffer(stream) || stream instanceof Uint8Array) return [stream];
|
|
1142
|
+
return undefined;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function structuredSnapshotFromOpaque(snapshot, effectId, maxBytes) {
|
|
1146
|
+
const chunks = bodyStreamChunks(snapshot);
|
|
1147
|
+
const status = Number(snapshot.status || 0);
|
|
1148
|
+
const allowsBody = status !== 204 && status !== 205 && status !== 304 && !(status >= 100 && status < 200);
|
|
1149
|
+
if (!allowsBody) return Object.freeze({ ...snapshot, kind: 'text', bodyClass: 'structured', body: '' });
|
|
1150
|
+
if (!chunks) {
|
|
1151
|
+
throw new CanonicalRuntimeError(
|
|
1152
|
+
'OpaqueBodyInspectionError',
|
|
1153
|
+
'PULSE_OPAQUE_BODY_INSPECTION',
|
|
1154
|
+
'The provider returned a pass-through-only response that cannot satisfy an explicit structured fetch projection.',
|
|
1155
|
+
{ effectId, bodyCopiedIntoWasm: false }
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
const buffers = chunks.map((chunk) => Buffer.isBuffer(chunk)
|
|
1159
|
+
? chunk
|
|
1160
|
+
: chunk instanceof Uint8Array
|
|
1161
|
+
? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)
|
|
1162
|
+
: Buffer.from(String(chunk)));
|
|
1163
|
+
const bytes = buffers.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
1164
|
+
const limit = Number.isSafeInteger(Number(maxBytes)) && Number(maxBytes) > 0 ? Number(maxBytes) : 65_536;
|
|
1165
|
+
if (bytes > limit) {
|
|
1166
|
+
throw new CanonicalRuntimeError(
|
|
1167
|
+
'BodyTooLargeError',
|
|
1168
|
+
'PULSE_BODY_TOO_LARGE',
|
|
1169
|
+
`Fetched response exceeds the ${limit} byte structured-body limit.`,
|
|
1170
|
+
{ effectId, bytes, maxBytes: limit }
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return Object.freeze({
|
|
1174
|
+
...snapshot,
|
|
1175
|
+
kind: 'text',
|
|
1176
|
+
bodyClass: 'structured',
|
|
1177
|
+
body: Buffer.concat(buffers).toString('utf8'),
|
|
1178
|
+
bodyStream: undefined,
|
|
1179
|
+
bodyHandle: undefined
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function normalizeFetchResponse(handle, effectId, providerSource, schemaCodecs, options = {}) {
|
|
1184
|
+
const responseMode = options.responseMode || 'auto';
|
|
1185
|
+
if (responseMode === 'structured' && handle && handle.bodyClass === 'structured' && typeof handle.text === 'function') return handle;
|
|
1186
|
+
if (responseMode === 'opaque' && handle && handle.bodyClass === 'opaque' && handle.bodyHandle) return handle;
|
|
1187
|
+
const snapshot = responseSnapshot(handle);
|
|
1188
|
+
if (!snapshot || typeof snapshot !== 'object') throw new CanonicalRuntimeError('FetchNetworkError', 'PULSE_FETCH_NETWORK', 'Provider returned no fetch response.', { effectId });
|
|
1189
|
+
const opaque = snapshot.kind === 'stream' || snapshot.bodyClass === 'opaque' || snapshot.bodyStream !== undefined || snapshot.bodyHandle !== undefined;
|
|
1190
|
+
if (responseMode === 'opaque') {
|
|
1191
|
+
return createOpaqueFetchResponse(opaque ? snapshot : opaqueSnapshotFromStructured(snapshot), effectId, providerSource);
|
|
1192
|
+
}
|
|
1193
|
+
if (responseMode === 'structured') {
|
|
1194
|
+
const structured = opaque
|
|
1195
|
+
? structuredSnapshotFromOpaque(snapshot, effectId, options.maxBytes)
|
|
1196
|
+
: snapshot;
|
|
1197
|
+
return createStructuredFetchResponse(structured, schemaCodecs, options);
|
|
1198
|
+
}
|
|
1199
|
+
return opaque
|
|
1200
|
+
? createOpaqueFetchResponse(snapshot, effectId, providerSource)
|
|
1201
|
+
: createStructuredFetchResponse(snapshot, schemaCodecs, options);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function responseAllowsBody(status, requestMethod = 'GET') {
|
|
1205
|
+
const normalizedStatus = Number(status);
|
|
1206
|
+
return String(requestMethod || 'GET').toUpperCase() !== 'HEAD'
|
|
1207
|
+
&& normalizedStatus !== 204
|
|
1208
|
+
&& normalizedStatus !== 205
|
|
1209
|
+
&& normalizedStatus !== 304
|
|
1210
|
+
&& !(normalizedStatus >= 100 && normalizedStatus < 200);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function releaseDiscardedStream(stream) {
|
|
1214
|
+
if (!stream) return;
|
|
1215
|
+
try {
|
|
1216
|
+
if (typeof stream.destroy === 'function') stream.destroy();
|
|
1217
|
+
else if (typeof stream.cancel === 'function') Promise.resolve(stream.cancel()).catch(() => undefined);
|
|
1218
|
+
} catch (_) { /* bodyless response cleanup is best effort */ }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function finalResponse(value, requestMethod = 'GET') {
|
|
1222
|
+
if (value === undefined || value === null) return Object.freeze({ status: 204, kind: 'empty', bodyClass: 'structured', headers: [], body: '' });
|
|
1223
|
+
if (value && value.bodyClass === 'opaque') {
|
|
1224
|
+
const allowsBody = responseAllowsBody(value.status, requestMethod);
|
|
1225
|
+
if (!allowsBody) releaseDiscardedStream(value.bodyStream);
|
|
1226
|
+
const detached = runtimeApi.opaque.detach(value.bodyHandle, 'terminal-response-handoff');
|
|
1227
|
+
return Object.freeze({
|
|
1228
|
+
status: Number(value.status || 200),
|
|
1229
|
+
kind: 'stream',
|
|
1230
|
+
bodyClass: 'opaque',
|
|
1231
|
+
headers: normalizeHeaders(value.headers),
|
|
1232
|
+
bodyHandle: detached,
|
|
1233
|
+
bodyStream: allowsBody ? value.bodyStream : undefined,
|
|
1234
|
+
responseRef: value.responseRef,
|
|
1235
|
+
streamRef: value.streamRef,
|
|
1236
|
+
hostOwnsStream: true,
|
|
1237
|
+
wasmOwnsBytes: false,
|
|
1238
|
+
streamResultAbiVersion: streamingContract.STREAM_RESULT_ABI_VERSION
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
if (value && value.bodyClass === 'structured' && Object.prototype.hasOwnProperty.call(value, 'status')) {
|
|
1242
|
+
const body = responseAllowsBody(value.status, requestMethod)
|
|
1243
|
+
? (Object.prototype.hasOwnProperty.call(value, 'body') ? value.body : (typeof value.text === 'function' ? value.text() : ''))
|
|
1244
|
+
: '';
|
|
1245
|
+
return Object.freeze({
|
|
1246
|
+
status: Number(value.status || 200),
|
|
1247
|
+
kind: String(value.kind || 'text'),
|
|
1248
|
+
bodyClass: 'structured',
|
|
1249
|
+
headers: normalizeHeaders(value.headers),
|
|
1250
|
+
body: body === undefined || body === null ? '' : String(body)
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'status')) {
|
|
1254
|
+
const allowsBody = responseAllowsBody(value.status, requestMethod);
|
|
1255
|
+
if (!allowsBody) releaseDiscardedStream(value.bodyStream);
|
|
1256
|
+
return Object.freeze({
|
|
1257
|
+
...value,
|
|
1258
|
+
headers: normalizeHeaders(value.headers),
|
|
1259
|
+
body: allowsBody ? value.body : '',
|
|
1260
|
+
bodyStream: allowsBody ? value.bodyStream : undefined
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
throw new CanonicalRuntimeError('ResponseEncodeError', 'PULSE_RESPONSE_ENCODE', 'Canonical handler returned a non-response value. Use ctx.json, ctx.text, ctx.response, or return ctx.fetch for opaque pass-through.', { valueType: typeof value });
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function responseIsStructured(method, headers) {
|
|
1267
|
+
if (String(method).toUpperCase() === 'HEAD') return true;
|
|
1268
|
+
const contentType = String(headerValue(headers, 'content-type') || '').toLowerCase();
|
|
1269
|
+
return contentType.startsWith('text/')
|
|
1270
|
+
|| contentType.includes('application/json')
|
|
1271
|
+
|| contentType.includes('+json')
|
|
1272
|
+
|| contentType.includes('application/xml')
|
|
1273
|
+
|| contentType.includes('+xml')
|
|
1274
|
+
|| contentType.includes('application/x-www-form-urlencoded');
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function normalizeProviderAdapter(input = {}) {
|
|
1278
|
+
if (!input || typeof input !== 'object') throw new TypeError('Canonical host runtime requires a provider adapter.');
|
|
1279
|
+
const id = String(input.id || input.name || '').trim();
|
|
1280
|
+
if (!id) throw new TypeError('Canonical provider adapter requires an id.');
|
|
1281
|
+
const dispatchEffect = typeof input.dispatchEffect === 'function'
|
|
1282
|
+
? input.dispatchEffect
|
|
1283
|
+
: (typeof input.dispatchFetch === 'function'
|
|
1284
|
+
? (effect, context) => {
|
|
1285
|
+
if (effect.kind !== 'fetch') throw new CanonicalRuntimeError('ProviderCapabilityError', 'PULSE_PROVIDER_CAPABILITY_UNSUPPORTED', `Canonical provider ${id} does not implement ${effect.kind}.`, { provider: id, kind: effect.kind, effectId: effect.id });
|
|
1286
|
+
return input.dispatchFetch(effect, context);
|
|
1287
|
+
}
|
|
1288
|
+
: undefined);
|
|
1289
|
+
if (!dispatchEffect) throw new TypeError(`Canonical provider ${id} must implement dispatchEffect() or dispatchFetch().`);
|
|
1290
|
+
return Object.freeze({
|
|
1291
|
+
id,
|
|
1292
|
+
version: String(input.version || `pulse.canonical-${id}-provider.v1`),
|
|
1293
|
+
createCapabilities: typeof input.createCapabilities === 'function' ? input.createCapabilities : () => ({}),
|
|
1294
|
+
dispatchEffect,
|
|
1295
|
+
prepareConditionalKv: typeof input.prepareConditionalKv === 'function' ? input.prepareConditionalKv.bind(input) : undefined,
|
|
1296
|
+
resultMetadata: typeof input.resultMetadata === 'function' ? input.resultMetadata : () => undefined,
|
|
1297
|
+
disposeExecution: typeof input.disposeExecution === 'function' ? input.disposeExecution : () => undefined
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function createCanonicalHostRuntime(options = {}) {
|
|
1302
|
+
const adapter = normalizeProviderAdapter(options.providerAdapter || options.provider);
|
|
1303
|
+
const runtimeTrace = [];
|
|
1304
|
+
const registry = options.continuationRegistry || createContinuationRegistry({ ttlMs: options.continuationTtlMs, clock: options.clock });
|
|
1305
|
+
|
|
1306
|
+
async function execute(programModule, executionOptions = {}) {
|
|
1307
|
+
if (!programModule || typeof programModule.createHandler !== 'function') throw new TypeError('Canonical runtime requires a compiled canonical module.');
|
|
1308
|
+
const metadata = assertCanonicalProgramCompatibility(programModule);
|
|
1309
|
+
executionSequence += 1;
|
|
1310
|
+
const executionId = String(executionOptions.executionId || `canonical-${adapter.id}-${executionSequence}`);
|
|
1311
|
+
const sensitiveValues = initialSensitiveValues({ ...options, ...executionOptions });
|
|
1312
|
+
let executionStatus = 'failed';
|
|
1313
|
+
let executionFailure;
|
|
1314
|
+
try {
|
|
1315
|
+
const trace = [];
|
|
1316
|
+
function recordTrace(...events) {
|
|
1317
|
+
for (const entry of events) {
|
|
1318
|
+
const safeEntry = redactRuntimeValue(entry, sensitiveValues);
|
|
1319
|
+
trace.push(safeEntry);
|
|
1320
|
+
runtimeTrace.push(safeEntry);
|
|
1321
|
+
}
|
|
1322
|
+
return trace.length;
|
|
1323
|
+
}
|
|
1324
|
+
const traceSink = Object.freeze({
|
|
1325
|
+
push: recordTrace,
|
|
1326
|
+
json(input, value) {
|
|
1327
|
+
const safeValue = value === undefined
|
|
1328
|
+
? undefined
|
|
1329
|
+
: redactRuntimeValue(value, sensitiveValues);
|
|
1330
|
+
const safeHeaders = input.headers === undefined
|
|
1331
|
+
? undefined
|
|
1332
|
+
: redactRuntimeValue(input.headers, sensitiveValues);
|
|
1333
|
+
recordTrace(jsonSemanticTrace.normalizeJsonTraceEvent({
|
|
1334
|
+
...input,
|
|
1335
|
+
...(safeHeaders === undefined ? {} : { headers: safeHeaders }),
|
|
1336
|
+
valueDigest: safeValue === undefined
|
|
1337
|
+
? null
|
|
1338
|
+
: jsonSemanticTrace.semanticValueDigest(safeValue)
|
|
1339
|
+
}));
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
const providerCapabilities = adapter.createCapabilities({ ...options, ...executionOptions, metadata, executionId, trace: traceSink });
|
|
1343
|
+
const schemaCodecs = programModule.schemaCodecs;
|
|
1344
|
+
const runtimeOptions = {
|
|
1345
|
+
...options,
|
|
1346
|
+
...executionOptions,
|
|
1347
|
+
strict: executionOptions.strict === undefined
|
|
1348
|
+
? (metadata.json ? metadata.json.strict === true : options.strict === true)
|
|
1349
|
+
: executionOptions.strict === true,
|
|
1350
|
+
schemaCodecs,
|
|
1351
|
+
trace: traceSink,
|
|
1352
|
+
target: executionOptions.target || metadata.target || options.target || 'javascript',
|
|
1353
|
+
provider: adapter.id,
|
|
1354
|
+
reporting: executionOptions.reporting || options.reporting || loggingContract.DEFAULT_REPORTING_LEVEL,
|
|
1355
|
+
redactLogMessage(message) { return redactString(message, sensitiveValues); },
|
|
1356
|
+
capabilities: executionOptions.capabilities || providerCapabilities
|
|
1357
|
+
};
|
|
1358
|
+
const context = createCanonicalContext(runtimeOptions, traceSink);
|
|
1359
|
+
const resolutionOrder = [];
|
|
1360
|
+
let dispatchedEffects = 0;
|
|
1361
|
+
|
|
1362
|
+
function executionContinuations() {
|
|
1363
|
+
const prefix = `${executionId}:`;
|
|
1364
|
+
return registry.list().filter((entry) => entry.id.startsWith(prefix));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
async function dispatchOne(effect) {
|
|
1368
|
+
const normalized = normalizeProviderEffect(effect, schemaCodecs, runtimeOptions);
|
|
1369
|
+
const conditional = portableKv.isConditionalKv(normalized.kind);
|
|
1370
|
+
if (conditional) portableKv.registerKvRedactions(normalized, (value) => sensitiveValues.add(value));
|
|
1371
|
+
recordTrace(effectTraceStart(adapter.id, executionId, normalized, sensitiveValues));
|
|
1372
|
+
dispatchedEffects += 1;
|
|
1373
|
+
try {
|
|
1374
|
+
const effectExecution = { ...options, ...executionOptions, metadata, executionId, trace: traceSink, registerRedactionValue: (value) => sensitiveValues.add(value), onKvObservation: recordTrace };
|
|
1375
|
+
let snapshot = conditional
|
|
1376
|
+
? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)), effectExecution, runtimeOptions)
|
|
1377
|
+
: await adapter.dispatchEffect(normalized, effectExecution);
|
|
1378
|
+
if (normalized.kind === 'config.get' || normalized.kind === 'secret.get') {
|
|
1379
|
+
if (snapshot !== undefined && typeof snapshot !== 'string') {
|
|
1380
|
+
throw new CanonicalRuntimeError(
|
|
1381
|
+
'BindingValueError',
|
|
1382
|
+
'PULSE_BINDING_VALUE_INVALID',
|
|
1383
|
+
`Pulse ${normalized.kind} must resolve to a string or undefined.`,
|
|
1384
|
+
{ capability: normalized.kind, name: normalized.name, receivedType: Array.isArray(snapshot) ? 'array' : snapshot === null ? 'null' : typeof snapshot }
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
if (normalized.kind === 'secret.get' && typeof snapshot === 'string') sensitiveValues.add(snapshot);
|
|
1388
|
+
}
|
|
1389
|
+
if (normalized.kind === 'kv.get') snapshot = runtimeApi.body.immutableClone(snapshot);
|
|
1390
|
+
if (normalized.kind === 'kv.put' && typeof snapshot !== 'boolean') {
|
|
1391
|
+
throw new CanonicalRuntimeError(
|
|
1392
|
+
'KvAcknowledgementError',
|
|
1393
|
+
'PULSE_KV_ACK_INVALID',
|
|
1394
|
+
'Pulse KV put providers must resolve to a boolean acknowledgement.',
|
|
1395
|
+
{ receivedType: Array.isArray(snapshot) ? 'array' : snapshot === null ? 'null' : typeof snapshot }
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
if (normalized.kind === 'event.emit' && snapshot !== undefined) {
|
|
1399
|
+
throw new CanonicalRuntimeError(
|
|
1400
|
+
'EventAcceptanceError',
|
|
1401
|
+
'PULSE_RUNTIME_EVENT_EMIT_ACCEPTANCE_INVALID',
|
|
1402
|
+
'Pulse event emit providers must acknowledge host acceptance with undefined.',
|
|
1403
|
+
{ receivedType: Array.isArray(snapshot) ? 'array' : snapshot === null ? 'null' : typeof snapshot }
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
const value = normalized.kind === 'fetch' || normalized.result === 'opaque-response'
|
|
1407
|
+
? normalizeFetchResponse(snapshot, effect.id, adapter.id, schemaCodecs, {
|
|
1408
|
+
maxBytes: executionOptions.maxFetchBodyBytes
|
|
1409
|
+
|| executionOptions.maxStructuredBodyBytes
|
|
1410
|
+
|| executionOptions.maxBodyBytes
|
|
1411
|
+
|| options.maxFetchBodyBytes
|
|
1412
|
+
|| options.maxStructuredBodyBytes
|
|
1413
|
+
|| options.maxBodyBytes,
|
|
1414
|
+
strict: runtimeOptions.strict,
|
|
1415
|
+
responseMode: normalized.kind === 'fetch' ? normalized.responseMode : normalized.result === 'opaque-response' ? 'opaque' : 'auto',
|
|
1416
|
+
trace: traceSink,
|
|
1417
|
+
target: runtimeOptions.target,
|
|
1418
|
+
provider: adapter.id,
|
|
1419
|
+
effectId: normalized.id,
|
|
1420
|
+
groupId: normalized.groupKey
|
|
1421
|
+
})
|
|
1422
|
+
: snapshot;
|
|
1423
|
+
resolutionOrder.push(effect.id);
|
|
1424
|
+
recordTrace(effectTraceResolved(adapter.id, executionId, normalized, value));
|
|
1425
|
+
return value;
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
recordTrace(Object.freeze({ type: 'effect-failed', provider: adapter.id, executionId, effectId: effect.id, kind: normalized.kind, error: error.name || 'Error', code: error.code }));
|
|
1428
|
+
throw redactRuntimeError(error, sensitiveValues);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async function dispatchGroup(effects) {
|
|
1433
|
+
const settled = await Promise.allSettled(effects.map(dispatchOne));
|
|
1434
|
+
const failures = settled
|
|
1435
|
+
.map((entry, index) => entry.status === 'rejected' ? Object.freeze({
|
|
1436
|
+
index,
|
|
1437
|
+
effectId: effects[index].id,
|
|
1438
|
+
kind: effects[index].kind,
|
|
1439
|
+
groupKey: effects[index].groupKey,
|
|
1440
|
+
error: entry.reason
|
|
1441
|
+
}) : undefined)
|
|
1442
|
+
.filter(Boolean);
|
|
1443
|
+
if (failures.length > 0) {
|
|
1444
|
+
const rawPrimary = failures[0].error;
|
|
1445
|
+
const primary = rawPrimary instanceof Error ? rawPrimary : new Error(String(rawPrimary));
|
|
1446
|
+
const keyed = failures.every((entry) => entry.groupKey !== undefined);
|
|
1447
|
+
primary.effectFailures = Object.freeze(failures.map((entry) => Object.freeze(keyed
|
|
1448
|
+
? {
|
|
1449
|
+
key: String(entry.groupKey),
|
|
1450
|
+
index: entry.index,
|
|
1451
|
+
effectId: entry.effectId,
|
|
1452
|
+
kind: entry.kind,
|
|
1453
|
+
name: entry.error && entry.error.name,
|
|
1454
|
+
code: entry.error && entry.error.code
|
|
1455
|
+
}
|
|
1456
|
+
: { effectId: entry.effectId, name: entry.error && entry.error.name, code: entry.error && entry.error.code })));
|
|
1457
|
+
throw primary;
|
|
1458
|
+
}
|
|
1459
|
+
return settled.map((entry) => entry.value);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const pulse = Object.freeze({
|
|
1463
|
+
effect(effect, continuationId) { return Object.freeze({ protocol: canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION, kind: 'effect', continuationId: String(continuationId), effect }); },
|
|
1464
|
+
group(effects, continuationId) { return Object.freeze({ protocol: canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION, kind: 'group', continuationId: String(continuationId), effects: Object.freeze([...effects]) }); }
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
const generator = programModule.createHandler()(context.ctx, pulse);
|
|
1468
|
+
if (!generator || typeof generator.next !== 'function') throw new CanonicalRuntimeError('CanonicalHandlerProtocolError', 'PULSE_CANONICAL_HANDLER_PROTOCOL', 'Compiled canonical handler did not return a generator.');
|
|
1469
|
+
let step;
|
|
1470
|
+
try { step = generator.next(); }
|
|
1471
|
+
catch (error) { error.execution = { executionId, trace: [...trace], continuations: executionContinuations() }; throw error; }
|
|
1472
|
+
|
|
1473
|
+
while (!step.done) {
|
|
1474
|
+
const marker = step.value;
|
|
1475
|
+
if (!marker || marker.protocol !== canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION || !canonicalRuntimeContract.CANONICAL_EFFECT_MARKER_KINDS.includes(marker.kind)) throw new CanonicalRuntimeError('CanonicalEffectProtocolError', 'PULSE_CANONICAL_EFFECT_PROTOCOL', 'Canonical handler yielded an unsupported runtime marker.', { marker });
|
|
1476
|
+
const effects = marker.kind === 'group' ? marker.effects : [marker.effect];
|
|
1477
|
+
const continuationId = `${executionId}:${marker.continuationId}`;
|
|
1478
|
+
registry.create({ id: continuationId, branchPoint: marker.continuationId, effectIds: effects.map((effect) => effect.id), ttlMs: executionOptions.continuationTtlMs || options.continuationTtlMs });
|
|
1479
|
+
registry.wait(continuationId);
|
|
1480
|
+
recordTrace(Object.freeze({ type: 'continuation-waiting', provider: adapter.id, executionId, continuationId, effectIds: effects.map((effect) => effect.id) }));
|
|
1481
|
+
let value;
|
|
1482
|
+
try {
|
|
1483
|
+
value = marker.kind === 'group' ? await dispatchGroup(effects) : await dispatchOne(effects[0]);
|
|
1484
|
+
registry.resume(continuationId);
|
|
1485
|
+
recordTrace(Object.freeze({ type: 'continuation-resumed', provider: adapter.id, executionId, continuationId, branchPoint: marker.continuationId }));
|
|
1486
|
+
step = generator.next(value);
|
|
1487
|
+
registry.complete(continuationId);
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
const current = registry.get(continuationId);
|
|
1490
|
+
if (!['failed', 'expired', 'completed', 'cancelled'].includes(current.state)) {
|
|
1491
|
+
try { registry.fail(continuationId, error); } catch (_) { /* preserve primary failure */ }
|
|
1492
|
+
}
|
|
1493
|
+
recordTrace(Object.freeze({ type: 'continuation-failed', provider: adapter.id, executionId, continuationId, error: error.name || 'Error' }));
|
|
1494
|
+
error.execution = Object.freeze({ executionId, trace: Object.freeze([...trace]), continuations: Object.freeze(executionContinuations()), resolutionOrder: Object.freeze([...resolutionOrder]) });
|
|
1495
|
+
throw error;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
let response;
|
|
1500
|
+
try { response = finalResponse(step.value, context.request.method); }
|
|
1501
|
+
catch (error) {
|
|
1502
|
+
error.execution = Object.freeze({ executionId, trace: Object.freeze([...trace]), continuations: Object.freeze(executionContinuations()), resolutionOrder: Object.freeze([...resolutionOrder]) });
|
|
1503
|
+
throw error;
|
|
1504
|
+
}
|
|
1505
|
+
recordTrace(Object.freeze({ type: 'execution-completed', provider: adapter.id, executionId, status: response.status, bodyClass: response.bodyClass }));
|
|
1506
|
+
executionStatus = 'completed';
|
|
1507
|
+
return Object.freeze({
|
|
1508
|
+
status: 'completed',
|
|
1509
|
+
version: String(options.runtimeVersion || CANONICAL_HOST_RUNTIME_VERSION),
|
|
1510
|
+
protocol: canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION,
|
|
1511
|
+
provider: adapter.id,
|
|
1512
|
+
providerVersion: adapter.version,
|
|
1513
|
+
providerMetadata: adapter.resultMetadata({ ...options, ...executionOptions, metadata, executionId }),
|
|
1514
|
+
executionId,
|
|
1515
|
+
metadata,
|
|
1516
|
+
response,
|
|
1517
|
+
requestBodyStats: context.requestBody.stats(),
|
|
1518
|
+
effectCount: dispatchedEffects,
|
|
1519
|
+
resolutionOrder: Object.freeze([...resolutionOrder]),
|
|
1520
|
+
continuations: Object.freeze(executionContinuations()),
|
|
1521
|
+
trace: Object.freeze([...trace])
|
|
1522
|
+
});
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
executionFailure = redactRuntimeError(error, sensitiveValues);
|
|
1525
|
+
throw executionFailure;
|
|
1526
|
+
} finally {
|
|
1527
|
+
try { adapter.disposeExecution(Object.freeze({ executionId, metadata, status: executionStatus, error: executionFailure })); }
|
|
1528
|
+
catch (_) { /* provider cleanup must not mask execution results */ }
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
return Object.freeze({
|
|
1533
|
+
version: String(options.runtimeVersion || CANONICAL_HOST_RUNTIME_VERSION),
|
|
1534
|
+
protocol: canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION,
|
|
1535
|
+
provider: adapter.id,
|
|
1536
|
+
execute,
|
|
1537
|
+
registry,
|
|
1538
|
+
trace() { return Object.freeze([...runtimeTrace]); }
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
async function executeCanonicalProgram(programModule, options = {}) {
|
|
1543
|
+
return createCanonicalHostRuntime(options).execute(programModule, options);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
module.exports = {
|
|
1547
|
+
CANONICAL_HOST_RUNTIME_VERSION,
|
|
1548
|
+
CANONICAL_RUNTIME_PROTOCOL_VERSION: canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION,
|
|
1549
|
+
CANONICAL_EFFECT_PROTOCOL_VERSION: canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION,
|
|
1550
|
+
CanonicalRuntimeError,
|
|
1551
|
+
assertCanonicalProgramCompatibility,
|
|
1552
|
+
createCanonicalHostRuntime,
|
|
1553
|
+
executeCanonicalProgram,
|
|
1554
|
+
createCanonicalContext,
|
|
1555
|
+
normalizeFetchResponse,
|
|
1556
|
+
normalizeFetchEffect,
|
|
1557
|
+
normalizeProviderEffect,
|
|
1558
|
+
normalizedInit,
|
|
1559
|
+
urlParts,
|
|
1560
|
+
finalResponse,
|
|
1561
|
+
redactedHeaders,
|
|
1562
|
+
redactRuntimeError,
|
|
1563
|
+
redactRuntimeValue,
|
|
1564
|
+
responseIsStructured,
|
|
1565
|
+
createStructuredFetchResponse,
|
|
1566
|
+
createOpaqueFetchResponse,
|
|
1567
|
+
normalizeHeaders,
|
|
1568
|
+
headerValue
|
|
1569
|
+
};
|