# Core Private Node Routes These routes are consumed only by the node agent and require the `x-node-auth-key` header. ## Transport The node uses two transports to communicate with Core: - **HTTP** — registration only (`POST /node/register`). - **gRPC** — all other node-to-core calls: heartbeat, usage reporting, config sync, and stats. Core listens on `GRPC_PORT` (default `3002`). The node connects via `CORE_GRPC_URL`. Set `CORE_INSECURE=true` on the node to use plaintext instead of TLS. The HTTP routes for heartbeat, usage, config, and error remain available as a fallback but the node agent uses gRPC by default. ## Authentication - `POST /node/register` is the only public route in this group. - All other HTTP routes require the `x-node-auth-key` header. - gRPC calls pass the auth key as the `x-node-auth-key` metadata header (or in the `authKey` request field as fallback). - The auth key is issued by Core during registration and persisted on the node side. ## Route table | Method | Route | Input | Output / Notes | | --- | --- | --- | --- | | `POST` | `/node/register` | `RegisterNodeDto` | Returns `{ authKey, nodeId }` | | `POST` | `/node/heartbeat` | `NodeHeartbeatDto` | Returns `{ ok: true, needSync }` | | `POST` | `/node/usage` | `NodeUsageDto` | Returns `{ ok: true }` | | `POST` | `/node/outbound-usage` | `NodeOutboundUsageDto` | Returns `{ ok: true }` | | `POST` | `/node/session-events` | `SessionEventsDto` | Returns `{ ok: true }` | | `GET` | `/node/config/latest` | none | Returns `NodeConfig` | | `POST` | `/node/config/applied` | `NodeConfigAppliedDto` | Returns `{ ok: true }` | | `POST` | `/node/error` | `NodeErrorDto` | Returns `{ ok: true }` | ## Request payloads ### `RegisterNodeDto` - `code: string` - `name: string` - `ip: string` - `port: number` — agent HTTP port (required; auto-detected from `PORT` env on the node) - `region: string` - `domain?: string` — optional domain name (omitted on auto-registration; can be set later via admin panel) ### `NodeHeartbeatDto` - `ip?: string` - `revision?: number` The node sends its current revision in heartbeats. Core uses it to decide whether a sync is needed. ### `NodeUsageDto` - `entries: UsageEntryDto[]` `UsageEntryDto` fields: - `userId: number` - `subscriptionId?: number | null` - `nodeProtocolId: number` - `bytesUp: number` - `bytesDown: number` - `recordedAt: string` ### `NodeOutboundUsageDto` - `items: OutboundUsageItem[]` `OutboundUsageItem` fields: - `tag: string` - `bytesUp: number` - `bytesDown: number` ### `SessionEventsDto` - `events: SessionEventEntryDto[]` `SessionEventEntryDto` fields: - `userId: number` - `protocol: VpnProtocol` - `eventType: SessionEventType` - `occurredAt: string` - `ipAddress?: string` ### `NodeConfigAppliedDto` - `revision: number` ### `NodeErrorDto` - `message: string` - `code?: string` - `details?: Record` ## `NodeConfig` The config returned by `GET /node/config/latest` has this structure: ```ts { revision: number clientMap: Record mtprotoClientMap: Record services: { xray?: XrayServiceConfig mtproto?: MtProtoServiceConfig } } ``` Important notes: - `clientMap` maps subscription-link IDs (lsId) to the user and subscription — used for both xray and telemt traffic/online-IP attribution (the telemt username now mirrors the xray email `u{userId}.l{linkId}.s{lsId}.i{protoId}`). - `mtprotoClientMap` (lsUuid-keyed) is retained on the wire for backward compatibility but is no longer used for attribution. - `services.xray` contains the Xray inbounds, routing, and outbounds for the node. - `services.mtproto` contains MTProto inbounds for `telemt`. - The node stores the current revision and client maps locally after a successful apply. ## Route semantics - `POST /node/heartbeat` updates `lastSeenAt` and can request a sync when the node revision is behind. - `POST /node/usage` stores per-minute usage rows and increments subscription traffic counters. - `POST /node/outbound-usage` increments `node_outbounds.traffic_bytes_up/down`. - `POST /node/session-events` stores online/offline events in `session_events`. - `POST /node/config/applied` marks the node sync status as `synced`. - `POST /node/error` marks the node as `error` and the sync status as `failed`. ## gRPC interface (`NodeService`) Core exposes a gRPC service that the node agent uses for all real-time communication. The service name is `nodeservice.NodeService` and is defined in `proto/node-service.proto`. Authentication: pass `x-node-auth-key` as gRPC metadata (or in the `authKey` field of the request as fallback). | Method | Request fields | Response fields | Notes | | --- | --- | --- | --- | | `Heartbeat` | `authKey`, `revision?`, `ip?` | `ok`, `needSync` | Equivalent to `POST /node/heartbeat` | | `ReportUsage` | `authKey`, `entries[]` | `ok` | Equivalent to `POST /node/usage` | | `ReportOutboundUsage` | `authKey`, `items[]` | `ok` | Equivalent to `POST /node/outbound-usage` | | `GetLatestConfig` | `authKey` | `revision`, `clientMapJson`, `mtprotoClientMapJson`, `servicesJson` | Config fields are JSON-encoded strings | | `ConfirmConfigApplied` | `authKey`, `revision` | `ok` | Equivalent to `POST /node/config/applied` | | `ReportNodeStats` | `authKey`, `system{}`, `xray{}`, `mtprotoJson` | `ok` | Reports CPU/RAM/disk/network/xray/mtproto snapshot; stored in Redis; drives `GET /admin/nodes/:id/runtime-status` and stats history | | `ReportOnlineUsers` | `authKey`, `users[]` | `ok` | Reports currently connected users with their IPs every 5 s; Core stores Redis TTL keys and updates IP history | `ReportNodeStats` nested fields: - `system`: `uptimeSeconds`, `cpuPercent`, `cpuCores`, `ramUsed`, `ramTotal`, `diskUsed`, `diskTotal`, `netRxPerSec`, `netTxPerSec`, `netRxTotal`, `netTxTotal` - `xray`: `running`, `uptimeSeconds?` - `mtprotoJson`: JSON string `Record` `ReportOnlineUsers.users[]` entry: `userId`, `nodeProtocolId`, `ips[]{ip, lastSeen}`. The node sends `ReportNodeStats` every 2 s. `GET /admin/nodes/:id/runtime-status` returns the latest stored snapshot without polling the node; `?fromNode=true` forces a live HTTP fetch instead. The node sends `ReportOnlineUsers` every 5 s. Core sets `online:user:{userId}` and `online:inbound:{nodeProtocolId}:user:{userId}` Redis keys with TTL 20 s. Existing `(userId, ip)` rows only receive a new `lastSeenAt`; missing pairs are inserted. Private RFC1918 IPv4 addresses, IPv4-mapped private addresses, and IPv6 ULA addresses can still establish online presence but are excluded from `user_ips` writes. IP information enrichment supports IPinfo.io, ip-api.com, and FindIP.net. FindIP requests are sent sequentially per IP, and failed lookups are skipped. A daily job refreshes all known addresses, while a job running every 10 minutes processes addresses with `firstSeenAt > now - 10 minutes`. Results are stored in `user_ips.info`. Core also emits a `user.online` domain event when the user had a gap > 10 s.