Convro Tunnel
Status + API (canonical v1.x)
Checking /health…

Status

Endpoint
GET /health
DB
Timestamp
Raw JSON
Waiting…

Canonical rules (wire + DB)

Socket.IO (realtime)
WS endpoint: same host/port, path: /socket.io
Auth: accessToken w handshake:
  - socket.handshake.auth.token = "<JWT>"
  - albo query ?token=<JWT>
Room: device:<deviceId>
Minimal events:
  - client -> "ping"
  - server -> "pong"
Server log: "[WS] Connected userId=... deviceId=..."
        

Endpoints (aktywnie zarejestrowane)

HealthGET /health
AuthPOST /v1/auth/register
AuthPOST /v1/auth/login
AuthPOST /v1/auth/refresh
AuthPOST /v1/auth/logout
AuthGET /v1/auth/me
DevicesPOST /v1/auth/devices/register
DevicesGET /v1/auth/devices
UsersGET /v1/users/resolve
PrekeysPOST /v1/prekeys/upload
PrekeysGET /v1/prekeys/bundle
DM SessionsPOST /v1/dm/sessions/open
DM SessionsGET /v1/dm/sessions/incoming
DM SessionsPOST /v1/dm/sessions/accept
DMPOST /v1/dm/messages/send
DMGET /v1/dm/messages/history
DMGET /v1/dm/messages/conversations
ReceiptsPOST /v1/dm/messages/receipts

