@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,215 @@
1
+ import { MongoClient, Binary, type Db, type Collection, type AnyBulkWriteOperation } from 'mongodb'
2
+ import type {
3
+ AuthKeyMeta,
4
+ AuthKeyRecord,
5
+ AuthKeyRepo,
6
+ SaltRepo,
7
+ SaltScheduleEntry,
8
+ SessionRecord,
9
+ SessionRepo,
10
+ Storage,
11
+ } from './types.js'
12
+
13
+ /**
14
+ * MongoDB-backed storage. Uses the gateway's own collections (auth keys are not
15
+ * shared byte-for-byte with the legacy server's encoding — that's a migration
16
+ * concern, out of Phase-1 scope). bigints are stored as decimal strings.
17
+ */
18
+
19
+ interface AuthKeyDoc {
20
+ _id: string
21
+ key: Binary
22
+ expiresIn: boolean
23
+ createdAt: Date
24
+ subject: string | null
25
+ isBlocked?: boolean
26
+ meta?: AuthKeyRecord['meta']
27
+ }
28
+ interface SaltDoc {
29
+ /** `${authKeyId}:${validSince}` — one document per scheduled window. */
30
+ _id: string
31
+ authKeyId: string
32
+ salt: string
33
+ validSince: number
34
+ validUntil: number
35
+ }
36
+ interface SessionDoc {
37
+ _id: string
38
+ authKeyId: string
39
+ uniqueId: string
40
+ apiLayer: number
41
+ subject?: string
42
+ lastActivity: number
43
+ }
44
+
45
+ function toBuf(b: Binary): Buffer {
46
+ return Buffer.from(b.buffer)
47
+ }
48
+
49
+ class MongoAuthKeyRepo implements AuthKeyRepo {
50
+ constructor(private readonly col: Collection<AuthKeyDoc>) {}
51
+ async create(rec: AuthKeyRecord): Promise<void> {
52
+ await this.col.updateOne(
53
+ { _id: rec.id.toString() },
54
+ {
55
+ $set: {
56
+ key: new Binary(rec.key),
57
+ expiresIn: rec.expiresIn,
58
+ createdAt: rec.createdAt,
59
+ subject: rec.subject,
60
+ isBlocked: rec.isBlocked ?? false,
61
+ meta: rec.meta,
62
+ },
63
+ },
64
+ { upsert: true },
65
+ )
66
+ }
67
+ async getById(id: bigint): Promise<AuthKeyRecord | null> {
68
+ const doc = await this.col.findOne({ _id: id.toString() })
69
+ if (!doc) return null
70
+ return {
71
+ id,
72
+ key: toBuf(doc.key),
73
+ expiresIn: doc.expiresIn,
74
+ createdAt: doc.createdAt,
75
+ subject: doc.subject,
76
+ isBlocked: doc.isBlocked,
77
+ meta: doc.meta,
78
+ }
79
+ }
80
+ async setBlocked(id: bigint, blocked: boolean): Promise<void> {
81
+ await this.col.updateOne({ _id: id.toString() }, { $set: { isBlocked: blocked } })
82
+ }
83
+ async bindUser(id: bigint, subject: string | null): Promise<void> {
84
+ await this.col.updateOne({ _id: id.toString() }, { $set: { subject } })
85
+ }
86
+ async updateMeta(id: bigint, patch: AuthKeyMeta): Promise<void> {
87
+ // Set defined fields under dotted `meta.*` paths so we never clobber the
88
+ // sibling fields (e.g. `meta.apiLayer` set at handshake time).
89
+ const set: Record<string, unknown> = {}
90
+ for (const [k, v] of Object.entries(patch)) {
91
+ if (v !== undefined) set[`meta.${k}`] = v
92
+ }
93
+ if (Object.keys(set).length) await this.col.updateOne({ _id: id.toString() }, { $set: set })
94
+ }
95
+ }
96
+
97
+ class MongoSaltRepo implements SaltRepo {
98
+ constructor(private readonly col: Collection<SaltDoc>) {}
99
+ async append(authKeyId: bigint, entries: SaltScheduleEntry[]): Promise<void> {
100
+ if (!entries.length) return
101
+ const key = authKeyId.toString()
102
+ // $setOnInsert keeps the first writer's salt for a window, so all nodes
103
+ // that derive the same (deterministic) window converge on one salt.
104
+ const ops: AnyBulkWriteOperation<SaltDoc>[] = entries.map(e => ({
105
+ updateOne: {
106
+ filter: { _id: `${key}:${e.validSince}` },
107
+ update: {
108
+ $setOnInsert: {
109
+ authKeyId: key,
110
+ salt: e.salt.toString(),
111
+ validSince: e.validSince,
112
+ validUntil: e.validUntil,
113
+ },
114
+ },
115
+ upsert: true,
116
+ },
117
+ }))
118
+ await this.col.bulkWrite(ops, { ordered: false })
119
+ }
120
+ async list(authKeyId: bigint): Promise<SaltScheduleEntry[]> {
121
+ const docs = await this.col
122
+ .find({ authKeyId: authKeyId.toString() })
123
+ .sort({ validSince: 1 })
124
+ .toArray()
125
+ return docs.map(d => ({ salt: BigInt(d.salt), validSince: d.validSince, validUntil: d.validUntil }))
126
+ }
127
+ async prune(authKeyId: bigint, before: number): Promise<void> {
128
+ await this.col.deleteMany({ authKeyId: authKeyId.toString(), validUntil: { $lte: before } })
129
+ }
130
+ }
131
+
132
+ class MongoSessionRepo implements SessionRepo {
133
+ constructor(private readonly col: Collection<SessionDoc>) {}
134
+ async get(sessionId: bigint): Promise<SessionRecord | null> {
135
+ const doc = await this.col.findOne({ _id: sessionId.toString() })
136
+ if (!doc) return null
137
+ return {
138
+ sessionId,
139
+ authKeyId: BigInt(doc.authKeyId),
140
+ uniqueId: BigInt(doc.uniqueId),
141
+ apiLayer: doc.apiLayer,
142
+ subject: doc.subject,
143
+ lastActivity: doc.lastActivity,
144
+ }
145
+ }
146
+ async save(rec: SessionRecord): Promise<void> {
147
+ await this.col.updateOne(
148
+ { _id: rec.sessionId.toString() },
149
+ {
150
+ $set: {
151
+ authKeyId: rec.authKeyId.toString(),
152
+ uniqueId: rec.uniqueId.toString(),
153
+ apiLayer: rec.apiLayer,
154
+ subject: rec.subject,
155
+ lastActivity: rec.lastActivity,
156
+ },
157
+ },
158
+ { upsert: true },
159
+ )
160
+ }
161
+ async update(sessionId: bigint, patch: Partial<SessionRecord>): Promise<void> {
162
+ const set: Record<string, unknown> = {}
163
+ if (patch.apiLayer !== undefined) set.apiLayer = patch.apiLayer
164
+ if (patch.subject !== undefined) set.subject = patch.subject
165
+ if (patch.lastActivity !== undefined) set.lastActivity = patch.lastActivity
166
+ if (Object.keys(set).length) await this.col.updateOne({ _id: sessionId.toString() }, { $set: set })
167
+ }
168
+ async delete(sessionId: bigint): Promise<void> {
169
+ await this.col.deleteOne({ _id: sessionId.toString() })
170
+ }
171
+ async touch(sessionId: bigint): Promise<boolean> {
172
+ const res = await this.col.updateOne(
173
+ { _id: sessionId.toString() },
174
+ { $set: { lastActivity: Date.now() } },
175
+ )
176
+ return res.matchedCount > 0
177
+ }
178
+ }
179
+
180
+ export async function createMongoStorage(url: string, dbName: string): Promise<Storage> {
181
+ const client = new MongoClient(url)
182
+ await client.connect()
183
+ const db: Db = client.db(dbName)
184
+ await ensureIndexes(db)
185
+ return {
186
+ authKeys: new MongoAuthKeyRepo(db.collection<AuthKeyDoc>('authKeys')),
187
+ salts: new MongoSaltRepo(db.collection<SaltDoc>('serverSalts')),
188
+ sessions: new MongoSessionRepo(db.collection<SessionDoc>('sessions')),
189
+ async close() {
190
+ await client.close()
191
+ },
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Create the secondary indexes the protocol collections rely on. Idempotent
197
+ * (Mongo no-ops an index that already exists), so it's safe to run on every
198
+ * startup. `_id`-keyed lookups (get/save/touch by id) need no extra index.
199
+ */
200
+ async function ensureIndexes(db: Db): Promise<void> {
201
+ await Promise.all([
202
+ // serverSalts: list() filters by authKeyId + sorts by validSince; prune()
203
+ // filters by authKeyId + validUntil.
204
+ db.collection('serverSalts').createIndex({ authKeyId: 1, validSince: 1 }),
205
+ db.collection('serverSalts').createIndex({ authKeyId: 1, validUntil: 1 }),
206
+ // sessions: looked up / aggregated by the key, the user, and recency.
207
+ db.collection('sessions').createIndex({ authKeyId: 1 }),
208
+ db.collection('sessions').createIndex({ subject: 1 }),
209
+ db.collection('sessions').createIndex({ lastActivity: 1 }),
210
+ // authKeys: "all devices for a user", moderation, and creation-over-time.
211
+ db.collection('authKeys').createIndex({ subject: 1 }),
212
+ db.collection('authKeys').createIndex({ isBlocked: 1 }),
213
+ db.collection('authKeys').createIndex({ createdAt: 1 }),
214
+ ])
215
+ }
@@ -0,0 +1,92 @@
1
+ /** Persistence contracts for auth keys, server salts and sessions. */
2
+
3
+ /**
4
+ * Connection/device metadata for an auth key. An auth key is one device
5
+ * authorization (one app install), so this — captured from `initConnection` — is
6
+ * stable per key and lives here rather than being duplicated across the key's
7
+ * sessions. `apiLayer` is the last layer negotiated via `invokeWithLayer`,
8
+ * restored for a fresh session before it re-negotiates.
9
+ */
10
+ export interface AuthKeyMeta {
11
+ apiLayer?: number
12
+ /** `initConnection.api_id` — the client app id. */
13
+ apiId?: number
14
+ /** `initConnection.device_model`. */
15
+ deviceModel?: string
16
+ /** `initConnection.system_version`. */
17
+ systemVersion?: string
18
+ /** `initConnection.app_version`. */
19
+ appVersion?: string
20
+ /** `initConnection.system_lang_code`. */
21
+ systemLangCode?: string
22
+ /** `initConnection.lang_code`. */
23
+ langCode?: string
24
+ }
25
+
26
+ export interface AuthKeyRecord {
27
+ id: bigint
28
+ key: Buffer
29
+ expiresIn: boolean
30
+ createdAt: Date
31
+ /** The bound subject (your internal user id), or null for an anonymous key. */
32
+ subject: string | null
33
+ isBlocked?: boolean
34
+ meta?: AuthKeyMeta
35
+ }
36
+
37
+ export interface SessionRecord {
38
+ sessionId: bigint
39
+ authKeyId: bigint
40
+ uniqueId: bigint
41
+ apiLayer: number
42
+ subject?: string
43
+ lastActivity: number
44
+ }
45
+
46
+ export interface AuthKeyRepo {
47
+ create(rec: AuthKeyRecord): Promise<void>
48
+ getById(id: bigint): Promise<AuthKeyRecord | null>
49
+ setBlocked(id: bigint, blocked: boolean): Promise<void>
50
+ /** Bind (or clear, with null) the subject authorized on this auth key. */
51
+ bindUser(id: bigint, subject: string | null): Promise<void>
52
+ /** Merge `initConnection`-derived device/app fields into the key's meta. */
53
+ updateMeta(id: bigint, patch: AuthKeyMeta): Promise<void>
54
+ }
55
+
56
+ /** One scheduled server salt and its validity window (unix seconds). */
57
+ export interface SaltScheduleEntry {
58
+ salt: bigint
59
+ /** Inclusive lower bound (unix seconds). */
60
+ validSince: number
61
+ /** Exclusive upper bound (unix seconds). */
62
+ validUntil: number
63
+ }
64
+
65
+ export interface SaltRepo {
66
+ /**
67
+ * Persist salt-schedule entries for an auth key. Inserts only windows not
68
+ * already present (keyed by `validSince`) and never overwrites an existing
69
+ * window's salt, so concurrent gateway nodes converge on one salt per window.
70
+ */
71
+ append(authKeyId: bigint, entries: SaltScheduleEntry[]): Promise<void>
72
+ /** Full salt schedule for an auth key, ascending by `validSince`. */
73
+ list(authKeyId: bigint): Promise<SaltScheduleEntry[]>
74
+ /** Drop entries that fully expired before `before` (unix seconds). */
75
+ prune(authKeyId: bigint, before: number): Promise<void>
76
+ }
77
+
78
+ export interface SessionRepo {
79
+ get(sessionId: bigint): Promise<SessionRecord | null>
80
+ save(rec: SessionRecord): Promise<void>
81
+ update(sessionId: bigint, patch: Partial<SessionRecord>): Promise<void>
82
+ delete(sessionId: bigint): Promise<void>
83
+ /** Refresh last-activity; returns true if the session existed. */
84
+ touch(sessionId: bigint): Promise<boolean>
85
+ }
86
+
87
+ export interface Storage {
88
+ authKeys: AuthKeyRepo
89
+ salts: SaltRepo
90
+ sessions: SessionRepo
91
+ close(): Promise<void>
92
+ }
package/src/testkit.ts ADDED
@@ -0,0 +1,19 @@
1
+ // @mt-tl/server/testkit — protocol primitives a TEST CLIENT needs (TL codec +
2
+ // the client side of the MTProto 2.0 crypto). NOT part of the consumer surface:
3
+ // servers are built with `createServer` from the package root. This subpath
4
+ // exists so `@mt-tl/testing` can drive a real handshake + encrypted RPC
5
+ // against a gateway without deep-importing internal files. The crypto here is
6
+ // the same code pinned byte-for-byte by `test/crypto.kat.test.ts`.
7
+
8
+ export { TlReader } from './tl/reader.js'
9
+ export { TlWriter } from './tl/writer.js'
10
+ export { TlCodec } from './tl/codec.js'
11
+ export { loadSchema, type LoadSchemaResult } from './tl/registry.js'
12
+
13
+ export { igeEncrypt, igeDecrypt } from './crypto/aes-ige.js'
14
+ export { sha1 } from './crypto/hashes.js'
15
+ export { generateMessageKey, computeMsgKey } from './crypto/msg-key.js'
16
+ export { modPow } from './crypto/dh.js'
17
+ export { rsaEncryptNoPadding } from './crypto/rsa.js'
18
+
19
+ export { toBigIntBE, toBigIntLE, toBufferBE, toBufferLE, xorBuffers } from './util/bytes.js'
@@ -0,0 +1,292 @@
1
+ import type { TlDef, TlType } from '@mt-tl/tl'
2
+ import type { TlRegistry } from './registry.js'
3
+ import type { LayeredRegistry } from './layered-registry.js'
4
+ import { TlReader, BOOL_TRUE_ID, BOOL_FALSE_ID, VECTOR_ID } from './reader.js'
5
+ import { TlWriter } from './writer.js'
6
+ import type { TlObject, TlValue } from '@mt-tl/tl'
7
+
8
+ /**
9
+ * IR-driven (de)serializer for arbitrary TL values. Constructed types are tagged
10
+ * objects `{ _: name, ...fields }`. Hand-written protocol codecs (registered on
11
+ * the registry) take precedence for their ids; everything else routes here.
12
+ *
13
+ * When a {@link LayeredRegistry} is supplied, `encode(value, layer)` writes each
14
+ * type with the constructor id/fields valid at the client's layer (decoding
15
+ * stays global by id). Without a layer, the merged registry is used.
16
+ */
17
+ export class TlCodec {
18
+ /** Set transiently during a single `encode(value, layer)` call. */
19
+ private encodeLayer?: number
20
+
21
+ constructor(
22
+ private readonly registry: TlRegistry,
23
+ private readonly layered?: LayeredRegistry,
24
+ ) {}
25
+
26
+ encode(value: TlObject, layer?: number): Buffer {
27
+ this.encodeLayer = this.layered && layer !== undefined ? layer : undefined
28
+ try {
29
+ const w = new TlWriter()
30
+ this.writeObject(w, value)
31
+ return w.toBuffer()
32
+ } finally {
33
+ this.encodeLayer = undefined
34
+ }
35
+ }
36
+
37
+ decode(buf: Buffer): TlValue {
38
+ return this.readObject(new TlReader(buf))
39
+ }
40
+
41
+ /** Resolve the def to serialize a name with, honoring the active encode layer. */
42
+ private defForWrite(name: string): TlDef | undefined {
43
+ if (this.layered && this.encodeLayer !== undefined) {
44
+ const layerDef = this.layered.resolve(name, this.encodeLayer)
45
+ if (layerDef) return layerDef
46
+ }
47
+ return this.registry.getByName(name)
48
+ }
49
+
50
+ // --- boxed object (with constructor id) ---------------------------------
51
+
52
+ writeObject(w: TlWriter, value: TlValue): void {
53
+ if (typeof value === 'boolean') {
54
+ w.writeBool(value)
55
+ return
56
+ }
57
+ if (Array.isArray(value)) {
58
+ w.writeUInt32(VECTOR_ID)
59
+ w.writeUInt32(value.length)
60
+ for (const el of value) this.writeObject(w, el)
61
+ return
62
+ }
63
+ if (value && typeof value === 'object' && '_' in value) {
64
+ const obj = value as TlObject
65
+ const codec = this.protocolCodecByName(obj._)
66
+ if (codec) {
67
+ codec.write(w, obj)
68
+ return
69
+ }
70
+ const def = this.defForWrite(obj._)
71
+ if (!def) throw new Error(`Cannot serialize unknown TL type: ${obj._}`)
72
+ w.writeUInt32(def.idNum)
73
+ this.writeFields(w, def.params, obj)
74
+ return
75
+ }
76
+ throw new Error(`Cannot serialize value as boxed object: ${String(value)}`)
77
+ }
78
+
79
+ readObject(r: TlReader): TlValue {
80
+ const id = r.readUInt32()
81
+ if (id === BOOL_TRUE_ID) return true
82
+ if (id === BOOL_FALSE_ID) return false
83
+ if (id === VECTOR_ID) {
84
+ const count = r.readUInt32()
85
+ const out: TlValue[] = []
86
+ for (let i = 0; i < count; i++) out.push(this.readObject(r))
87
+ return out
88
+ }
89
+ const protocolCodec = this.registry.getProtocolCodec(id)
90
+ if (protocolCodec) return protocolCodec.read(r) as TlValue
91
+ const def = this.registry.getById(id)
92
+ if (!def) {
93
+ throw new Error(`Cannot read unknown TL id 0x${(id >>> 0).toString(16).padStart(8, '0')}`)
94
+ }
95
+ const obj: TlObject = { _: def.name }
96
+ this.readFields(r, def.params, obj)
97
+ return obj
98
+ }
99
+
100
+ // --- bare constructor (no id) -------------------------------------------
101
+
102
+ private writeBare(w: TlWriter, name: string, value: TlValue): void {
103
+ const def = this.defForWrite(name)
104
+ if (!def) throw new Error(`Cannot serialize unknown bare type: ${name}`)
105
+ this.writeFields(w, def.params, (value ?? { _: name }) as TlObject)
106
+ }
107
+
108
+ private readBare(r: TlReader, name: string): TlObject {
109
+ const def = this.registry.getByName(name)
110
+ if (!def) throw new Error(`Cannot read unknown bare type: ${name}`)
111
+ const obj: TlObject = { _: name }
112
+ this.readFields(r, def.params, obj)
113
+ return obj
114
+ }
115
+
116
+ // --- fields (with flags handling) ---------------------------------------
117
+
118
+ private writeFields(w: TlWriter, params: import('@mt-tl/tl').TlParam[], obj: TlObject): void {
119
+ const bitmaskFields = collectBitmaskFields(params)
120
+ const flags: Record<string, number> = {}
121
+ for (const p of params) {
122
+ if (p.type.kind !== 'flag') continue
123
+ const v = obj[p.name]
124
+ const present = p.type.inner.kind === 'true' ? v === true : v !== undefined && v !== null
125
+ if (present) flags[p.type.flagsField] = (flags[p.type.flagsField] ?? 0) | (1 << p.type.bit)
126
+ }
127
+
128
+ for (const p of params) {
129
+ const t = p.type
130
+ if (bitmaskFields.has(p.name)) {
131
+ // Bitmask field (`flags:#` or `flags:int`): write the value computed
132
+ // from which conditional fields are present.
133
+ w.writeUInt32((flags[p.name] ?? 0) >>> 0)
134
+ } else if (t.kind === 'flag') {
135
+ const set = ((flags[t.flagsField] ?? 0) >>> t.bit) & 1
136
+ if (set && t.inner.kind !== 'true') this.writeType(w, t.inner, obj[p.name])
137
+ } else {
138
+ this.writeType(w, t, obj[p.name])
139
+ }
140
+ }
141
+ }
142
+
143
+ private readFields(r: TlReader, params: import('@mt-tl/tl').TlParam[], obj: TlObject): void {
144
+ const bitmaskFields = collectBitmaskFields(params)
145
+ const flags: Record<string, number> = {}
146
+ for (const p of params) {
147
+ const t = p.type
148
+ if (bitmaskFields.has(p.name)) {
149
+ const v = r.readUInt32()
150
+ flags[p.name] = v
151
+ obj[p.name] = v
152
+ } else if (t.kind === 'flag') {
153
+ const set = ((flags[t.flagsField] ?? 0) >>> t.bit) & 1
154
+ if (set) obj[p.name] = t.inner.kind === 'true' ? true : this.readType(r, t.inner)
155
+ } else {
156
+ obj[p.name] = this.readType(r, t)
157
+ }
158
+ }
159
+ }
160
+
161
+ // --- single typed value --------------------------------------------------
162
+
163
+ private writeType(w: TlWriter, t: TlType, value: TlValue): void {
164
+ switch (t.kind) {
165
+ case 'int':
166
+ w.writeInt32(Number(value ?? 0))
167
+ return
168
+ case 'long':
169
+ w.writeLong(typeof value === 'bigint' ? value : BigInt((value as number | string) ?? 0))
170
+ return
171
+ case 'double':
172
+ w.writeDouble(Number(value ?? 0))
173
+ return
174
+ case 'string':
175
+ // Binary may be supplied for a `string` field (the protocol schema
176
+ // declares pq/g_a/g_b/encrypted_data as `string`). Preserve bytes
177
+ // exactly; `string` and `bytes` share the same wire encoding.
178
+ if (Buffer.isBuffer(value)) w.writeBytes(value)
179
+ else w.writeString(value == null ? '' : String(value))
180
+ return
181
+ case 'bytes':
182
+ w.writeBytes(asBuffer(value))
183
+ return
184
+ case 'int128':
185
+ w.writeFixed(asBuffer(value), 16)
186
+ return
187
+ case 'int256':
188
+ w.writeFixed(asBuffer(value), 32)
189
+ return
190
+ case 'bool':
191
+ w.writeBool(Boolean(value))
192
+ return
193
+ case 'true':
194
+ return
195
+ case 'flags':
196
+ w.writeUInt32(Number(value ?? 0) >>> 0)
197
+ return
198
+ case 'vector':
199
+ this.writeVector(w, t, Array.isArray(value) ? value : [])
200
+ return
201
+ case 'object':
202
+ case 'boxed':
203
+ this.writeObject(w, value)
204
+ return
205
+ case 'bare':
206
+ this.writeBare(w, t.name, value)
207
+ return
208
+ case 'flag':
209
+ throw new Error('flag must be handled by writeFields')
210
+ }
211
+ }
212
+
213
+ private readType(r: TlReader, t: TlType): TlValue {
214
+ switch (t.kind) {
215
+ case 'int':
216
+ return r.readInt32()
217
+ case 'long':
218
+ return r.readLong()
219
+ case 'double':
220
+ return r.readDouble()
221
+ case 'string':
222
+ return r.readString()
223
+ case 'bytes':
224
+ return r.readBytes()
225
+ case 'int128':
226
+ return r.readInt128()
227
+ case 'int256':
228
+ return r.readInt256()
229
+ case 'bool':
230
+ return r.readUInt32() === BOOL_TRUE_ID
231
+ case 'true':
232
+ return true
233
+ case 'flags':
234
+ return r.readUInt32()
235
+ case 'vector':
236
+ return this.readVector(r, t)
237
+ case 'object':
238
+ case 'boxed':
239
+ return this.readObject(r)
240
+ case 'bare':
241
+ return this.readBare(r, t.name)
242
+ case 'flag':
243
+ throw new Error('flag must be handled by readFields')
244
+ }
245
+ }
246
+
247
+ private writeVector(w: TlWriter, t: Extract<TlType, { kind: 'vector' }>, arr: TlValue[]): void {
248
+ if (t.boxed) w.writeUInt32(VECTOR_ID)
249
+ w.writeUInt32(arr.length)
250
+ for (const el of arr) this.writeType(w, t.inner, el)
251
+ }
252
+
253
+ private readVector(r: TlReader, t: Extract<TlType, { kind: 'vector' }>): TlValue[] {
254
+ if (t.boxed) {
255
+ const id = r.readUInt32()
256
+ if (id !== VECTOR_ID) {
257
+ throw new Error(`Expected Vector id, got 0x${(id >>> 0).toString(16).padStart(8, '0')}`)
258
+ }
259
+ }
260
+ const count = r.readUInt32()
261
+ const out: TlValue[] = []
262
+ for (let i = 0; i < count; i++) out.push(this.readType(r, t.inner))
263
+ return out
264
+ }
265
+
266
+ private protocolCodecByName(name: string) {
267
+ const def = this.registry.getByName(name)
268
+ if (!def) return undefined
269
+ return this.registry.getProtocolCodec(def.idNum)
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Names of the bitmask fields in a param list: any `#` field plus any field
275
+ * referenced by a conditional (`name.bit?...`). Handles schemas that declare
276
+ * the bitmask as `flags:int` rather than the canonical `flags:#`.
277
+ */
278
+ function collectBitmaskFields(params: import('@mt-tl/tl').TlParam[]): Set<string> {
279
+ const names = new Set<string>()
280
+ for (const p of params) {
281
+ if (p.type.kind === 'flags') names.add(p.name)
282
+ else if (p.type.kind === 'flag') names.add(p.type.flagsField)
283
+ }
284
+ return names
285
+ }
286
+
287
+ function asBuffer(value: TlValue): Buffer {
288
+ if (Buffer.isBuffer(value)) return value
289
+ if (value == null) return Buffer.alloc(0)
290
+ if (typeof value === 'string') return Buffer.from(value, 'hex')
291
+ throw new Error(`Expected Buffer, got ${typeof value}`)
292
+ }