@mt-tl/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/auth/handshake.d.ts +35 -0
  4. package/dist/auth/handshake.d.ts.map +1 -0
  5. package/dist/auth/handshake.js +208 -0
  6. package/dist/auth/handshake.js.map +1 -0
  7. package/dist/auth/nonce-store.d.ts +22 -0
  8. package/dist/auth/nonce-store.d.ts.map +1 -0
  9. package/dist/auth/nonce-store.js +23 -0
  10. package/dist/auth/nonce-store.js.map +1 -0
  11. package/dist/bootstrap.d.ts +36 -0
  12. package/dist/bootstrap.d.ts.map +1 -0
  13. package/dist/bootstrap.js +82 -0
  14. package/dist/bootstrap.js.map +1 -0
  15. package/dist/config.d.ts +103 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +2 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/core/context.d.ts +57 -0
  20. package/dist/core/context.d.ts.map +1 -0
  21. package/dist/core/context.js +27 -0
  22. package/dist/core/context.js.map +1 -0
  23. package/dist/core/errors.d.ts +33 -0
  24. package/dist/core/errors.d.ts.map +1 -0
  25. package/dist/core/errors.js +47 -0
  26. package/dist/core/errors.js.map +1 -0
  27. package/dist/core/index.d.ts +5 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +5 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/rpc.d.ts +83 -0
  32. package/dist/core/rpc.d.ts.map +1 -0
  33. package/dist/core/rpc.js +102 -0
  34. package/dist/core/rpc.js.map +1 -0
  35. package/dist/core/updates.d.ts +56 -0
  36. package/dist/core/updates.d.ts.map +1 -0
  37. package/dist/core/updates.js +34 -0
  38. package/dist/core/updates.js.map +1 -0
  39. package/dist/create-server.d.ts +89 -0
  40. package/dist/create-server.d.ts.map +1 -0
  41. package/dist/create-server.js +109 -0
  42. package/dist/create-server.js.map +1 -0
  43. package/dist/crypto/aes-ige.d.ts +3 -0
  44. package/dist/crypto/aes-ige.d.ts.map +1 -0
  45. package/dist/crypto/aes-ige.js +55 -0
  46. package/dist/crypto/aes-ige.js.map +1 -0
  47. package/dist/crypto/dh.d.ts +21 -0
  48. package/dist/crypto/dh.d.ts.map +1 -0
  49. package/dist/crypto/dh.js +99 -0
  50. package/dist/crypto/dh.js.map +1 -0
  51. package/dist/crypto/hashes.d.ts +6 -0
  52. package/dist/crypto/hashes.d.ts.map +1 -0
  53. package/dist/crypto/hashes.js +14 -0
  54. package/dist/crypto/hashes.js.map +1 -0
  55. package/dist/crypto/msg-key.d.ts +15 -0
  56. package/dist/crypto/msg-key.d.ts.map +1 -0
  57. package/dist/crypto/msg-key.js +24 -0
  58. package/dist/crypto/msg-key.js.map +1 -0
  59. package/dist/crypto/rsa.d.ts +27 -0
  60. package/dist/crypto/rsa.d.ts.map +1 -0
  61. package/dist/crypto/rsa.js +50 -0
  62. package/dist/crypto/rsa.js.map +1 -0
  63. package/dist/dispatch/dispatcher.d.ts +72 -0
  64. package/dist/dispatch/dispatcher.d.ts.map +1 -0
  65. package/dist/dispatch/dispatcher.js +503 -0
  66. package/dist/dispatch/dispatcher.js.map +1 -0
  67. package/dist/dispatch/forwarders/in-process.d.ts +12 -0
  68. package/dist/dispatch/forwarders/in-process.d.ts.map +1 -0
  69. package/dist/dispatch/forwarders/in-process.js +15 -0
  70. package/dist/dispatch/forwarders/in-process.js.map +1 -0
  71. package/dist/dispatch/forwarders/print.d.ts +14 -0
  72. package/dist/dispatch/forwarders/print.d.ts.map +1 -0
  73. package/dist/dispatch/forwarders/print.js +23 -0
  74. package/dist/dispatch/forwarders/print.js.map +1 -0
  75. package/dist/dispatch/rpc-forwarder.d.ts +14 -0
  76. package/dist/dispatch/rpc-forwarder.d.ts.map +1 -0
  77. package/dist/dispatch/rpc-forwarder.js +2 -0
  78. package/dist/dispatch/rpc-forwarder.js.map +1 -0
  79. package/dist/dispatch/types.d.ts +32 -0
  80. package/dist/dispatch/types.d.ts.map +1 -0
  81. package/dist/dispatch/types.js +23 -0
  82. package/dist/dispatch/types.js.map +1 -0
  83. package/dist/gateway.d.ts +57 -0
  84. package/dist/gateway.d.ts.map +1 -0
  85. package/dist/gateway.js +150 -0
  86. package/dist/gateway.js.map +1 -0
  87. package/dist/index.d.ts +7 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +25 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/lib.d.ts +12 -0
  92. package/dist/lib.d.ts.map +1 -0
  93. package/dist/lib.js +13 -0
  94. package/dist/lib.js.map +1 -0
  95. package/dist/server/message-pipeline.d.ts +57 -0
  96. package/dist/server/message-pipeline.d.ts.map +1 -0
  97. package/dist/server/message-pipeline.js +199 -0
  98. package/dist/server/message-pipeline.js.map +1 -0
  99. package/dist/session/inbound-tracker.d.ts +118 -0
  100. package/dist/session/inbound-tracker.d.ts.map +1 -0
  101. package/dist/session/inbound-tracker.js +170 -0
  102. package/dist/session/inbound-tracker.js.map +1 -0
  103. package/dist/session/message-id.d.ts +19 -0
  104. package/dist/session/message-id.d.ts.map +1 -0
  105. package/dist/session/message-id.js +30 -0
  106. package/dist/session/message-id.js.map +1 -0
  107. package/dist/session/salts.d.ts +51 -0
  108. package/dist/session/salts.d.ts.map +1 -0
  109. package/dist/session/salts.js +132 -0
  110. package/dist/session/salts.js.map +1 -0
  111. package/dist/session/session-manager.d.ts +18 -0
  112. package/dist/session/session-manager.d.ts.map +1 -0
  113. package/dist/session/session-manager.js +43 -0
  114. package/dist/session/session-manager.js.map +1 -0
  115. package/dist/storage/index.d.ts +14 -0
  116. package/dist/storage/index.d.ts.map +1 -0
  117. package/dist/storage/index.js +16 -0
  118. package/dist/storage/index.js.map +1 -0
  119. package/dist/storage/memory.d.ts +3 -0
  120. package/dist/storage/memory.d.ts.map +1 -0
  121. package/dist/storage/memory.js +91 -0
  122. package/dist/storage/memory.js.map +1 -0
  123. package/dist/storage/mongo.d.ts +3 -0
  124. package/dist/storage/mongo.d.ts.map +1 -0
  125. package/dist/storage/mongo.js +175 -0
  126. package/dist/storage/mongo.js.map +1 -0
  127. package/dist/storage/types.d.ts +85 -0
  128. package/dist/storage/types.d.ts.map +1 -0
  129. package/dist/storage/types.js +3 -0
  130. package/dist/storage/types.js.map +1 -0
  131. package/dist/testkit.d.ts +11 -0
  132. package/dist/testkit.d.ts.map +1 -0
  133. package/dist/testkit.js +17 -0
  134. package/dist/testkit.js.map +1 -0
  135. package/dist/tl/codec.d.ts +37 -0
  136. package/dist/tl/codec.d.ts.map +1 -0
  137. package/dist/tl/codec.js +297 -0
  138. package/dist/tl/codec.js.map +1 -0
  139. package/dist/tl/layered-registry.d.ts +29 -0
  140. package/dist/tl/layered-registry.d.ts.map +1 -0
  141. package/dist/tl/layered-registry.js +118 -0
  142. package/dist/tl/layered-registry.js.map +1 -0
  143. package/dist/tl/protocol.d.ts +126 -0
  144. package/dist/tl/protocol.d.ts.map +1 -0
  145. package/dist/tl/protocol.js +22 -0
  146. package/dist/tl/protocol.js.map +1 -0
  147. package/dist/tl/reader.d.ts +30 -0
  148. package/dist/tl/reader.d.ts.map +1 -0
  149. package/dist/tl/reader.js +87 -0
  150. package/dist/tl/reader.js.map +1 -0
  151. package/dist/tl/registry.d.ts +39 -0
  152. package/dist/tl/registry.d.ts.map +1 -0
  153. package/dist/tl/registry.js +52 -0
  154. package/dist/tl/registry.js.map +1 -0
  155. package/dist/tl/writer.d.ts +24 -0
  156. package/dist/tl/writer.d.ts.map +1 -0
  157. package/dist/tl/writer.js +95 -0
  158. package/dist/tl/writer.js.map +1 -0
  159. package/dist/transport/connection-registry.d.ts +31 -0
  160. package/dist/transport/connection-registry.d.ts.map +1 -0
  161. package/dist/transport/connection-registry.js +84 -0
  162. package/dist/transport/connection-registry.js.map +1 -0
  163. package/dist/transport/connection.d.ts +62 -0
  164. package/dist/transport/connection.d.ts.map +1 -0
  165. package/dist/transport/connection.js +77 -0
  166. package/dist/transport/connection.js.map +1 -0
  167. package/dist/transport/framing.d.ts +39 -0
  168. package/dist/transport/framing.d.ts.map +1 -0
  169. package/dist/transport/framing.js +212 -0
  170. package/dist/transport/framing.js.map +1 -0
  171. package/dist/transport/proxy-protocol.d.ts +23 -0
  172. package/dist/transport/proxy-protocol.d.ts.map +1 -0
  173. package/dist/transport/proxy-protocol.js +108 -0
  174. package/dist/transport/proxy-protocol.js.map +1 -0
  175. package/dist/transport/server-common.d.ts +27 -0
  176. package/dist/transport/server-common.d.ts.map +1 -0
  177. package/dist/transport/server-common.js +27 -0
  178. package/dist/transport/server-common.js.map +1 -0
  179. package/dist/transport/tcp-server.d.ts +26 -0
  180. package/dist/transport/tcp-server.d.ts.map +1 -0
  181. package/dist/transport/tcp-server.js +91 -0
  182. package/dist/transport/tcp-server.js.map +1 -0
  183. package/dist/transport/ws-server.d.ts +19 -0
  184. package/dist/transport/ws-server.d.ts.map +1 -0
  185. package/dist/transport/ws-server.js +78 -0
  186. package/dist/transport/ws-server.js.map +1 -0
  187. package/dist/update-publisher.d.ts +34 -0
  188. package/dist/update-publisher.d.ts.map +1 -0
  189. package/dist/update-publisher.js +29 -0
  190. package/dist/update-publisher.js.map +1 -0
  191. package/dist/updates/mongo-update-log.d.ts +13 -0
  192. package/dist/updates/mongo-update-log.d.ts.map +1 -0
  193. package/dist/updates/mongo-update-log.js +39 -0
  194. package/dist/updates/mongo-update-log.js.map +1 -0
  195. package/dist/updates/presence-binder.d.ts +29 -0
  196. package/dist/updates/presence-binder.d.ts.map +1 -0
  197. package/dist/updates/presence-binder.js +36 -0
  198. package/dist/updates/presence-binder.js.map +1 -0
  199. package/dist/updates/presence.d.ts +31 -0
  200. package/dist/updates/presence.d.ts.map +1 -0
  201. package/dist/updates/presence.js +44 -0
  202. package/dist/updates/presence.js.map +1 -0
  203. package/dist/updates/push.d.ts +25 -0
  204. package/dist/updates/push.d.ts.map +1 -0
  205. package/dist/updates/push.js +72 -0
  206. package/dist/updates/push.js.map +1 -0
  207. package/dist/updates/redis-bus.d.ts +45 -0
  208. package/dist/updates/redis-bus.d.ts.map +1 -0
  209. package/dist/updates/redis-bus.js +59 -0
  210. package/dist/updates/redis-bus.js.map +1 -0
  211. package/dist/updates/redis-presence.d.ts +43 -0
  212. package/dist/updates/redis-presence.d.ts.map +1 -0
  213. package/dist/updates/redis-presence.js +65 -0
  214. package/dist/updates/redis-presence.js.map +1 -0
  215. package/dist/updates/render.d.ts +16 -0
  216. package/dist/updates/render.d.ts.map +1 -0
  217. package/dist/updates/render.js +46 -0
  218. package/dist/updates/render.js.map +1 -0
  219. package/dist/updates/router.d.ts +27 -0
  220. package/dist/updates/router.d.ts.map +1 -0
  221. package/dist/updates/router.js +36 -0
  222. package/dist/updates/router.js.map +1 -0
  223. package/dist/updates/types.d.ts +23 -0
  224. package/dist/updates/types.d.ts.map +1 -0
  225. package/dist/updates/types.js +2 -0
  226. package/dist/updates/types.js.map +1 -0
  227. package/dist/updates/update-bus.d.ts +29 -0
  228. package/dist/updates/update-bus.d.ts.map +1 -0
  229. package/dist/updates/update-bus.js +24 -0
  230. package/dist/updates/update-bus.js.map +1 -0
  231. package/dist/util/bytes.d.ts +12 -0
  232. package/dist/util/bytes.d.ts.map +1 -0
  233. package/dist/util/bytes.js +46 -0
  234. package/dist/util/bytes.js.map +1 -0
  235. package/package.json +84 -0
  236. package/src/auth/handshake.ts +262 -0
  237. package/src/auth/nonce-store.ts +39 -0
  238. package/src/bootstrap.ts +114 -0
  239. package/src/config.ts +103 -0
  240. package/src/core/context.ts +94 -0
  241. package/src/core/errors.ts +52 -0
  242. package/src/core/index.ts +4 -0
  243. package/src/core/rpc.ts +165 -0
  244. package/src/core/updates.ts +69 -0
  245. package/src/create-server.ts +181 -0
  246. package/src/crypto/aes-ige.ts +57 -0
  247. package/src/crypto/dh.ts +101 -0
  248. package/src/crypto/hashes.ts +17 -0
  249. package/src/crypto/msg-key.ts +29 -0
  250. package/src/crypto/rsa.ts +70 -0
  251. package/src/dispatch/dispatcher.ts +586 -0
  252. package/src/dispatch/forwarders/in-process.ts +14 -0
  253. package/src/dispatch/forwarders/print.ts +22 -0
  254. package/src/dispatch/rpc-forwarder.ts +15 -0
  255. package/src/dispatch/types.ts +60 -0
  256. package/src/gateway.ts +214 -0
  257. package/src/index.ts +53 -0
  258. package/src/lib.ts +24 -0
  259. package/src/server/message-pipeline.ts +256 -0
  260. package/src/session/inbound-tracker.ts +221 -0
  261. package/src/session/message-id.ts +43 -0
  262. package/src/session/salts.ts +162 -0
  263. package/src/session/session-manager.ts +66 -0
  264. package/src/storage/index.ts +26 -0
  265. package/src/storage/memory.ts +101 -0
  266. package/src/storage/mongo.ts +215 -0
  267. package/src/storage/types.ts +92 -0
  268. package/src/testkit.ts +19 -0
  269. package/src/tl/codec.ts +292 -0
  270. package/src/tl/layered-registry.ts +132 -0
  271. package/src/tl/protocol.ts +146 -0
  272. package/src/tl/reader.ts +99 -0
  273. package/src/tl/registry.ts +78 -0
  274. package/src/tl/writer.ts +104 -0
  275. package/src/transport/connection-registry.ts +91 -0
  276. package/src/transport/connection.ts +113 -0
  277. package/src/transport/framing.ts +223 -0
  278. package/src/transport/proxy-protocol.ts +109 -0
  279. package/src/transport/server-common.ts +49 -0
  280. package/src/transport/tcp-server.ts +102 -0
  281. package/src/transport/ws-server.ts +94 -0
  282. package/src/update-publisher.ts +47 -0
  283. package/src/updates/mongo-update-log.ts +49 -0
  284. package/src/updates/presence-binder.ts +51 -0
  285. package/src/updates/presence.ts +61 -0
  286. package/src/updates/push.ts +90 -0
  287. package/src/updates/redis-bus.ts +86 -0
  288. package/src/updates/redis-presence.ts +87 -0
  289. package/src/updates/render.ts +53 -0
  290. package/src/updates/router.ts +52 -0
  291. package/src/updates/types.ts +24 -0
  292. package/src/updates/update-bus.ts +49 -0
  293. package/src/util/bytes.ts +49 -0
