@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.

Potentially problematic release.


This version of @mt-tl/server might be problematic. Click here for more details.

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,165 @@
1
+ import { fromJson, toJson, noopLogger } from '@mt-tl/tl'
2
+ import type { Logger, RpcRequest, RpcResponse, TlValue } from '@mt-tl/tl'
3
+ import { AppError } from './errors.js'
4
+ import { createHandlerCtx, type HandlerCtx } from './context.js'
5
+ import type { UpdateEmitter } from './updates.js'
6
+
7
+ /**
8
+ * Shape of one TL method's I/O. An app's generated `RpcMethods` (one entry per
9
+ * method, `{ params, result }`) structurally matches `RpcMethodMap` — the
10
+ * framework is generic over it, not bound to any specific schema.
11
+ */
12
+ export interface RpcMethodSpec {
13
+ params: unknown
14
+ result: unknown
15
+ }
16
+ export type RpcMethodMap = Record<string, RpcMethodSpec>
17
+
18
+ /** A typed handler for one method `M` of the app's method map `RM`. */
19
+ export type RpcHandlerOf<RM extends Record<keyof RM, RpcMethodSpec>, M extends keyof RM> = (
20
+ params: RM[M]['params'],
21
+ ctx: HandlerCtx,
22
+ ) => Promise<RM[M]['result']>
23
+
24
+ /**
25
+ * A reusable pre-handler. Runs before the handler with the same `ctx`; throw an
26
+ * `AppError` to reject (→ `rpc_error`), or `ctx.set(...)` to pass data forward.
27
+ * Method-agnostic, so `params` is `unknown` (narrow if a hook needs them).
28
+ */
29
+ export type Hook = (params: unknown, ctx: HandlerCtx) => Promise<void> | void
30
+
31
+ /** Identity helper for authoring a reusable hook. */
32
+ export function defineHook(fn: Hook): Hook {
33
+ return fn
34
+ }
35
+
36
+ export type RpcEntryOf<RM extends Record<keyof RM, RpcMethodSpec>, M extends keyof RM> =
37
+ | RpcHandlerOf<RM, M>
38
+ | { auth?: boolean; preHandlers?: Hook[]; handler: RpcHandlerOf<RM, M> }
39
+
40
+ /** A fully-typed module of RPC handlers, keyed by the app's method names. */
41
+ export type RpcModuleOf<RM extends Record<keyof RM, RpcMethodSpec>> = { [M in keyof RM]?: RpcEntryOf<RM, M> }
42
+
43
+ // Runtime/erased shapes the registry consumes — any RpcModuleOf<RM> is assignable.
44
+ type LooseHandler = (params: never, ctx: HandlerCtx) => Promise<unknown>
45
+ type LooseEntry = LooseHandler | { auth?: boolean; preHandlers?: Hook[]; handler: LooseHandler }
46
+ /** Erased module shape — what module factories expose and the registry consumes. */
47
+ export type RpcModule = Record<string, LooseEntry | undefined>
48
+
49
+ /**
50
+ * Creates a `defineRpc` typed against the app's generated `RpcMethods`. Use once
51
+ * per app: `export const { defineRpc } = createRpc<RpcMethods>()`, then write
52
+ * modules with full param/result inference per method.
53
+ */
54
+ export function createRpc<RM extends Record<keyof RM, RpcMethodSpec>>() {
55
+ return {
56
+ defineRpc(mod: RpcModuleOf<RM>): RpcModuleOf<RM> {
57
+ return mod
58
+ },
59
+ }
60
+ }
61
+
62
+ /** Untyped authoring helper — for fixtures/demos with no generated types. */
63
+ export function defineRpc(mod: RpcModule): RpcModule {
64
+ return mod
65
+ }
66
+
67
+ interface Route {
68
+ handler: (params: unknown, ctx: HandlerCtx) => Promise<unknown>
69
+ auth: boolean
70
+ preHandlers: Hook[]
71
+ }
72
+
73
+ /**
74
+ * The method table behind a server. `createServer().method(...)` adds to one for
75
+ * you; you rarely touch it directly (pass your own via `createServer(config, {
76
+ * registry })` only for advanced wiring or tests).
77
+ */
78
+ export class RpcRegistry {
79
+ private routes = new Map<string, Route>()
80
+
81
+ /** Register a module of handlers (later entries override earlier same-named ones). */
82
+ add(mod: RpcModule): this {
83
+ for (const [name, entry] of Object.entries(mod)) {
84
+ if (!entry) continue
85
+ const route: Route =
86
+ typeof entry === 'function'
87
+ ? { handler: entry as Route['handler'], auth: true, preHandlers: [] }
88
+ : {
89
+ handler: entry.handler as Route['handler'],
90
+ auth: entry.auth ?? true,
91
+ preHandlers: entry.preHandlers ?? [],
92
+ }
93
+ this.routes.set(name, route)
94
+ }
95
+ return this
96
+ }
97
+
98
+ get(method: string): Route | undefined {
99
+ return this.routes.get(method)
100
+ }
101
+
102
+ /** Registered method names. */
103
+ methods(): string[] {
104
+ return [...this.routes.keys()]
105
+ }
106
+
107
+ get size(): number {
108
+ return this.routes.size
109
+ }
110
+ }
111
+
112
+ /** Process-wide dependencies {@link dispatchRpc} threads into each handler's `ctx`. */
113
+ export interface DispatchDeps {
114
+ updates: UpdateEmitter
115
+ /** Observability sink; a per-request child becomes `ctx.log`. Defaults to no-op. */
116
+ logger?: Logger
117
+ }
118
+
119
+ /**
120
+ * Routes a forwarded request to its handler and returns the gateway envelope.
121
+ * 404 for unknown methods, 401 for auth-required methods on an anonymous key,
122
+ * AppError → its code, anything else → 500. Effects accompany result or error.
123
+ */
124
+ export async function dispatchRpc(
125
+ registry: RpcRegistry,
126
+ request: RpcRequest,
127
+ deps: DispatchDeps,
128
+ ): Promise<RpcResponse> {
129
+ const route = registry.get(request.method)
130
+ if (!route) return { error: { code: 404, message: 'METHOD_NOT_FOUND' } }
131
+ if (route.auth && request.context.subject === undefined) {
132
+ return { error: { code: 401, message: 'AUTH_KEY_UNREGISTERED' } }
133
+ }
134
+
135
+ // Bind the full request identity onto every handler line: reqId (the client's
136
+ // msg_id) ties the context to one request, plus authKeyId/sessionId/subject.
137
+ const log = (deps.logger ?? noopLogger).child({
138
+ reqId: request.id,
139
+ method: request.method,
140
+ subject: request.context.subject,
141
+ authKeyId: request.context.authKeyId,
142
+ sessionId: request.context.sessionId,
143
+ })
144
+ const ctx = createHandlerCtx(request.context, deps.updates, log)
145
+ try {
146
+ const params = fromJson(request.params)
147
+ for (const hook of route.preHandlers) await hook(params, ctx)
148
+ const result = await route.handler(params, ctx)
149
+ return { result: toJson(result as TlValue), effects: effectsOf(ctx) }
150
+ } catch (err) {
151
+ if (err instanceof AppError) {
152
+ // Expected business rejection (→ rpc_error with the app's code).
153
+ log.debug('handler.reject', { code: err.code, error: err.message })
154
+ return { error: { code: err.code, message: err.message }, effects: effectsOf(ctx) }
155
+ }
156
+ // Unexpected throw — a real bug. Log it with the stack (errorStack-gated) so
157
+ // the failed request is traceable; the client only sees a generic 500.
158
+ log.error('handler.fail', { err })
159
+ return { error: { code: 500, message: 'INTERNAL' }, effects: effectsOf(ctx) }
160
+ }
161
+ }
162
+
163
+ function effectsOf(ctx: HandlerCtx) {
164
+ return ctx.effects.length ? [...ctx.effects] : undefined
165
+ }
@@ -0,0 +1,69 @@
1
+ import type { JsonValue } from '@mt-tl/tl'
2
+
3
+ /**
4
+ * Durable per-subject update log: assigns the next `pts` and persists the update.
5
+ * `updates.getDifference` reads from here. The gateway never sees pts — it only
6
+ * delivers live updates best-effort; correctness is this log + getDifference.
7
+ * Keyed by `subject` (your internal user id), like the rest of the push path.
8
+ */
9
+ export interface UpdateLog {
10
+ append(subject: string, update: JsonValue): Promise<{ pts: number }>
11
+ /** Updates with pts in (sincePts, +inf], for getDifference. */
12
+ since(subject: string, sincePts: number): Promise<Array<{ pts: number; update: JsonValue }>>
13
+ currentPts(subject: string): Promise<number>
14
+ }
15
+
16
+ /** Publishes a routed live update onto the update bus (in-memory, or Redis pub/sub in prod). */
17
+ export type UpdatePublish = (msg: {
18
+ subject?: string
19
+ authKeyId?: string
20
+ update: JsonValue
21
+ pts?: number
22
+ }) => Promise<void>
23
+
24
+ /**
25
+ * Emits a server update. `emit(subject, …)` is the common path: append to the
26
+ * durable log (assigns pts) then publish — this backs `ctx.push`. `emitToAuthKey`
27
+ * targets a specific (possibly anonymous) connection by auth key, with no pts
28
+ * (anonymous connections have no durable update state).
29
+ */
30
+ export interface UpdateEmitter {
31
+ emit(subject: string, update: JsonValue): Promise<void>
32
+ emitToAuthKey(authKeyId: string, update: JsonValue): Promise<void>
33
+ }
34
+
35
+ /** The default {@link UpdateEmitter}: log (for pts) then publish to the bus. */
36
+ export class LoggingUpdateEmitter implements UpdateEmitter {
37
+ constructor(
38
+ private readonly log: UpdateLog,
39
+ private readonly publish: UpdatePublish,
40
+ ) {}
41
+
42
+ async emit(subject: string, update: JsonValue): Promise<void> {
43
+ const { pts } = await this.log.append(subject, update)
44
+ await this.publish({ subject, update, pts })
45
+ }
46
+
47
+ async emitToAuthKey(authKeyId: string, update: JsonValue): Promise<void> {
48
+ await this.publish({ authKeyId, update })
49
+ }
50
+ }
51
+
52
+ /** In-memory update log (single process / tests). Use a Mongo impl in prod. */
53
+ export class InMemoryUpdateLog implements UpdateLog {
54
+ private bySubject = new Map<string, Array<{ pts: number; update: JsonValue }>>()
55
+
56
+ async append(subject: string, update: JsonValue): Promise<{ pts: number }> {
57
+ const list = this.bySubject.get(subject) ?? []
58
+ const pts = (list.at(-1)?.pts ?? 0) + 1
59
+ list.push({ pts, update })
60
+ this.bySubject.set(subject, list)
61
+ return { pts }
62
+ }
63
+ async since(subject: string, sincePts: number): Promise<Array<{ pts: number; update: JsonValue }>> {
64
+ return (this.bySubject.get(subject) ?? []).filter(e => e.pts > sincePts)
65
+ }
66
+ async currentPts(subject: string): Promise<number> {
67
+ return this.bySubject.get(subject)?.at(-1)?.pts ?? 0
68
+ }
69
+ }
@@ -0,0 +1,181 @@
1
+ import type { KeyObject } from 'node:crypto'
2
+ import { bootstrap, type MTProtoConfig, type Gateway, type UpdatePublish } from './lib.js'
3
+ import {
4
+ createLogger,
5
+ type Logger,
6
+ type MigrationRegistry,
7
+ type RpcRequest,
8
+ type RpcResponse,
9
+ } from '@mt-tl/tl'
10
+ import {
11
+ RpcRegistry,
12
+ dispatchRpc,
13
+ LoggingUpdateEmitter,
14
+ type Hook,
15
+ type HandlerCtx,
16
+ type RpcMethodSpec,
17
+ type UpdateEmitter,
18
+ } from './core/index.js'
19
+
20
+ /** Per-method options. */
21
+ export interface MethodOpts {
22
+ /** Require an authorized auth key (a bound `subject`). Defaults to `true`. */
23
+ auth?: boolean
24
+ /** Reusable pre-handlers, run in order before the handler (see `defineHook`). */
25
+ preHandlers?: Hook[]
26
+ }
27
+
28
+ /** A handler for method `M` of the app's generated method map `RM`. */
29
+ export type MethodHandler<RM, M extends keyof RM> = RM[M] extends RpcMethodSpec
30
+ ? (params: RM[M]['params'], ctx: HandlerCtx) => Promise<RM[M]['result']>
31
+ : never
32
+
33
+ /** A plugin: registers routes on the server, given its declared dependencies. */
34
+ export type Plugin<RM, D = void> = (app: MtprotoServer<RM>, deps: D) => void
35
+
36
+ /**
37
+ * The server instance (Fastify-style): register routes and listen. It wraps the
38
+ * MTProto engine + the in-process handler dispatch — there is no broker and no
39
+ * separate "worker" to wire. Generic over the app's generated `RpcMethods` so
40
+ * `.method()` infers `params`/`result` (and the method name) per method.
41
+ */
42
+ export interface MtprotoServer<RM = Record<string, RpcMethodSpec>> {
43
+ /** Register a route. `auth` defaults to true. */
44
+ method<M extends keyof RM>(name: M, handler: MethodHandler<RM, M>): this
45
+ method<M extends keyof RM>(name: M, opts: MethodOpts, handler: MethodHandler<RM, M>): this
46
+ /** Run a plugin, passing its dependencies by value (Style-A DI; omit for `void` deps). */
47
+ register<D = void>(plugin: Plugin<RM, D>, deps?: D): this
48
+ /** Open the configured transports (and the in-process push loop if enabled). */
49
+ listen(): Promise<void>
50
+ close(): Promise<void>
51
+ /** Dispatch a request against the registry without a socket — for tests. */
52
+ inject(req: RpcRequest): Promise<RpcResponse>
53
+ /** The server's root logger — use it (or `ctx.log` in a handler) for a unified log style. */
54
+ readonly log: Logger
55
+ /** Registered method names. */
56
+ readonly methods: string[]
57
+ /** Bound WebSocket port after {@link listen} (resolves `wsPort: 0`); else `undefined`. */
58
+ readonly wsPort: number | undefined
59
+ /** Bound raw-TCP port after {@link listen} (resolves `tcpPort: 0`); else `undefined`. */
60
+ readonly tcpPort: number | undefined
61
+ /** The server's RSA public key after {@link listen}; clients encrypt the handshake with it. */
62
+ readonly publicKey: KeyObject | undefined
63
+ }
64
+
65
+ const noopEmitter: UpdateEmitter = { async emit() {}, async emitToAuthKey() {} }
66
+
67
+ /**
68
+ * Creates an MTProto server (Fastify-style). Pass the {@link MTProtoConfig},
69
+ * register routes with `.method()` / plugins with `.register()`, then
70
+ * `await app.listen()`. The framework owns the whole protocol — transport,
71
+ * handshake, crypto, sessions, TL (de)serialization, layered encoding,
72
+ * server-push — you write methods.
73
+ *
74
+ * Type it with your generated `RpcMethods` (`createServer<RpcMethods>(config)`)
75
+ * so every route's name, `params`, and `result` are checked.
76
+ *
77
+ * @param config - the server configuration (your app builds it from env).
78
+ * @param opts.registry - adopt an existing {@link RpcRegistry} instead of a fresh one (advanced/tests).
79
+ * @param opts.migrations - per-layer migration ladders applied on input/output.
80
+ * @param opts.logger - structured logger; defaults to `createLogger({ name: config.nodeId })`
81
+ * (env-configured via `LOG_LEVEL`/`LOG_FORMAT`). Exposed as `app.log`; handlers get `ctx.log`.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const app = createServer<RpcMethods>(config)
86
+ * app.method('help.getConfig', { auth: false }, async () => ({ _: 'config' }))
87
+ * await app.listen() // opens the WS + raw-TCP carriers
88
+ * ```
89
+ */
90
+ export function createServer<RM = Record<string, RpcMethodSpec>>(
91
+ config: MTProtoConfig,
92
+ opts: { registry?: RpcRegistry; migrations?: MigrationRegistry; logger?: Logger } = {},
93
+ ): MtprotoServer<RM> {
94
+ const registry = opts.registry ?? new RpcRegistry()
95
+ const logger = opts.logger ?? createLogger({ name: config.nodeId })
96
+ let gateway: Gateway | undefined
97
+ let emitter: UpdateEmitter = noopEmitter
98
+
99
+ const app: MtprotoServer<RM> = {
100
+ method(
101
+ name: keyof RM,
102
+ optsOrHandler: MethodOpts | Function,
103
+ maybeHandler?: Function,
104
+ ): MtprotoServer<RM> {
105
+ const handler = (maybeHandler ?? optsOrHandler) as (
106
+ params: unknown,
107
+ ctx: HandlerCtx,
108
+ ) => Promise<unknown>
109
+ const methodOpts: MethodOpts = maybeHandler ? (optsOrHandler as MethodOpts) : {}
110
+ registry.add({
111
+ [name as string]: {
112
+ auth: methodOpts.auth ?? true,
113
+ preHandlers: methodOpts.preHandlers,
114
+ handler,
115
+ },
116
+ })
117
+ return app
118
+ },
119
+ register<D = void>(plugin: Plugin<RM, D>, deps?: D): MtprotoServer<RM> {
120
+ plugin(app, deps as D)
121
+ return app
122
+ },
123
+ async listen(): Promise<void> {
124
+ if (gateway) throw new Error('server already listening')
125
+ gateway = await bootstrap({
126
+ config,
127
+ migrations: opts.migrations,
128
+ logger,
129
+ createForward: (publish: UpdatePublish, updateLog) => {
130
+ // Handler-emitted updates (ctx.push / ctx.updates) flow to the push loop,
131
+ // logged (for pts) via the shared update log bootstrap provides.
132
+ emitter = new LoggingUpdateEmitter(updateLog, publish)
133
+ return req => dispatchRpc(registry, req, { updates: emitter, logger })
134
+ },
135
+ })
136
+ await gateway.listen()
137
+ },
138
+ async close(): Promise<void> {
139
+ await gateway?.close()
140
+ },
141
+ inject(req: RpcRequest): Promise<RpcResponse> {
142
+ return dispatchRpc(registry, req, { updates: emitter, logger })
143
+ },
144
+ get log(): Logger {
145
+ return logger
146
+ },
147
+ get methods(): string[] {
148
+ return registry.methods()
149
+ },
150
+ get wsPort(): number | undefined {
151
+ return gateway?.wsServer?.port
152
+ },
153
+ get tcpPort(): number | undefined {
154
+ return gateway?.tcpServer?.port
155
+ },
156
+ get publicKey(): KeyObject | undefined {
157
+ return gateway?.publicKey
158
+ },
159
+ }
160
+ return app
161
+ }
162
+
163
+ /**
164
+ * Authoring helper for a typed plugin — a function that registers a group of
165
+ * related routes, taking its dependencies by value (Style-A DI, like
166
+ * `fastify.register`). Pin it to your `RpcMethods` once (see your app's
167
+ * `framework.ts`) so routes inside infer their types.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * export const walletsPlugin = definePlugin<RpcMethods, { wallets: WalletService }>(
172
+ * (app, { wallets }) => {
173
+ * app.method('wallets.getBalance', async (_p, ctx) => wallets.balanceOf(ctx.subject!))
174
+ * },
175
+ * )
176
+ * app.register(walletsPlugin, { wallets: new WalletService() })
177
+ * ```
178
+ */
179
+ export function definePlugin<RM = Record<string, RpcMethodSpec>, D = void>(fn: Plugin<RM, D>): Plugin<RM, D> {
180
+ return fn
181
+ }
@@ -0,0 +1,57 @@
1
+ import { createCipheriv, createDecipheriv } from 'node:crypto'
2
+
3
+ /**
4
+ * AES-256 in IGE (Infinite Garble Extension) mode, as used by MTProto.
5
+ *
6
+ * Reimplemented on Node's `aes-256-ecb` (the existing server used CryptoJS's
7
+ * IGE). Verified byte-identical against the old lib via a known-answer test.
8
+ * Input length must be a multiple of 16; IV is 32 bytes (two blocks).
9
+ */
10
+
11
+ function xor16(a: Buffer, b: Buffer): Buffer {
12
+ const out = Buffer.allocUnsafe(16)
13
+ for (let i = 0; i < 16; i++) out[i] = a[i]! ^ b[i]!
14
+ return out
15
+ }
16
+
17
+ export function igeEncrypt(data: Buffer, key: Buffer, iv: Buffer): Buffer {
18
+ if (data.length % 16 !== 0) throw new Error('IGE: data length must be a multiple of 16')
19
+ if (iv.length !== 32) throw new Error('IGE: iv must be 32 bytes')
20
+ const cipher = createCipheriv('aes-256-ecb', key, null)
21
+ cipher.setAutoPadding(false)
22
+
23
+ let prevCipher = iv.subarray(0, 16)
24
+ let prevPlain = iv.subarray(16, 32)
25
+ const out = Buffer.allocUnsafe(data.length)
26
+
27
+ for (let i = 0; i < data.length; i += 16) {
28
+ const block = data.subarray(i, i + 16)
29
+ const enc = cipher.update(xor16(block, prevCipher))
30
+ const c = xor16(enc, prevPlain)
31
+ c.copy(out, i)
32
+ prevCipher = c
33
+ prevPlain = Buffer.from(block)
34
+ }
35
+ return out
36
+ }
37
+
38
+ export function igeDecrypt(data: Buffer, key: Buffer, iv: Buffer): Buffer {
39
+ if (data.length % 16 !== 0) throw new Error('IGE: data length must be a multiple of 16')
40
+ if (iv.length !== 32) throw new Error('IGE: iv must be 32 bytes')
41
+ const decipher = createDecipheriv('aes-256-ecb', key, null)
42
+ decipher.setAutoPadding(false)
43
+
44
+ let prevCipher = iv.subarray(0, 16)
45
+ let prevPlain = iv.subarray(16, 32)
46
+ const out = Buffer.allocUnsafe(data.length)
47
+
48
+ for (let i = 0; i < data.length; i += 16) {
49
+ const block = Buffer.from(data.subarray(i, i + 16))
50
+ const dec = decipher.update(xor16(block, prevPlain))
51
+ const p = xor16(dec, prevCipher)
52
+ p.copy(out, i)
53
+ prevCipher = block
54
+ prevPlain = p
55
+ }
56
+ return out
57
+ }
@@ -0,0 +1,101 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { toBigIntBE } from '../util/bytes.js'
3
+
4
+ /**
5
+ * Diffie-Hellman parameters and helpers for the auth-key exchange.
6
+ * The 2048-bit prime and generator are the exact values the existing server
7
+ * uses (`libs/mtproto-tools`), required for wire-compatibility.
8
+ */
9
+ export const DH_PRIME = Buffer.from(
10
+ 'C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F' +
11
+ '48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C37' +
12
+ '20FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F64' +
13
+ '2477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4' +
14
+ 'A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754' +
15
+ 'FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4' +
16
+ 'E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F' +
17
+ '0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B',
18
+ 'hex',
19
+ )
20
+ export const DH_PRIME_BIGINT = toBigIntBE(DH_PRIME)
21
+ export const DH_G = 3
22
+
23
+ export function modPow(base: bigint, exp: bigint, mod: bigint): bigint {
24
+ let result = 1n
25
+ let b = base % mod
26
+ let e = exp
27
+ while (e > 0n) {
28
+ if (e & 1n) result = (result * b) % mod
29
+ e >>= 1n
30
+ b = (b * b) % mod
31
+ }
32
+ return result
33
+ }
34
+
35
+ /** MTProto padding: smallest r >= 0 with (a + r) divisible by b. */
36
+ export function calculatePadding(a: number, b: number, min = 0): number {
37
+ let r = -a % b
38
+ while (r < 0 || (min && r < min)) r += b
39
+ return r + 0 // normalize -0 -> 0
40
+ }
41
+
42
+ // --- p, q factorization proof-of-work --------------------------------------
43
+
44
+ const MAX_INT = 0x7fffffffn
45
+ const MAX_LONG = 0x7fffffffffffffffn
46
+ const MR_WITNESSES = [2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n]
47
+
48
+ function isProbablePrime(n: bigint): boolean {
49
+ if (n < 2n) return false
50
+ for (const p of MR_WITNESSES) {
51
+ if (n === p) return true
52
+ if (n % p === 0n) return false
53
+ }
54
+ let d = n - 1n
55
+ let r = 0n
56
+ while ((d & 1n) === 0n) {
57
+ d >>= 1n
58
+ r++
59
+ }
60
+ for (const a of MR_WITNESSES) {
61
+ let x = modPow(a, d, n)
62
+ if (x === 1n || x === n - 1n) continue
63
+ let composite = true
64
+ for (let i = 0n; i < r - 1n; i++) {
65
+ x = (x * x) % n
66
+ if (x === n - 1n) {
67
+ composite = false
68
+ break
69
+ }
70
+ }
71
+ if (composite) return false
72
+ }
73
+ return true
74
+ }
75
+
76
+ function randomPrime31(): bigint {
77
+ for (;;) {
78
+ let n = BigInt(randomBytes(4).readUInt32BE(0) & 0x7fffffff)
79
+ n |= 1n
80
+ if (n < 3n || n > MAX_INT) continue
81
+ if (isProbablePrime(n)) return n
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Returns [p, q, pq] where p < q are ~31-bit primes and pq = p*q fits in a
87
+ * signed 64-bit long. Mirrors the existing server's `makePQ`.
88
+ */
89
+ export function makePQ(): { p: bigint; q: bigint; pq: Buffer } {
90
+ let a: bigint
91
+ let b: bigint
92
+ let ab: bigint
93
+ do {
94
+ a = randomPrime31()
95
+ b = randomPrime31()
96
+ ab = a * b
97
+ } while (a > MAX_INT || b > MAX_INT || ab > MAX_LONG)
98
+
99
+ const pq = Buffer.from(ab.toString(16).padStart(16, '0'), 'hex')
100
+ return a < b ? { p: a, q: b, pq } : { p: b, q: a, pq }
101
+ }
@@ -0,0 +1,17 @@
1
+ import { createHash, randomBytes } from 'node:crypto'
2
+ import { toBigIntBE } from '../util/bytes.js'
3
+
4
+ export { xorBuffers } from '../util/bytes.js'
5
+
6
+ export function sha1(buf: Buffer): Buffer {
7
+ return createHash('sha1').update(buf).digest()
8
+ }
9
+
10
+ export function sha256(buf: Buffer): Buffer {
11
+ return createHash('sha256').update(buf).digest()
12
+ }
13
+
14
+ /** Cryptographically-random bigint of the given bit width (multiple of 8). */
15
+ export function randomBigInt(bits: number): bigint {
16
+ return toBigIntBE(randomBytes(bits / 8))
17
+ }
@@ -0,0 +1,29 @@
1
+ import { sha256 } from './hashes.js'
2
+
3
+ /**
4
+ * MTProto 2.0 message-key derivation. `outgoing` selects the key half
5
+ * (server->client uses x=8). Ported from the existing `messageEncryption.js`;
6
+ * pinned by a known-answer test.
7
+ */
8
+ export function generateMessageKey(
9
+ authKey: Buffer,
10
+ msgKey: Buffer,
11
+ outgoing: boolean,
12
+ ): { aesKey: Buffer; aesIv: Buffer } {
13
+ const x = outgoing ? 8 : 0
14
+ const a = sha256(Buffer.concat([msgKey, authKey.subarray(x, x + 36)]))
15
+ const b = sha256(Buffer.concat([authKey.subarray(x + 40, x + 76), msgKey]))
16
+ return {
17
+ aesKey: Buffer.concat([a.subarray(0, 8), b.subarray(8, 24), a.subarray(24, 32)]),
18
+ aesIv: Buffer.concat([b.subarray(0, 8), a.subarray(8, 24), b.subarray(24, 32)]),
19
+ }
20
+ }
21
+
22
+ /**
23
+ * msg_key = SHA256(authKey[88+x : 88+x+32] ‖ plaintext)[8:24] (MTProto 2.0),
24
+ * where x = 8 for server->client (outgoing) and x = 0 for client->server.
25
+ */
26
+ export function computeMsgKey(authKey: Buffer, plaintext: Buffer, outgoing: boolean): Buffer {
27
+ const x = outgoing ? 8 : 0
28
+ return sha256(Buffer.concat([authKey.subarray(88 + x, 88 + x + 32), plaintext])).subarray(8, 24)
29
+ }