@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,262 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { noopLogger, type Logger } from '@mt-tl/tl'
3
+ import type { TlCodec } from '../tl/codec.js'
4
+ import type { TlObject } from '@mt-tl/tl'
5
+ import { TlReader } from '../tl/reader.js'
6
+ import type { Storage } from '../storage/index.js'
7
+ import type { SaltService } from '../session/salts.js'
8
+ import { NonceStore } from './nonce-store.js'
9
+ import { igeEncrypt, igeDecrypt } from '../crypto/aes-ige.js'
10
+ import { sha1, xorBuffers } from '../crypto/hashes.js'
11
+ import { rsaDecryptNoPadding, type RsaKeyPair } from '../crypto/rsa.js'
12
+ import { DH_G, DH_PRIME, DH_PRIME_BIGINT, makePQ, modPow, calculatePadding } from '../crypto/dh.js'
13
+ import { toBigIntBE, toBigIntLE, toBufferBE } from '../util/bytes.js'
14
+
15
+ // Immutable protocol constructor ids.
16
+ const ID_REQ_PQ = 0x60469778
17
+ const ID_REQ_PQ_MULTI = 0xbe7e8ef1
18
+ const ID_REQ_DH_PARAMS = 0xd712e4be
19
+ const ID_SET_CLIENT_DH_PARAMS = 0xf5045f1f
20
+
21
+ const ID_P_Q_INNER_DATA = 0x83c95aec
22
+ const ID_P_Q_INNER_DATA_DC = 0xa9f55f95
23
+ const ID_P_Q_INNER_DATA_TEMP = 0x3c6a84d4
24
+ const ID_P_Q_INNER_DATA_TEMP_DC = 0x56fddf88
25
+ const ID_CLIENT_DH_INNER_DATA = 0x6643b654
26
+
27
+ /** Raw -404 (regenerate key) sent when handshake state is missing/invalid. */
28
+ const ERR_404 = Buffer.from('6cfeffff', 'hex')
29
+
30
+ export interface HandshakeDeps {
31
+ codec: TlCodec
32
+ rsa: RsaKeyPair
33
+ storage: Storage
34
+ saltService: SaltService
35
+ nonceStore: NonceStore
36
+ defaultLayer: number
37
+ /** Observability sink; defaults to a no-op logger. */
38
+ logger?: Logger
39
+ }
40
+
41
+ export type HandshakeReply = { reply: TlObject } | { raw: Buffer } | null
42
+
43
+ export class Handshake {
44
+ private readonly logger: Logger
45
+
46
+ constructor(private readonly deps: HandshakeDeps) {
47
+ this.logger = deps.logger ?? noopLogger
48
+ }
49
+
50
+ static isHandshakeId(id: number): boolean {
51
+ return (
52
+ id === ID_REQ_PQ ||
53
+ id === ID_REQ_PQ_MULTI ||
54
+ id === ID_REQ_DH_PARAMS ||
55
+ id === ID_SET_CLIENT_DH_PARAMS
56
+ )
57
+ }
58
+
59
+ /** `reader` is positioned just after the 4-byte constructor id. */
60
+ async handle(id: number, reader: TlReader): Promise<HandshakeReply> {
61
+ try {
62
+ switch (id) {
63
+ case ID_REQ_PQ:
64
+ case ID_REQ_PQ_MULTI:
65
+ return this.handleReqPq(reader.readInt128())
66
+ case ID_REQ_DH_PARAMS:
67
+ return this.handleReqDhParams(reader)
68
+ case ID_SET_CLIENT_DH_PARAMS:
69
+ return this.handleSetClientDhParams(reader)
70
+ default:
71
+ return null
72
+ }
73
+ } catch (e) {
74
+ // A malformed/forged handshake step; reply -404 (regenerate key). Client
75
+ // fault, not server fault → warn, with the step id for correlation.
76
+ this.logger.warn('handshake.error', { step: id.toString(16), err: e })
77
+ return { raw: ERR_404 }
78
+ }
79
+ }
80
+
81
+ private handleReqPq(clientNonce: Buffer): HandshakeReply {
82
+ const serverNonce = randomBytes(16)
83
+ const { p, q, pq } = makePQ()
84
+ this.deps.nonceStore.set(clientNonce.toString('hex'), { clientNonce, serverNonce, p, q, pq })
85
+
86
+ const reply: TlObject = {
87
+ _: 'resPQ',
88
+ nonce: clientNonce,
89
+ server_nonce: serverNonce,
90
+ pq,
91
+ server_public_key_fingerprints: [this.deps.rsa.fingerprint],
92
+ }
93
+ return { reply }
94
+ }
95
+
96
+ private handleReqDhParams(reader: TlReader): HandshakeReply {
97
+ const nonce = reader.readInt128()
98
+ const serverNonce = reader.readInt128()
99
+ reader.readBytes() // p
100
+ reader.readBytes() // q
101
+ reader.readLong() // public_key_fingerprint
102
+ const encryptedData = reader.readBytes()
103
+
104
+ const nd = this.deps.nonceStore.get(nonce.toString('hex'))
105
+ if (!nd) return { raw: ERR_404 }
106
+
107
+ // RSA decrypt -> [0x00][sha1(20)][ctor id (4)][p_q_inner_data fields][padding]
108
+ const data = rsaDecryptNoPadding(this.deps.rsa.privateKey, encryptedData)
109
+ const inner = readPqInnerData(data.subarray(21))
110
+ if (!inner) return { raw: ERR_404 }
111
+
112
+ nd.newClientNonce = inner.newNonce
113
+ nd.expiresIn = inner.expiresIn ?? false
114
+
115
+ // server_DH_inner_data
116
+ const a = randomBelow(DH_PRIME_BIGINT)
117
+ nd.a = a
118
+ const gA = toBufferBE(modPow(BigInt(DH_G), a, DH_PRIME_BIGINT), 256)
119
+
120
+ const innerData: TlObject = {
121
+ _: 'server_DH_inner_data',
122
+ nonce,
123
+ server_nonce: serverNonce,
124
+ g: DH_G,
125
+ dh_prime: DH_PRIME,
126
+ g_a: gA,
127
+ server_time: Math.floor(Date.now() / 1000),
128
+ }
129
+ const innerBytes = this.deps.codec.encode(innerData)
130
+ const innerHash = sha1(innerBytes)
131
+ const padLen = calculatePadding(innerHash.length + innerBytes.length, 16)
132
+ const plainAnswer = Buffer.concat([
133
+ innerHash,
134
+ innerBytes,
135
+ padLen > 0 ? randomBytes(padLen) : Buffer.alloc(0),
136
+ ])
137
+
138
+ const { tmpAesKey, tmpAesIv } = deriveTmpAes(nd.newClientNonce, serverNonce)
139
+ nd.tmpAesKey = tmpAesKey
140
+ nd.tmpAesIv = tmpAesIv
141
+ this.deps.nonceStore.set(nonce.toString('hex'), nd)
142
+
143
+ const reply: TlObject = {
144
+ _: 'server_DH_params_ok',
145
+ nonce,
146
+ server_nonce: serverNonce,
147
+ encrypted_answer: igeEncrypt(plainAnswer, tmpAesKey, tmpAesIv),
148
+ }
149
+ return { reply }
150
+ }
151
+
152
+ private async handleSetClientDhParams(reader: TlReader): Promise<HandshakeReply> {
153
+ const nonce = reader.readInt128()
154
+ reader.readInt128() // server_nonce
155
+ const encryptedData = reader.readBytes()
156
+
157
+ const nd = this.deps.nonceStore.get(nonce.toString('hex'))
158
+ if (!nd || !nd.tmpAesKey || !nd.tmpAesIv || !nd.a || !nd.newClientNonce) {
159
+ return { raw: ERR_404 }
160
+ }
161
+
162
+ const data = igeDecrypt(encryptedData, nd.tmpAesKey, nd.tmpAesIv)
163
+ // [sha1(20)][ctor id (4)][client_DH_inner_data fields]
164
+ if (data.readUInt32LE(20) !== ID_CLIENT_DH_INNER_DATA) return { raw: ERR_404 }
165
+ const gB = readClientDhInner(data.subarray(24))
166
+
167
+ const key = toBufferBE(modPow(toBigIntBE(gB), nd.a, DH_PRIME_BIGINT), 256)
168
+ const keyHash = sha1(key)
169
+ const keyId = toBigIntLE(keyHash.subarray(-8))
170
+ // Wire-compat: the FIRST salt keeps its legacy xor(newNonce, serverNonce)
171
+ // derivation; it just becomes window 0 of the rolling schedule.
172
+ const serverSalt = toBigIntLE(
173
+ xorBuffers(nd.newClientNonce.subarray(0, 8), nd.serverNonce.subarray(0, 8)),
174
+ )
175
+
176
+ await this.deps.storage.authKeys.create({
177
+ id: keyId,
178
+ key,
179
+ expiresIn: nd.expiresIn ? true : false,
180
+ createdAt: new Date(),
181
+ subject: null,
182
+ meta: { apiLayer: this.deps.defaultLayer },
183
+ })
184
+ await this.deps.saltService.seed(keyId, serverSalt)
185
+ // A new auth key was negotiated and persisted (an anonymous key until a
186
+ // handler binds a user to it).
187
+ this.logger.info('authkey.create', { authKeyId: keyId, temp: !!nd.expiresIn })
188
+
189
+ const newNonceHash1 = sha1(
190
+ Buffer.concat([nd.newClientNonce, Buffer.from([1]), keyHash.subarray(0, 8)]),
191
+ ).subarray(-16)
192
+
193
+ this.deps.nonceStore.delete(nonce.toString('hex'))
194
+
195
+ const reply: TlObject = {
196
+ _: 'dh_gen_ok',
197
+ nonce,
198
+ server_nonce: nd.serverNonce,
199
+ new_nonce_hash1: Buffer.from(newNonceHash1),
200
+ }
201
+ return { reply }
202
+ }
203
+ }
204
+
205
+ // --- hand-decoders for the binary-bearing inner structures -----------------
206
+
207
+ interface PqInner {
208
+ newNonce: Buffer
209
+ expiresIn?: number
210
+ }
211
+
212
+ function readPqInnerData(buf: Buffer): PqInner | null {
213
+ const r = new TlReader(buf)
214
+ const id = r.readUInt32()
215
+ if (
216
+ id !== ID_P_Q_INNER_DATA &&
217
+ id !== ID_P_Q_INNER_DATA_DC &&
218
+ id !== ID_P_Q_INNER_DATA_TEMP &&
219
+ id !== ID_P_Q_INNER_DATA_TEMP_DC
220
+ ) {
221
+ return null
222
+ }
223
+ r.readBytes() // pq
224
+ r.readBytes() // p
225
+ r.readBytes() // q
226
+ r.readInt128() // nonce
227
+ r.readInt128() // server_nonce
228
+ const newNonce = r.readInt256()
229
+ if (id === ID_P_Q_INNER_DATA_DC || id === ID_P_Q_INNER_DATA_TEMP_DC) r.readInt32() // dc
230
+ let expiresIn: number | undefined
231
+ if (id === ID_P_Q_INNER_DATA_TEMP || id === ID_P_Q_INNER_DATA_TEMP_DC) expiresIn = r.readInt32()
232
+ return { newNonce, expiresIn }
233
+ }
234
+
235
+ /** Reads client_DH_inner_data fields (after its ctor id) and returns g_b bytes. */
236
+ function readClientDhInner(buf: Buffer): Buffer {
237
+ const r = new TlReader(buf)
238
+ r.readInt128() // nonce
239
+ r.readInt128() // server_nonce
240
+ r.readLong() // retry_id
241
+ return r.readBytes() // g_b
242
+ }
243
+
244
+ // --- crypto helpers ---------------------------------------------------------
245
+
246
+ function deriveTmpAes(newNonce: Buffer, serverNonce: Buffer): { tmpAesKey: Buffer; tmpAesIv: Buffer } {
247
+ const nsn = sha1(Buffer.concat([newNonce, serverNonce]))
248
+ const sns = sha1(Buffer.concat([serverNonce, newNonce]))
249
+ const nnn = sha1(Buffer.concat([newNonce, newNonce]))
250
+ return {
251
+ tmpAesKey: Buffer.concat([nsn, sns.subarray(0, 12)]),
252
+ tmpAesIv: Buffer.concat([sns.subarray(12, 20), nnn, newNonce.subarray(0, 4)]),
253
+ }
254
+ }
255
+
256
+ function randomBelow(limit: bigint): bigint {
257
+ let a: bigint
258
+ do {
259
+ a = toBigIntBE(randomBytes(256))
260
+ } while (a >= limit || a < 2n)
261
+ return a
262
+ }
@@ -0,0 +1,39 @@
1
+ /** In-flight auth-key-exchange state, keyed by the client nonce (hex). */
2
+ export interface NonceData {
3
+ clientNonce: Buffer
4
+ serverNonce: Buffer
5
+ newClientNonce?: Buffer
6
+ p?: bigint
7
+ q?: bigint
8
+ pq?: Buffer
9
+ /** server DH secret exponent */
10
+ a?: bigint
11
+ tmpAesKey?: Buffer
12
+ tmpAesIv?: Buffer
13
+ expiresIn?: number | false
14
+ timer?: NodeJS.Timeout
15
+ }
16
+
17
+ const TTL_MS = 10 * 60 * 1000 // 10 minutes (Telegram drops stale handshakes)
18
+
19
+ export class NonceStore {
20
+ private map = new Map<string, NonceData>()
21
+
22
+ set(nonceHex: string, data: NonceData): void {
23
+ const existing = this.map.get(nonceHex)
24
+ if (existing?.timer) clearTimeout(existing.timer)
25
+ data.timer = setTimeout(() => this.map.delete(nonceHex), TTL_MS)
26
+ if (typeof data.timer.unref === 'function') data.timer.unref()
27
+ this.map.set(nonceHex, data)
28
+ }
29
+
30
+ get(nonceHex: string): NonceData | undefined {
31
+ return this.map.get(nonceHex)
32
+ }
33
+
34
+ delete(nonceHex: string): void {
35
+ const existing = this.map.get(nonceHex)
36
+ if (existing?.timer) clearTimeout(existing.timer)
37
+ this.map.delete(nonceHex)
38
+ }
39
+ }
@@ -0,0 +1,114 @@
1
+ import { buildGateway, type BuildOptions, type Gateway } from './gateway.js'
2
+ import { InProcessForwarder } from './dispatch/forwarders/in-process.js'
3
+ import { InMemoryUpdateBus, type UpdateBus } from './updates/update-bus.js'
4
+ import { InMemoryPresence, type Presence } from './updates/presence.js'
5
+ import { createRedisPresence } from './updates/redis-presence.js'
6
+ import { createRedisUpdateBus } from './updates/redis-bus.js'
7
+ import { createMongoUpdateLog } from './updates/mongo-update-log.js'
8
+ import { UpdateRouter } from './updates/router.js'
9
+ import { InMemoryUpdateLog, type UpdateLog } from './core/updates.js'
10
+ import { createLogger, type Logger } from '@mt-tl/tl'
11
+ import type { MTProtoConfig } from './config.js'
12
+ import type { RpcRequest, RpcResponse } from './dispatch/rpc-forwarder.js'
13
+ import type { UpdateMessage } from './updates/types.js'
14
+ import type { MigrationRegistry } from '@mt-tl/tl'
15
+
16
+ /** The app's forward handler — typically `req => dispatchRpc(app.rpc, req, app.deps)`. */
17
+ export type ForwardHandler = (req: RpcRequest) => Promise<RpcResponse>
18
+
19
+ /** Publishes a server update onto the gateway's push loop (no-op when push is off). */
20
+ export type UpdatePublish = (msg: UpdateMessage) => Promise<void>
21
+
22
+ export interface BootstrapOptions {
23
+ config: MTProtoConfig
24
+ /**
25
+ * Builds the app's forward handler. Receives a `publish` wired to the
26
+ * gateway's in-process push loop and the shared {@link UpdateLog} (durable
27
+ * when `config.updates.managed`) — feed both into the app's update emitter
28
+ * (`new LoggingUpdateEmitter(updateLog, publish)`) so handler-emitted updates
29
+ * reach connected clients and, when managed, persist with a pts.
30
+ */
31
+ createForward: (publish: UpdatePublish, updateLog: UpdateLog) => ForwardHandler
32
+ logger?: Logger
33
+ /** Per-predicate migration ladders (input `up` / output `down`). */
34
+ migrations?: MigrationRegistry
35
+ }
36
+
37
+ /**
38
+ * The in-process-first entrypoint: runs the gateway and an app in ONE process.
39
+ * The app is reached via an {@link InProcessForwarder} (no broker). When
40
+ * `config.updates.enabled`, server-push is wired in-process: the app's
41
+ * `publish` → update bus → {@link UpdateRouter} (presence lookup) → this node →
42
+ * client. Uses an in-memory bus/presence for a single process, or Redis (pub/sub
43
+ * bus + presence) when `config.updates.redisUrl` is set (then scale
44
+ * horizontally). Returns the gateway; call `listen()`.
45
+ */
46
+ export async function bootstrap(opts: BootstrapOptions): Promise<Gateway> {
47
+ const logger = opts.logger ?? createLogger({ name: opts.config.nodeId })
48
+ const buildOpts: BuildOptions = { logger, migrations: opts.migrations }
49
+ const closers: Array<() => Promise<void>> = []
50
+ let publish: UpdatePublish = async () => {}
51
+
52
+ if (opts.config.updates.enabled) {
53
+ const presence = await makePresence(opts.config)
54
+ const bus = await makeBus(opts.config)
55
+ new UpdateRouter(bus.bus, presence.presence).start()
56
+ publish = msg => bus.bus.publishUpdate(msg)
57
+ buildOpts.presence = presence.presence
58
+ buildOpts.bus = bus.bus
59
+ closers.push(bus.close, presence.close)
60
+ logger.info('updates.inprocess', {
61
+ backend: opts.config.updates.redisUrl ? 'redis' : 'memory',
62
+ })
63
+ }
64
+
65
+ // Update state (pts log). Durable + engine-answered when `updates.managed`.
66
+ const updateLog = await makeUpdateLog(opts.config)
67
+ closers.push(updateLog.close)
68
+ buildOpts.updateLog = updateLog.log
69
+ buildOpts.managedUpdates = !!opts.config.updates.managed
70
+
71
+ buildOpts.forwarder = new InProcessForwarder(opts.createForward(publish, updateLog.log))
72
+ const gateway = await buildGateway(opts.config, buildOpts)
73
+
74
+ // Extend close() to also tear down the in-process update infra.
75
+ const closeGateway = gateway.close.bind(gateway)
76
+ gateway.close = async () => {
77
+ await closeGateway()
78
+ for (const close of closers) await close().catch(() => {})
79
+ }
80
+ return gateway
81
+ }
82
+
83
+ /** In-memory for a single process; Redis once `redisUrl` is set (multi-instance). */
84
+ async function makePresence(
85
+ config: MTProtoConfig,
86
+ ): Promise<{ presence: Presence; close: () => Promise<void> }> {
87
+ const u = config.updates
88
+ if (!u.redisUrl) return { presence: new InMemoryPresence(), close: async () => {} }
89
+ return createRedisPresence(u.redisUrl, u.presenceTtlMs)
90
+ }
91
+
92
+ async function makeBus(config: MTProtoConfig): Promise<{ bus: UpdateBus; close: () => Promise<void> }> {
93
+ const u = config.updates
94
+ if (!u.redisUrl) {
95
+ const bus = new InMemoryUpdateBus()
96
+ return { bus, close: () => bus.close() }
97
+ }
98
+ return createRedisUpdateBus(u.redisUrl)
99
+ }
100
+
101
+ /**
102
+ * The pts log behind `ctx.push` and (when `updates.managed`) `updates.getState`/
103
+ * `getDifference`. Durable on Mongo when managed + `storage.backend: 'mongo'`;
104
+ * in-memory otherwise (the emitter still uses it to stamp a pts).
105
+ */
106
+ async function makeUpdateLog(config: MTProtoConfig): Promise<{ log: UpdateLog; close: () => Promise<void> }> {
107
+ if (config.updates.managed && config.storage.backend === 'mongo') {
108
+ if (!config.storage.mongoUrl || !config.storage.mongoDb) {
109
+ throw new Error('updates.managed with mongo storage requires MONGO_URL and MONGO_DB')
110
+ }
111
+ return createMongoUpdateLog(config.storage.mongoUrl, config.storage.mongoDb)
112
+ }
113
+ return { log: new InMemoryUpdateLog(), close: async () => {} }
114
+ }
package/src/config.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { StorageBackend } from './storage/index.js'
2
+
3
+ /**
4
+ * The configuration object you pass to {@link createServer}. The framework reads
5
+ * **no** environment of its own — your app builds this (its composition root) and
6
+ * hands it in. Only `nodeId`, `defaultLayer`, `schemaDir`, `schemaLayersDir`,
7
+ * `storage`, and `updates` are required.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const config: MTProtoConfig = {
12
+ * nodeId: 'node-1',
13
+ * wsPort: 8081,
14
+ * defaultLayer: 204,
15
+ * schemaDir, // your business .tl
16
+ * schemaLayersDir: layersDir,
17
+ * rsaKeyPath: process.env.RSA_PRIVATE_KEY_PATH,
18
+ * storage: { backend: 'mongo', mongoUrl: process.env.MONGO_URL },
19
+ * updates: { enabled: true, redisUrl: process.env.REDIS_URL, presenceTtlMs: 60_000 },
20
+ * }
21
+ * ```
22
+ */
23
+ export interface MTProtoConfig {
24
+ /** Stable id of this instance, unique per replica — the presence routing key. */
25
+ nodeId: string
26
+ /** WebSocket listen port. Omit to disable the WS carrier. */
27
+ wsPort?: number
28
+ /** Raw-TCP listen port. Omit to disable the TCP carrier. */
29
+ tcpPort?: number
30
+ /** TL layer assumed for a connection until it negotiates one via `invokeWithLayer`. */
31
+ defaultLayer: number
32
+ /**
33
+ * Whitelist of accepted `initConnection.api_id`s. Omit (default) to accept any
34
+ * id. When set, an `initConnection` carrying an id outside the list is rejected
35
+ * with `rpc_error` `API_ID_INVALID` (400) and its wrapped query is not run —
36
+ * so an unregistered app can't reach your handlers.
37
+ */
38
+ allowedApiIds?: number[]
39
+ /** Directory of your business `.tl` schema (the protocol schema is bundled). */
40
+ schemaDir: string
41
+ /** Directory of per-layer snapshots (`scheme_N.json`) that drive layered encoding. */
42
+ schemaLayersDir: string
43
+ /**
44
+ * Path to the server's RSA private key (PEM). Clients pin its fingerprint, so a
45
+ * real client needs the production key here. Omitted → an ephemeral key is
46
+ * generated (handshake works only for test clients).
47
+ */
48
+ rsaKeyPath?: string
49
+ /**
50
+ * Disable the inbound MTProto 2.0 `msg_key` integrity check. ⚠️ INSECURE — keep
51
+ * `false` (the default). Only enable as a temporary interop shim for a
52
+ * non-compliant client. See docs/internals/msgkey-v1-quirk.md.
53
+ */
54
+ disableMsgKeyCheck?: boolean
55
+ /**
56
+ * Disable inbound sequence-number validation (`bad_msg_notification` codes
57
+ * 32/34/35). Default `false` = enforced: content-related messages (RPC queries)
58
+ * must carry an odd, strictly increasing `seqno` and pure service messages an
59
+ * even one. Set `true` as an interop shim for a client that does not set `seqno`
60
+ * to spec — the same escape-hatch pattern as {@link disableMsgKeyCheck}.
61
+ */
62
+ disableSeqNoCheck?: boolean
63
+ /**
64
+ * Trust an upstream proxy/load balancer for the client address: parse the
65
+ * PROXY-protocol header (v1/v2) on the raw-TCP carrier, and trust
66
+ * `X-Forwarded-For` on WebSocket. Default `false` — leave off when clients
67
+ * connect directly, since both are spoofable by a direct client. When `true`,
68
+ * the announced IP surfaces as `ctx.request.ip`.
69
+ */
70
+ trustProxy?: boolean
71
+ /** Where auth keys, server salts, and sessions persist. */
72
+ storage: {
73
+ /** `'memory'` (single process, dev) or `'mongo'` (shared, multi-replica). */
74
+ backend: StorageBackend
75
+ /** Mongo connection string (required when `backend: 'mongo'`). */
76
+ mongoUrl?: string
77
+ /** Mongo database name (defaults to the driver's database in the URL). */
78
+ mongoDb?: string
79
+ }
80
+ /** Server-push (updates) delivery. */
81
+ updates: {
82
+ /** Master switch for server-push. When `false`, `ctx.push` is a no-op. */
83
+ enabled: boolean
84
+ /**
85
+ * Redis URL for cross-instance presence + the pub/sub update bus. Omit for a
86
+ * single process (in-memory presence/bus); required to deliver push across
87
+ * replicas.
88
+ */
89
+ redisUrl?: string
90
+ /** Presence entry TTL in ms; the node refreshes it on a heartbeat. */
91
+ presenceTtlMs: number
92
+ /**
93
+ * Who owns the update state (`pts` + `updates.getState`/`getDifference`).
94
+ * `false` (default) → your app owns it: handle those methods yourself and
95
+ * embed `pts` in the updates you push. `true` → the engine owns it: it keeps
96
+ * a durable per-user pts log (Mongo when `storage.backend: 'mongo'`, else
97
+ * in-memory) and answers `updates.getState`/`updates.getDifference` itself.
98
+ * Requires the `updates.*` types in your schema. Common-pts only — no qts,
99
+ * seq, or per-channel pts.
100
+ */
101
+ managed?: boolean
102
+ }
103
+ }
@@ -0,0 +1,94 @@
1
+ import { noopLogger, type Logger, type RpcContext, type SessionEffect } from '@mt-tl/tl'
2
+ import type { UpdateEmitter } from './updates.js'
3
+
4
+ /**
5
+ * Handler-facing context: the request data plus session effects, server-push,
6
+ * and a per-request value bag. Business dependencies are NOT here — a handler
7
+ * (or a plugin) closes over its service (Style A DI), so `ctx` carries only
8
+ * request-scoped + cross-cutting concerns. Handlers stay thin: call the service,
9
+ * shape the result, optionally `login()`/`push()`.
10
+ */
11
+ export interface HandlerCtx {
12
+ /** Raw request context forwarded by the gateway (sessionId, authKeyId, ip, …). */
13
+ readonly request: RpcContext
14
+ /** The bound **subject** — your app's internal user id (opaque string, e.g. a
15
+ * uuid), shareable across your services. `undefined` if the auth key is
16
+ * anonymous; on an `auth: true` method it is guaranteed present, so
17
+ * `ctx.subject!` is safe there. Distinct from any wire `user_id:int` your TL
18
+ * schema exposes — map between them in your app (see the demo `users` module). */
19
+ readonly subject: string | undefined
20
+ /** The TL layer this request came in on (the client's negotiated layer).
21
+ * Read-only — layer negotiation is the protocol's job. Branch on it when an
22
+ * old client needs a different response shape. */
23
+ readonly layer: number
24
+ /** Per-request logger (a child bound with method/subject) — log with the same
25
+ * style as the framework so app and engine lines interleave coherently. */
26
+ readonly log: Logger
27
+ /** Low-level update emitter behind {@link push}; prefer `ctx.push(subject, update)`. */
28
+ readonly updates: UpdateEmitter
29
+ /** Collected session effects (applied by the gateway). */
30
+ readonly effects: readonly SessionEffect[]
31
+
32
+ // ── session effects ──────────────────────────────────────────────────────
33
+ /** Bind the auth key to a `subject` — your internal user id (device login). */
34
+ login(subject: string): void
35
+ /** Unbind the auth key (logout). */
36
+ logout(): void
37
+ /** Revoke the auth key entirely. */
38
+ revoke(): void
39
+
40
+ // ── server push ──────────────────────────────────────────────────────────
41
+ /** Push a TL update to a `subject` (delivered via the update bus to whatever node holds them). */
42
+ push(subject: string, update: unknown): Promise<void>
43
+ /**
44
+ * Push to a specific auth key — including an anonymous (not-logged-in)
45
+ * connection, e.g. to deliver API to a client before it registers. Pass
46
+ * `ctx.request.authKeyId` for the current connection. No pts (anonymous
47
+ * connections have no durable update state).
48
+ */
49
+ pushToAuthKey(authKeyId: string, update: unknown): Promise<void>
50
+
51
+ // ── per-request value bag (pre-handler → handler) ─────────────────────────
52
+ /** Stash a value for the handler (e.g. data a pre-handler already fetched). */
53
+ set(key: string, value: unknown): void
54
+ /** Read a value stashed earlier in this request. */
55
+ get<T = unknown>(key: string): T | undefined
56
+
57
+ // ── deprecated aliases (use login/logout/revoke) ──────────────────────────
58
+ /** @deprecated use {@link login} */
59
+ bindUser(subject: string): void
60
+ /** @deprecated use {@link logout} */
61
+ unbindUser(): void
62
+ /** @deprecated use {@link revoke} */
63
+ revokeKey(): void
64
+ }
65
+
66
+ export function createHandlerCtx(
67
+ request: RpcContext,
68
+ updates: UpdateEmitter,
69
+ log: Logger = noopLogger,
70
+ ): HandlerCtx {
71
+ const effects: SessionEffect[] = []
72
+ const bag = new Map<string, unknown>()
73
+ const login = (subject: string) => void effects.push({ type: 'bindUser', subject })
74
+ const logout = () => void effects.push({ type: 'unbindUser' })
75
+ const revoke = () => void effects.push({ type: 'revokeKey' })
76
+ return {
77
+ request,
78
+ subject: request.subject,
79
+ layer: request.apiLayer,
80
+ log,
81
+ updates,
82
+ effects,
83
+ login,
84
+ logout,
85
+ revoke,
86
+ push: (subject, update) => updates.emit(subject, update as never),
87
+ pushToAuthKey: (authKeyId, update) => updates.emitToAuthKey(authKeyId, update as never),
88
+ set: (key, value) => void bag.set(key, value),
89
+ get: <T>(key: string) => bag.get(key) as T | undefined,
90
+ bindUser: login,
91
+ unbindUser: logout,
92
+ revokeKey: revoke,
93
+ }
94
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Business errors. `code`/`message` map straight to the gateway's `rpc_error`
3
+ * (Telegram-style: 400/401/404/420/500…). Throw these from handlers; anything
4
+ * else becomes a 500 INTERNAL.
5
+ */
6
+ export class AppError extends Error {
7
+ constructor(
8
+ readonly code: number,
9
+ message: string,
10
+ ) {
11
+ super(message)
12
+ this.name = new.target.name
13
+ }
14
+ }
15
+
16
+ /** Invalid input or a violated precondition → `rpc_error 400`. */
17
+ export class BadRequestError extends AppError {
18
+ constructor(message = 'BAD_REQUEST') {
19
+ super(400, message)
20
+ }
21
+ }
22
+
23
+ /** Method requires an authorized user but the auth key is anonymous → `rpc_error 401`. */
24
+ export class AuthRequiredError extends AppError {
25
+ constructor(message = 'AUTH_KEY_UNREGISTERED') {
26
+ super(401, message)
27
+ }
28
+ }
29
+
30
+ /** The requested entity does not exist → `rpc_error 404`. */
31
+ export class NotFoundError extends AppError {
32
+ constructor(message = 'NOT_FOUND') {
33
+ super(404, message)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Rate-limited → `rpc_error 420 FLOOD_WAIT_<seconds>`. Clients read the number
39
+ * off the message and retry after that many seconds.
40
+ */
41
+ export class FloodWaitError extends AppError {
42
+ constructor(seconds: number) {
43
+ super(420, `FLOOD_WAIT_${seconds}`)
44
+ }
45
+ }
46
+
47
+ /** An unexpected server-side failure → `rpc_error 500`. */
48
+ export class InternalError extends AppError {
49
+ constructor(message = 'INTERNAL') {
50
+ super(500, message)
51
+ }
52
+ }
@@ -0,0 +1,4 @@
1
+ export * from './errors.js'
2
+ export * from './updates.js'
3
+ export * from './context.js'
4
+ export * from './rpc.js'