9.0 KiB
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
POST /init
Content-Type: application/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
{
"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,
"heartbeat_min_interval_ms": 12000,
"heartbeat_max_interval_ms": 25000,
"gene_size": 512,
"mutation_step": 1,
"mutation_order_b64": "base64-encoded mutation program"
}
| 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 |
heartbeat_min_interval_ms |
number |
Lower bound for randomized heartbeat scheduling |
heartbeat_max_interval_ms |
number |
Upper bound for randomized heartbeat scheduling |
gene_size |
number |
Initial synthetic gene size used by server and WASM (default 512) |
mutation_step |
number |
Server-issued mutation order step expected on next heartbeat |
mutation_order_b64 |
string |
Base64-encoded mutation opcode program for the current step |
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
POST /hb
Content-Type: application/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
},
"mutation_step": 1,
"gene_commitment": "64-char hex Blake3 commitment",
"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 |
mutation_step |
number |
Must match server-side pending mutation step |
gene_commitment |
string |
Commitment of the locally previewed candidate gene after applying mutation_order_b64 |
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.
{
"entropyData": { "events": [{ "t": …, "x": …, "y": … }] },
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
"geneCommitment":"…",
"mutationStep": …,
"prevHash": "…",
"sessionId": "…",
"stackState": { "ip": …, "stack": […] },
"timestamp": …
}
Note: field names in the signing payload use camelCase (sessionId,
prevHash, entropyData, stackState, mutationStep, geneCommitment)
while the request body uses snake_case (session_id, prev_hash,
entropy_data, stack_state, mutation_step, gene_commitment).
Response 200 OK — Accepted
{
"status": "ok",
"next_salt": "32-char hex string (16 bytes)",
"next_mutation_step": 2,
"next_mutation_order_b64": "base64-encoded mutation program"
}
The client must:
- Preview commitment locally from
mutation_order_b64and send it in the heartbeat. - Capture
sentSalt = currentSaltbefore updating. - Set
currentSalt = next_salt. - Compute
prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt). - Commit the previewed gene state.
- Replace pending mutation values with
next_mutation_stepandnext_mutation_order_b64.
Response 200 OK — Rejected
{
"status": "ok"
}
next_salt, next_mutation_step, and next_mutation_order_b64 are 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 |
| Mutation step mismatch | mutation_step ≠ pending_mutation_step |
| Mutation commitment mismatch | gene_commitment does not match server-computed candidate commitment |
| 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 (chronoseal_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 }. |
init_gene_state(gene_size) |
(u32) → bool |
Initialise synthetic gene state in WASM memory. |
preview_gene_commitment(order_b64) |
(string) → string |
Apply mutation order on preview state and return commitment hex. |
commit_gene_preview() |
() → bool |
Commit previewed mutation state after accepted heartbeat. |
discard_gene_preview() |
() → void |
Discard previewed mutation state after rejection/error. |
current_gene_commitment() |
() → string |
Return current committed gene commitment hex. |
String-returning functions return "" on error rather than panicking. Callers
must check for empty strings and boolean return values before use.