Files
nx9-chronoseal-rs/docs/API.md
T
2026-05-29 14:50:45 +05:30

9.0 KiB
Raw Blame History

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:

  1. Preview commitment locally from mutation_order_b64 and send it in the heartbeat.
  2. Capture sentSalt = currentSalt before updating.
  3. Set currentSalt = next_salt.
  4. Compute prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt).
  5. Commit the previewed gene state.
  6. Replace pending mutation values with next_mutation_step and next_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.