Files

6.2 KiB

Kiloparsec TypeScript + WASM

Full-protocol TypeScript client for the existing Kotlin Kiloparsec server, backed by libsodium WASM. No Kotlin runtime, no light handshake, no TLS-only identity fallback. This is an evaluated implementation candidate, not a production release.

This module is maintained alongside the Kotlin implementation in this repository. Both peers use the hardened 0.8 protocol: encrypted packets authenticate their RPC envelope as well as their payload. Legacy peers are deliberately rejected; there is no automatic downgrade. See security and migration.

The assessment records the original implementation choice and historical size measurements. Re-run the measurement script for this revision; those recorded measurements describe the earlier published protocol baseline.

API

import {
  Client, LocalInterface, command, string, generateSigningKey,
} from '@sergeych/kiloparsec';

const echo = command('echo', string, string);
const clientEcho = command('clientEcho', string, string);
const local = new LocalInterface<{ requests: number }>()
  .on(clientEcho, (scope, text) => {
    scope.session.requests++;
    return text;
  });
const identity = await generateSigningKey();
// Persist identity.exportSecret() in your secure store before registering it.
// Register identity.publicKey with the application separately.
const client = await Client.connect('wss://your-service.example/kp', {
  expectedServerKey, // trusted raw 32-byte Ed25519 key, provisioned independently
  identity,
  local,
  session: { requests: 0 },
});
try {
  const text = await client.call(echo, 'Hello');
} finally {
  client.close();
  identity.destroy();
}

expectedServerKey is mandatory. A signature by an arbitrary key supplied by the peer is insufficient. Anonymous client sessions are available by omitting identity; their permissions remain a server application decision. restoreSigningKey restores raw 64-byte secret exports; generic Kiloparsec uses bytes, while an SDK such as GrandMobile may wrap them in its own opaque versioned format.

command(name, argsCodec, resultCodec) mirrors Kotlin Command<A,R>. LocalInterface.on registers typed callbacks before connection; each connection gets a snapshot of the registry and a session object. A callback can call its peer and await the result without blocking packet dispatch. Methods on Client:

  • call(command, args, {signal, timeoutMs, priority}): typed authenticated payload or RemoteError. ConnectionError describes local transport failure.
  • push(command, args): sends an id=0 notification; successful return means local enqueue only, not acknowledged remote execution.
  • ping(): encrypted liveness check. Automatic heartbeat defaults to 30 seconds; set heartbeatMs: 0 to disable it.
  • close() and closed: dispose the connection and observe local disposal.

Timeout/abort stops waiting, not remote execution. No transparent replay or reconnect: create a fresh connection and let the application resolve uncertain business outcomes using its durable operation IDs. Transport IDs are unrelated. Local handler failures are sanitized unless explicitly returned as RemoteError.

Node >=22 uses its standard WebSocket. Other runtimes can provide socketFactory. Both ws and wss carry the same encrypted protocol; deployment policy chooses the URL. Browser type/build compatibility is present but browser runtime tests have not been performed. Do not treat Node interoperability as browser qualification.

Serialization compatibility

Interop tests build the Kotlin server from this checkout through a Gradle composite build; no published Kiloparsec snapshot is needed. src/bipack.ts implements explicit codecs; it does not dynamically recreate all Kotlin serialization. Exported codecs cover strings, bytes, bool, Unit, integers, nullable values and lists. Compose records with codec, BipackReader and BipackWriter in the same field order as Kotlin; no field names go on the wire.

uint32/uint64 encode unsigned Smartint (@Unsigned semantics). Published mp_bintools 0.3.2 serializes unannotated Kotlin UInt through its signed backing Int: the handshake codec preserves this behavior. Newer source 0.4.0 changes that behavior. Do not upgrade either peer silently. Signed Smartint cannot represent Long.MIN_VALUE correctly in this baseline; TS rejects it explicitly.

Packet sizes, concurrent incoming work, pending calls and send buffers are bounded. Receive replay tracking permits out-of-order counters within 4096 positions and silently discards duplicates and packets outside the window without closing the session. Authenticated envelopes additionally bind packet kind, RPC ID and priority to the encrypted payload. Priority trailers are preserved, but this first client uses WebSocket FIFO rather than a priority scheduler. No TCP/UDP adapters, carrier cycling, automatic reconnection, or code generator are included in this version.

Build and verify

npm ci --ignore-scripts
npm test              # build + local codec/crypto/RPC regression tests
npm run test:interop  # build current Kotlin fixture + cross-language tests
npm run test:all      # both suites
# Optional optimized Kotlin comparison baseline:
npm run baseline:build
npm ci --prefix evaluation --ignore-scripts
npm run measure

JDK 21 and the Android SDK configuration required by the parent Kotlin build must be available for interoperability and baseline builds. test:interop starts and stops its own disposable localhost JVM fixture. Generated outputs and dependencies are not source-controlled. Package publication remains disabled pending release qualification. From the parent repository, bin/test-all.sh runs Kotlin and TS unit tests followed by interoperability tests sequentially.

Follow-up: reusable utility exports

After the GrandMobile game API, complete the supported public utility surface: BiPack, key generation/restoration, signing/verification, standalone encryption and hashes. BiPack and signing identities are already exported from index.ts; hash and session-crypto implementations currently remain internal package modules. Define documented stable entry points, formats and usage examples before exposing additional utilities. This is deferred work, not a prerequisite for the game API.