@@ -0,0 +1,22 @@
1
+ import { noopLogger, type Logger } from '@mt-tl/tl'
2
+ import type { RpcForwarder, RpcRequest, RpcResponse } from '../rpc-forwarder.js'
3
+
4
+ /**
5
+ * Dev fallback forwarder, used when no app is registered: logs the JSON-RPC 2.0
6
+ * envelope the handler *would* receive (info level), then returns NOT_IMPLEMENTED.
7
+ * Wire a real app via `createServer(...).register(app)` to get an
8
+ * {@link InProcessForwarder}.
9
+ */
10
+ export class PrintForwarder implements RpcForwarder {
11
+ constructor(private readonly logger: Logger = noopLogger) {}
12
+
13
+ async forward(req: RpcRequest): Promise<RpcResponse> {
14
+ this.logger.info('rpc.print', {
15
+ id: req.id,
16
+ method: req.method,
17
+ params: req.params,
18
+ context: req.context,
19
+ })
20
+ return { error: { code: 501, message: 'NOT_IMPLEMENTED' } }
21
+ }
22
+ }
@@ -0,0 +1,15 @@
1
+ // The envelope types describe what the engine hands the handler layer.
2
+ export type { RpcContext, RpcRequest, RpcResponse, SessionEffect } from '@mt-tl/tl'
3
+ import type { RpcRequest, RpcResponse } from '@mt-tl/tl'
4
+
5
+ /**
6
+ * Bridge from a decoded business TL method to its handler. The framework is
7
+ * in-process, so there are two implementations:
8
+ * - {@link InProcessForwarder} — calls the app's `dispatchRpc` directly (prod
9
+ * and tests); what `createServer(...).listen()` wires.
10
+ * - `PrintForwarder` — the dev fallback when no app is registered; logs the
11
+ * decoded request envelope and returns NOT_IMPLEMENTED.
12
+ */
13
+ export interface RpcForwarder {
14
+ forward(req: RpcRequest): Promise<RpcResponse>
15
+ }
@@ -0,0 +1,60 @@
1
+ import type { Connection } from '../transport/connection.js'
2
+ import type { TlObject } from '@mt-tl/tl'
3
+ import type { AcceptResult } from '../session/inbound-tracker.js'
4
+
5
+ /** Per-message context extracted from the encrypted envelope. */
6
+ export interface MessageContext {
7
+ msgId: bigint
8
+ seqNo: number
9
+ sessionId: bigint
10
+ authKeyId: bigint
11
+ salt: bigint
12
+ }
13
+
14
+ export interface SendOptions {
15
+ /** Notification (msg_id ends in 3), not a direct response. */
16
+ isNotification?: boolean
17
+ /** Consumes a seqno slot and gets an odd seqno (default true). */
18
+ contentRelated?: boolean
19
+ }
20
+
21
+ /** Sends an encrypted message back to a connected client. */
22
+ export interface Responder {
23
+ sendEncrypted(conn: Connection, body: TlObject, opts?: SendOptions): void
24
+ }
25
+
26
+ /**
27
+ * Reply to a rejected inbound message (the non-`ok` result of
28
+ * `InboundTracker.accept`): `bad_msg_notification` for a coded violation,
29
+ * `msg_detailed_info` for a duplicate whose answer is cached, or nothing for a
30
+ * benign silent drop. Shared by the pipeline (outer envelope) and the dispatcher
31
+ * (container-inner messages).
32
+ */
33
+ export function replyToBadAccept(
34
+ responder: Responder,
35
+ conn: Connection,
36
+ result: Exclude<AcceptResult, { ok: true }>,
37
+ msgId: bigint,
38
+ seqNo: number,
39
+ ): void {
40
+ if ('code' in result) {
41
+ responder.sendEncrypted(
42
+ conn,
43
+ { _: 'bad_msg_notification', bad_msg_id: msgId, bad_msg_seqno: seqNo, error_code: result.code },
44
+ { contentRelated: false },
45
+ )
46
+ } else if ('detailed' in result) {
47
+ responder.sendEncrypted(
48
+ conn,
49
+ {
50
+ _: 'msg_detailed_info',
51
+ msg_id: msgId,
52
+ answer_msg_id: result.detailed.answerMsgId,
53
+ bytes: result.detailed.bytes,
54
+ status: 0,
55
+ },
56
+ { contentRelated: false },
57
+ )
58
+ }
59
+ // { drop: true } → no reply.
60
+ }
package/src/gateway.ts ADDED
@@ -0,0 +1,214 @@
1
+ import { protocolSchemaDir } from '@mt-tl/tl'
2
+ import { loadSchema } from './tl/registry.js'
3
+ import { TlCodec } from './tl/codec.js'
4
+ import { loadLayeredRegistry } from './tl/layered-registry.js'
5
+ import { loadRsaKeyPair } from './crypto/rsa.js'
6
+ import { createStorage, type Storage } from './storage/index.js'
7
+ import { NonceStore } from './auth/nonce-store.js'
8
+ import { Handshake } from './auth/handshake.js'
9
+ import { SaltService } from './session/salts.js'
10
+ import { Dispatcher } from './dispatch/dispatcher.js'
11
+ import { PrintForwarder } from './dispatch/forwarders/print.js'
12
+ import type { RpcForwarder } from './dispatch/rpc-forwarder.js'
13
+ import { MessagePipeline } from './server/message-pipeline.js'
14
+ import { MtprotoWsServer } from './transport/ws-server.js'
15
+ import { MtprotoTcpServer } from './transport/tcp-server.js'
16
+ import { Connection } from './transport/connection.js'
17
+ import { ConnectionRegistry } from './transport/connection-registry.js'
18
+ import { NodePresenceBinder, NoopPresenceBinder, type PresenceBinder } from './updates/presence-binder.js'
19
+ import { PushService } from './updates/push.js'
20
+ import type { Presence } from './updates/presence.js'
21
+ import type { UpdateBus } from './updates/update-bus.js'
22
+ import type { UpdateLog } from './core/updates.js'
23
+ import type { MTProtoConfig } from './config.js'
24
+ import { createLogger, type Logger } from '@mt-tl/tl'
25
+ import type { MigrationRegistry } from '@mt-tl/tl'
26
+ import type { KeyObject } from 'node:crypto'
27
+
28
+ export interface Gateway {
29
+ /** Present when `config.wsPort` is set. */
30
+ wsServer?: MtprotoWsServer
31
+ /** Present when `config.tcpPort` is set. */
32
+ tcpServer?: MtprotoTcpServer
33
+ pipeline: MessagePipeline
34
+ storage: Storage
35
+ registry: ConnectionRegistry
36
+ nodeId: string
37
+ fingerprint: bigint
38
+ /** Gateway's RSA public key (clients encrypt pq_inner_data with it). */
39
+ publicKey: KeyObject
40
+ stats: { constructors: number; methods: number; crcMismatches: number; layers: number[] }
41
+ /** Start every configured carrier. */
42
+ listen(): Promise<void>
43
+ close(): Promise<void>
44
+ }
45
+
46
+ export interface BuildOptions {
47
+ forwarder?: RpcForwarder
48
+ /** Structured logger for observability; defaults to env-configured. */
49
+ logger?: Logger
50
+ /** Enables server-push: presence map (Redis in prod, in-memory for tests). */
51
+ presence?: Presence
52
+ /** Update bus this node consumes routed deliveries from (Redis pub/sub in prod). */
53
+ bus?: UpdateBus
54
+ /** Per-predicate migration ladders (input up / output down). */
55
+ migrations?: MigrationRegistry
56
+ /** Durable pts log read by the engine to answer getState/getDifference when managed. */
57
+ updateLog?: UpdateLog
58
+ /** When true, the engine answers `updates.getState`/`getDifference` from `updateLog`. */
59
+ managedUpdates?: boolean
60
+ }
61
+
62
+ /**
63
+ * Wires the gateway from config: load schema -> codec, RSA key, storage,
64
+ * handshake, dispatcher, pipeline, and the WS carrier. Returns the assembled
65
+ * gateway; call `wsServer.listen()` to start accepting clients.
66
+ */
67
+ export async function buildGateway(config: MTProtoConfig, opts: BuildOptions = {}): Promise<Gateway> {
68
+ const logger = opts.logger ?? createLogger({ name: config.nodeId })
69
+
70
+ // Merge the framework's protocol schema with the app's business schema — the
71
+ // consumer ships only business `.tl`; the protocol layer lives in @mt-tl/tl.
72
+ const { registry, constructors, methods, crcMismatches } = loadSchema([
73
+ protocolSchemaDir,
74
+ config.schemaDir,
75
+ ])
76
+ const layeredAll = loadLayeredRegistry(config.schemaLayersDir)
77
+ const layered = layeredAll.hasLayers() ? layeredAll : undefined
78
+ // Decode-union: register every layer's constructor ids so older-layer clients
79
+ // decode by id (the name index keeps the newest, so encode is unaffected).
80
+ for (const def of layeredAll.allDefs()) registry.register(def)
81
+ const codec = new TlCodec(registry, layered)
82
+ const rsa = loadRsaKeyPair(config.rsaKeyPath)
83
+ const storage = await createStorage(config.storage)
84
+ const saltService = new SaltService(storage.salts)
85
+
86
+ const nonceStore = new NonceStore()
87
+ const handshake = new Handshake({
88
+ codec,
89
+ rsa,
90
+ storage,
91
+ saltService,
92
+ nonceStore,
93
+ defaultLayer: config.defaultLayer,
94
+ logger: logger.child({ scope: 'handshake' }),
95
+ })
96
+
97
+ const forwarder = opts.forwarder ?? new PrintForwarder(logger.child({ scope: 'rpc' }))
98
+
99
+ // Presence / server-push wiring (no-op unless a presence map is supplied).
100
+ const connRegistry = new ConnectionRegistry()
101
+ const binder: PresenceBinder = opts.presence
102
+ ? new NodePresenceBinder(config.nodeId, connRegistry, opts.presence)
103
+ : new NoopPresenceBinder()
104
+
105
+ const migrations = opts.migrations
106
+ const pipeline = new MessagePipeline({
107
+ codec,
108
+ storage,
109
+ handshake,
110
+ saltService,
111
+ defaultLayer: config.defaultLayer,
112
+ binder,
113
+ disableMsgKeyCheck: config.disableMsgKeyCheck,
114
+ disableSeqNoCheck: config.disableSeqNoCheck,
115
+ logger: logger.child({ scope: 'mtproto' }),
116
+ })
117
+ pipeline.dispatcher = new Dispatcher({
118
+ codec,
119
+ registry,
120
+ storage,
121
+ saltService,
122
+ responder: pipeline,
123
+ forwarder,
124
+ binder,
125
+ migrations,
126
+ logger: logger.child({ scope: 'rpc' }),
127
+ disableSeqNoCheck: config.disableSeqNoCheck,
128
+ updateLog: opts.updateLog,
129
+ managedUpdates: opts.managedUpdates,
130
+ allowedApiIds: config.allowedApiIds,
131
+ })
132
+
133
+ // Consume routed updates addressed to this node and push to local sockets.
134
+ if (opts.bus && opts.presence) {
135
+ const push = new PushService(
136
+ connRegistry,
137
+ pipeline,
138
+ layered,
139
+ migrations,
140
+ logger.child({ scope: 'push' }),
141
+ )
142
+ opts.bus.subscribeNode(config.nodeId, msg => {
143
+ if (msg.authKeyId !== undefined) push.deliverToAuthKey(msg.authKeyId, msg.update)
144
+ else if (msg.subject !== undefined) push.deliver(msg.subject, msg.update)
145
+ })
146
+ }
147
+
148
+ // Refresh presence TTL for locally-connected users + auth keys (heartbeat).
149
+ let heartbeat: ReturnType<typeof setInterval> | undefined
150
+ if (opts.presence) {
151
+ const presence = opts.presence
152
+ heartbeat = setInterval(
153
+ () => {
154
+ for (const subject of connRegistry.subjects())
155
+ void presence.add(subject, config.nodeId).catch(() => {})
156
+ for (const authKeyId of connRegistry.authKeys())
157
+ void presence.addAuthKey(authKeyId, config.nodeId).catch(() => {})
158
+ },
159
+ Math.max(5_000, Math.floor(config.updates.presenceTtlMs / 3)),
160
+ )
161
+ if (typeof heartbeat.unref === 'function') heartbeat.unref()
162
+ }
163
+
164
+ const handlers = {
165
+ onPacket: (packet: Buffer, conn: Connection) => pipeline.handlePacket(packet, conn),
166
+ onClose: (conn: Connection) => binder.unbind(conn),
167
+ }
168
+
169
+ const wsServer =
170
+ config.wsPort !== undefined
171
+ ? new MtprotoWsServer(
172
+ {
173
+ port: config.wsPort,
174
+ defaultLayer: config.defaultLayer,
175
+ trustProxy: config.trustProxy,
176
+ logger,
177
+ },
178
+ handlers,
179
+ )
180
+ : undefined
181
+ const tcpServer =
182
+ config.tcpPort !== undefined
183
+ ? new MtprotoTcpServer(
184
+ {
185
+ port: config.tcpPort,
186
+ defaultLayer: config.defaultLayer,
187
+ trustProxy: config.trustProxy,
188
+ logger,
189
+ },
190
+ handlers,
191
+ )
192
+ : undefined
193
+
194
+ return {
195
+ wsServer,
196
+ tcpServer,
197
+ pipeline,
198
+ storage,
199
+ registry: connRegistry,
200
+ nodeId: config.nodeId,
201
+ fingerprint: rsa.fingerprint,
202
+ publicKey: rsa.publicKey,
203
+ stats: { constructors, methods, crcMismatches, layers: layeredAll.layerNumbers() },
204
+ async listen() {
205
+ await Promise.all([wsServer?.listen(), tcpServer?.listen()])
206
+ },
207
+ async close() {
208
+ if (heartbeat) clearInterval(heartbeat)
209
+ wsServer?.close()
210
+ tcpServer?.close()
211
+ await storage.close()
212
+ },
213
+ }
214
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ // @mt-tl/server — the consumer-facing framework facade. Install this (plus
2
+ // @mt-tl/tl for codegen) to build an MTProto server: define routes, listen.
3
+ // The protocol engine (transport/crypto/session/dispatch) and the handler layer
4
+ // (registry/dispatch/hooks/context/errors, under ./core) live in this same
5
+ // package; this file re-exports just the consumer surface — not the internal
6
+ // event/job/runner machinery.
7
+
8
+ export * from './create-server.js' // createServer, definePlugin, MtprotoServer, Plugin, MethodOpts/Handler
9
+ export * from './update-publisher.js' // createUpdatePublisher
10
+
11
+ // Handler surface (curated from ./core).
12
+ export {
13
+ // errors → rpc_error
14
+ AppError,
15
+ BadRequestError,
16
+ AuthRequiredError,
17
+ NotFoundError,
18
+ FloodWaitError,
19
+ InternalError,
20
+ // hooks
21
+ defineHook,
22
+ type Hook,
23
+ // dispatch + registry (for tests / advanced wiring)
24
+ RpcRegistry,
25
+ dispatchRpc,
26
+ type DispatchDeps,
27
+ // context + typing
28
+ type HandlerCtx,
29
+ type RpcMethodSpec,
30
+ type RpcMethodMap,
31
+ type RpcModule,
32
+ type UpdateEmitter,
33
+ } from './core/index.js'
34
+
35
+ // The system config object the server takes.
36
+ export type { MTProtoConfig } from './lib.js'
37
+
38
+ // Structured logger (re-exported from @mt-tl/tl) — build one with createLogger
39
+ // and pass it to createServer, or use the same factory in your app code for a
40
+ // unified log style. Handlers get a per-request child via `ctx.log`; the server
41
+ // exposes its root logger as `app.log`. See docs/guide/observability.md.
42
+ export {
43
+ createLogger,
44
+ noopLogger,
45
+ type Logger,
46
+ type LogLevel,
47
+ type LoggerOptions,
48
+ type Fields,
49
+ } from './lib.js'
50
+
51
+ // Schema-version migration ladders (input `up` / output `down`) — pass via
52
+ // createServer(config, { migrations }). See docs/guide/releasing-a-version.md.
53
+ export { MigrationRegistry, type MigrationRung } from '@mt-tl/tl'
package/src/lib.ts ADDED
@@ -0,0 +1,24 @@
1
+ // The protocol ENGINE's internal API (transport/crypto/session/dispatch wiring).
2
+ // Consumers don't import this directly; they use `createServer()` (./create-server),
3
+ // which wraps it. Env-free and side-effect-free: the caller builds an MTProtoConfig.
4
+ export { bootstrap, type BootstrapOptions, type ForwardHandler, type UpdatePublish } from './bootstrap.js'
5
+ export { buildGateway, type Gateway, type BuildOptions } from './gateway.js'
6
+ export { type MTProtoConfig } from './config.js'
7
+ export { InProcessForwarder } from './dispatch/forwarders/in-process.js'
8
+ export {
9
+ createLogger,
10
+ noopLogger,
11
+ type Logger,
12
+ type LogLevel,
13
+ type LoggerOptions,
14
+ type Fields,
15
+ } from '@mt-tl/tl'
16
+ export type { RpcForwarder } from './dispatch/rpc-forwarder.js'
17
+ export type { RpcContext, RpcRequest, RpcResponse, SessionEffect } from '@mt-tl/tl'
18
+ export type { UpdateMessage, NodeDelivery } from './updates/types.js'
19
+
20
+ // Update-delivery adapters — the Redis pub/sub bus + presence behind the
21
+ // in-process push loop (multi-instance), and the router that fans updates to nodes.
22
+ export { createRedisPresence, type RedisPresenceHandle } from './updates/redis-presence.js'
23
+ export { createRedisUpdateBus, type RedisBusHandle } from './updates/redis-bus.js'
24
+ export { UpdateRouter, type RouterOptions } from './updates/router.js'
@@ -0,0 +1,256 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { noopLogger, type Logger } from '@mt-tl/tl'
3
+ import { TlReader } from '../tl/reader.js'
4
+ import { TlWriter } from '../tl/writer.js'
5
+ import type { TlCodec } from '../tl/codec.js'
6
+ import type { TlObject } from '@mt-tl/tl'
7
+ import type { Connection } from '../transport/connection.js'
8
+ import type { Storage } from '../storage/index.js'
9
+ import type { SaltService } from '../session/salts.js'
10
+ import { Handshake } from '../auth/handshake.js'
11
+ import { Dispatcher } from '../dispatch/dispatcher.js'
12
+ import { replyToBadAccept, type MessageContext, type Responder, type SendOptions } from '../dispatch/types.js'
13
+ import { ensureSession } from '../session/session-manager.js'
14
+ import { messageClass } from '../session/inbound-tracker.js'
15
+ import { igeDecrypt, igeEncrypt } from '../crypto/aes-ige.js'
16
+ import { generateMessageKey, computeMsgKey } from '../crypto/msg-key.js'
17
+ import { toBigIntLE, toBufferLE } from '../util/bytes.js'
18
+ import type { PresenceBinder } from '../updates/presence-binder.js'
19
+ import { NoopPresenceBinder } from '../updates/presence-binder.js'
20
+
21
+ export interface PipelineDeps {
22
+ codec: TlCodec
23
+ storage: Storage
24
+ handshake: Handshake
25
+ saltService: SaltService
26
+ defaultLayer: number
27
+ /** Presence/registry binder; defaults to a no-op (push disabled). */
28
+ binder?: PresenceBinder
29
+ /**
30
+ * Disable the inbound MTProto 2.0 `msg_key` integrity check.
31
+ *
32
+ * ⚠️ INSECURE — leave this `false` (the default). When `false`, every inbound
33
+ * encrypted message must carry a `msg_key` equal to the 2.0 recompute
34
+ * `SHA256(authKey[88:120] ‖ plaintext)[8:24]`, which authenticates the
35
+ * ciphertext (integrity + binding to the auth key). Setting it `true` makes the
36
+ * gateway accept any ciphertext that merely decrypts to a well-formed message,
37
+ * dropping that authentication — only enable it as a temporary interop shim for
38
+ * non-compliant clients (e.g. ones still on the MTProto 1.0 msg_key scheme; see
39
+ * docs/internals/msgkey-v1-quirk.md).
40
+ */
41
+ disableMsgKeyCheck?: boolean
42
+ /** Disable inbound seqno validation (bad_msg codes 32/34/35); default enforced.
43
+ * Interop shim for clients that don't set seqno to spec. */
44
+ disableSeqNoCheck?: boolean
45
+ /** Observability sink; defaults to a no-op logger. */
46
+ logger?: Logger
47
+ }
48
+
49
+ /**
50
+ * Central message pipeline. Routes plaintext (handshake) vs encrypted packets,
51
+ * performs MTProto 2.0 decrypt/encrypt, ensures the session, and hands decoded
52
+ * payloads to the dispatcher. Implements {@link Responder} so the dispatcher and
53
+ * session manager can send encrypted replies.
54
+ */
55
+ export class MessagePipeline implements Responder {
56
+ /** Set once after construction (the dispatcher needs this pipeline as its Responder). */
57
+ dispatcher!: Dispatcher
58
+ private readonly binder: PresenceBinder
59
+ private readonly log: Logger
60
+
61
+ constructor(private readonly deps: PipelineDeps) {
62
+ this.binder = deps.binder ?? new NoopPresenceBinder()
63
+ this.log = deps.logger ?? noopLogger
64
+ // Surface the insecure interop shim ONCE at startup, not per message.
65
+ if (deps.disableMsgKeyCheck) {
66
+ this.log.warn('enc.msgkey.disabled', {
67
+ insecure: true,
68
+ hint: 'inbound ciphertext integrity not verified; see docs/internals/msgkey-v1-quirk.md',
69
+ })
70
+ }
71
+ }
72
+
73
+ async handlePacket(packet: Buffer, conn: Connection): Promise<void> {
74
+ if (packet.length < 8) {
75
+ conn.close()
76
+ return
77
+ }
78
+ // Any inbound traffic resets a pending ping_delay_disconnect idle timer.
79
+ conn.resetDisconnect()
80
+ const isPlaintext = packet.readUInt32LE(0) === 0 && packet.readUInt32LE(4) === 0
81
+ if (isPlaintext) return this.handlePlaintext(packet, conn)
82
+ return this.handleEncrypted(packet, conn)
83
+ }
84
+
85
+ // --- plaintext (handshake) ---------------------------------------------
86
+
87
+ private async handlePlaintext(packet: Buffer, conn: Connection): Promise<void> {
88
+ // [auth_key_id=0 (8)][msg_id (8)][len (4)][body]
89
+ if (packet.length < 20) return conn.close()
90
+ const len = packet.readUInt32LE(16)
91
+ if (len < 4 || packet.length < 20 + len) return conn.close()
92
+
93
+ const reader = new TlReader(packet.subarray(20, 20 + len))
94
+ const id = reader.readUInt32()
95
+ if (!Handshake.isHandshakeId(id)) return
96
+
97
+ const res = await this.deps.handshake.handle(id, reader)
98
+ if (!res) return
99
+ if ('raw' in res) {
100
+ conn.send(res.raw)
101
+ return
102
+ }
103
+ this.sendPlain(conn, res.reply)
104
+ }
105
+
106
+ private sendPlain(conn: Connection, body: TlObject): void {
107
+ const bodyBuf = this.deps.codec.encode(body)
108
+ const w = new TlWriter(bodyBuf.length + 24)
109
+ w.writeLong(0n) // auth_key_id
110
+ w.writeLong(conn.nextMessageId())
111
+ w.writeUInt32(bodyBuf.length)
112
+ w.writeRaw(bodyBuf)
113
+ conn.send(w.toBuffer())
114
+ }
115
+
116
+ // --- encrypted ----------------------------------------------------------
117
+
118
+ private async handleEncrypted(packet: Buffer, conn: Connection): Promise<void> {
119
+ if (packet.length < 24) return conn.close()
120
+ const authKeyId = toBigIntLE(packet.subarray(0, 8))
121
+ const msgKey = packet.subarray(8, 24)
122
+ const ciphertext = packet.subarray(24)
123
+ if (ciphertext.length % 16 !== 0 || ciphertext.length === 0) {
124
+ this.log.debug('enc.badlen', { authKeyId, len: ciphertext.length })
125
+ return conn.close()
126
+ }
127
+
128
+ const rec = await this.deps.storage.authKeys.getById(authKeyId)
129
+ this.log.debug('enc.key', { authKeyId, found: !!rec, blocked: rec?.isBlocked })
130
+ if (!rec || rec.isBlocked) return conn.close()
131
+
132
+ const { aesKey, aesIv } = generateMessageKey(rec.key, msgKey, false)
133
+ const plain = igeDecrypt(ciphertext, aesKey, aesIv)
134
+
135
+ // Inbound msg_key integrity (MTProto 2.0): the packet's msg_key must equal
136
+ // SHA256(authKey[88:120] ‖ plaintext)[8:24]. This authenticates the ciphertext
137
+ // and binds it to the auth key. Disabling the check (deps.disableMsgKeyCheck)
138
+ // is insecure and only intended as a temporary interop shim for non-compliant
139
+ // clients — see docs/internals/msgkey-v1-quirk.md.
140
+ if (!this.deps.disableMsgKeyCheck) {
141
+ if (!computeMsgKey(rec.key, plain, false).equals(msgKey)) {
142
+ // Ciphertext failed integrity/binding — a forged or non-2.0 client.
143
+ this.log.warn('enc.msgkey.reject', { authKeyId })
144
+ return conn.close()
145
+ }
146
+ }
147
+
148
+ const r = new TlReader(plain)
149
+ const salt = r.readLong()
150
+ const sessionId = r.readLong()
151
+ const msgId = r.readLong()
152
+ const seqNo = r.readUInt32()
153
+ const len = r.readUInt32()
154
+ this.log.trace('enc.ok', { authKeyId, len })
155
+ if (len < 4 || len > plain.length) return conn.close()
156
+ const payload = r.read(len)
157
+
158
+ // Bind auth/session state to the connection.
159
+ conn.ctx.authKeyId = authKeyId
160
+ conn.ctx.authKey = rec.key
161
+ conn.ctx.sessionId = sessionId
162
+ if (rec.meta?.apiLayer && conn.ctx.apiLayer === this.deps.defaultLayer) {
163
+ conn.ctx.apiLayer = rec.meta.apiLayer
164
+ }
165
+
166
+ // Server-salt schedule: advertise the current salt and validate the one the
167
+ // client encrypted with. A wrong/expired salt earns a `bad_server_salt`
168
+ // carrying the current salt — the client re-sends with it — and we drop this
169
+ // message. See docs/internals/protocol-compliance.md.
170
+ const { current, valid } = await this.deps.saltService.resolve(authKeyId, salt)
171
+ conn.ctx.serverSalt = current
172
+ if (!valid) {
173
+ this.log.debug('salt.bad', { authKeyId })
174
+ this.sendEncrypted(
175
+ conn,
176
+ {
177
+ _: 'bad_server_salt',
178
+ bad_msg_id: msgId,
179
+ bad_msg_seqno: seqNo,
180
+ error_code: 48,
181
+ new_server_salt: current,
182
+ },
183
+ { contentRelated: false },
184
+ )
185
+ return
186
+ }
187
+
188
+ // Inbound msg_id / seqno validation (https://core.telegram.org/mtproto/description):
189
+ // a wrong/duplicate/out-of-window msg_id (or bad seqno) earns a
190
+ // `bad_msg_notification` and the message is dropped (not dispatched, no session
191
+ // touched). Runs after the salt gate so a salt re-send keeps its original
192
+ // msg_id. Content-relatedness is read from the payload's constructor id. See
193
+ // docs/internals/protocol-compliance.md.
194
+ const seqCheck = !this.deps.disableSeqNoCheck
195
+ const check = conn.tracker.accept(msgId, seqNo, {
196
+ ...messageClass(payload),
197
+ checkSeqNo: seqCheck,
198
+ checkOrder: seqCheck,
199
+ })
200
+ if (!check.ok) {
201
+ this.log.debug('msg.rejected', { authKeyId, result: check })
202
+ replyToBadAccept(this, conn, check, msgId, seqNo)
203
+ return
204
+ }
205
+
206
+ await ensureSession(
207
+ this.deps.storage,
208
+ this,
209
+ conn,
210
+ { sessionId, authKeyId, firstMsgId: msgId, subject: rec.subject ?? undefined },
211
+ this.log,
212
+ )
213
+
214
+ // Register presence: by auth key (any connection — enables anonymous push)
215
+ // and, for already-authorized keys, by subject.
216
+ this.binder.bindAuthKey(conn, authKeyId.toString())
217
+ if (conn.ctx.subject !== undefined) this.binder.bind(conn, conn.ctx.subject)
218
+
219
+ const ctx: MessageContext = { msgId, seqNo, sessionId, authKeyId, salt }
220
+ await this.dispatcher.dispatchPayload(payload, ctx, conn)
221
+ }
222
+
223
+ // --- Responder ----------------------------------------------------------
224
+
225
+ sendEncrypted(conn: Connection, body: TlObject, opts: SendOptions = {}): void {
226
+ const authKey = conn.ctx.authKey
227
+ const authKeyId = conn.ctx.authKeyId
228
+ if (!authKey || authKeyId === undefined) return
229
+
230
+ const bodyBuf = this.deps.codec.encode(body, conn.ctx.apiLayer)
231
+ const outMsgId = conn.nextMessageId(opts.isNotification)
232
+ // Cache the answer to a request so a later duplicate of it gets a
233
+ // `msg_detailed_info` (the request id is the `rpc_result.req_msg_id`).
234
+ if (body._ === 'rpc_result' && typeof body.req_msg_id === 'bigint') {
235
+ conn.tracker.recordAnswer(body.req_msg_id, outMsgId, bodyBuf.length)
236
+ }
237
+ const w = new TlWriter(bodyBuf.length + 32)
238
+ w.writeLong(conn.ctx.serverSalt ?? 0n)
239
+ w.writeLong(conn.ctx.sessionId ?? 0n)
240
+ w.writeLong(outMsgId)
241
+ w.writeUInt32(conn.nextSeqNo(opts.contentRelated ?? true))
242
+ w.writeUInt32(bodyBuf.length)
243
+ w.writeRaw(bodyBuf)
244
+ let plain = w.toBuffer()
245
+
246
+ // MTProto 2.0 padding: 12..1024 random bytes, total length divisible by 16.
247
+ const minPad = 12
248
+ const pad = minPad + ((16 - ((plain.length + minPad) % 16)) % 16)
249
+ plain = Buffer.concat([plain, randomBytes(pad)])
250
+
251
+ const msgKey = computeMsgKey(authKey, plain, true)
252
+ const { aesKey, aesIv } = generateMessageKey(authKey, msgKey, true)
253
+ const ciphertext = igeEncrypt(plain, aesKey, aesIv)
254
+ conn.send(Buffer.concat([toBufferLE(authKeyId, 8), msgKey, ciphertext]))
255
+ }
256
+ }