Add authenticated protocol 2 and TypeScript WASM client
This commit is contained in:
1 parent
88ceeb2ae9
commit
237d404bcd
52 files changed
+3249
-150
No files matched your search
@@ -0,0 +1,5 @@
|
||||
.gradle/
|
||||
.kotlin/
|
||||
**/build/
|
||||
node_modules/
|
||||
baseline-browser.min.js
|
||||
@@ -0,0 +1,147 @@
|
||||
# Kotlin compatibility fixture and size baseline
|
||||
|
||||
This fixture uses a Gradle composite build to consume Kiloparsec source from
|
||||
the repository root (`../..`), with explicit dependency substitution. The JVM fixture uses
|
||||
`setupWebsocketServer` and `KiloInterface` unchanged.
|
||||
|
||||
## Build and start the reference server
|
||||
|
||||
From the TypeScript module directory (requires JDK 21 and the parent build's Android SDK configuration):
|
||||
|
||||
```sh
|
||||
npm run fixture:build
|
||||
npm run baseline:build
|
||||
JAVA_HOME=/path/to/jdk21 evaluation/server/build/install/server/bin/server
|
||||
```
|
||||
|
||||
Default endpoint: `ws://127.0.0.1:18732/kp`; optional first argument overrides port.
|
||||
The server binds localhost only. Its deterministic Ed25519 **TEST-ONLY** seed is
|
||||
bytes 0 through 31. Public key:
|
||||
|
||||
```text
|
||||
03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8
|
||||
```
|
||||
|
||||
Commands use ordinary Kotlin serialization/BiPack:
|
||||
|
||||
| Command | Arguments | Result / behavior |
|
||||
|---|---|---|
|
||||
| `echo` | String | same String |
|
||||
| `sum` | List<Int> | Int sum |
|
||||
| `identity` | Unit | authenticated remote public key raw lowercase hex, or `anonymous` |
|
||||
| `callback` | String | server calls `clientEcho(String): String`, returns its result |
|
||||
| `push` | String | server calls `notice(String): Unit`, waits for acknowledgment |
|
||||
| `fail` | String | throws registered `IllegalArgumentException` with that message |
|
||||
|
||||
The fixture supports anonymous clients to allow explicit coverage of identity
|
||||
presence/absence; a production application's authorization policy must decide
|
||||
which identities may invoke its methods.
|
||||
|
||||
## Optimized Kotlin/JS baseline
|
||||
|
||||
`baseline` exports a usable `BaselineClient` accepting binary block callbacks.
|
||||
It retains full DH, signed identity, expected server identity checking and RPC,
|
||||
without retaining the Ktor WebSocket client intentionally. This is the smaller
|
||||
bridge option rather than a deliberately inflated full library bundle.
|
||||
|
||||
```sh
|
||||
npm ci --prefix evaluation --ignore-scripts
|
||||
node evaluation/baseline-smoke.cjs
|
||||
node_modules/.bin/esbuild evaluation/baseline/build/compileSync/js/main/productionExecutable/kotlin/kiloparsec-evaluation-baseline.js --bundle --minify --platform=browser --external:fs --external:path --external:crypto --outfile=evaluation/baseline-browser.min.js
|
||||
```
|
||||
|
||||
`fs`, `path`, `crypto` are libsodium's conditional Node imports, unused in browser
|
||||
execution. Bundle includes libsodium-sumo WASM payload. Source maps are excluded.
|
||||
Kotlin production compiler performs dead-code elimination, followed by esbuild
|
||||
minification. npm explicitly pins sodium wrapper 0.7.15 for comparison with TS.
|
||||
|
||||
For the common comparison, use the same **Node CJS** platform for both libraries:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
node scripts/measure.mjs
|
||||
node evaluation/baseline-smoke.cjs --bundled
|
||||
```
|
||||
|
||||
Results are saved in `evaluation/build/measurement/results.json`. A recorded run
|
||||
is preserved in [measurements.json](measurements.json). The script performs five
|
||||
fresh subprocess measurements, from immediately before module loading through
|
||||
WASM initialization and generation of one Ed25519 identity. It does not include
|
||||
process startup. RSS includes Node; RSS delta is resident growth during workload.
|
||||
|
||||
Historical measurements of the pre-hardening published 0.7.2 baseline,
|
||||
2026-10-18 on Node 24.19.0, Linux x64 (not regenerated for current source):
|
||||
|
||||
| Measure | TS full exports | Kotlin minimal bridge |
|
||||
|---|---:|---:|
|
||||
| Minified self-contained Node bundle, bytes | 1,124,612 | 1,982,755 |
|
||||
| Gzip level 9, bytes | 330,101 | 559,003 |
|
||||
| Cold init + identity median, ms | 88.59 | 166.50 |
|
||||
| Resident memory median, bytes | 62,898,176 | 70,574,080 |
|
||||
| Resident growth median, bytes | 19,300,352 | 26,976,256 |
|
||||
|
||||
The Kotlin baseline exposes only echo and the bridge plus the measurement identity
|
||||
helper; TS includes all exported API. This is not a claim of full feature parity.
|
||||
Both include sodium's WASM payload. Both actual standalone bundles initialize
|
||||
successfully; the measured Kotlin bundle additionally passed authenticated,
|
||||
pinned-server echo to the JVM fixture. Measurements vary by host and revision.
|
||||
|
||||
The Kotlin sodium bindings eagerly capture imported functions: await
|
||||
`require('libsodium-wrappers-sumo').ready` **before loading the compiled bridge**,
|
||||
as shown in the smoke script. Loading the bridge first produces `_sodium_init is
|
||||
not a function` with these published dependencies. That observation concerns the historical published baseline.
|
||||
The browser bundle's size was measured; browser execution is not claimed by the
|
||||
Node smoke check. The bridge is an evaluation fixture, not a polished production
|
||||
wrapper (bounded buffering, lifecycle error propagation, and key ownership would
|
||||
need finishing if this alternative were selected).
|
||||
|
||||
## Protocol 2 validation and measurements
|
||||
|
||||
Current-source protocol 2 results are in [measurements-v2.json](measurements-v2.json).
|
||||
TS: 1,126,081 bytes / gzip330,527; Kotlin baseline: 1,993,070 / gzip561,965.
|
||||
Both measured bundles completed pinned-server echo against the local protocol2
|
||||
fixture. Historical measurements above remain unchanged for traceability.
|
||||
The full root verification command passed 43 JVM tests, 28 Kotlin/JS Node tests,
|
||||
35 TS unit/security tests, and the real TS/JVM interoperability scenario.
|
||||
|
||||
## Optimized Kotlin/Wasm comparison
|
||||
|
||||
`wasm-baseline` retains the same full handshake, server key pin, fresh client
|
||||
identity and echo RPC as the JS baseline. It exports a small string/callback
|
||||
bridge, avoiding non-exportable Kotlin objects at the JS/Wasm boundary.
|
||||
|
||||
From the TypeScript module directory:
|
||||
|
||||
```sh
|
||||
npm run wasm:build
|
||||
npm ci --prefix evaluation --ignore-scripts
|
||||
npm run measure:wasm
|
||||
npm run fixture:build
|
||||
npm run test:wasm-baseline
|
||||
```
|
||||
|
||||
The measurement uses the **optimized production executable** `.wasm` emitted by
|
||||
Binaryen, together with an esbuild-minified ESM loader containing sodium and joda.
|
||||
The raw `.klib`, unoptimized compiler output, maps and test runtime are excluded.
|
||||
Both the raw and wrapper sodium imports share the same CommonJS module instance,
|
||||
so the crypto payload is counted once. The wrapper is pinned to 0.7.15; its locked
|
||||
transitive raw sodium dependency is 0.7.16, as in the JS/TS comparison.
|
||||
|
||||
The Kotlin loader must receive the sodium modules after `sodium.ready`. The small
|
||||
`wasm-entry.mjs` adapter does this, supplies the expected default/namespace shapes,
|
||||
and includes Node CommonJS helpers for the bundled sodium dependency. It does not
|
||||
change the protocol or cryptography. The optimized asset stays beside `client.mjs`.
|
||||
|
||||
Recorded Node/Linux results are in [wasm-measurements.json](wasm-measurements.json).
|
||||
The runnable distribution consists of two assets:
|
||||
|
||||
| Asset | Bytes | Gzip level 9 bytes |
|
||||
|---|---:|---:|
|
||||
| Minified loader, sodium and joda | 1,319,098 | 368,938 |
|
||||
| Optimized Kotlin executable | 764,584 | 247,073 |
|
||||
| Total | 2,083,682 | 616,011 |
|
||||
|
||||
Gzip is applied separately to each transferred asset and the sizes are summed.
|
||||
The distribution passed initialization, Ed25519 identity generation, full DH,
|
||||
server pinning and authenticated echo against the local protocol-2 JVM fixture.
|
||||
This qualifies the comparison on Node; no browser runtime result is claimed.
|
||||
@@ -0,0 +1,5 @@
|
||||
// Kotlin binding imports must execute after sodium resolves its WASM functions.
|
||||
exports.load = async function load() {
|
||||
await require('libsodium-wrappers-sumo').ready;
|
||||
return require('./baseline/build/compileSync/js/main/productionExecutable/kotlin/kiloparsec-evaluation-baseline.js');
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// Check that the measured baseline is a usable full client, not dead-code-only output.
|
||||
const { WebSocket } = require('ws');
|
||||
async function main() {
|
||||
if (!process.argv.includes('--bundled')) await require('libsodium-wrappers-sumo').ready;
|
||||
const { BaselineClient } = process.argv.includes('--bundled')
|
||||
? await require('./build/measurement/kotlin.cjs').load()
|
||||
: require('./baseline/build/compileSync/js/main/productionExecutable/kotlin/kiloparsec-evaluation-baseline.js');
|
||||
const socket = new WebSocket(process.env.KILOPARSEC_TEST_URL ?? 'ws://127.0.0.1:18732/kp');
|
||||
const client = new BaselineClient(bytes => socket.send(Buffer.from(bytes)));
|
||||
socket.on('message', bytes => client.receiveBlock(new Int8Array(bytes)));
|
||||
socket.on('open', async () => {
|
||||
try {
|
||||
await client.connect('03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8');
|
||||
const result = await client.echo('Kotlin baseline ✓');
|
||||
if (result !== 'Kotlin baseline ✓') throw Error('Wrong echo');
|
||||
console.log('BASELINE_AUTHENTICATED_ECHO_OK');
|
||||
} catch (error) { console.error(error); process.exitCode = 1; }
|
||||
finally { client.close(); socket.close(); }
|
||||
});
|
||||
socket.on('error', error => { console.error(error); process.exitCode = 1; });
|
||||
setTimeout(() => { console.error('Baseline timeout'); process.exit(1); }, 10000).unref();
|
||||
|
||||
}
|
||||
main().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,5 @@
|
||||
plugins { kotlin("multiplatform"); kotlin("plugin.serialization") }
|
||||
kotlin {
|
||||
js { nodejs(); binaries.executable(); generateTypeScriptDefinitions() }
|
||||
sourceSets { jsMain.dependencies { implementation("net.sergeych:kiloparsec:0.8.0-SNAPSHOT") } }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
@file:OptIn(ExperimentalJsExport::class, kotlin.ExperimentalUnsignedTypes::class)
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import net.sergeych.crypto2.*
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.kiloparsec.adapter.ProxyDevice
|
||||
import kotlin.js.Promise
|
||||
|
||||
/** Native JS WebSocket bridge: retains the full authenticated protocol without Ktor client. */
|
||||
@JsExport
|
||||
class BaselineClient(private val sendBlock: (ByteArray) -> Unit) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val input = Channel<UByteArray>(Channel.UNLIMITED)
|
||||
private val output = Channel<UByteArray>(Channel.UNLIMITED)
|
||||
private var client: KiloClientConnection<Unit>? = null
|
||||
|
||||
fun receiveBlock(bytes: ByteArray) { check(input.trySend(bytes.toUByteArray()).isSuccess) }
|
||||
fun connect(expectedPublicKeyHex: String): Promise<Unit> = scope.promise {
|
||||
initCrypto()
|
||||
val local = KiloInterface<Unit>()
|
||||
val connection = KiloClientConnection(local, ProxyDevice(input, output), Unit, SigningSecretKey.new())
|
||||
client = connection
|
||||
scope.launch { for (block in output) sendBlock(block.asByteArray()) }
|
||||
scope.launch { connection.run() }
|
||||
val actual = connection.remoteId()?.keyBytes?.joinToString("") { it.toString(16).padStart(2, '0') }
|
||||
if (actual != expectedPublicKeyHex) { close(); error("Unexpected server identity") }
|
||||
}
|
||||
fun echo(text: String): Promise<String> = scope.promise {
|
||||
val echo by command<String, String>()
|
||||
requireNotNull(client).call(echo, text)
|
||||
}
|
||||
fun close() { scope.cancel() }
|
||||
}
|
||||
fun main() {}
|
||||
|
||||
/** Evaluation-only cold startup workload: initialize sodium and generate one identity. */
|
||||
@JsExport
|
||||
fun initializeIdentity(): Promise<String> = CoroutineScope(Dispatchers.Default).promise {
|
||||
initCrypto()
|
||||
SigningSecretKey.new().verifyingKey.keyBytes.joinToString("") { it.toString(16).padStart(2, '0') }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.21" apply false
|
||||
kotlin("multiplatform") version "2.2.21" apply false
|
||||
kotlin("plugin.serialization") version "2.2.21" apply false
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal(); mavenCentral()
|
||||
maven("https://maven.universablockchain.com/")
|
||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
|
||||
}
|
||||
}
|
||||
Generated
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "evaluation",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@js-joda/core": "3.2.0",
|
||||
"format-util": "1.0.5",
|
||||
"libsodium-wrappers-sumo": "0.7.15",
|
||||
"ws": "8.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-joda/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/format-util": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz",
|
||||
"integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/libsodium-sumo": {
|
||||
"version": "0.7.16",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.16.tgz",
|
||||
"integrity": "sha512-x6atrz2AdXCJg6G709x9W9TTJRI6/0NcL5dD0l5GGVqNE48UJmDsjO4RUWYTeyXXUpg+NXZ2SHECaZnFRYzwGA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/libsodium-wrappers-sumo": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz",
|
||||
"integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"libsodium-sumo": "^0.7.15"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"private":true,"dependencies":{"@js-joda/core":"3.2.0","libsodium-wrappers-sumo":"0.7.15","format-util":"1.0.5","ws":"8.18.0"}}
|
||||
@@ -0,0 +1,8 @@
|
||||
plugins { kotlin("jvm"); kotlin("plugin.serialization"); application }
|
||||
kotlin { jvmToolchain(21); compilerOptions { optIn.add("kotlin.ExperimentalUnsignedTypes") } }
|
||||
dependencies {
|
||||
implementation("net.sergeych:kiloparsec:0.8.0-SNAPSHOT")
|
||||
implementation("io.ktor:ktor-server-netty:3.1.1")
|
||||
implementation("org.slf4j:slf4j-simple:2.0.17")
|
||||
}
|
||||
application { mainClass.set("InteropServerKt") }
|
||||
@@ -0,0 +1,36 @@
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.crypto2.*
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.kiloparsec.adapter.setupWebsocketServer
|
||||
|
||||
// Public reproducible TEST KEY ONLY. Never use this identity outside tests.
|
||||
fun main(args: Array<String>) = runBlocking {
|
||||
initCrypto()
|
||||
val key = SigningSecretKey(("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
|
||||
"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8").chunked(2).map { it.toUByte(16) }.toUByteArray())
|
||||
val echo by command<String, String>()
|
||||
val sum by command<List<Int>, Int>()
|
||||
val identity by command<Unit, String>()
|
||||
val callback by command<String, String>()
|
||||
val clientEcho by command<String, String>()
|
||||
val push by command<String, Unit>()
|
||||
val notice by command<String, Unit>()
|
||||
val fail by command<String, String>()
|
||||
val api = KiloInterface<Unit>().apply {
|
||||
on(echo) { it }
|
||||
on(sum) { it.sum() }
|
||||
on(identity) { remoteIdentity?.keyBytes?.joinToString("") { b -> b.toString(16).padStart(2, '0') } ?: "anonymous" }
|
||||
on(callback) { remote.call(clientEcho, it) }
|
||||
on(push) { remote.call(notice, it) }
|
||||
on(fail) { throw IllegalArgumentException(it) }
|
||||
}
|
||||
val port = args.firstOrNull()?.toInt() ?: 18732
|
||||
println("TEST_SERVER_PUBLIC_KEY_HEX=" + key.verifyingKey.keyBytes.joinToString("") { it.toString(16).padStart(2, '0') })
|
||||
println("TEST_SERVER_URL=ws://127.0.0.1:$port/kp")
|
||||
embeddedServer(Netty, port = port, host = "127.0.0.1") {
|
||||
setupWebsocketServer(api, serverKey = key) { Unit }
|
||||
}.start(wait = true)
|
||||
Unit
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pluginManagement { repositories { mavenCentral(); gradlePluginPortal() } }
|
||||
rootProject.name = "kiloparsec-evaluation"
|
||||
include("server", "baseline", "wasm-baseline")
|
||||
|
||||
// Both fixtures resolve this checkout, never a stale published snapshot.
|
||||
includeBuild("../..") {
|
||||
dependencySubstitution {
|
||||
substitute(module("net.sergeych:kiloparsec")).using(project(":"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
plugins { kotlin("multiplatform"); kotlin("plugin.serialization") }
|
||||
kotlin {
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs { nodejs(); binaries.executable() }
|
||||
sourceSets { wasmJsMain.dependencies { implementation("net.sergeych:kiloparsec:0.8.0-SNAPSHOT") } }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
@file:OptIn(kotlin.js.ExperimentalJsExport::class, kotlin.ExperimentalUnsignedTypes::class)
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import net.sergeych.crypto2.*
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.kiloparsec.adapter.ProxyDevice
|
||||
import kotlin.js.JsExport
|
||||
|
||||
private var scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private var input = Channel<UByteArray>(Channel.UNLIMITED)
|
||||
private var client: KiloClientConnection<Unit>? = null
|
||||
private fun UByteArray.hex() = joinToString("") { it.toString(16).padStart(2, '0') }
|
||||
private fun String.bytes() = chunked(2).map { it.toUByte(16) }.toUByteArray()
|
||||
|
||||
/** Full-protocol evaluation bridge. Hex avoids exposing Kotlin objects to JS. */
|
||||
@JsExport
|
||||
fun start(expectedPublicKeyHex: String, sendBlock: (String) -> Unit, onReady: () -> Unit, onError: (String) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
initCrypto()
|
||||
val output = Channel<UByteArray>(Channel.UNLIMITED)
|
||||
val connection = KiloClientConnection(KiloInterface<Unit>(), ProxyDevice(input, output), Unit, SigningSecretKey.new())
|
||||
client = connection
|
||||
scope.launch { for (block in output) sendBlock(block.hex()) }
|
||||
scope.launch { connection.run() }
|
||||
if (connection.remoteId()?.keyBytes?.hex() != expectedPublicKeyHex) error("Unexpected server identity")
|
||||
onReady()
|
||||
} catch (e: Throwable) { onError(e.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
@JsExport
|
||||
fun receiveBlock(hex: String) { check(input.trySend(hex.bytes()).isSuccess) }
|
||||
|
||||
@JsExport
|
||||
fun echo(text: String, onResult: (String) -> Unit, onError: (String) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
val echo by command<String, String>()
|
||||
onResult(requireNotNull(client).call(echo, text))
|
||||
} catch (e: Throwable) { onError(e.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
@JsExport
|
||||
fun close() { scope.cancel() }
|
||||
|
||||
/** Same startup workload as the JS and TS comparison: sodium plus one identity. */
|
||||
@JsExport
|
||||
fun initializeIdentity(onResult: (String) -> Unit, onError: (String) -> Unit) {
|
||||
scope.launch {
|
||||
try { initCrypto(); onResult(SigningSecretKey.new().verifyingKey.keyBytes.hex()) }
|
||||
catch (e: Throwable) { onError(e.toString()) }
|
||||
}
|
||||
}
|
||||
fun main() {}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Sodium must be initialized before instantiating Kotlin/Wasm bindings.
|
||||
import sodiumModules from './wasm-sodium.cjs';
|
||||
const { sodium, rawSodium } = sodiumModules;
|
||||
import * as joda from '@js-joda/core';
|
||||
import { instantiate } from './wasm-baseline/build/compileSync/wasmJs/main/productionExecutable/kotlin/kiloparsec-evaluation-wasm-baseline.uninstantiated.mjs';
|
||||
await sodium.ready;
|
||||
const { exports: api } = await instantiate({
|
||||
'@js-joda/core': joda,
|
||||
'libsodium-sumo': rawSodium,
|
||||
'libsodium-wrappers-sumo': { default: sodium },
|
||||
});
|
||||
export { api };
|
||||
@@ -0,0 +1,43 @@
|
||||
// Disposable localhost interop of the measured optimized Wasm distribution.
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
import { setTimeout as pause } from 'node:timers/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { WebSocket } from 'ws';
|
||||
const listener = net.createServer();
|
||||
await new Promise((resolve,reject)=>listener.once('error',reject).listen(0,'127.0.0.1',resolve));
|
||||
const port=listener.address().port;
|
||||
await new Promise(resolve=>listener.close(resolve));
|
||||
const server=spawn(fileURLToPath(new URL('./server/build/install/server/bin/server',import.meta.url)),[String(port)],{stdio:['ignore','pipe','pipe']});
|
||||
let diagnostics=''; let exited=false; let socket; let api;
|
||||
server.stdout.on('data',b=>{ diagnostics=(diagnostics+b).slice(-8000); });
|
||||
server.stderr.on('data',b=>{ diagnostics=(diagnostics+b).slice(-8000); });
|
||||
server.on('exit',()=>{exited=true;});
|
||||
try {
|
||||
let ready=false;
|
||||
for(let n=0;n<100&&!exited;n++) {
|
||||
ready=await new Promise(resolve=>{const s=net.connect(port,'127.0.0.1');s.once('connect',()=>{s.destroy();resolve(true);});s.once('error',()=>resolve(false));});
|
||||
if(ready) break;
|
||||
await pause(100);
|
||||
}
|
||||
if(!ready) throw Error(`Fixture did not start: ${diagnostics}`);
|
||||
({api}=await import('./build/measurement/wasm/client.mjs'));
|
||||
socket=new WebSocket(`ws://127.0.0.1:${port}/kp`);
|
||||
socket.on('message',bytes=>api.receiveBlock(Buffer.from(bytes).toString('hex')));
|
||||
await new Promise((resolve,reject)=>{
|
||||
const timeout=setTimeout(()=>reject(Error('Wasm handshake timeout')),10000);
|
||||
socket.once('error',reject);
|
||||
socket.once('open',()=>api.start('03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8',
|
||||
hex=>socket.send(Buffer.from(hex,'hex')),()=>{clearTimeout(timeout);resolve();},message=>{clearTimeout(timeout);reject(Error(message));}));
|
||||
});
|
||||
const result=await new Promise((resolve,reject)=>{
|
||||
const timeout=setTimeout(()=>reject(Error('Wasm echo timeout')),10000);
|
||||
api.echo('Kotlin Wasm baseline ✓',value=>{clearTimeout(timeout);resolve(value);},message=>{clearTimeout(timeout);reject(Error(message));});
|
||||
});
|
||||
if(result!=='Kotlin Wasm baseline ✓') throw Error('Wrong echo');
|
||||
console.log('WASM_BASELINE_AUTHENTICATED_PINNED_ECHO_OK');
|
||||
} catch(e) { console.error(String(e));process.exitCode=1; }
|
||||
finally {
|
||||
api?.close(); socket?.terminate(); server.kill('SIGTERM');
|
||||
if(!exited){const timer=setTimeout(()=>server.kill('SIGKILL'),3000);await new Promise(resolve=>server.once('exit',resolve));clearTimeout(timer);}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Match the wrapper's CommonJS import path to retain a single sodium instance.
|
||||
exports.sodium = require('libsodium-wrappers-sumo');
|
||||
exports.rawSodium = require('libsodium-sumo');
|
||||
Reference in new issue
Block a user