@pulse-compute/wasm-host-runtime 0.0.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -1
- package/package.json +44 -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 +1559 -0
- package/src/runtime/canonical-native-host.js +1173 -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 +330 -0
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const {
|
|
5
|
+
createNativeGuestSourceCryptoVerifier
|
|
6
|
+
} = require('./native-crypto-verifier.js');
|
|
7
|
+
|
|
8
|
+
function loadRuntimeContract() {
|
|
9
|
+
try { return require('@pulse-compute/wasm-contracts/handler/canonical-native-runtime'); }
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/handler/canonical-native-runtime.js');
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadCanonicalRuntime() {
|
|
17
|
+
try { return require('@pulse-compute/wasm-host-runtime/runtime/canonical-api-runtime'); }
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('./canonical-api-runtime.js');
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadSchemaCodecTools() {
|
|
25
|
+
try { return require('@pulse-compute/wasm-schema-json/compiler/canonical-schema-codecs'); }
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
28
|
+
return require('../../../schema-json/src/compiler/canonical-schema-codecs.js');
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadEventContract() {
|
|
35
|
+
try { return require('@pulse-compute/wasm-contracts/events'); }
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/events/contracts.js');
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const runtimeContract = loadRuntimeContract();
|
|
43
|
+
const canonicalRuntime = loadCanonicalRuntime();
|
|
44
|
+
const schemaCodecTools = loadSchemaCodecTools();
|
|
45
|
+
const eventContract = loadEventContract();
|
|
46
|
+
let executionSequence = 0;
|
|
47
|
+
const DEFAULT_MAX_EFFECTS = 1024;
|
|
48
|
+
|
|
49
|
+
class CanonicalNativeHostError extends Error {
|
|
50
|
+
constructor(message, code = 'PULSE_CANONICAL_NATIVE_HOST_FAILED', detail = {}) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'CanonicalNativeHostError';
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.detail = Object.freeze({ ...detail });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sha256(value) {
|
|
59
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeMaxEffects(value) {
|
|
63
|
+
const maxEffects = value === undefined ? DEFAULT_MAX_EFFECTS : Number(value);
|
|
64
|
+
if (!Number.isSafeInteger(maxEffects) || maxEffects <= 0) {
|
|
65
|
+
throw new TypeError('Pulse maxEffects must be a positive safe integer.');
|
|
66
|
+
}
|
|
67
|
+
return maxEffects;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function staticDecoderArgument(expression, effect) {
|
|
71
|
+
if (expression && expression.kind === 'literal' && ['string', 'number', 'boolean'].includes(typeof expression.value)) return expression.value;
|
|
72
|
+
if (expression && expression.kind === 'literal' && expression.value === null) return null;
|
|
73
|
+
throw new CanonicalNativeHostError(
|
|
74
|
+
'Native decoder arguments must be canonical literal values.',
|
|
75
|
+
'PULSE_CANONICAL_NATIVE_DECODER_ARGUMENT_UNSUPPORTED',
|
|
76
|
+
{ effectId: effect && effect.id, expression }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeProviderAdapter(input) {
|
|
81
|
+
if (!input || typeof input !== 'object') {
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
id: 'unconfigured',
|
|
84
|
+
version: 'pulse.canonical-native-unconfigured-provider.v1',
|
|
85
|
+
async dispatchEffect(effect) {
|
|
86
|
+
throw new CanonicalNativeHostError(`No provider adapter is configured for native effect ${effect.kind}.`, 'PULSE_PROVIDER_CAPABILITY_MISSING', { effectId: effect.id, kind: effect.kind });
|
|
87
|
+
},
|
|
88
|
+
resultMetadata() { return undefined; },
|
|
89
|
+
disposeExecution() {}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const id = String(input.id || input.name || '').trim();
|
|
93
|
+
if (!id) throw new TypeError('Canonical native provider adapter requires an id.');
|
|
94
|
+
const dispatchEffect = typeof input.dispatchEffect === 'function'
|
|
95
|
+
? input.dispatchEffect.bind(input)
|
|
96
|
+
: (typeof input.dispatchFetch === 'function'
|
|
97
|
+
? (effect, context) => {
|
|
98
|
+
if (effect.kind !== 'fetch') throw new CanonicalNativeHostError(`Provider ${id} does not implement ${effect.kind}.`, 'PULSE_PROVIDER_CAPABILITY_MISSING', { provider: id, effectId: effect.id, kind: effect.kind });
|
|
99
|
+
return input.dispatchFetch(effect, context);
|
|
100
|
+
}
|
|
101
|
+
: undefined);
|
|
102
|
+
if (!dispatchEffect) throw new TypeError(`Canonical native provider ${id} must implement dispatchEffect() or dispatchFetch().`);
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
id,
|
|
105
|
+
version: String(input.version || `pulse.canonical-native-${id}-provider.v1`),
|
|
106
|
+
dispatchEffect,
|
|
107
|
+
resultMetadata: typeof input.resultMetadata === 'function' ? input.resultMetadata.bind(input) : () => undefined,
|
|
108
|
+
disposeExecution: typeof input.disposeExecution === 'function' ? input.disposeExecution.bind(input) : () => undefined
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createSchemaCodecs(registryInput = {}) {
|
|
113
|
+
return schemaCodecTools.createCanonicalSchemaCodecs(registryInput);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class ValueHeap {
|
|
117
|
+
constructor() {
|
|
118
|
+
this.values = new Map();
|
|
119
|
+
this.next = 1;
|
|
120
|
+
}
|
|
121
|
+
put(value) {
|
|
122
|
+
const handle = this.next++;
|
|
123
|
+
this.values.set(handle, value);
|
|
124
|
+
return handle;
|
|
125
|
+
}
|
|
126
|
+
has(handle) { return this.values.has(Number(handle)); }
|
|
127
|
+
get(handle) {
|
|
128
|
+
const key = Number(handle);
|
|
129
|
+
if (!this.values.has(key)) throw new CanonicalNativeHostError(`Unknown native value handle ${key}.`, 'PULSE_CANONICAL_NATIVE_VALUE_HANDLE_INVALID', { handle: key });
|
|
130
|
+
return this.values.get(key);
|
|
131
|
+
}
|
|
132
|
+
size() { return this.values.size; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function binary(operator, left, right) {
|
|
136
|
+
switch (operator) {
|
|
137
|
+
case '===': return left === right;
|
|
138
|
+
case '!==': return left !== right;
|
|
139
|
+
case '==': return left == right; // intentional canonical JavaScript compatibility
|
|
140
|
+
case '!=': return left != right; // intentional canonical JavaScript compatibility
|
|
141
|
+
case '<': return left < right;
|
|
142
|
+
case '<=': return left <= right;
|
|
143
|
+
case '>': return left > right;
|
|
144
|
+
case '>=': return left >= right;
|
|
145
|
+
case '+': return left + right;
|
|
146
|
+
case '-': return left - right;
|
|
147
|
+
case '*': return left * right;
|
|
148
|
+
case '/': return left / right;
|
|
149
|
+
case '%': return left % right;
|
|
150
|
+
case '**': return left ** right;
|
|
151
|
+
case '&&': return left && right;
|
|
152
|
+
case '||': return left || right;
|
|
153
|
+
case '??': return left ?? right;
|
|
154
|
+
case '&': return left & right;
|
|
155
|
+
case '|': return left | right;
|
|
156
|
+
case '^': return left ^ right;
|
|
157
|
+
case '<<': return left << right;
|
|
158
|
+
case '>>': return left >> right;
|
|
159
|
+
case '>>>': return left >>> right;
|
|
160
|
+
case 'in': return left in right;
|
|
161
|
+
default: throw new CanonicalNativeHostError(`Unsupported native binary operator ${String(operator)}.`, 'PULSE_CANONICAL_NATIVE_OPERATOR_UNSUPPORTED', { operator });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function unary(operator, value) {
|
|
166
|
+
switch (operator) {
|
|
167
|
+
case '!': return !value;
|
|
168
|
+
case '+': return +value;
|
|
169
|
+
case '-': return -value;
|
|
170
|
+
case '~': return ~value;
|
|
171
|
+
case 'typeof': return typeof value;
|
|
172
|
+
case 'void': return undefined;
|
|
173
|
+
default: throw new CanonicalNativeHostError(`Unsupported native unary operator ${String(operator)}.`, 'PULSE_CANONICAL_NATIVE_OPERATOR_UNSUPPORTED', { operator });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeHeaders(headers) {
|
|
178
|
+
return canonicalRuntime.normalizeHeaders(headers || []);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function routerSegments(input) {
|
|
182
|
+
const text = String(input || '/');
|
|
183
|
+
if (text === '/') return [];
|
|
184
|
+
return text.replace(/^\/+|\/+$/g, '').split('/');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function routerMatch(pathValue, patternValue) {
|
|
188
|
+
const actual = routerSegments(pathValue);
|
|
189
|
+
const expected = routerSegments(patternValue);
|
|
190
|
+
const wildcard = expected.length > 0 && expected[expected.length - 1] === '*';
|
|
191
|
+
if ((!wildcard && actual.length !== expected.length) || (wildcard && actual.length < expected.length - 1)) return false;
|
|
192
|
+
const limit = wildcard ? expected.length - 1 : expected.length;
|
|
193
|
+
for (let index = 0; index < limit; index += 1) {
|
|
194
|
+
const part = expected[index];
|
|
195
|
+
if (part.startsWith(':')) { if (!actual[index]) return false; continue; }
|
|
196
|
+
if (actual[index] !== part) return false;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function routerParam(pathValue, patternValue, nameValue) {
|
|
202
|
+
const actual = routerSegments(pathValue);
|
|
203
|
+
const expected = routerSegments(patternValue);
|
|
204
|
+
const target = `:${String(nameValue)}`;
|
|
205
|
+
for (let index = 0; index < expected.length; index += 1) if (expected[index] === target) return actual[index];
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function kvStoreName(value) {
|
|
210
|
+
if (value && typeof value === 'object' && value.kind === 'kv-namespace' && value.name !== undefined) return String(value.name);
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function literalPlanEffectInput(planEffect, name) {
|
|
215
|
+
const input = (planEffect.inputs || []).find((entry) => entry && entry.name === name);
|
|
216
|
+
const value = input && input.value;
|
|
217
|
+
return value && value.kind === 'literal' ? value.value : undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function nativeFetchResponseContract(planEffect, payload = {}) {
|
|
221
|
+
const explicitMode = payload.responseMode === undefined
|
|
222
|
+
? literalPlanEffectInput(planEffect, 'responseMode')
|
|
223
|
+
: payload.responseMode;
|
|
224
|
+
const decoder = planEffect.decoder || (planEffect.result && planEffect.result.decoder && planEffect.result.decoder.kind);
|
|
225
|
+
const responseMode = explicitMode === undefined
|
|
226
|
+
? (decoder ? 'structured' : planEffect.result && planEffect.result.valueKind === 'opaque-response' ? 'opaque' : 'auto')
|
|
227
|
+
: String(explicitMode);
|
|
228
|
+
const explicitProjection = payload.projection === undefined
|
|
229
|
+
? literalPlanEffectInput(planEffect, 'projection')
|
|
230
|
+
: payload.projection;
|
|
231
|
+
const projection = explicitProjection === undefined
|
|
232
|
+
? (decoder || (responseMode === 'structured' ? 'text' : 'response'))
|
|
233
|
+
: String(explicitProjection);
|
|
234
|
+
return Object.freeze({ responseMode, projection });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function nativeEffect(planEffect, payload) {
|
|
238
|
+
const base = {
|
|
239
|
+
id: String(planEffect.id),
|
|
240
|
+
kind: String(planEffect.kind),
|
|
241
|
+
resource: planEffect.resource,
|
|
242
|
+
source: planEffect.source
|
|
243
|
+
};
|
|
244
|
+
if (planEffect.kind === 'fetch') {
|
|
245
|
+
const response = nativeFetchResponseContract(planEffect, payload);
|
|
246
|
+
return { ...base, url: payload.url, init: payload.init, ...response };
|
|
247
|
+
}
|
|
248
|
+
if (planEffect.kind === 'config.get' || planEffect.kind === 'secret.get') return { ...base, name: payload.name };
|
|
249
|
+
if (planEffect.kind === 'kv.get' || planEffect.kind === 'kv.put') return { ...base, store: kvStoreName(payload.store), key: payload.key, value: planEffect.kind === 'kv.put' ? payload.value : undefined };
|
|
250
|
+
if (planEffect.kind === 'event.emit') {
|
|
251
|
+
return {
|
|
252
|
+
...base,
|
|
253
|
+
providerKind: 'event',
|
|
254
|
+
operation: 'emit',
|
|
255
|
+
capability: 'event.emit',
|
|
256
|
+
type: payload.type,
|
|
257
|
+
emission: payload.emission
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (planEffect.package && planEffect.contractId && planEffect.operation) {
|
|
261
|
+
const staticPayload = payload && payload.payload && typeof payload.payload === 'object'
|
|
262
|
+
? payload.payload
|
|
263
|
+
: {};
|
|
264
|
+
const invocation = payload && payload.invocation && typeof payload.invocation === 'object'
|
|
265
|
+
? payload.invocation
|
|
266
|
+
: {};
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
package: String(planEffect.package),
|
|
270
|
+
contractId: String(planEffect.contractId),
|
|
271
|
+
providerKind: String(planEffect.providerKind || 'package'),
|
|
272
|
+
operation: String(planEffect.operation),
|
|
273
|
+
capability: String(planEffect.capability || planEffect.kind),
|
|
274
|
+
result: String(planEffect.declaredResult || (planEffect.result && planEffect.result.valueKind) || 'value'),
|
|
275
|
+
payload: { ...staticPayload, ...invocation }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return { ...base, ...payload };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function decodeAssemblyScriptString(memory, pointer, length) {
|
|
282
|
+
if (!(memory instanceof WebAssembly.Memory)) throw new CanonicalNativeHostError('AssemblyScript string decode requires WebAssembly memory.', 'PULSE_CANONICAL_NATIVE_MEMORY_MISSING');
|
|
283
|
+
const ptr = Number(pointer);
|
|
284
|
+
const size = Number(length);
|
|
285
|
+
if (!Number.isSafeInteger(ptr) || ptr < 0 || (ptr & 1) !== 0 || !Number.isSafeInteger(size) || size < 0) {
|
|
286
|
+
throw new CanonicalNativeHostError('AssemblyScript string pointer or length is invalid.', 'PULSE_CANONICAL_NATIVE_STRING_BOUNDS', { pointer, length });
|
|
287
|
+
}
|
|
288
|
+
if (size === 0) return '';
|
|
289
|
+
const byteLength = size * 2;
|
|
290
|
+
if (!Number.isSafeInteger(byteLength) || ptr + byteLength > memory.buffer.byteLength) {
|
|
291
|
+
throw new CanonicalNativeHostError('AssemblyScript string exceeds module memory.', 'PULSE_CANONICAL_NATIVE_STRING_BOUNDS', { pointer: ptr, length: size, memoryBytes: memory.buffer.byteLength });
|
|
292
|
+
}
|
|
293
|
+
const view = new Uint16Array(memory.buffer, ptr, size);
|
|
294
|
+
let output = '';
|
|
295
|
+
const chunk = 8192;
|
|
296
|
+
for (let index = 0; index < view.length; index += chunk) output += String.fromCharCode(...view.subarray(index, Math.min(view.length, index + chunk)));
|
|
297
|
+
return output;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function decodeManagedAssemblyScriptString(memory, pointer) {
|
|
301
|
+
const ptr = Number(pointer);
|
|
302
|
+
if (!Number.isSafeInteger(ptr) || ptr < 4 || ptr > memory.buffer.byteLength) {
|
|
303
|
+
throw new CanonicalNativeHostError('AssemblyScript managed string pointer is invalid.', 'PULSE_CANONICAL_NATIVE_STRING_BOUNDS', {
|
|
304
|
+
pointer,
|
|
305
|
+
memoryBytes: memory.buffer.byteLength
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const byteLength = new DataView(memory.buffer).getUint32(ptr - 4, true);
|
|
309
|
+
if ((byteLength & 1) !== 0) {
|
|
310
|
+
throw new CanonicalNativeHostError('AssemblyScript managed string byte length is invalid.', 'PULSE_CANONICAL_NATIVE_STRING_BOUNDS', {
|
|
311
|
+
pointer: ptr,
|
|
312
|
+
byteLength
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return decodeAssemblyScriptString(memory, ptr, byteLength >>> 1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function decodePlanHash(instance) {
|
|
319
|
+
const pointer = instance.exports.pulse_plan_hash_ptr();
|
|
320
|
+
const length = instance.exports.pulse_plan_hash_length();
|
|
321
|
+
return decodeAssemblyScriptString(instance.exports.memory, pointer, length);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validateNativeModule(module, plan) {
|
|
325
|
+
const imports = WebAssembly.Module.imports(module);
|
|
326
|
+
const exports = WebAssembly.Module.exports(module);
|
|
327
|
+
const unsupportedModules = [...new Set(imports.map((entry) => entry.module).filter((name) => !['pulse_host', 'env'].includes(name)))];
|
|
328
|
+
if (unsupportedModules.length > 0) {
|
|
329
|
+
throw new CanonicalNativeHostError('Native module imports an unsupported module.', 'PULSE_CANONICAL_NATIVE_ABI_IMPORT_MISMATCH', { unsupportedModules, imports });
|
|
330
|
+
}
|
|
331
|
+
const allowedPulseImports = new Set(runtimeContract.CANONICAL_NATIVE_IMPORT_NAMES);
|
|
332
|
+
const pulseImports = imports.filter((entry) => entry.module === 'pulse_host').map((entry) => entry.name).sort();
|
|
333
|
+
const unexpectedPulseImports = pulseImports.filter((name) => !allowedPulseImports.has(name));
|
|
334
|
+
if (unexpectedPulseImports.length > 0) {
|
|
335
|
+
throw new CanonicalNativeHostError('Native module import surface exceeds the versioned pulse_host ABI.', 'PULSE_CANONICAL_NATIVE_ABI_IMPORT_MISMATCH', { unexpectedPulseImports, pulseImports });
|
|
336
|
+
}
|
|
337
|
+
const allowedEnvImports = new Set(runtimeContract.CANONICAL_NATIVE_ALLOWED_ENV_IMPORTS);
|
|
338
|
+
const unexpectedEnvImports = imports.filter((entry) => entry.module === 'env' && !allowedEnvImports.has(entry.name));
|
|
339
|
+
if (unexpectedEnvImports.length > 0) {
|
|
340
|
+
throw new CanonicalNativeHostError('Native module imports an unsupported AssemblyScript environment function.', 'PULSE_CANONICAL_NATIVE_ABI_IMPORT_MISMATCH', { unexpectedEnvImports });
|
|
341
|
+
}
|
|
342
|
+
const exportNames = new Set(exports.map((entry) => entry.name));
|
|
343
|
+
const missing = runtimeContract.CANONICAL_NATIVE_EXPORT_NAMES.filter((name) => !exportNames.has(name));
|
|
344
|
+
if (missing.length > 0) throw new CanonicalNativeHostError('Native module is missing required ABI exports.', 'PULSE_CANONICAL_NATIVE_ABI_EXPORT_MISMATCH', { missing });
|
|
345
|
+
if (!plan || typeof plan !== 'object' || !plan.planHash) throw new TypeError('Native module execution requires its canonical native plan.');
|
|
346
|
+
const eventReachable = Boolean(plan.events && plan.events.catalog && plan.events.catalog.events.length > 0);
|
|
347
|
+
const eventExportNames = eventContract.EVENT_NATIVE_ABI_EXTENSION.exports.map((entry) => entry.name);
|
|
348
|
+
const presentEventExports = eventExportNames.filter((name) => exportNames.has(name));
|
|
349
|
+
if (eventReachable && presentEventExports.length !== eventExportNames.length) {
|
|
350
|
+
throw new CanonicalNativeHostError('Event-reachable Native module is missing its conditional event ABI.', 'PULSE_CANONICAL_NATIVE_EVENT_ABI_MISMATCH', {
|
|
351
|
+
missing: eventExportNames.filter((name) => !exportNames.has(name))
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (!eventReachable && presentEventExports.length > 0) {
|
|
355
|
+
throw new CanonicalNativeHostError('HTTP-only Native module unexpectedly exposes the conditional event ABI.', 'PULSE_CANONICAL_NATIVE_EVENT_ABI_MISMATCH', { present: presentEventExports });
|
|
356
|
+
}
|
|
357
|
+
return Object.freeze({ imports, exports });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
361
|
+
const plan = options.plan || compiled && compiled.plan;
|
|
362
|
+
const wasm = Buffer.isBuffer(compiled) ? compiled : compiled && compiled.wasm;
|
|
363
|
+
if (!Buffer.isBuffer(wasm)) throw new TypeError('instantiateCanonicalNativeModule requires compiled Wasm bytes.');
|
|
364
|
+
if (!WebAssembly.validate(wasm)) throw new CanonicalNativeHostError('Canonical native Wasm validation failed.', 'PULSE_CANONICAL_NATIVE_WASM_INVALID');
|
|
365
|
+
const module = new WebAssembly.Module(wasm);
|
|
366
|
+
const moduleShape = validateNativeModule(module, plan);
|
|
367
|
+
const adapter = normalizeProviderAdapter(options.providerAdapter || options.provider);
|
|
368
|
+
const executionPlane = options.executionPlane === 'event' ? 'event' : 'http';
|
|
369
|
+
const heap = new ValueHeap();
|
|
370
|
+
const pending = [];
|
|
371
|
+
const trace = [];
|
|
372
|
+
const sensitiveValues = new Set();
|
|
373
|
+
const configuredSecrets = options.secrets && typeof options.secrets === 'object' ? options.secrets : {};
|
|
374
|
+
for (const value of Object.values(configuredSecrets)) if (typeof value === 'string' && value.length > 0) sensitiveValues.add(value);
|
|
375
|
+
for (const value of Array.isArray(options.redactionValues) ? options.redactionValues : []) {
|
|
376
|
+
if (typeof value === 'string' && value.length > 0) sensitiveValues.add(value);
|
|
377
|
+
}
|
|
378
|
+
let instance;
|
|
379
|
+
const preflightSchemaCodecs = options.schemaCodecs || createSchemaCodecs(plan.schemas && plan.schemas.registry);
|
|
380
|
+
const schemaIndexes = new Map((preflightSchemaCodecs.ids || []).map((id, index) => [String(id), index]));
|
|
381
|
+
|
|
382
|
+
function nativeCodecRoundtrip(mode, schemaId, text) {
|
|
383
|
+
const schemaIndex = schemaIndexes.get(String(schemaId));
|
|
384
|
+
if (!Number.isInteger(schemaIndex)) {
|
|
385
|
+
const failure = new Error(`Unknown compiled schema ${String(schemaId)}.`);
|
|
386
|
+
failure.name = 'SchemaReferenceError';
|
|
387
|
+
failure.code = 'PULSE_SCHEMA_REFERENCE';
|
|
388
|
+
failure.detail = Object.freeze({ schemaId: String(schemaId), source: mode });
|
|
389
|
+
throw failure;
|
|
390
|
+
}
|
|
391
|
+
const exports = instance && instance.exports;
|
|
392
|
+
const method = mode === 'encode' ? 'pulse_schema_encode' : 'pulse_schema_decode';
|
|
393
|
+
if (!exports
|
|
394
|
+
|| typeof exports.__new !== 'function'
|
|
395
|
+
|| typeof exports.__pin !== 'function'
|
|
396
|
+
|| typeof exports.__unpin !== 'function'
|
|
397
|
+
|| typeof exports.pulse_schema_string_id !== 'function'
|
|
398
|
+
|| typeof exports[method] !== 'function') {
|
|
399
|
+
throw new CanonicalNativeHostError(
|
|
400
|
+
'Native schema codec exports are unavailable.',
|
|
401
|
+
'PULSE_CANONICAL_NATIVE_SCHEMA_ABI_MISSING',
|
|
402
|
+
{ schemaId: String(schemaId), mode, required: runtimeContract.CANONICAL_NATIVE_SCHEMA_EXPORT_NAMES }
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const input = String(text);
|
|
406
|
+
const pointer = exports.__pin(exports.__new(input.length * 2, exports.pulse_schema_string_id()));
|
|
407
|
+
try {
|
|
408
|
+
if (input.length > 0) {
|
|
409
|
+
const view = new Uint16Array(memory().buffer, pointer, input.length);
|
|
410
|
+
for (let index = 0; index < input.length; index += 1) view[index] = input.charCodeAt(index);
|
|
411
|
+
}
|
|
412
|
+
const outputPointer = exports[method](schemaIndex, pointer);
|
|
413
|
+
return decodeManagedAssemblyScriptString(memory(), outputPointer);
|
|
414
|
+
} catch (cause) {
|
|
415
|
+
if (cause && cause.code === 'PULSE_SCHEMA_REFERENCE') throw cause;
|
|
416
|
+
const failure = new Error(`Native json-as ${mode} failed for schema ${String(schemaId)}.`);
|
|
417
|
+
failure.name = mode === 'encode' ? 'SchemaEncodeError' : 'SchemaDecodeError';
|
|
418
|
+
failure.code = mode === 'encode' ? 'PULSE_SCHEMA_ENCODE' : 'PULSE_SCHEMA_DECODE';
|
|
419
|
+
failure.detail = Object.freeze({
|
|
420
|
+
schemaId: String(schemaId),
|
|
421
|
+
source: mode,
|
|
422
|
+
backend: 'json-as',
|
|
423
|
+
causeCode: cause && cause.code || null
|
|
424
|
+
});
|
|
425
|
+
failure.cause = cause;
|
|
426
|
+
throw failure;
|
|
427
|
+
} finally {
|
|
428
|
+
exports.__unpin(pointer);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const schemaCodecs = Object.freeze({
|
|
433
|
+
...preflightSchemaCodecs,
|
|
434
|
+
version: 'pulse.canonical-native-json-as-schema-codecs.v2',
|
|
435
|
+
backend: 'json-as',
|
|
436
|
+
decodeJsonText(schemaId, text, source = 'json') {
|
|
437
|
+
const normalized = preflightSchemaCodecs.decodeJsonText(schemaId, text, source);
|
|
438
|
+
const filtered = preflightSchemaCodecs.encodeJsonText(schemaId, normalized, `${source}-preflight`);
|
|
439
|
+
const output = nativeCodecRoundtrip('decode', schemaId, filtered);
|
|
440
|
+
return preflightSchemaCodecs.decodeJsonText(schemaId, output, `${source}-postflight`);
|
|
441
|
+
},
|
|
442
|
+
encodeJsonText(schemaId, value, source = 'response') {
|
|
443
|
+
const filtered = preflightSchemaCodecs.encodeJsonText(schemaId, value, `${source}-preflight`);
|
|
444
|
+
const output = nativeCodecRoundtrip('encode', schemaId, filtered);
|
|
445
|
+
preflightSchemaCodecs.decodeJsonText(schemaId, output, `${source}-postflight`);
|
|
446
|
+
return output;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
const traceSink = Object.freeze({
|
|
450
|
+
push(...events) {
|
|
451
|
+
for (const event of events) trace.push(canonicalRuntime.redactRuntimeValue(event, sensitiveValues));
|
|
452
|
+
},
|
|
453
|
+
json(input, value) {
|
|
454
|
+
const safeValue = value === undefined
|
|
455
|
+
? undefined
|
|
456
|
+
: canonicalRuntime.redactRuntimeValue(value, sensitiveValues);
|
|
457
|
+
const safeHeaders = input.headers === undefined
|
|
458
|
+
? undefined
|
|
459
|
+
: canonicalRuntime.redactRuntimeValue(input.headers, sensitiveValues);
|
|
460
|
+
trace.push(schemaCodecs.createTraceEvent({
|
|
461
|
+
...input,
|
|
462
|
+
...(safeHeaders === undefined ? {} : { headers: safeHeaders }),
|
|
463
|
+
...(safeValue === undefined ? {} : { semanticValue: safeValue }),
|
|
464
|
+
target: input.target || 'native',
|
|
465
|
+
provider: input.provider || adapter.id
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
const plannedMaxBodyBytes = Number(plan.json && plan.json.limits && plan.json.limits.maxBytes || plan.schemas && plan.schemas.registry && plan.schemas.registry.maxBytes || 65536);
|
|
470
|
+
const strict = options.strict === undefined ? plan.json && plan.json.strict === true : options.strict === true;
|
|
471
|
+
const context = canonicalRuntime.createCanonicalContext({
|
|
472
|
+
...options,
|
|
473
|
+
strict,
|
|
474
|
+
maxBodyBytes: options.maxBodyBytes === undefined ? plannedMaxBodyBytes : options.maxBodyBytes,
|
|
475
|
+
schemaCodecs,
|
|
476
|
+
trace: traceSink,
|
|
477
|
+
target: 'native',
|
|
478
|
+
provider: adapter.id,
|
|
479
|
+
reporting: options.reporting === undefined
|
|
480
|
+
? plan.logging && plan.logging.reporting && plan.logging.reporting.name
|
|
481
|
+
: options.reporting,
|
|
482
|
+
redactLogMessage(message) {
|
|
483
|
+
return canonicalRuntime.redactRuntimeValue(message, sensitiveValues);
|
|
484
|
+
}
|
|
485
|
+
}, traceSink);
|
|
486
|
+
function value(handle) { return heap.get(handle); }
|
|
487
|
+
function put(input) { return heap.put(input); }
|
|
488
|
+
function requireHttpSurface(surface) {
|
|
489
|
+
if (executionPlane === 'event') {
|
|
490
|
+
throw new CanonicalNativeHostError(`Event execution cannot access HTTP-only Native surface ${surface}.`, 'PULSE_CANONICAL_NATIVE_EVENT_HTTP_SURFACE', { surface });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function requireExecutionPlane(expected) {
|
|
494
|
+
if (executionPlane !== expected) {
|
|
495
|
+
throw new CanonicalNativeHostError(
|
|
496
|
+
`Native ${expected} entry cannot run through an ${executionPlane} execution controller.`,
|
|
497
|
+
'PULSE_CANONICAL_NATIVE_EXECUTION_PLANE_MISMATCH',
|
|
498
|
+
{ expected, actual: executionPlane }
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function memory() {
|
|
503
|
+
if (!instance || !instance.exports || !(instance.exports.memory instanceof WebAssembly.Memory)) throw new CanonicalNativeHostError('Native module memory is unavailable.', 'PULSE_CANONICAL_NATIVE_MEMORY_MISSING');
|
|
504
|
+
return instance.exports.memory;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function gripHeaderPairs(input) {
|
|
508
|
+
const pairs = [];
|
|
509
|
+
if (Array.isArray(input)) {
|
|
510
|
+
for (const pair of input) {
|
|
511
|
+
if (Array.isArray(pair) && pair.length === 2) pairs.push([String(pair[0]), String(pair[1])]);
|
|
512
|
+
}
|
|
513
|
+
} else if (input && typeof input === 'object') {
|
|
514
|
+
for (const [name, entry] of Object.entries(input)) {
|
|
515
|
+
if (Array.isArray(entry)) {
|
|
516
|
+
for (const item of entry) pairs.push([String(name), String(item)]);
|
|
517
|
+
} else pairs.push([String(name), String(entry)]);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return pairs;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function gripHeaderValue(input, name) {
|
|
524
|
+
const lower = String(name).toLowerCase();
|
|
525
|
+
if (input && typeof input.header === 'function') return input.header(name);
|
|
526
|
+
const matches = gripHeaderPairs(input && input.headers !== undefined ? input.headers : input)
|
|
527
|
+
.filter(([key]) => key.toLowerCase() === lower)
|
|
528
|
+
.map(([, entry]) => entry);
|
|
529
|
+
return matches.length > 0 ? matches.join(', ') : undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function gripChannels(options) {
|
|
533
|
+
const input = options && options.channels !== undefined
|
|
534
|
+
? options.channels
|
|
535
|
+
: (options && options.channel !== undefined ? [options.channel] : []);
|
|
536
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
537
|
+
throw new CanonicalNativeHostError('GRIP framing requires at least one channel.', 'PULSE_GRIP_CHANNEL_INVALID');
|
|
538
|
+
}
|
|
539
|
+
return input.map((entry) => {
|
|
540
|
+
const channel = typeof entry === 'string' ? entry.trim() : '';
|
|
541
|
+
if (!channel || /[,\r\n]/.test(channel)) {
|
|
542
|
+
throw new CanonicalNativeHostError('GRIP channel names must be non-empty strings without commas or line breaks.', 'PULSE_GRIP_CHANNEL_INVALID');
|
|
543
|
+
}
|
|
544
|
+
return channel;
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function gripSubscriptionHeaders(inputHeaders, options) {
|
|
549
|
+
const mode = options && options.mode === undefined ? 'stream' : options && options.mode;
|
|
550
|
+
if (!['stream', 'response'].includes(mode)) {
|
|
551
|
+
throw new CanonicalNativeHostError('GRIP subscription mode must be stream or response.', 'PULSE_GRIP_MODE_UNSUPPORTED');
|
|
552
|
+
}
|
|
553
|
+
const headers = gripHeaderPairs(inputHeaders).filter(([name]) => !['grip-hold', 'grip-channel', 'grip-timeout'].includes(name.toLowerCase()));
|
|
554
|
+
headers.push(['Grip-Hold', mode]);
|
|
555
|
+
for (const channel of gripChannels(options || {})) headers.push(['Grip-Channel', channel]);
|
|
556
|
+
if (options && options.timeoutMs !== undefined) {
|
|
557
|
+
const timeout = Number(options.timeoutMs);
|
|
558
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0) {
|
|
559
|
+
throw new CanonicalNativeHostError('GRIP timeoutMs must be a non-negative safe integer.', 'PULSE_GRIP_TIMEOUT_INVALID');
|
|
560
|
+
}
|
|
561
|
+
headers.push(['Grip-Timeout', String(timeout)]);
|
|
562
|
+
}
|
|
563
|
+
return Object.freeze(headers.map((pair) => Object.freeze(pair)));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const pulseHost = {
|
|
567
|
+
value_undefined() { return put(undefined); },
|
|
568
|
+
value_null() { return put(null); },
|
|
569
|
+
value_boolean(input) { return put(Number(input) !== 0); },
|
|
570
|
+
value_number(input) { return put(Number(input)); },
|
|
571
|
+
value_string(pointer, length) { return put(decodeAssemblyScriptString(memory(), pointer, length)); },
|
|
572
|
+
value_array() { return put([]); },
|
|
573
|
+
value_array_push(arrayHandle, valueHandle) {
|
|
574
|
+
const target = value(arrayHandle);
|
|
575
|
+
if (!Array.isArray(target)) throw new CanonicalNativeHostError('value_array_push target is not an array.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { arrayHandle });
|
|
576
|
+
target.push(value(valueHandle));
|
|
577
|
+
},
|
|
578
|
+
value_array_spread(arrayHandle, valueHandle) {
|
|
579
|
+
const target = value(arrayHandle);
|
|
580
|
+
const source = value(valueHandle);
|
|
581
|
+
if (!Array.isArray(target) || !source || typeof source[Symbol.iterator] !== 'function') throw new CanonicalNativeHostError('value_array_spread requires iterable input.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
582
|
+
target.push(...source);
|
|
583
|
+
},
|
|
584
|
+
value_object() { return put({}); },
|
|
585
|
+
value_object_set(objectHandle, keyHandle, valueHandle) {
|
|
586
|
+
const target = value(objectHandle);
|
|
587
|
+
if (!target || typeof target !== 'object') throw new CanonicalNativeHostError('value_object_set target is not an object.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
588
|
+
target[String(value(keyHandle))] = value(valueHandle);
|
|
589
|
+
},
|
|
590
|
+
value_object_spread(objectHandle, valueHandle) {
|
|
591
|
+
const target = value(objectHandle);
|
|
592
|
+
const source = value(valueHandle);
|
|
593
|
+
if (!target || typeof target !== 'object') throw new CanonicalNativeHostError('value_object_spread target is not an object.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
594
|
+
if (source === null || source === undefined) return;
|
|
595
|
+
Object.assign(target, source);
|
|
596
|
+
},
|
|
597
|
+
value_property(objectHandle, keyHandle) {
|
|
598
|
+
const target = value(objectHandle);
|
|
599
|
+
if (target === null || target === undefined) throw new CanonicalNativeHostError('Cannot read a property from null or undefined.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
600
|
+
return put(target[String(value(keyHandle))]);
|
|
601
|
+
},
|
|
602
|
+
value_property_set(objectHandle, keyHandle, valueHandle) {
|
|
603
|
+
const target = value(objectHandle);
|
|
604
|
+
if (target === null || target === undefined) throw new CanonicalNativeHostError('Cannot write a property on null or undefined.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
605
|
+
const assigned = value(valueHandle);
|
|
606
|
+
target[String(value(keyHandle))] = assigned;
|
|
607
|
+
return valueHandle;
|
|
608
|
+
},
|
|
609
|
+
value_element(objectHandle, keyHandle) {
|
|
610
|
+
const target = value(objectHandle);
|
|
611
|
+
if (target === null || target === undefined) throw new CanonicalNativeHostError('Cannot read an element from null or undefined.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
612
|
+
return put(target[value(keyHandle)]);
|
|
613
|
+
},
|
|
614
|
+
value_element_set(objectHandle, keyHandle, valueHandle) {
|
|
615
|
+
const target = value(objectHandle);
|
|
616
|
+
if (target === null || target === undefined) throw new CanonicalNativeHostError('Cannot write an element on null or undefined.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
617
|
+
const assigned = value(valueHandle);
|
|
618
|
+
target[value(keyHandle)] = assigned;
|
|
619
|
+
return valueHandle;
|
|
620
|
+
},
|
|
621
|
+
value_binary(operatorIndex, leftHandle, rightHandle) {
|
|
622
|
+
const operator = runtimeContract.CANONICAL_NATIVE_BINARY_OPERATORS[Number(operatorIndex)];
|
|
623
|
+
return put(binary(operator, value(leftHandle), value(rightHandle)));
|
|
624
|
+
},
|
|
625
|
+
value_unary(operatorIndex, valueHandle) {
|
|
626
|
+
const operator = runtimeContract.CANONICAL_NATIVE_UNARY_OPERATORS[Number(operatorIndex)];
|
|
627
|
+
return put(unary(operator, value(valueHandle)));
|
|
628
|
+
},
|
|
629
|
+
value_truthy(valueHandle) { return value(valueHandle) ? 1 : 0; },
|
|
630
|
+
value_nullish(valueHandle) { return value(valueHandle) === null || value(valueHandle) === undefined ? 1 : 0; },
|
|
631
|
+
log(level, messageHandle) {
|
|
632
|
+
const methods = Object.freeze({ 1: 'error', 2: 'warn', 3: 'info', 4: 'debug' });
|
|
633
|
+
const method = methods[Number(level)];
|
|
634
|
+
if (!method) return;
|
|
635
|
+
const message = value(messageHandle);
|
|
636
|
+
if (typeof message !== 'string') return;
|
|
637
|
+
try { context.ctx.log[method](message); }
|
|
638
|
+
catch (_) { /* Logging is best effort at the host boundary. */ }
|
|
639
|
+
},
|
|
640
|
+
request_method() { requireHttpSurface('ctx.req.method'); return put(context.ctx.req.method); },
|
|
641
|
+
request_url() { requireHttpSurface('ctx.req.url'); return put(context.ctx.req.url); },
|
|
642
|
+
request_path() { requireHttpSurface('ctx.req.path'); return put(context.ctx.req.path); },
|
|
643
|
+
request_headers() { requireHttpSurface('ctx.req.headers'); return put(context.ctx.req.headers); },
|
|
644
|
+
request_header(nameHandle) { requireHttpSurface('ctx.req.header'); return put(context.ctx.req.header(value(nameHandle))); },
|
|
645
|
+
request_text() { requireHttpSurface('ctx.req.text'); return put(context.ctx.req.text()); },
|
|
646
|
+
request_json(schemaHandle) {
|
|
647
|
+
requireHttpSurface('ctx.req.json');
|
|
648
|
+
const schema = value(schemaHandle);
|
|
649
|
+
return put(schema === undefined ? context.ctx.req.json() : context.ctx.req.json(schema));
|
|
650
|
+
},
|
|
651
|
+
router_match(pathHandle, patternHandle) { return put(routerMatch(value(pathHandle), value(patternHandle))); },
|
|
652
|
+
router_param(pathHandle, patternHandle, nameHandle) { return put(routerParam(value(pathHandle), value(patternHandle), value(nameHandle))); },
|
|
653
|
+
response_json(valueHandle, optionsHandle) { requireHttpSurface('ctx.json'); return put(context.ctx.json(value(valueHandle), value(optionsHandle))); },
|
|
654
|
+
response_text(valueHandle, optionsHandle) { requireHttpSurface('ctx.text'); return put(context.ctx.text(value(valueHandle), value(optionsHandle))); },
|
|
655
|
+
response_custom(valueHandle) { requireHttpSurface('ctx.response'); return put(context.ctx.response(value(valueHandle))); },
|
|
656
|
+
grip_is_websocket() {
|
|
657
|
+
requireHttpSurface('grip.isWebSocket');
|
|
658
|
+
const request = context.ctx.req;
|
|
659
|
+
const contentType = String(gripHeaderValue(request, 'content-type') || '').toLowerCase();
|
|
660
|
+
const accept = String(gripHeaderValue(request, 'accept') || '').toLowerCase();
|
|
661
|
+
const upgrade = String(gripHeaderValue(request, 'upgrade') || '').toLowerCase();
|
|
662
|
+
const connection = String(gripHeaderValue(request, 'connection') || '').toLowerCase();
|
|
663
|
+
return put(
|
|
664
|
+
contentType.includes('application/websocket-events')
|
|
665
|
+
|| accept.includes('application/websocket-events')
|
|
666
|
+
|| (upgrade === 'websocket' && connection.split(',').some((entry) => entry.trim() === 'upgrade'))
|
|
667
|
+
);
|
|
668
|
+
},
|
|
669
|
+
grip_subscribe(responseHandle, optionsHandle) {
|
|
670
|
+
requireHttpSurface('grip.subscribe');
|
|
671
|
+
const response = value(responseHandle);
|
|
672
|
+
const optionsValue = value(optionsHandle);
|
|
673
|
+
if (!response || typeof response !== 'object') {
|
|
674
|
+
throw new CanonicalNativeHostError('grip.subscribe requires a canonical response.', 'PULSE_GRIP_RESPONSE_REQUIRED');
|
|
675
|
+
}
|
|
676
|
+
return put(Object.freeze({
|
|
677
|
+
...response,
|
|
678
|
+
headers: gripSubscriptionHeaders(response.headers, optionsValue || {})
|
|
679
|
+
}));
|
|
680
|
+
},
|
|
681
|
+
grip_handoff(optionsHandle) {
|
|
682
|
+
requireHttpSurface('grip.handoff');
|
|
683
|
+
const optionsValue = value(optionsHandle);
|
|
684
|
+
if (!optionsValue || typeof optionsValue !== 'object' || Array.isArray(optionsValue)) {
|
|
685
|
+
throw new CanonicalNativeHostError('grip.handoff requires a static options object.', 'PULSE_GRIP_OPTIONS_REQUIRED');
|
|
686
|
+
}
|
|
687
|
+
const headers = gripHeaderPairs(optionsValue.headers);
|
|
688
|
+
if (!headers.some(([name]) => name.toLowerCase() === 'content-type')) {
|
|
689
|
+
headers.push(['Content-Type', 'application/websocket-events']);
|
|
690
|
+
}
|
|
691
|
+
const framed = gripSubscriptionHeaders(headers, optionsValue);
|
|
692
|
+
const status = optionsValue.status === undefined ? 200 : Number(optionsValue.status);
|
|
693
|
+
if (!Number.isSafeInteger(status) || status < 200 || status > 599) {
|
|
694
|
+
throw new CanonicalNativeHostError('GRIP handoff status must be an integer from 200 through 599.', 'PULSE_GRIP_STATUS_INVALID');
|
|
695
|
+
}
|
|
696
|
+
return put(Object.freeze({
|
|
697
|
+
status,
|
|
698
|
+
kind: optionsValue.body === undefined || optionsValue.body === null || String(optionsValue.body).length === 0 ? 'empty' : 'text',
|
|
699
|
+
bodyClass: 'structured',
|
|
700
|
+
body: optionsValue.body === undefined || optionsValue.body === null ? '' : String(optionsValue.body),
|
|
701
|
+
headers: framed,
|
|
702
|
+
builder: 'grip.handoff'
|
|
703
|
+
}));
|
|
704
|
+
},
|
|
705
|
+
kv_namespace(nameHandle) { return put(Object.freeze({ kind: 'kv-namespace', name: String(value(nameHandle)) })); },
|
|
706
|
+
fetch_json(responseHandle, schemaHandle) {
|
|
707
|
+
const response = value(responseHandle);
|
|
708
|
+
if (!response || typeof response.json !== 'function') throw new CanonicalNativeHostError('fetch_json requires a structured fetch response.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
709
|
+
const schema = value(schemaHandle);
|
|
710
|
+
return put(schema === undefined ? response.json() : response.json(schema));
|
|
711
|
+
},
|
|
712
|
+
fetch_text(responseHandle) {
|
|
713
|
+
const response = value(responseHandle);
|
|
714
|
+
if (!response || typeof response.text !== 'function') throw new CanonicalNativeHostError('fetch_text requires a structured fetch response.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
715
|
+
return put(response.text());
|
|
716
|
+
},
|
|
717
|
+
fetch_header(responseHandle, nameHandle) {
|
|
718
|
+
const response = value(responseHandle);
|
|
719
|
+
if (!response || typeof response.header !== 'function') throw new CanonicalNativeHostError('fetch_header requires a fetch response.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
720
|
+
return put(response.header(value(nameHandle)));
|
|
721
|
+
},
|
|
722
|
+
effect_begin(effectIndex, payloadHandle) {
|
|
723
|
+
const index = Number(effectIndex);
|
|
724
|
+
const effect = plan.effects && plan.effects[index];
|
|
725
|
+
if (!effect) throw new CanonicalNativeHostError(`Native module requested unknown effect index ${index}.`, 'PULSE_CANONICAL_NATIVE_EFFECT_INDEX_INVALID', { effectIndex: index });
|
|
726
|
+
const payload = value(payloadHandle);
|
|
727
|
+
pending.push(Object.freeze({ index, effect, payload }));
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const env = {};
|
|
732
|
+
for (const item of moduleShape.imports.filter((entry) => entry.module === 'env')) {
|
|
733
|
+
if (item.kind !== 'function') continue;
|
|
734
|
+
if (item.name === 'abort') {
|
|
735
|
+
env.abort = (messagePointer, filePointer, line, column) => {
|
|
736
|
+
const message = decodeAssemblyScriptString(memory(), messagePointer, new DataView(memory().buffer).getUint32((Number(messagePointer) >>> 0) - 4, true) >>> 1);
|
|
737
|
+
const file = decodeAssemblyScriptString(memory(), filePointer, new DataView(memory().buffer).getUint32((Number(filePointer) >>> 0) - 4, true) >>> 1);
|
|
738
|
+
throw new CanonicalNativeHostError(`AssemblyScript abort: ${message}`, 'PULSE_CANONICAL_NATIVE_AS_ABORT', { file, line, column });
|
|
739
|
+
};
|
|
740
|
+
} else if (item.name === 'seed') env.seed = () => 0;
|
|
741
|
+
else env[item.name] = () => { throw new CanonicalNativeHostError(`Unsupported AssemblyScript env import ${item.name}.`, 'PULSE_CANONICAL_NATIVE_ENV_IMPORT_UNSUPPORTED', { name: item.name }); };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
instance = new WebAssembly.Instance(module, { pulse_host: pulseHost, env });
|
|
745
|
+
if (schemaIndexes.size > 0) {
|
|
746
|
+
const available = new Set(moduleShape.exports.map((entry) => entry.name));
|
|
747
|
+
const missingSchemaExports = runtimeContract.CANONICAL_NATIVE_SCHEMA_EXPORT_NAMES.filter((name) => !available.has(name));
|
|
748
|
+
if (missingSchemaExports.length > 0) {
|
|
749
|
+
throw new CanonicalNativeHostError(
|
|
750
|
+
'Native module is missing its generated schema codec ABI.',
|
|
751
|
+
'PULSE_CANONICAL_NATIVE_SCHEMA_ABI_MISSING',
|
|
752
|
+
{ missing: missingSchemaExports, schemaIds: [...schemaIndexes.keys()] }
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (instance.exports.pulse_abi_version() !== runtimeContract.CANONICAL_NATIVE_ABI_VERSION) {
|
|
757
|
+
throw new CanonicalNativeHostError('Native module ABI version mismatch.', 'PULSE_CANONICAL_NATIVE_ABI_VERSION_MISMATCH', { expected: runtimeContract.CANONICAL_NATIVE_ABI_VERSION, actual: instance.exports.pulse_abi_version() });
|
|
758
|
+
}
|
|
759
|
+
const eventReachable = Boolean(plan.events && plan.events.catalog && plan.events.catalog.events.length > 0);
|
|
760
|
+
if (eventReachable && instance.exports.pulse_event_abi_version() !== eventContract.EVENT_NATIVE_ABI_EXTENSION.abiVersion) {
|
|
761
|
+
throw new CanonicalNativeHostError('Native module event ABI version mismatch.', 'PULSE_CANONICAL_NATIVE_EVENT_ABI_VERSION_MISMATCH', {
|
|
762
|
+
expected: eventContract.EVENT_NATIVE_ABI_EXTENSION.abiVersion,
|
|
763
|
+
actual: instance.exports.pulse_event_abi_version()
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const actualPlanHash = decodePlanHash(instance);
|
|
767
|
+
if (actualPlanHash !== plan.planHash) throw new CanonicalNativeHostError('Native module plan hash does not match its supplied plan.', 'PULSE_CANONICAL_NATIVE_PLAN_HASH_MISMATCH', { expected: plan.planHash, actual: actualPlanHash });
|
|
768
|
+
const nativeCrypto = createNativeGuestSourceCryptoVerifier({
|
|
769
|
+
plan,
|
|
770
|
+
exports: instance.exports
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
function takePending() {
|
|
774
|
+
const items = pending.splice(0, pending.length);
|
|
775
|
+
return Object.freeze(items);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function prepareEffectResult(index, rawResult) {
|
|
779
|
+
const effectIndex = Number(index);
|
|
780
|
+
const effect = plan.effects && plan.effects[effectIndex];
|
|
781
|
+
if (!effect) {
|
|
782
|
+
throw new CanonicalNativeHostError(`Native host cannot prepare an unknown effect index ${effectIndex}.`, 'PULSE_CANONICAL_NATIVE_EFFECT_INDEX_INVALID', { effectIndex });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
let result = rawResult;
|
|
786
|
+
if (effect.kind === 'event.emit' && result !== undefined) {
|
|
787
|
+
throw new canonicalRuntime.CanonicalRuntimeError(
|
|
788
|
+
'EventAcceptanceError',
|
|
789
|
+
'PULSE_RUNTIME_EVENT_EMIT_ACCEPTANCE_INVALID',
|
|
790
|
+
'Pulse event emit providers must acknowledge host acceptance with undefined.',
|
|
791
|
+
{ receivedType: Array.isArray(result) ? 'array' : result === null ? 'null' : typeof result }
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
if (effect.kind === 'fetch' || (effect.result && effect.result.valueKind === 'opaque-response')) {
|
|
795
|
+
const response = effect.kind === 'fetch'
|
|
796
|
+
? nativeFetchResponseContract(effect)
|
|
797
|
+
: Object.freeze({ responseMode: 'opaque', projection: 'response' });
|
|
798
|
+
result = canonicalRuntime.normalizeFetchResponse(rawResult, effect.id, adapter.id, schemaCodecs, {
|
|
799
|
+
maxBytes: options.maxFetchBodyBytes || options.maxStructuredBodyBytes || options.maxBodyBytes,
|
|
800
|
+
strict,
|
|
801
|
+
responseMode: response.responseMode,
|
|
802
|
+
trace: traceSink,
|
|
803
|
+
target: 'native',
|
|
804
|
+
provider: adapter.id,
|
|
805
|
+
effectId: effect.id,
|
|
806
|
+
groupId: effect.groupKey
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const decoder = effect.result && effect.result.decoder;
|
|
811
|
+
if (decoder && decoder.kind === 'json') {
|
|
812
|
+
const decoderArguments = (decoder.arguments || []).map((argument) => staticDecoderArgument(argument, effect));
|
|
813
|
+
result = result.json(...decoderArguments);
|
|
814
|
+
} else if (decoder && decoder.kind === 'text') {
|
|
815
|
+
result = result.text();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (effect.kind === 'secret.get' && typeof result === 'string') sensitiveValues.add(result);
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function setEffectResult(index, result) {
|
|
823
|
+
const handle = put(result);
|
|
824
|
+
const accepted = instance.exports.pulse_set_effect_result(Number(index), handle);
|
|
825
|
+
if (accepted !== runtimeContract.CANONICAL_NATIVE_RESULT_STATUS.ACCEPTED) {
|
|
826
|
+
throw new CanonicalNativeHostError(`Native module rejected effect result index ${index}.`, 'PULSE_CANONICAL_NATIVE_EFFECT_INDEX_INVALID', { effectIndex: Number(index), errorCode: instance.exports.pulse_last_error_code() });
|
|
827
|
+
}
|
|
828
|
+
return handle;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function resultValue() {
|
|
832
|
+
const handle = instance.exports.pulse_result_handle();
|
|
833
|
+
return handle === 0 ? undefined : value(handle);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
let activeEvent;
|
|
837
|
+
function startEvent(inputFrame) {
|
|
838
|
+
requireExecutionPlane('event');
|
|
839
|
+
if (!eventReachable) {
|
|
840
|
+
throw new CanonicalNativeHostError('Native plan does not contain an event entry.', 'PULSE_CANONICAL_NATIVE_EVENT_UNREACHABLE');
|
|
841
|
+
}
|
|
842
|
+
const frame = eventContract.normalizeEventFrame(inputFrame, options.eventLimits);
|
|
843
|
+
const selected = plan.events.catalog.events.find((entry) => entry.type === frame.type);
|
|
844
|
+
if (!selected) {
|
|
845
|
+
throw new CanonicalNativeHostError(`Native event type ${JSON.stringify(frame.type)} is not registered.`, 'PULSE_CANONICAL_NATIVE_EVENT_HANDLER_NOT_FOUND', { type: frame.type });
|
|
846
|
+
}
|
|
847
|
+
if (selected.schemaId !== frame.schemaId) {
|
|
848
|
+
throw new CanonicalNativeHostError('Native event frame schema does not match its registration.', 'PULSE_CANONICAL_NATIVE_EVENT_SCHEMA_MISMATCH', {
|
|
849
|
+
type: frame.type,
|
|
850
|
+
expectedSchemaId: selected.schemaId,
|
|
851
|
+
actualSchemaId: frame.schemaId
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const validated = frame.schemaId === null
|
|
855
|
+
? frame
|
|
856
|
+
: eventContract.normalizeEventFrame({
|
|
857
|
+
...frame,
|
|
858
|
+
payload: schemaCodecs.decode(frame.schemaId, frame.payload, 'event-ingress')
|
|
859
|
+
}, options.eventLimits);
|
|
860
|
+
const payloadHandle = frame.schemaId === null ? 0 : put(validated.payload);
|
|
861
|
+
activeEvent = Object.freeze({
|
|
862
|
+
runtimeId: selected.runtimeId,
|
|
863
|
+
stableId: selected.stableId,
|
|
864
|
+
type: selected.type,
|
|
865
|
+
schemaId: selected.schemaId,
|
|
866
|
+
payloadHandle
|
|
867
|
+
});
|
|
868
|
+
return instance.exports.pulse_event_start(selected.runtimeId, payloadHandle);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return Object.freeze({
|
|
872
|
+
version: runtimeContract.CANONICAL_NATIVE_HOST_VERSION,
|
|
873
|
+
plan,
|
|
874
|
+
module,
|
|
875
|
+
instance,
|
|
876
|
+
exports: instance.exports,
|
|
877
|
+
heap,
|
|
878
|
+
adapter,
|
|
879
|
+
context,
|
|
880
|
+
schemaCodecs,
|
|
881
|
+
trace,
|
|
882
|
+
traceSink,
|
|
883
|
+
sensitiveValues,
|
|
884
|
+
cryptoVerifier: nativeCrypto.verifier,
|
|
885
|
+
cryptoRealization: nativeCrypto.realization,
|
|
886
|
+
executionPlane,
|
|
887
|
+
start() { requireExecutionPlane('http'); return instance.exports.pulse_start(); },
|
|
888
|
+
startEvent,
|
|
889
|
+
eventSelection() { return activeEvent; },
|
|
890
|
+
resume() { return instance.exports.pulse_resume(); },
|
|
891
|
+
pendingEffects: takePending,
|
|
892
|
+
prepareEffectResult,
|
|
893
|
+
setEffectResult,
|
|
894
|
+
resultValue,
|
|
895
|
+
response() { requireHttpSurface('HTTP completion'); return canonicalRuntime.finalResponse(resultValue(), context.ctx.req.method); },
|
|
896
|
+
programCounter() { return instance.exports.pulse_program_counter(); },
|
|
897
|
+
continuationState() { return instance.exports.pulse_continuation_state(); },
|
|
898
|
+
lastErrorCode() { return instance.exports.pulse_last_error_code(); },
|
|
899
|
+
wasmSha256: sha256(wasm)
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function redactValue(value, sensitiveValues) {
|
|
904
|
+
return canonicalRuntime.redactRuntimeValue(value, sensitiveValues);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function nativeEventCancelled(message) {
|
|
908
|
+
return new CanonicalNativeHostError(message, 'PULSE_CANONICAL_NATIVE_EVENT_CANCELLED');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function raceNativeEventSignal(value, signal) {
|
|
912
|
+
if (!signal) return Promise.resolve(value);
|
|
913
|
+
if (signal.aborted) return Promise.reject(nativeEventCancelled('Native event execution was cancelled during effect dispatch.'));
|
|
914
|
+
return new Promise((resolve, reject) => {
|
|
915
|
+
let settled = false;
|
|
916
|
+
const finish = (callback, result) => {
|
|
917
|
+
if (settled) return;
|
|
918
|
+
settled = true;
|
|
919
|
+
signal.removeEventListener('abort', onAbort);
|
|
920
|
+
callback(result);
|
|
921
|
+
};
|
|
922
|
+
const onAbort = () => finish(reject, nativeEventCancelled('Native event execution was cancelled during effect dispatch.'));
|
|
923
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
924
|
+
Promise.resolve(value).then(
|
|
925
|
+
(result) => finish(resolve, result),
|
|
926
|
+
(error) => finish(reject, error)
|
|
927
|
+
);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function executeCanonicalNativeInvocation(compiled, options = {}, invocation = Object.freeze({ plane: 'http' })) {
|
|
932
|
+
const eventMode = invocation && invocation.plane === 'event';
|
|
933
|
+
const maxEffects = normalizeMaxEffects(options.maxEffects);
|
|
934
|
+
const controller = instantiateCanonicalNativeModule(compiled, { ...options, executionPlane: eventMode ? 'event' : 'http' });
|
|
935
|
+
const adapter = controller.adapter;
|
|
936
|
+
const packageArtifacts = Object.freeze([
|
|
937
|
+
...(Array.isArray(options.packageArtifacts)
|
|
938
|
+
? options.packageArtifacts
|
|
939
|
+
: compiled && Array.isArray(compiled.realizationArtifacts)
|
|
940
|
+
? compiled.realizationArtifacts
|
|
941
|
+
: [])
|
|
942
|
+
]);
|
|
943
|
+
const executionId = String(options.executionId || `canonical-native-${eventMode ? 'event-' : ''}${++executionSequence}`);
|
|
944
|
+
const resolutionOrder = [];
|
|
945
|
+
const continuations = [];
|
|
946
|
+
let effectCount = 0;
|
|
947
|
+
let status;
|
|
948
|
+
let executionFailure;
|
|
949
|
+
let activeContinuation;
|
|
950
|
+
try {
|
|
951
|
+
if (eventMode && options.signal && options.signal.aborted) {
|
|
952
|
+
throw nativeEventCancelled('Native event execution was cancelled before handler entry.');
|
|
953
|
+
}
|
|
954
|
+
status = eventMode ? controller.startEvent(invocation.frame) : controller.start();
|
|
955
|
+
while (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.SUSPENDED) {
|
|
956
|
+
if (eventMode && options.signal && options.signal.aborted) {
|
|
957
|
+
throw nativeEventCancelled('Native event execution was cancelled while suspended.');
|
|
958
|
+
}
|
|
959
|
+
const pending = controller.pendingEffects();
|
|
960
|
+
if (pending.length !== controller.exports.pulse_pending_count()) {
|
|
961
|
+
throw new CanonicalNativeHostError('Native pending-effect count differs from host queue.', 'PULSE_CANONICAL_NATIVE_PENDING_MISMATCH', { moduleCount: controller.exports.pulse_pending_count(), hostCount: pending.length });
|
|
962
|
+
}
|
|
963
|
+
const continuationState = controller.continuationState();
|
|
964
|
+
const continuation = controller.plan.continuations.find((entry) => Number(entry.stateIndex) === Number(continuationState));
|
|
965
|
+
if (!continuation) throw new CanonicalNativeHostError('Native suspension references an unknown continuation state.', 'PULSE_CANONICAL_NATIVE_CONTINUATION_MISMATCH', { continuationState });
|
|
966
|
+
const pendingEffectIds = pending.map((entry) => entry.effect.id);
|
|
967
|
+
if (JSON.stringify(pendingEffectIds) !== JSON.stringify(continuation.effectIds)) {
|
|
968
|
+
throw new CanonicalNativeHostError('Native pending effects do not match the continuation contract.', 'PULSE_CANONICAL_NATIVE_CONTINUATION_MISMATCH', { continuationId: continuation.id, expected: continuation.effectIds, actual: pendingEffectIds });
|
|
969
|
+
}
|
|
970
|
+
activeContinuation = {
|
|
971
|
+
id: continuation.id,
|
|
972
|
+
kind: continuation.kind,
|
|
973
|
+
effectIds: Object.freeze([...pendingEffectIds]),
|
|
974
|
+
stateIndex: continuationState,
|
|
975
|
+
state: 'waiting',
|
|
976
|
+
states: ['created', 'waiting']
|
|
977
|
+
};
|
|
978
|
+
continuations.push(activeContinuation);
|
|
979
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-waiting', executionId, continuationId: continuation.id, continuationState, effectIds: Object.freeze([...pendingEffectIds]) }, controller.sensitiveValues)));
|
|
980
|
+
if (effectCount + pending.length > maxEffects) {
|
|
981
|
+
throw new CanonicalNativeHostError(
|
|
982
|
+
eventMode
|
|
983
|
+
? `Pulse event invocation exceeded the maximum of ${maxEffects} execution-owned effects.`
|
|
984
|
+
: `Pulse request exceeded the maximum of ${maxEffects} request-owned effects.`,
|
|
985
|
+
'PULSE_RUNTIME_EFFECT_LIMIT_EXCEEDED',
|
|
986
|
+
{ effectCount, pendingEffects: pending.length, maxEffects, executionKind: eventMode ? 'event' : 'request' }
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
effectCount += pending.length;
|
|
990
|
+
|
|
991
|
+
const settled = await Promise.allSettled(pending.map(async (entry) => {
|
|
992
|
+
if (eventMode && options.signal && options.signal.aborted) {
|
|
993
|
+
throw nativeEventCancelled('Native event execution was cancelled before effect dispatch.');
|
|
994
|
+
}
|
|
995
|
+
const rawEffect = nativeEffect(entry.effect, entry.payload);
|
|
996
|
+
const normalized = canonicalRuntime.normalizeProviderEffect(rawEffect, controller.schemaCodecs, {
|
|
997
|
+
...options,
|
|
998
|
+
strict: options.strict === undefined
|
|
999
|
+
? controller.plan.json && controller.plan.json.strict === true
|
|
1000
|
+
: options.strict === true,
|
|
1001
|
+
trace: controller.traceSink,
|
|
1002
|
+
target: 'native',
|
|
1003
|
+
provider: adapter.id
|
|
1004
|
+
});
|
|
1005
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-effect-start', executionId, provider: adapter.id, effectId: normalized.id, kind: normalized.kind, payload: entry.payload }, controller.sensitiveValues)));
|
|
1006
|
+
const rawResult = await raceNativeEventSignal(adapter.dispatchEffect(normalized, {
|
|
1007
|
+
...options,
|
|
1008
|
+
executionId,
|
|
1009
|
+
metadata: compiled && compiled.plan ? compiled.plan.canonical : controller.plan.canonical,
|
|
1010
|
+
plan: controller.plan,
|
|
1011
|
+
packageArtifacts,
|
|
1012
|
+
cryptoVerifier: controller.cryptoVerifier,
|
|
1013
|
+
cryptoRealization: controller.cryptoRealization,
|
|
1014
|
+
trace: controller.trace,
|
|
1015
|
+
registerRedactionValue(value) {
|
|
1016
|
+
if (typeof value === 'string' && value.length > 0) controller.sensitiveValues.add(value);
|
|
1017
|
+
},
|
|
1018
|
+
validateSchemaValue(schemaId, value, context = {}) {
|
|
1019
|
+
return controller.schemaCodecs.decode(String(schemaId), value, context.source || 'package-effect');
|
|
1020
|
+
}
|
|
1021
|
+
}), eventMode ? options.signal : undefined);
|
|
1022
|
+
const result = controller.prepareEffectResult(entry.index, rawResult);
|
|
1023
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-effect-resolved', executionId, provider: adapter.id, effectId: normalized.id, kind: normalized.kind, result: entry.effect.kind === 'secret.get' ? '<redacted>' : result && typeof result.toJSON === 'function' ? result.toJSON() : result }, controller.sensitiveValues)));
|
|
1024
|
+
resolutionOrder.push(entry.effect.id);
|
|
1025
|
+
return { entry, result };
|
|
1026
|
+
}));
|
|
1027
|
+
|
|
1028
|
+
if (eventMode && options.signal && options.signal.aborted) {
|
|
1029
|
+
throw nativeEventCancelled('Native event execution was cancelled while suspended.');
|
|
1030
|
+
}
|
|
1031
|
+
const failed = settled.find((entry) => entry.status === 'rejected');
|
|
1032
|
+
if (failed) throw canonicalRuntime.redactRuntimeError(failed.reason, controller.sensitiveValues);
|
|
1033
|
+
for (const item of settled) controller.setEffectResult(item.value.entry.index, item.value.result);
|
|
1034
|
+
const previousPc = controller.programCounter();
|
|
1035
|
+
activeContinuation.state = 'resumed';
|
|
1036
|
+
activeContinuation.states.push('resumed');
|
|
1037
|
+
status = controller.resume();
|
|
1038
|
+
if (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.INVALID_RESUME) {
|
|
1039
|
+
throw new CanonicalNativeHostError('Native module rejected a complete effect-result set.', 'PULSE_CANONICAL_NATIVE_INVALID_RESUME', { programCounter: previousPc, errorCode: controller.lastErrorCode() });
|
|
1040
|
+
}
|
|
1041
|
+
activeContinuation.state = 'completed';
|
|
1042
|
+
activeContinuation.states.push('completed');
|
|
1043
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-resumed', executionId, continuationId: continuation.id, continuationState }, controller.sensitiveValues)));
|
|
1044
|
+
activeContinuation = undefined;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (status !== runtimeContract.CANONICAL_NATIVE_RUN_STATUS.COMPLETE) {
|
|
1048
|
+
throw new CanonicalNativeHostError('Native module execution failed.', 'PULSE_CANONICAL_NATIVE_EXECUTION_FAILED', { status, errorCode: controller.lastErrorCode(), programCounter: controller.programCounter() });
|
|
1049
|
+
}
|
|
1050
|
+
if (eventMode) {
|
|
1051
|
+
if (options.signal && options.signal.aborted) {
|
|
1052
|
+
throw nativeEventCancelled('Native event execution was cancelled before completion.');
|
|
1053
|
+
}
|
|
1054
|
+
if (controller.resultValue() !== undefined) {
|
|
1055
|
+
throw new CanonicalNativeHostError('Native event handlers must complete with void.', 'PULSE_CANONICAL_NATIVE_EVENT_RESULT_INVALID', {
|
|
1056
|
+
resultType: typeof controller.resultValue()
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const selection = controller.eventSelection();
|
|
1060
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-event-execution-completed', executionId, eventStableId: selection.stableId, eventType: selection.type }, controller.sensitiveValues)));
|
|
1061
|
+
return Object.freeze({
|
|
1062
|
+
kind: 'event',
|
|
1063
|
+
executionId,
|
|
1064
|
+
planHash: controller.plan.planHash,
|
|
1065
|
+
wasmSha256: controller.wasmSha256,
|
|
1066
|
+
event: selection,
|
|
1067
|
+
effectCount,
|
|
1068
|
+
resolutionOrder: Object.freeze([...resolutionOrder]),
|
|
1069
|
+
continuations: Object.freeze(continuations.map((entry) => Object.freeze({ ...entry, states: Object.freeze([...entry.states]) }))),
|
|
1070
|
+
trace: Object.freeze([...controller.trace]),
|
|
1071
|
+
valueHandleCount: controller.heap.size()
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const response = controller.response();
|
|
1075
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-execution-completed', executionId, provider: adapter.id, status: response.status, bodyClass: response.bodyClass }, controller.sensitiveValues)));
|
|
1076
|
+
return Object.freeze({
|
|
1077
|
+
status: 'completed',
|
|
1078
|
+
version: runtimeContract.CANONICAL_NATIVE_HOST_VERSION,
|
|
1079
|
+
abiVersion: runtimeContract.CANONICAL_NATIVE_ABI_VERSION,
|
|
1080
|
+
provider: adapter.id,
|
|
1081
|
+
providerVersion: adapter.version,
|
|
1082
|
+
providerMetadata: adapter.resultMetadata({ ...options, executionId, metadata: controller.plan.canonical, plan: controller.plan }),
|
|
1083
|
+
executionId,
|
|
1084
|
+
planHash: controller.plan.planHash,
|
|
1085
|
+
wasmSha256: controller.wasmSha256,
|
|
1086
|
+
response,
|
|
1087
|
+
effectCount,
|
|
1088
|
+
resolutionOrder: Object.freeze([...resolutionOrder]),
|
|
1089
|
+
continuations: Object.freeze(continuations.map((entry) => Object.freeze({ ...entry, states: Object.freeze([...entry.states]) }))),
|
|
1090
|
+
trace: Object.freeze([...controller.trace]),
|
|
1091
|
+
valueHandleCount: controller.heap.size()
|
|
1092
|
+
});
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
executionFailure = canonicalRuntime.redactRuntimeError(error, controller.sensitiveValues);
|
|
1095
|
+
if (activeContinuation && activeContinuation.state !== 'completed') {
|
|
1096
|
+
activeContinuation.state = 'failed';
|
|
1097
|
+
activeContinuation.states.push('failed');
|
|
1098
|
+
}
|
|
1099
|
+
Object.defineProperty(executionFailure, 'execution', { configurable: true, value: Object.freeze({
|
|
1100
|
+
executionId,
|
|
1101
|
+
planHash: controller.plan.planHash,
|
|
1102
|
+
trace: Object.freeze([...controller.trace]),
|
|
1103
|
+
resolutionOrder: Object.freeze([...resolutionOrder]),
|
|
1104
|
+
continuations: Object.freeze(continuations.map((entry) => Object.freeze({ ...entry, states: Object.freeze([...entry.states]) })))
|
|
1105
|
+
}) });
|
|
1106
|
+
throw executionFailure;
|
|
1107
|
+
} finally {
|
|
1108
|
+
try { adapter.disposeExecution(Object.freeze({ executionId, metadata: controller.plan.canonical, plan: controller.plan, status: executionFailure ? 'failed' : 'completed', error: executionFailure })); }
|
|
1109
|
+
catch (_) { /* provider cleanup must not mask execution */ }
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
async function executeCanonicalNativeModule(compiled, options = {}) {
|
|
1114
|
+
return executeCanonicalNativeInvocation(compiled, options, Object.freeze({ plane: 'http' }));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function nativeEventFailure(error) {
|
|
1118
|
+
const code = error && typeof error.code === 'string' ? error.code : 'PULSE_CANONICAL_NATIVE_EVENT_FAILED';
|
|
1119
|
+
const category = code === 'PULSE_CANONICAL_NATIVE_EVENT_HANDLER_NOT_FOUND'
|
|
1120
|
+
? 'handler-not-found'
|
|
1121
|
+
: code === 'PULSE_CANONICAL_NATIVE_EVENT_CANCELLED'
|
|
1122
|
+
? 'cancelled'
|
|
1123
|
+
: code === 'PULSE_CANONICAL_NATIVE_EVENT_SCHEMA_MISMATCH'
|
|
1124
|
+
? 'schema-mismatch'
|
|
1125
|
+
: code.startsWith('PULSE_SCHEMA_')
|
|
1126
|
+
? 'schema-validation-failed'
|
|
1127
|
+
: code.startsWith('PULSEWASM_EVENTS_')
|
|
1128
|
+
? 'invalid-frame'
|
|
1129
|
+
: code === 'PULSE_CANONICAL_NATIVE_EVENT_RESULT_INVALID'
|
|
1130
|
+
? 'completion-invalid'
|
|
1131
|
+
: 'handler-failed';
|
|
1132
|
+
return Object.freeze({
|
|
1133
|
+
category,
|
|
1134
|
+
name: String(error && error.name || 'Error').slice(0, 256),
|
|
1135
|
+
code: String(code).slice(0, 256),
|
|
1136
|
+
message: String(error && error.message || 'Native event execution failed.').slice(0, eventContract.EVENT_DEFAULT_LIMITS.maxErrorBytes)
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function executeCanonicalNativeEvent(compiled, frame, options = {}) {
|
|
1141
|
+
try {
|
|
1142
|
+
const evidence = await executeCanonicalNativeInvocation(compiled, options, Object.freeze({ plane: 'event', frame }));
|
|
1143
|
+
if (typeof options.onEventExecutionEvidence === 'function') options.onEventExecutionEvidence(evidence);
|
|
1144
|
+
return eventContract.normalizeEventExecutionResult({
|
|
1145
|
+
version: eventContract.EVENT_EXECUTION_RESULT_VERSION,
|
|
1146
|
+
status: eventContract.EVENT_EXECUTION_STATUS.COMPLETED
|
|
1147
|
+
}, options.eventLimits);
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
const evidence = error && error.execution;
|
|
1150
|
+
if (evidence && typeof options.onEventExecutionEvidence === 'function') options.onEventExecutionEvidence(evidence);
|
|
1151
|
+
return eventContract.normalizeEventExecutionResult({
|
|
1152
|
+
version: eventContract.EVENT_EXECUTION_RESULT_VERSION,
|
|
1153
|
+
status: eventContract.EVENT_EXECUTION_STATUS.FAILED,
|
|
1154
|
+
error: nativeEventFailure(error)
|
|
1155
|
+
}, options.eventLimits);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
module.exports = Object.freeze({
|
|
1160
|
+
CANONICAL_NATIVE_HOST_VERSION: runtimeContract.CANONICAL_NATIVE_HOST_VERSION,
|
|
1161
|
+
CANONICAL_NATIVE_ABI_VERSION: runtimeContract.CANONICAL_NATIVE_ABI_VERSION,
|
|
1162
|
+
CANONICAL_NATIVE_RUN_STATUS: runtimeContract.CANONICAL_NATIVE_RUN_STATUS,
|
|
1163
|
+
CANONICAL_NATIVE_ERROR_CODES: runtimeContract.CANONICAL_NATIVE_ERROR_CODES,
|
|
1164
|
+
CanonicalNativeHostError,
|
|
1165
|
+
ValueHeap,
|
|
1166
|
+
createSchemaCodecs,
|
|
1167
|
+
createNativeGuestSourceCryptoVerifier,
|
|
1168
|
+
instantiateCanonicalNativeModule,
|
|
1169
|
+
executeCanonicalNativeModule,
|
|
1170
|
+
executeCanonicalNativeEvent,
|
|
1171
|
+
decodeAssemblyScriptString,
|
|
1172
|
+
redactValue
|
|
1173
|
+
});
|