@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,32 @@
|
|
|
1
|
+
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { createKvProvider, createKvError } = require('./kv-provider.js');
|
|
6
|
+
const RESULT_NONE = 0; const RESULT_TEXT = 1; const RESULT_JSON = 2; const RESULT_BINARY = 3; const RESULT_EMPTY = 4; const RESULT_STREAM = 5;
|
|
7
|
+
function createSetupError(code, message, details) { const err = new Error(message); err.code = code; err.details = details || null; return err; }
|
|
8
|
+
function normalizeHeaders(headers) { if (!headers) return []; if (Array.isArray(headers)) return headers.map((pair) => [String(pair[0]), String(pair[1])]); if (typeof headers[Symbol.iterator] === 'function') return Array.from(headers, ([name, value]) => [String(name), String(value)]); return Object.entries(headers).map(([name, value]) => [String(name), String(value)]); }
|
|
9
|
+
function methodNameToCode(exports, method) { const upper = String(method || '').toUpperCase(); if (upper === 'GET') return exports.pulse_bridge_method_get(); if (upper === 'POST') return exports.pulse_bridge_method_post(); return 0; }
|
|
10
|
+
function createPulseWasmCompiledHostRuntimeWithKv(options = {}) {
|
|
11
|
+
const wasmBytes = options.wasmBytes || (options.wasmPath ? fs.readFileSync(path.resolve(options.wasmPath)) : undefined);
|
|
12
|
+
if (!wasmBytes) throw createSetupError('PULSEWASM_COMPILED_RUNTIME_WASM_REQUIRED', 'createPulseWasmCompiledHostRuntimeWithKv requires wasmBytes or wasmPath.');
|
|
13
|
+
const deploymentPosture = options.deploymentPosture || { engine: 'wasm' };
|
|
14
|
+
if ((deploymentPosture.engine || 'wasm') === 'js' && !options.allowJsEngine) throw createSetupError('PULSEWASM_COMPILED_RUNTIME_ENGINE_MISMATCH', 'compiled-wasm host runtime refuses JS engine posture.', { engine: deploymentPosture.engine });
|
|
15
|
+
const encoder = new TextEncoder(); const decoder = new TextDecoder();
|
|
16
|
+
const module = new WebAssembly.Module(wasmBytes);
|
|
17
|
+
const imports = { env: { abort() { throw new Error('AssemblyScript abort'); } } };
|
|
18
|
+
const instance = new WebAssembly.Instance(module, imports); const exports = instance.exports;
|
|
19
|
+
const hostResponses = new Map();
|
|
20
|
+
for (const [key, value] of Object.entries(options.hostResponses || {})) hostResponses.set(Number(key), value);
|
|
21
|
+
if (!hostResponses.has(77)) hostResponses.set(77, { status: 206, headers: [['Content-Type', 'application/octet-stream'], ['Grip-Channel', 'backend:stream']], bodyStream: { chunks: ['asset:42'] } });
|
|
22
|
+
const kvProvider = options.kvProvider || createKvProvider({ provider: options.runtimeProvider || { kind: 'local', kv: {} } });
|
|
23
|
+
function memory() { return exports.memory; }
|
|
24
|
+
function allocText(text) { const bytes = encoder.encode(String(text ?? '')); const ptr = exports.pulse_alloc(bytes.length || 1); new Uint8Array(memory().buffer, ptr, bytes.length).set(bytes); return { ptr, len: bytes.length }; }
|
|
25
|
+
function readBytes(ptr, len) { if (!ptr || !len) return ''; return decoder.decode(new Uint8Array(memory().buffer, ptr, len)); }
|
|
26
|
+
function readResultHeaders() { const headers = []; const count = exports.pulse_result_header_count ? exports.pulse_result_header_count() : 0; for (let i = 0; i < count; i += 1) headers.push([readBytes(exports.pulse_result_header_name_ptr(i), exports.pulse_result_header_name_len(i)), readBytes(exports.pulse_result_header_value_ptr(i), exports.pulse_result_header_value_len(i)), exports.pulse_result_header_mode ? exports.pulse_result_header_mode(i) : 2]); return headers; }
|
|
27
|
+
function mergeHeaders(base, resultHeaders) { const merged = normalizeHeaders(base); for (const item of resultHeaders || []) { const name = item[0]; const value = item[1]; const mode = Number(item[2] || 2); const lower = String(name).toLowerCase(); if (mode === 1) { for (let i = merged.length - 1; i >= 0; i -= 1) if (String(merged[i][0]).toLowerCase() === lower) merged.splice(i, 1); } merged.push([String(name), String(value)]); } return merged; }
|
|
28
|
+
function decodeResult(status) { const kindCode = exports.pulse_result_kind ? exports.pulse_result_kind() : RESULT_NONE; const resultStatus = exports.pulse_result_status ? exports.pulse_result_status() : status; const resultHeaders = readResultHeaders(); if (kindCode === RESULT_STREAM) { const responseRef = exports.pulse_result_response_ref ? exports.pulse_result_response_ref() : 0; const streamRef = exports.pulse_result_stream_ref ? exports.pulse_result_stream_ref() : 0; const response = hostResponses.get(responseRef) || (streamRef ? { status: resultStatus || status, headers: [], bodyStream: hostResponses.get(streamRef) } : undefined); return { status: resultStatus || (response && response.status) || status, kind: 'stream', kindCode, headers: mergeHeaders(response && response.headers, resultHeaders), bodyStream: response && response.bodyStream, responseRef, streamRef, response, diagnostics: [] }; } const body = exports.pulse_result_body_ptr ? readBytes(exports.pulse_result_body_ptr(), exports.pulse_result_body_len()) : ''; const kind = kindCode === RESULT_JSON ? 'json' : kindCode === RESULT_TEXT ? 'text' : kindCode === RESULT_EMPTY ? 'empty' : 'none'; return { status: resultStatus || status, kind, kindCode, headers: resultHeaders.map(([name, value]) => [name, value]), body: body || undefined, diagnostics: [] }; }
|
|
29
|
+
function executeRequest(input = {}) { const body = allocText(input.bodyText || ''); if (exports.pulse_bridge_set_body_text) exports.pulse_bridge_set_body_text(body.ptr, body.len); const path = allocText(input.path || '/'); const status = exports.pulse_execute_request(methodNameToCode(exports, input.method), path.ptr, path.len); return decodeResult(status); }
|
|
30
|
+
return { version: 'pulsewasm.compiled-wasm-host-runtime-kv.v1', handlerExecutionMode: 'compiled-wasm', provider: kvProvider, kv(name){ return kvProvider.kv(name); }, executeRequest, executeConnect(){ return { event: 'connect', invoked: 0, status: 200 }; }, executeDisconnect(){ return { event: 'disconnect', invoked: 0, status: 200 }; }, close(){}, exports, imports: WebAssembly.Module.imports(module), hostResponses };
|
|
31
|
+
}
|
|
32
|
+
module.exports = { createPulseWasmCompiledHostRuntimeWithKv, RESULT_NONE, RESULT_TEXT, RESULT_JSON, RESULT_BINARY, RESULT_EMPTY, RESULT_STREAM, createKvError };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CONTINUATION_REGISTRY_VERSION = 'pulse.continuation-registry.v1';
|
|
4
|
+
const CONTINUATION_STATES = Object.freeze(['created', 'waiting', 'resumed', 'completed', 'failed', 'expired', 'cancelled']);
|
|
5
|
+
const TERMINAL_STATES = new Set(['completed', 'failed', 'expired', 'cancelled']);
|
|
6
|
+
|
|
7
|
+
class ContinuationRegistryError extends Error {
|
|
8
|
+
constructor(name, code, message, detail = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.detail = Object.freeze({ ...detail });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stateError(record, operation, expected) {
|
|
17
|
+
const detail = { id: record.id, state: record.state, operation, expected };
|
|
18
|
+
if (record.state === 'expired') return new ContinuationRegistryError('ContinuationExpiredError', 'PULSE_CONTINUATION_EXPIRED', `Continuation ${record.id} expired before ${operation}.`, detail);
|
|
19
|
+
if (['resumed', 'completed', 'failed'].includes(record.state) && operation === 'resume') return new ContinuationRegistryError('ContinuationDoubleResumeError', 'PULSE_CONTINUATION_DOUBLE_RESUME', `Continuation ${record.id} cannot be resumed from ${record.state}.`, detail);
|
|
20
|
+
return new ContinuationRegistryError('ContinuationStateError', 'PULSE_CONTINUATION_STATE', `Continuation ${record.id} cannot ${operation} from ${record.state}.`, detail);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cloneRecord(record) {
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
id: record.id,
|
|
26
|
+
branchPoint: record.branchPoint,
|
|
27
|
+
effectIds: Object.freeze([...record.effectIds]),
|
|
28
|
+
state: record.state,
|
|
29
|
+
states: Object.freeze([...record.states]),
|
|
30
|
+
createdAt: record.createdAt,
|
|
31
|
+
expiresAt: record.expiresAt,
|
|
32
|
+
resumedAt: record.resumedAt,
|
|
33
|
+
completedAt: record.completedAt,
|
|
34
|
+
failedAt: record.failedAt,
|
|
35
|
+
expiredAt: record.expiredAt,
|
|
36
|
+
cancelledAt: record.cancelledAt,
|
|
37
|
+
resumeCount: record.resumeCount,
|
|
38
|
+
error: record.error
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createContinuationRegistry(options = {}) {
|
|
43
|
+
const clock = typeof options.clock === 'function' ? options.clock : () => Date.now();
|
|
44
|
+
const defaultTtlMs = Number.isFinite(Number(options.ttlMs)) ? Math.max(1, Number(options.ttlMs)) : 30000;
|
|
45
|
+
const records = new Map();
|
|
46
|
+
const trace = [];
|
|
47
|
+
let sequence = 0;
|
|
48
|
+
|
|
49
|
+
function event(record, type, detail = {}) {
|
|
50
|
+
trace.push(Object.freeze({
|
|
51
|
+
sequence: trace.length + 1,
|
|
52
|
+
type,
|
|
53
|
+
id: record.id,
|
|
54
|
+
branchPoint: record.branchPoint,
|
|
55
|
+
state: record.state,
|
|
56
|
+
at: clock(),
|
|
57
|
+
detail: Object.freeze({ ...detail })
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requireRecord(id) {
|
|
62
|
+
const record = records.get(String(id));
|
|
63
|
+
if (!record) throw new ContinuationRegistryError('ContinuationNotFoundError', 'PULSE_CONTINUATION_NOT_FOUND', `Unknown continuation ${id}.`, { id: String(id) });
|
|
64
|
+
return record;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function transition(record, state, at, extra = {}) {
|
|
68
|
+
record.state = state;
|
|
69
|
+
record.states.push(state);
|
|
70
|
+
Object.assign(record, extra);
|
|
71
|
+
event(record, state, extra);
|
|
72
|
+
return cloneRecord(record);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function create(input = {}) {
|
|
76
|
+
sequence += 1;
|
|
77
|
+
const now = clock();
|
|
78
|
+
const id = String(input.id || `continuation-${sequence}`);
|
|
79
|
+
if (records.has(id)) throw new ContinuationRegistryError('ContinuationDuplicateError', 'PULSE_CONTINUATION_DUPLICATE', `Continuation ${id} already exists.`, { id });
|
|
80
|
+
const ttlMs = Number.isFinite(Number(input.ttlMs)) ? Math.max(1, Number(input.ttlMs)) : defaultTtlMs;
|
|
81
|
+
const record = {
|
|
82
|
+
id,
|
|
83
|
+
branchPoint: String(input.branchPoint || id),
|
|
84
|
+
effectIds: Array.isArray(input.effectIds) ? input.effectIds.map(String) : [],
|
|
85
|
+
state: 'created',
|
|
86
|
+
states: ['created'],
|
|
87
|
+
createdAt: now,
|
|
88
|
+
expiresAt: now + ttlMs,
|
|
89
|
+
resumeCount: 0,
|
|
90
|
+
error: undefined
|
|
91
|
+
};
|
|
92
|
+
records.set(id, record);
|
|
93
|
+
event(record, 'created');
|
|
94
|
+
return cloneRecord(record);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function wait(id) {
|
|
98
|
+
const record = requireRecord(id);
|
|
99
|
+
if (record.state !== 'created') throw stateError(record, 'wait', 'created');
|
|
100
|
+
return transition(record, 'waiting', clock());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resume(id) {
|
|
104
|
+
const record = requireRecord(id);
|
|
105
|
+
const now = clock();
|
|
106
|
+
if (record.state === 'waiting' && now >= record.expiresAt) {
|
|
107
|
+
transition(record, 'expired', now, { expiredAt: now });
|
|
108
|
+
throw stateError(record, 'resume', 'waiting');
|
|
109
|
+
}
|
|
110
|
+
if (record.state !== 'waiting') throw stateError(record, 'resume', 'waiting');
|
|
111
|
+
record.resumeCount += 1;
|
|
112
|
+
return transition(record, 'resumed', now, { resumedAt: now });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function complete(id) {
|
|
116
|
+
const record = requireRecord(id);
|
|
117
|
+
if (record.state !== 'resumed') throw stateError(record, 'complete', 'resumed');
|
|
118
|
+
const now = clock();
|
|
119
|
+
return transition(record, 'completed', now, { completedAt: now });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function fail(id, error) {
|
|
123
|
+
const record = requireRecord(id);
|
|
124
|
+
if (!['created', 'waiting', 'resumed'].includes(record.state)) throw stateError(record, 'fail', 'created|waiting|resumed');
|
|
125
|
+
const now = clock();
|
|
126
|
+
const errorName = error && (error.name || error.code || error.message) ? String(error.name || error.code || error.message) : String(error || 'ContinuationFailure');
|
|
127
|
+
return transition(record, 'failed', now, { failedAt: now, error: errorName });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function expire(id) {
|
|
131
|
+
const record = requireRecord(id);
|
|
132
|
+
if (TERMINAL_STATES.has(record.state) || record.state === 'resumed') throw stateError(record, 'expire', 'created|waiting');
|
|
133
|
+
const now = clock();
|
|
134
|
+
return transition(record, 'expired', now, { expiredAt: now });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cancel(id) {
|
|
138
|
+
const record = requireRecord(id);
|
|
139
|
+
if (TERMINAL_STATES.has(record.state)) throw stateError(record, 'cancel', 'created|waiting|resumed');
|
|
140
|
+
const now = clock();
|
|
141
|
+
return transition(record, 'cancelled', now, { cancelledAt: now });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sweepExpired(now = clock()) {
|
|
145
|
+
const expired = [];
|
|
146
|
+
for (const record of records.values()) {
|
|
147
|
+
if (['created', 'waiting'].includes(record.state) && now >= record.expiresAt) {
|
|
148
|
+
transition(record, 'expired', now, { expiredAt: now });
|
|
149
|
+
expired.push(record.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return Object.freeze(expired);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function get(id) { return cloneRecord(requireRecord(id)); }
|
|
156
|
+
function list() { return Object.freeze([...records.values()].map(cloneRecord)); }
|
|
157
|
+
function traceSnapshot() { return Object.freeze([...trace]); }
|
|
158
|
+
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
version: CONTINUATION_REGISTRY_VERSION,
|
|
161
|
+
states: CONTINUATION_STATES,
|
|
162
|
+
create,
|
|
163
|
+
wait,
|
|
164
|
+
resume,
|
|
165
|
+
complete,
|
|
166
|
+
fail,
|
|
167
|
+
expire,
|
|
168
|
+
cancel,
|
|
169
|
+
sweepExpired,
|
|
170
|
+
get,
|
|
171
|
+
list,
|
|
172
|
+
trace: traceSnapshot
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
CONTINUATION_REGISTRY_VERSION,
|
|
178
|
+
CONTINUATION_STATES,
|
|
179
|
+
ContinuationRegistryError,
|
|
180
|
+
createContinuationRegistry
|
|
181
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const compiledWasmHostRuntimeKv = require('./compiled-wasm-host-runtime-kv.js');
|
|
4
|
+
const kvProvider = require('./kv-provider.js');
|
|
5
|
+
const continuationRegistry = require('./continuation-registry.js');
|
|
6
|
+
const canonicalApiRuntime = require('./canonical-api-runtime.js');
|
|
7
|
+
const canonicalNativeHost = require('./canonical-native-host.js');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
compiledWasmHostRuntimeKv,
|
|
11
|
+
kvProvider,
|
|
12
|
+
continuationRegistry,
|
|
13
|
+
canonicalApiRuntime,
|
|
14
|
+
canonicalNativeHost,
|
|
15
|
+
...canonicalApiRuntime,
|
|
16
|
+
...canonicalNativeHost,
|
|
17
|
+
...continuationRegistry,
|
|
18
|
+
createPulseWasmCompiledHostRuntimeWithKv: compiledWasmHostRuntimeKv.createPulseWasmCompiledHostRuntimeWithKv,
|
|
19
|
+
createKvProvider: kvProvider.createKvProvider,
|
|
20
|
+
createKvError: kvProvider.createKvError,
|
|
21
|
+
createNamespaceStore: kvProvider.createNamespaceStore
|
|
22
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createKvError(code, message, details) {
|
|
4
|
+
const err = new Error(message);
|
|
5
|
+
err.code = code;
|
|
6
|
+
err.details = details || null;
|
|
7
|
+
return err;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createNamespaceStore(namespace, map) {
|
|
11
|
+
return {
|
|
12
|
+
namespace,
|
|
13
|
+
getText(key) {
|
|
14
|
+
const normalized = String(key);
|
|
15
|
+
return map.has(normalized) ? String(map.get(normalized)) : null;
|
|
16
|
+
},
|
|
17
|
+
setText(key, value) {
|
|
18
|
+
map.set(String(key), String(value));
|
|
19
|
+
return { ok: true };
|
|
20
|
+
},
|
|
21
|
+
delete(key) {
|
|
22
|
+
const existed = map.delete(String(key));
|
|
23
|
+
return { ok: true, existed };
|
|
24
|
+
},
|
|
25
|
+
has(key) {
|
|
26
|
+
return map.has(String(key));
|
|
27
|
+
},
|
|
28
|
+
entries() {
|
|
29
|
+
return Array.from(map.entries());
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createKvProvider(options = {}) {
|
|
35
|
+
const provider = options.provider || options.runtimeProvider || { kind: 'local', kv: {} };
|
|
36
|
+
const stores = new Map();
|
|
37
|
+
const configured = provider && provider.kv ? provider.kv : {};
|
|
38
|
+
const seed = options.seed || {};
|
|
39
|
+
for (const [logicalName, physicalName] of Object.entries(configured)) {
|
|
40
|
+
const initial = new Map(Object.entries(seed[logicalName] || {}));
|
|
41
|
+
stores.set(String(logicalName), createNamespaceStore(String(physicalName), initial));
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: provider.kind || 'unknown',
|
|
45
|
+
configuredNamespaces: Array.from(stores.keys()),
|
|
46
|
+
kv(name) {
|
|
47
|
+
const key = String(name);
|
|
48
|
+
if (!stores.has(key)) {
|
|
49
|
+
throw createKvError('PULSEWASM_KV_NAMESPACE_UNKNOWN', `Unknown KV namespace: ${key}`, { namespace: key, configured: Array.from(stores.keys()) });
|
|
50
|
+
}
|
|
51
|
+
return stores.get(key);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { createKvProvider, createKvError, createNamespaceStore };
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION,
|
|
5
|
+
CRYPTO_ES256_GUEST_LINKED_REALIZATION,
|
|
6
|
+
CRYPTO_GUEST_SOURCE_IMPLEMENTATION,
|
|
7
|
+
CRYPTO_REALIZATION_PLAN_VERSION
|
|
8
|
+
} = require('@pulse-compute/wasm-contracts/crypto/contracts');
|
|
9
|
+
|
|
10
|
+
const NATIVE_CRYPTO_VERIFIER_VERSION = 'pulse.native-crypto-verifier.v1';
|
|
11
|
+
const HS256_REALIZATION = 'guest-source:pulse-hmac-as';
|
|
12
|
+
const HS256_EXPORT = 'pulse_crypto_hs256_verify';
|
|
13
|
+
const ES256_EXPORT = 'pulse_crypto_es256_verify';
|
|
14
|
+
const ES256_REALIZATION = CRYPTO_ES256_GUEST_LINKED_REALIZATION;
|
|
15
|
+
const PAGE_BYTES = 64 * 1024;
|
|
16
|
+
const GUARD_BYTES = PAGE_BYTES;
|
|
17
|
+
const KEY_BYTES_MINIMUM = 32;
|
|
18
|
+
const KEY_BYTES_MAXIMUM = 4 * 1024;
|
|
19
|
+
const DATA_BYTES_MAXIMUM = 1024 * 1024;
|
|
20
|
+
const TAG_BYTES = 32;
|
|
21
|
+
const MAX_SIGNED_POINTER = 0x7fffffff;
|
|
22
|
+
const ES256_MEMORY_BYTES = 32 * PAGE_BYTES;
|
|
23
|
+
const ES256_FRAME_POINTER = 8 * PAGE_BYTES;
|
|
24
|
+
const ES256_FRAME_CAPACITY = 16_640;
|
|
25
|
+
const ES256_FRAME_MAGIC = 0x3253_4550;
|
|
26
|
+
const ES256_FRAME_HEADER_BYTES = 64;
|
|
27
|
+
const ES256_KEY_BYTES = 64;
|
|
28
|
+
const ES256_SIGNATURE_BYTES = 64;
|
|
29
|
+
const ES256_DATA_BYTES_MAXIMUM = 16_340;
|
|
30
|
+
|
|
31
|
+
const RESULT_BY_CODE = Object.freeze({
|
|
32
|
+
1: 'valid',
|
|
33
|
+
0: 'invalid-authenticator',
|
|
34
|
+
'-1': 'invalid-key',
|
|
35
|
+
'-2': 'invalid-input',
|
|
36
|
+
'-3': 'realization-failure'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function result(status) {
|
|
40
|
+
return Object.freeze({ status });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ownData(value, name) {
|
|
44
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
45
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
46
|
+
return descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
47
|
+
? descriptor.value
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function bytes(value) {
|
|
52
|
+
return value instanceof Uint8Array ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function selectedAlgorithm(plan, algorithm, realization, kind, implementation) {
|
|
56
|
+
if (
|
|
57
|
+
!plan
|
|
58
|
+
|| typeof plan !== 'object'
|
|
59
|
+
|| !plan.crypto
|
|
60
|
+
|| plan.crypto.version !== CRYPTO_REALIZATION_PLAN_VERSION
|
|
61
|
+
|| plan.crypto.target !== 'native'
|
|
62
|
+
|| plan.crypto.automaticFallback !== false
|
|
63
|
+
|| !Array.isArray(plan.crypto.algorithms)
|
|
64
|
+
) return undefined;
|
|
65
|
+
const matches = plan.crypto.algorithms.filter((entry) => (
|
|
66
|
+
entry
|
|
67
|
+
&& entry.algorithm === algorithm
|
|
68
|
+
&& entry.realization === realization
|
|
69
|
+
&& entry.kind === kind
|
|
70
|
+
&& entry.implementation === implementation
|
|
71
|
+
&& entry.automaticFallback === false
|
|
72
|
+
&& entry.targetImplemented === true
|
|
73
|
+
));
|
|
74
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function selectedHs256(plan) {
|
|
78
|
+
return selectedAlgorithm(
|
|
79
|
+
plan,
|
|
80
|
+
'HS256',
|
|
81
|
+
HS256_REALIZATION,
|
|
82
|
+
'guest-source',
|
|
83
|
+
CRYPTO_GUEST_SOURCE_IMPLEMENTATION
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function selectedEs256(plan) {
|
|
88
|
+
return selectedAlgorithm(
|
|
89
|
+
plan,
|
|
90
|
+
'ES256',
|
|
91
|
+
ES256_REALIZATION,
|
|
92
|
+
'guest-linked',
|
|
93
|
+
CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeRequest(request) {
|
|
98
|
+
const algorithm = ownData(request, 'algorithm');
|
|
99
|
+
const key = ownData(request, 'key');
|
|
100
|
+
const keyType = ownData(key, 'type');
|
|
101
|
+
const keyBytes = bytes(ownData(key, 'bytes'));
|
|
102
|
+
const data = bytes(ownData(request, 'data'));
|
|
103
|
+
const tag = bytes(ownData(request, 'tag'));
|
|
104
|
+
if (
|
|
105
|
+
algorithm !== 'HS256'
|
|
106
|
+
|| keyType !== 'hmac-key-bytes'
|
|
107
|
+
|| !keyBytes
|
|
108
|
+
|| !data
|
|
109
|
+
|| !tag
|
|
110
|
+
) return Object.freeze({ result: result('invalid-input') });
|
|
111
|
+
if (
|
|
112
|
+
keyBytes.byteLength < KEY_BYTES_MINIMUM
|
|
113
|
+
|| keyBytes.byteLength > KEY_BYTES_MAXIMUM
|
|
114
|
+
) return Object.freeze({ result: result('invalid-key') });
|
|
115
|
+
if (
|
|
116
|
+
data.byteLength > DATA_BYTES_MAXIMUM
|
|
117
|
+
|| tag.byteLength !== TAG_BYTES
|
|
118
|
+
) return Object.freeze({ result: result('invalid-input') });
|
|
119
|
+
return Object.freeze({ keyBytes, data, tag });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeSignatureRequest(request) {
|
|
123
|
+
const algorithm = ownData(request, 'algorithm');
|
|
124
|
+
const key = ownData(request, 'key');
|
|
125
|
+
const keyType = ownData(key, 'type');
|
|
126
|
+
const keyBytes = bytes(ownData(key, 'bytes'));
|
|
127
|
+
const data = bytes(ownData(request, 'data'));
|
|
128
|
+
const signature = bytes(ownData(request, 'signature'));
|
|
129
|
+
if (
|
|
130
|
+
algorithm !== 'ES256'
|
|
131
|
+
|| keyType !== 'p256-public-key-bytes'
|
|
132
|
+
|| !keyBytes
|
|
133
|
+
|| !data
|
|
134
|
+
|| !signature
|
|
135
|
+
) return Object.freeze({ result: result('invalid-input') });
|
|
136
|
+
if (keyBytes.byteLength !== ES256_KEY_BYTES) {
|
|
137
|
+
return Object.freeze({ result: result('invalid-key') });
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
data.byteLength > ES256_DATA_BYTES_MAXIMUM
|
|
141
|
+
|| signature.byteLength !== ES256_SIGNATURE_BYTES
|
|
142
|
+
) return Object.freeze({ result: result('invalid-input') });
|
|
143
|
+
return Object.freeze({ keyBytes, data, signature });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stage(memory, inputs) {
|
|
147
|
+
const payloadBytes = inputs.reduce((total, input) => total + input.byteLength, 0);
|
|
148
|
+
const oldEnd = memory.buffer.byteLength;
|
|
149
|
+
const required = GUARD_BYTES + payloadBytes + 48;
|
|
150
|
+
const pages = Math.ceil(required / PAGE_BYTES);
|
|
151
|
+
const newEnd = oldEnd + pages * PAGE_BYTES;
|
|
152
|
+
if (
|
|
153
|
+
!Number.isSafeInteger(newEnd)
|
|
154
|
+
|| newEnd > MAX_SIGNED_POINTER
|
|
155
|
+
|| memory.grow(pages) < 0
|
|
156
|
+
) return undefined;
|
|
157
|
+
|
|
158
|
+
const view = new Uint8Array(memory.buffer);
|
|
159
|
+
const pointers = [];
|
|
160
|
+
let cursor = oldEnd + GUARD_BYTES;
|
|
161
|
+
for (const input of inputs) {
|
|
162
|
+
cursor = (cursor + 15) & ~15;
|
|
163
|
+
pointers.push(cursor);
|
|
164
|
+
view.set(input, cursor);
|
|
165
|
+
cursor += input.byteLength;
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
pointers: Object.freeze(pointers),
|
|
169
|
+
wipe() {
|
|
170
|
+
new Uint8Array(memory.buffer).fill(0, oldEnd + GUARD_BYTES, cursor);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function align16(value) {
|
|
176
|
+
return (value + 15) & ~15;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function stageEs256Frame(memory, normalized) {
|
|
180
|
+
if (
|
|
181
|
+
!(memory instanceof WebAssembly.Memory)
|
|
182
|
+
|| memory.buffer.byteLength !== ES256_MEMORY_BYTES
|
|
183
|
+
) return undefined;
|
|
184
|
+
const totalLength = align16(192 + normalized.data.byteLength);
|
|
185
|
+
if (totalLength > ES256_FRAME_CAPACITY) return undefined;
|
|
186
|
+
const frame = new Uint8Array(
|
|
187
|
+
memory.buffer,
|
|
188
|
+
ES256_FRAME_POINTER,
|
|
189
|
+
ES256_FRAME_CAPACITY
|
|
190
|
+
);
|
|
191
|
+
frame.fill(0);
|
|
192
|
+
const words = new DataView(
|
|
193
|
+
memory.buffer,
|
|
194
|
+
ES256_FRAME_POINTER,
|
|
195
|
+
ES256_FRAME_HEADER_BYTES
|
|
196
|
+
);
|
|
197
|
+
const fields = [
|
|
198
|
+
[0, ES256_FRAME_MAGIC],
|
|
199
|
+
[4, 2],
|
|
200
|
+
[8, ES256_FRAME_HEADER_BYTES],
|
|
201
|
+
[12, totalLength],
|
|
202
|
+
[16, 1],
|
|
203
|
+
[20, 0],
|
|
204
|
+
[24, 192],
|
|
205
|
+
[28, normalized.data.byteLength],
|
|
206
|
+
[32, 64],
|
|
207
|
+
[36, ES256_KEY_BYTES],
|
|
208
|
+
[40, 128],
|
|
209
|
+
[44, ES256_SIGNATURE_BYTES]
|
|
210
|
+
];
|
|
211
|
+
for (const [offset, value] of fields) words.setUint32(offset, value, true);
|
|
212
|
+
frame.set(normalized.keyBytes, 64);
|
|
213
|
+
frame.set(normalized.signature, 128);
|
|
214
|
+
frame.set(normalized.data, 192);
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
pointer: ES256_FRAME_POINTER,
|
|
217
|
+
capacity: ES256_FRAME_CAPACITY,
|
|
218
|
+
wipe() {
|
|
219
|
+
new Uint8Array(
|
|
220
|
+
memory.buffer,
|
|
221
|
+
ES256_FRAME_POINTER,
|
|
222
|
+
ES256_FRAME_CAPACITY
|
|
223
|
+
).fill(0);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createNativeGuestSourceCryptoVerifier(input = {}) {
|
|
229
|
+
const plan = input.plan;
|
|
230
|
+
const moduleExports = input.exports;
|
|
231
|
+
const selected = selectedHs256(plan);
|
|
232
|
+
const selectedSignature = selectedEs256(plan);
|
|
233
|
+
const memory = moduleExports && moduleExports.memory;
|
|
234
|
+
const verifyHs256 = moduleExports && moduleExports[HS256_EXPORT];
|
|
235
|
+
const verifyEs256 = moduleExports && moduleExports[ES256_EXPORT];
|
|
236
|
+
const hs256Available = Boolean(
|
|
237
|
+
selected
|
|
238
|
+
&& memory instanceof WebAssembly.Memory
|
|
239
|
+
&& typeof verifyHs256 === 'function'
|
|
240
|
+
);
|
|
241
|
+
const es256Available = Boolean(
|
|
242
|
+
selectedSignature
|
|
243
|
+
&& memory instanceof WebAssembly.Memory
|
|
244
|
+
&& memory.buffer.byteLength === ES256_MEMORY_BYTES
|
|
245
|
+
&& typeof verifyEs256 === 'function'
|
|
246
|
+
);
|
|
247
|
+
const selectedRealizations = Object.freeze([
|
|
248
|
+
...(selected ? [Object.freeze({
|
|
249
|
+
algorithm: 'HS256',
|
|
250
|
+
realization: HS256_REALIZATION,
|
|
251
|
+
implementation: CRYPTO_GUEST_SOURCE_IMPLEMENTATION,
|
|
252
|
+
guestUnitRequired: false,
|
|
253
|
+
available: hs256Available
|
|
254
|
+
})] : []),
|
|
255
|
+
...(selectedSignature ? [Object.freeze({
|
|
256
|
+
algorithm: 'ES256',
|
|
257
|
+
realization: ES256_REALIZATION,
|
|
258
|
+
implementation: CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION,
|
|
259
|
+
guestUnitRequired: true,
|
|
260
|
+
available: es256Available
|
|
261
|
+
})] : [])
|
|
262
|
+
]);
|
|
263
|
+
const realization = Object.freeze({
|
|
264
|
+
version: NATIVE_CRYPTO_VERIFIER_VERSION,
|
|
265
|
+
semanticOwner: '@pulse-compute/crypto',
|
|
266
|
+
automaticFallback: false,
|
|
267
|
+
algorithms: selectedRealizations,
|
|
268
|
+
available: selectedRealizations.length > 0
|
|
269
|
+
&& selectedRealizations.every((entry) => entry.available),
|
|
270
|
+
...(selectedRealizations.length === 1 ? selectedRealizations[0] : {})
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const verifier = Object.freeze({
|
|
274
|
+
mac: Object.freeze({
|
|
275
|
+
verify(request) {
|
|
276
|
+
const normalized = normalizeRequest(request);
|
|
277
|
+
if (normalized.result) return normalized.result;
|
|
278
|
+
if (!hs256Available) return result('realization-failure');
|
|
279
|
+
let staged;
|
|
280
|
+
try {
|
|
281
|
+
staged = stage(memory, [
|
|
282
|
+
normalized.keyBytes,
|
|
283
|
+
normalized.data,
|
|
284
|
+
normalized.tag
|
|
285
|
+
]);
|
|
286
|
+
if (!staged) return result('realization-failure');
|
|
287
|
+
const [keyPointer, dataPointer, tagPointer] = staged.pointers;
|
|
288
|
+
const code = verifyHs256(
|
|
289
|
+
keyPointer,
|
|
290
|
+
normalized.keyBytes.byteLength,
|
|
291
|
+
dataPointer,
|
|
292
|
+
normalized.data.byteLength,
|
|
293
|
+
tagPointer,
|
|
294
|
+
normalized.tag.byteLength
|
|
295
|
+
);
|
|
296
|
+
return result(RESULT_BY_CODE[String(code)] || 'realization-failure');
|
|
297
|
+
} catch {
|
|
298
|
+
return result('realization-failure');
|
|
299
|
+
} finally {
|
|
300
|
+
if (staged) staged.wipe();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}),
|
|
304
|
+
signature: Object.freeze({
|
|
305
|
+
verify(request) {
|
|
306
|
+
const normalized = normalizeSignatureRequest(request);
|
|
307
|
+
if (normalized.result) return normalized.result;
|
|
308
|
+
if (!es256Available) return result('realization-failure');
|
|
309
|
+
let staged;
|
|
310
|
+
try {
|
|
311
|
+
staged = stageEs256Frame(memory, normalized);
|
|
312
|
+
if (!staged) return result('realization-failure');
|
|
313
|
+
const code = verifyEs256(staged.pointer, staged.capacity);
|
|
314
|
+
return result(RESULT_BY_CODE[String(code)] || 'realization-failure');
|
|
315
|
+
} catch {
|
|
316
|
+
return result('realization-failure');
|
|
317
|
+
} finally {
|
|
318
|
+
if (staged) staged.wipe();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return Object.freeze({ verifier, realization });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = Object.freeze({
|
|
328
|
+
NATIVE_CRYPTO_VERIFIER_VERSION,
|
|
329
|
+
createNativeGuestSourceCryptoVerifier
|
|
330
|
+
});
|