Wszystkie endpointy pod /v1/* wymagają nagłówka: Authorization: Bearer <accessToken>, chyba że opis mówi inaczej.

Auth

POST /v1/auth/register (public)
Body JSON:
{
  "password": "min 8 chars",
  "username": "optional, 3..64, [a-zA-Z0-9_.]",
  "fullName": "optional, max 128",
  "deviceId": "optional 16-hex; jeśli brak -> server generuje",
  "platform": "ios|android|desktop|web|unknown",
  "deviceLabel": "optional, max 128"
}

201 Response:
{
  "userId": 1,
  "convroNumber": "+99xxxxxx",
  "username": "sigmabo" | null,
  "fullName": "..." | null,
  "deviceId": "16hex",
  "platform": "ios",
  "accessToken": "<JWT>",
  "refreshToken": "rt_<jti>_<secretB64url>"
}

Errors:
- 400 PASSWORD_TOO_SHORT / INVALID_USERNAME / INVALID_DEVICE_ID
- 409 USERNAME_TAKEN / DEVICE_ID_TAKEN
- 500 FAILED_TO_ALLOCATE_CONVRO_NUMBER / INTERNAL_ERROR
        
POST /v1/auth/login (public)
Body JSON:
{
  "password": "...",
  "username": "..." | null,
  "convroNumber": "+99xxxxxx" | null,
  "deviceId": "optional 16-hex",
  "platform": "ios|android|desktop|web|unknown",
  "deviceLabel": "optional"
}

200 Response:
{ same shape as /register }

Errors:
- 400 MISSING_LOGIN_IDENTIFIER / INVALID_DEVICE_ID
- 401 INVALID_CREDENTIALS
- 403 DEVICE_OWNED_BY_OTHER_USER
        
POST /v1/auth/refresh (public)
Body JSON:
{ "refreshToken": "rt_..." }

200 Response:
{
  "userId": 1,
  "deviceId": "16hex",
  "convroNumber": "+99xxxxxx",
  "username": "..." | null,
  "accessToken": "<JWT>",
  "refreshToken": "rt_..."  // nowy, stary revoke
}

Errors:
- 400 INVALID_REFRESH_TOKEN
- 401 REFRESH_NOT_FOUND / REFRESH_REVOKED / REFRESH_EXPIRED / REFRESH_INVALID
        
POST /v1/auth/logout (auth required)
Header:
Authorization: Bearer <accessToken>

Body JSON (jedna z opcji):
1) revoke single:
{ "refreshToken": "rt_..." }

2) revoke all for device (default gdy brak refreshToken):
{ "allForDevice": true }

3) revoke all for user (wszystkie device):
{ "allForUser": true }

200 Response:
{ "ok": true, "scope": "single|device|user" }
        
GET /v1/auth/me (auth required)
200 Response:
{
  "userId": 1,
  "deviceId": "16hex",
  "convroNumber": "+99xxxxxx",
  "username": "..." | null
}
        

Users (resolve)

GET /v1/users/resolve (auth required)
Cel: start DM po +99 / username (UI: wpisujesz numer -> dostajesz peerUserId).

Header:
Authorization: Bearer <accessToken>

Query (jedno z):
- ?convroNumber=+99xxxxxx
- ?username=sigmabo

200 Response (minimal):
{
  "userId": 123,
  "convroNumber": "+99xxxxxx",
  "username": "..." | null,
  "fullName": "..." | null
}

Errors:
- 400 INVALID_QUERY
- 404 USER_NOT_FOUND
        

Tip: do handshaku nie musisz znać deviceId peera — /v1/prekeys/bundle bez device_id wybierze pierwsze aktywne urządzenie użytkownika.

Prekeys

POST /v1/prekeys/upload (auth required)
Header:
Authorization: Bearer <accessToken>

Body JSON:
{
  "identity": { "publicKeyEd25519": "<base64 32B>", "fingerprint": "optional" },
  "signedPrekey": {
    "keyId": "<16hex>",
    "publicKeyX25519": "<base64 32B>",
    "signatureEd25519": "<base64 64B>"
  },
  "oneTimePrekeys": [
    { "keyId": "<16hex>", "publicKeyX25519": "<base64 32B>" }
  ]
}

200 Response:
{ "ok": true, "deviceId": "16hex", "storedOneTimePrekeys": 0..N }

Notes:
- identity i signedPrekey są wymagane
- OTP batch opcjonalny (ale zalecany)
        
GET /v1/prekeys/bundle?user_id=...&device_id=... (auth required)
Header:
Authorization: Bearer <accessToken>

Query:
- user_id (required) => peer userId
- device_id (optional) => 16hex peer deviceId

200 Response:
{
  "responderDeviceId": "16hex",
  "identityPublicKeyEd25519": "<base64 32B>",
  "signedPrekeyId": "<16hex>",
  "signedPrekeyPublicKeyX25519": "<base64 32B>",
  "signedPrekeySignature": "<base64 64B>",
  "oneTimePrekeyId": "<16hex>" | null,
  "oneTimePrekeyPublicKeyX25519": "<base64 32B>" | null
}

Canonical:
- OTP jest konsumowany tutaj (atomowo), nie w DM session.
        

DM Sessions (handshake)

POST /v1/dm/sessions/open (auth required)
Header:
Authorization: Bearer <accessToken>

Body JSON:
{
  "peerUserId": 123,
  "handshakeOffer": {
    "version": 1,
    "initiatorDeviceId": "16hex (musi == deviceId z JWT)",
    "responderDeviceId": "16hex",
    "sessionId": "8hex",
    "ephemeralPublicKeyX25519": "<base64url/base64 32B>",
    "usedSignedPrekeyPublicKeyX25519": "<base64url/base64 32B>",
    "usedOneTimePrekeyId": "<16hex>" | null,

    "...": "inne pola offer zależne od C6P v1"
  }
}

201 Response:
{ "ok": true, "sessionDbId": 1, "sessionId": "8hex", "responderUserId": 123, "responderDeviceId": "16hex", "state": "PENDING" }

Errors:
- 409 SESSION_ID_EXISTS
- 404 INITIATOR_DEVICE_NOT_FOUND / RESPONDER_DEVICE_NOT_FOUND
- 413 HANDSHAKE_OFFER_TOO_LARGE
        
GET /v1/dm/sessions/incoming (auth required)
Header:
Authorization: Bearer <accessToken>

200 Response:
{
  "sessions": [
    {
      "sessionDbId": 1,
      "sessionId": "8hex",
      "initiatorUserId": 55,
      "initiatorDeviceId": "16hex",
      "handshakeOffer": { ... },
      "createdAt": "ISO"
    }
  ]
}
        
POST /v1/dm/sessions/accept (auth required)
Header:
Authorization: Bearer <accessToken>

Body JSON:
{ "sessionDbId": 1 }

200 Response:
{ "ok": true, "sessionDbId": 1, "sessionId": "8hex", "state": "ACTIVE" }

Notes:
- akceptuje tylko responder device z sesji
        

DM Messages

POST /v1/dm/messages/send (auth required)
Header:
Authorization: Bearer <accessToken>

Body JSON (canonical minimum):
{
  "sessionId": "8hex",
  "recipientDeviceId": "16hex",
  "clientMessageId": "optional string up to 64",
  "clientTimestamp": "optional ISO",
  "ciphertext": "<base64>",
  "authTag": "<base64 (16B)>"
}

201/200 Response (typowo):
{
  "ok": true,
  "messageId": 123,
  "serverTimestamp": "ISO",
  "deliveryState": "PENDING"
}

Realtime:
- server emituje do room: device:<recipientDeviceId> (payload z message/meta)
        
GET /v1/dm/messages/history (auth required)
Header:
Authorization: Bearer <accessToken>

Query (typowo):
- ?sessionId=8hex
- ?beforeId=123 (optional) / ?limit=50 (optional)

200 Response:
{ "messages": [ ... ] }
        
GET /v1/dm/messages/conversations (auth required)
Header:
Authorization: Bearer <accessToken>

200 Response:
{ "conversations": [ ... ] }
        
POST /v1/dm/messages/receipts (auth required)
Header:
Authorization: Bearer <accessToken>

Body JSON:
{
  "messageId": 123,
  "status": "DELIVERED" | "READ"
}

200 Response:
{ "ok": true }

Notes:
- tylko recipient device może wystawić receipt
- DB: message_receipts ma uniq(message_id, device_id)
        

Flow (DM po numerze +99)

  1. Register/Login → dostajesz accessToken + refreshToken + deviceId + convroNumber.
  2. Upload prekeysPOST /v1/prekeys/upload (identity + signed prekey + OTP batch).
  3. Resolve peerGET /v1/users/resolve?convroNumber=+99xxxxxx → dostajesz peerUserId.
  4. Get prekey bundleGET /v1/prekeys/bundle?user_id=<peerUserId> (server wybiera aktywny device).
  5. Handshake offer → generujesz C6P offer → POST /v1/dm/sessions/open.
  6. ResponderGET /v1/dm/sessions/incomingPOST /v1/dm/sessions/accept → sesja ACTIVE.
  7. MessagingPOST /v1/dm/messages/send + realtime przez Socket.IO room device:<deviceId>.