# ChronoSeal — API Reference ## Base URL All endpoints are relative to the server root. In development: `http://localhost:3000`. In production: your HTTPS domain via reverse proxy. --- ## Endpoints ### `POST /init` Initialise a new session. Called once per page load, immediately after the WASM module generates an Ed25519 keypair. #### Request ```http POST /init Content-Type: application/json ``` ```json { "public_key": "hex-encoded 32-byte Ed25519 verifying key" } ``` | Field | Type | Description | |---|---|---| | `public_key` | `string` | Hex-encoded 32-byte Ed25519 verifying key generated by the WASM module | #### Response `200 OK` ```json { "session_id": "64-char hex string (32 bytes)", "salt": "32-char hex string (16 bytes)", "opcodes_b64": "base64-encoded VM program (8–16 opcodes)", "initial_hash": "64-char hex string (32 bytes Blake3)", "expires_at": 1234567890123 } ``` | Field | Type | Description | |---|---|---| | `session_id` | `string` | Opaque session identifier; include in every heartbeat | | `salt` | `string` | Initial salt; used to compute `H(0)` and first `H(1)` | | `opcodes_b64` | `string` | Base64 VM program; execute with `run_program()` on every heartbeat | | `initial_hash` | `string` | `H(0) = Blake3(session_id ║ pub_key ║ salt)`; the first `prev_hash` | | `expires_at` | `number` | Unix timestamp in milliseconds; session expires after 30 minutes of inactivity | #### Error Returns `500 Internal Server Error` only on server-side failures (DB errors, invalid public key length). No meaningful error body is returned. --- ### `POST /hb` Submit a heartbeat. Called every 12–25 seconds with uniform random jitter. #### Request ```http POST /hb Content-Type: application/json ``` ```json { "session_id": "64-char hex", "prev_hash": "64-char hex", "timestamp": 1234567890123, "entropy_data": { "events": [ { "x": 412.0, "y": 308.5, "t": 1234.567 }, { "x": 415.2, "y": 310.1, "t": 1285.123 } ] }, "stack_state": { "stack": [2971406957, 1234567890], "ip": 42 }, "fingerprint": { "aspectRatio": "1.7777777778", "devicePixelRatio": "2", "hardwareConcurrency": 8 }, "signature": "128-char hex Ed25519 signature" } ``` | Field | Type | Description | |---|---|---| | `session_id` | `string` | Session ID from `/init` | | `prev_hash` | `string` | Hash chain head from previous heartbeat (or `initial_hash` for the first) | | `timestamp` | `number` | `Date.now()` in milliseconds; must be within ±30s of server time | | `entropy_data.events` | `array` | Mouse events since previous heartbeat; each has `x`, `y` (px), `t` (performance.now ms) | | `stack_state.stack` | `array` | `u32[]` result of executing the VM program | | `stack_state.ip` | `number` | Instruction pointer after execution | | `fingerprint.aspectRatio` | `string` | `(screen.width / screen.height).toFixed(10)` | | `fingerprint.devicePixelRatio` | `string` | `String(window.devicePixelRatio)` | | `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency \|\| 1` | | `signature` | `string` | Hex-encoded 64-byte Ed25519 signature over the canonical payload | #### Canonical Signing Payload The client signs the following JSON object. Top-level keys must be sorted alphabetically. Nested object keys follow their natural serialisation order. ```json { "entropyData": { "events": [{ "t": …, "x": …, "y": … }] }, "fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … }, "prevHash": "…", "sessionId": "…", "stackState": { "ip": …, "stack": […] }, "timestamp": … } ``` Note: field names in the signing payload use camelCase (`sessionId`, `prevHash`, `entropyData`, `stackState`) while the request body uses snake_case (`session_id`, `prev_hash`, `entropy_data`, `stack_state`). #### Response `200 OK` — Accepted ```json { "status": "ok", "next_salt": "32-char hex string (16 bytes)" } ``` The client must: 1. Capture `sentSalt = currentSalt` before updating. 2. Set `currentSalt = next_salt`. 3. Compute `prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt)`. #### Response `200 OK` — Rejected ```json { "status": "ok" } ``` `next_salt` is absent. The response body is intentionally identical in structure. Rejections are silent — the caller cannot distinguish a validation failure from a rate limit hit or an expired session. The client should log a warning and continue scheduling heartbeats (they will continue to fail until the page is reloaded and a new session is established). --- ## Validation Rules (Server-Side) Heartbeats are rejected (silently) if any of the following checks fail: | Check | Condition for rejection | |---|---| | Rate limit | > 5 requests per 10-second window for this `session_id` | | Session not found | `session_id` not in SQLite | | Session expired | `current_time_ms > expires_at` | | Signature invalid | Ed25519 verification fails against stored public key | | Hash chain broken | `hex(prev_hash) ≠ stored last_hash` | | Timestamp drift | `\|server_now_ms - timestamp\| > 30 000` | | Insufficient mouse events | `events.len() < 3` | | Insufficient mouse distance | `total_dist < 10.0 px` | | Mouse speed too high | `total_dist / total_time_ms > 2.0 px/ms` | | No mouse pauses | `pause_count < 1` | | Invalid aspect ratio | `ar < 0.5` or `ar > 3.0` | | Invalid devicePixelRatio | `dpr ≤ 0.0` or `dpr > 5.0` | | Zero hardwareConcurrency | `hardware_concurrency == 0` | --- ## Hash Chain Specification ``` H(0) = Blake3( session_id_bytes ║ pub_key_bytes ║ salt₀_bytes ) H(n) = Blake3( saltₙ₋₁_bytes ║ H(n-1)_bytes ║ timestamp_u64_le_bytes ║ Blake3( UTF-8( JSON(entropy_data) ) ) ║ Blake3( UTF-8( JSON(stack_state) ) ) ) ``` All inputs are concatenated in the order shown. `timestamp` is encoded as a 64-bit unsigned integer in little-endian byte order. JSON serialisation of `entropy_data` and `stack_state` uses the field order defined by the shared Rust types (serde derive, no custom ordering). --- ## WASM API The WASM module (`antibot_wasm`) exports the following functions to JavaScript: | Function | Signature | Description | |---|---|---| | `generate_keypair()` | `() → string` | Generate Ed25519 keypair; return hex public key. Private key stored in WASM memory. | | `get_public_key()` | `() → string` | Return hex public key, or `""` if not initialised. | | `sign_message(msg)` | `(string) → string` | Sign UTF-8 string; return hex signature, or `""` if not initialised. | | `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) → string` | Compute next Blake3 chain hash; all inputs/output hex or JSON strings. | | `run_program(b64)` | `(string) → JsValue` | Execute base64 VM program; return `{ stack: u32[], ip: number }`. | All functions return empty strings on error rather than panicking. Callers must check for empty return values before using the result.