@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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/auth/handshake.d.ts +35 -0
- package/dist/auth/handshake.d.ts.map +1 -0
- package/dist/auth/handshake.js +208 -0
- package/dist/auth/handshake.js.map +1 -0
- package/dist/auth/nonce-store.d.ts +22 -0
- package/dist/auth/nonce-store.d.ts.map +1 -0
- package/dist/auth/nonce-store.js +23 -0
- package/dist/auth/nonce-store.js.map +1 -0
- package/dist/bootstrap.d.ts +36 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +82 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/core/context.d.ts +57 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +27 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/errors.d.ts +33 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/rpc.d.ts +83 -0
- package/dist/core/rpc.d.ts.map +1 -0
- package/dist/core/rpc.js +102 -0
- package/dist/core/rpc.js.map +1 -0
- package/dist/core/updates.d.ts +56 -0
- package/dist/core/updates.d.ts.map +1 -0
- package/dist/core/updates.js +34 -0
- package/dist/core/updates.js.map +1 -0
- package/dist/create-server.d.ts +89 -0
- package/dist/create-server.d.ts.map +1 -0
- package/dist/create-server.js +109 -0
- package/dist/create-server.js.map +1 -0
- package/dist/crypto/aes-ige.d.ts +3 -0
- package/dist/crypto/aes-ige.d.ts.map +1 -0
- package/dist/crypto/aes-ige.js +55 -0
- package/dist/crypto/aes-ige.js.map +1 -0
- package/dist/crypto/dh.d.ts +21 -0
- package/dist/crypto/dh.d.ts.map +1 -0
- package/dist/crypto/dh.js +99 -0
- package/dist/crypto/dh.js.map +1 -0
- package/dist/crypto/hashes.d.ts +6 -0
- package/dist/crypto/hashes.d.ts.map +1 -0
- package/dist/crypto/hashes.js +14 -0
- package/dist/crypto/hashes.js.map +1 -0
- package/dist/crypto/msg-key.d.ts +15 -0
- package/dist/crypto/msg-key.d.ts.map +1 -0
- package/dist/crypto/msg-key.js +24 -0
- package/dist/crypto/msg-key.js.map +1 -0
- package/dist/crypto/rsa.d.ts +27 -0
- package/dist/crypto/rsa.d.ts.map +1 -0
- package/dist/crypto/rsa.js +50 -0
- package/dist/crypto/rsa.js.map +1 -0
- package/dist/dispatch/dispatcher.d.ts +72 -0
- package/dist/dispatch/dispatcher.d.ts.map +1 -0
- package/dist/dispatch/dispatcher.js +503 -0
- package/dist/dispatch/dispatcher.js.map +1 -0
- package/dist/dispatch/forwarders/in-process.d.ts +12 -0
- package/dist/dispatch/forwarders/in-process.d.ts.map +1 -0
- package/dist/dispatch/forwarders/in-process.js +15 -0
- package/dist/dispatch/forwarders/in-process.js.map +1 -0
- package/dist/dispatch/forwarders/print.d.ts +14 -0
- package/dist/dispatch/forwarders/print.d.ts.map +1 -0
- package/dist/dispatch/forwarders/print.js +23 -0
- package/dist/dispatch/forwarders/print.js.map +1 -0
- package/dist/dispatch/rpc-forwarder.d.ts +14 -0
- package/dist/dispatch/rpc-forwarder.d.ts.map +1 -0
- package/dist/dispatch/rpc-forwarder.js +2 -0
- package/dist/dispatch/rpc-forwarder.js.map +1 -0
- package/dist/dispatch/types.d.ts +32 -0
- package/dist/dispatch/types.d.ts.map +1 -0
- package/dist/dispatch/types.js +23 -0
- package/dist/dispatch/types.js.map +1 -0
- package/dist/gateway.d.ts +57 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +150 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +12 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +13 -0
- package/dist/lib.js.map +1 -0
- package/dist/server/message-pipeline.d.ts +57 -0
- package/dist/server/message-pipeline.d.ts.map +1 -0
- package/dist/server/message-pipeline.js +199 -0
- package/dist/server/message-pipeline.js.map +1 -0
- package/dist/session/inbound-tracker.d.ts +118 -0
- package/dist/session/inbound-tracker.d.ts.map +1 -0
- package/dist/session/inbound-tracker.js +170 -0
- package/dist/session/inbound-tracker.js.map +1 -0
- package/dist/session/message-id.d.ts +19 -0
- package/dist/session/message-id.d.ts.map +1 -0
- package/dist/session/message-id.js +30 -0
- package/dist/session/message-id.js.map +1 -0
- package/dist/session/salts.d.ts +51 -0
- package/dist/session/salts.d.ts.map +1 -0
- package/dist/session/salts.js +132 -0
- package/dist/session/salts.js.map +1 -0
- package/dist/session/session-manager.d.ts +18 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +43 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +16 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/memory.d.ts +3 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +91 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/mongo.d.ts +3 -0
- package/dist/storage/mongo.d.ts.map +1 -0
- package/dist/storage/mongo.js +175 -0
- package/dist/storage/mongo.js.map +1 -0
- package/dist/storage/types.d.ts +85 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +3 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/testkit.d.ts +11 -0
- package/dist/testkit.d.ts.map +1 -0
- package/dist/testkit.js +17 -0
- package/dist/testkit.js.map +1 -0
- package/dist/tl/codec.d.ts +37 -0
- package/dist/tl/codec.d.ts.map +1 -0
- package/dist/tl/codec.js +297 -0
- package/dist/tl/codec.js.map +1 -0
- package/dist/tl/layered-registry.d.ts +29 -0
- package/dist/tl/layered-registry.d.ts.map +1 -0
- package/dist/tl/layered-registry.js +118 -0
- package/dist/tl/layered-registry.js.map +1 -0
- package/dist/tl/protocol.d.ts +126 -0
- package/dist/tl/protocol.d.ts.map +1 -0
- package/dist/tl/protocol.js +22 -0
- package/dist/tl/protocol.js.map +1 -0
- package/dist/tl/reader.d.ts +30 -0
- package/dist/tl/reader.d.ts.map +1 -0
- package/dist/tl/reader.js +87 -0
- package/dist/tl/reader.js.map +1 -0
- package/dist/tl/registry.d.ts +39 -0
- package/dist/tl/registry.d.ts.map +1 -0
- package/dist/tl/registry.js +52 -0
- package/dist/tl/registry.js.map +1 -0
- package/dist/tl/writer.d.ts +24 -0
- package/dist/tl/writer.d.ts.map +1 -0
- package/dist/tl/writer.js +95 -0
- package/dist/tl/writer.js.map +1 -0
- package/dist/transport/connection-registry.d.ts +31 -0
- package/dist/transport/connection-registry.d.ts.map +1 -0
- package/dist/transport/connection-registry.js +84 -0
- package/dist/transport/connection-registry.js.map +1 -0
- package/dist/transport/connection.d.ts +62 -0
- package/dist/transport/connection.d.ts.map +1 -0
- package/dist/transport/connection.js +77 -0
- package/dist/transport/connection.js.map +1 -0
- package/dist/transport/framing.d.ts +39 -0
- package/dist/transport/framing.d.ts.map +1 -0
- package/dist/transport/framing.js +212 -0
- package/dist/transport/framing.js.map +1 -0
- package/dist/transport/proxy-protocol.d.ts +23 -0
- package/dist/transport/proxy-protocol.d.ts.map +1 -0
- package/dist/transport/proxy-protocol.js +108 -0
- package/dist/transport/proxy-protocol.js.map +1 -0
- package/dist/transport/server-common.d.ts +27 -0
- package/dist/transport/server-common.d.ts.map +1 -0
- package/dist/transport/server-common.js +27 -0
- package/dist/transport/server-common.js.map +1 -0
- package/dist/transport/tcp-server.d.ts +26 -0
- package/dist/transport/tcp-server.d.ts.map +1 -0
- package/dist/transport/tcp-server.js +91 -0
- package/dist/transport/tcp-server.js.map +1 -0
- package/dist/transport/ws-server.d.ts +19 -0
- package/dist/transport/ws-server.d.ts.map +1 -0
- package/dist/transport/ws-server.js +78 -0
- package/dist/transport/ws-server.js.map +1 -0
- package/dist/update-publisher.d.ts +34 -0
- package/dist/update-publisher.d.ts.map +1 -0
- package/dist/update-publisher.js +29 -0
- package/dist/update-publisher.js.map +1 -0
- package/dist/updates/mongo-update-log.d.ts +13 -0
- package/dist/updates/mongo-update-log.d.ts.map +1 -0
- package/dist/updates/mongo-update-log.js +39 -0
- package/dist/updates/mongo-update-log.js.map +1 -0
- package/dist/updates/presence-binder.d.ts +29 -0
- package/dist/updates/presence-binder.d.ts.map +1 -0
- package/dist/updates/presence-binder.js +36 -0
- package/dist/updates/presence-binder.js.map +1 -0
- package/dist/updates/presence.d.ts +31 -0
- package/dist/updates/presence.d.ts.map +1 -0
- package/dist/updates/presence.js +44 -0
- package/dist/updates/presence.js.map +1 -0
- package/dist/updates/push.d.ts +25 -0
- package/dist/updates/push.d.ts.map +1 -0
- package/dist/updates/push.js +72 -0
- package/dist/updates/push.js.map +1 -0
- package/dist/updates/redis-bus.d.ts +45 -0
- package/dist/updates/redis-bus.d.ts.map +1 -0
- package/dist/updates/redis-bus.js +59 -0
- package/dist/updates/redis-bus.js.map +1 -0
- package/dist/updates/redis-presence.d.ts +43 -0
- package/dist/updates/redis-presence.d.ts.map +1 -0
- package/dist/updates/redis-presence.js +65 -0
- package/dist/updates/redis-presence.js.map +1 -0
- package/dist/updates/render.d.ts +16 -0
- package/dist/updates/render.d.ts.map +1 -0
- package/dist/updates/render.js +46 -0
- package/dist/updates/render.js.map +1 -0
- package/dist/updates/router.d.ts +27 -0
- package/dist/updates/router.d.ts.map +1 -0
- package/dist/updates/router.js +36 -0
- package/dist/updates/router.js.map +1 -0
- package/dist/updates/types.d.ts +23 -0
- package/dist/updates/types.d.ts.map +1 -0
- package/dist/updates/types.js +2 -0
- package/dist/updates/types.js.map +1 -0
- package/dist/updates/update-bus.d.ts +29 -0
- package/dist/updates/update-bus.d.ts.map +1 -0
- package/dist/updates/update-bus.js +24 -0
- package/dist/updates/update-bus.js.map +1 -0
- package/dist/util/bytes.d.ts +12 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +46 -0
- package/dist/util/bytes.js.map +1 -0
- package/package.json +84 -0
- package/src/auth/handshake.ts +262 -0
- package/src/auth/nonce-store.ts +39 -0
- package/src/bootstrap.ts +114 -0
- package/src/config.ts +103 -0
- package/src/core/context.ts +94 -0
- package/src/core/errors.ts +52 -0
- package/src/core/index.ts +4 -0
- package/src/core/rpc.ts +165 -0
- package/src/core/updates.ts +69 -0
- package/src/create-server.ts +181 -0
- package/src/crypto/aes-ige.ts +57 -0
- package/src/crypto/dh.ts +101 -0
- package/src/crypto/hashes.ts +17 -0
- package/src/crypto/msg-key.ts +29 -0
- package/src/crypto/rsa.ts +70 -0
- package/src/dispatch/dispatcher.ts +586 -0
- package/src/dispatch/forwarders/in-process.ts +14 -0
- package/src/dispatch/forwarders/print.ts +22 -0
- package/src/dispatch/rpc-forwarder.ts +15 -0
- package/src/dispatch/types.ts +60 -0
- package/src/gateway.ts +214 -0
- package/src/index.ts +53 -0
- package/src/lib.ts +24 -0
- package/src/server/message-pipeline.ts +256 -0
- package/src/session/inbound-tracker.ts +221 -0
- package/src/session/message-id.ts +43 -0
- package/src/session/salts.ts +162 -0
- package/src/session/session-manager.ts +66 -0
- package/src/storage/index.ts +26 -0
- package/src/storage/memory.ts +101 -0
- package/src/storage/mongo.ts +215 -0
- package/src/storage/types.ts +92 -0
- package/src/testkit.ts +19 -0
- package/src/tl/codec.ts +292 -0
- package/src/tl/layered-registry.ts +132 -0
- package/src/tl/protocol.ts +146 -0
- package/src/tl/reader.ts +99 -0
- package/src/tl/registry.ts +78 -0
- package/src/tl/writer.ts +104 -0
- package/src/transport/connection-registry.ts +91 -0
- package/src/transport/connection.ts +113 -0
- package/src/transport/framing.ts +223 -0
- package/src/transport/proxy-protocol.ts +109 -0
- package/src/transport/server-common.ts +49 -0
- package/src/transport/tcp-server.ts +102 -0
- package/src/transport/ws-server.ts +94 -0
- package/src/update-publisher.ts +47 -0
- package/src/updates/mongo-update-log.ts +49 -0
- package/src/updates/presence-binder.ts +51 -0
- package/src/updates/presence.ts +61 -0
- package/src/updates/push.ts +90 -0
- package/src/updates/redis-bus.ts +86 -0
- package/src/updates/redis-presence.ts +87 -0
- package/src/updates/render.ts +53 -0
- package/src/updates/router.ts +52 -0
- package/src/updates/types.ts +24 -0
- package/src/updates/update-bus.ts +49 -0
- 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
|
+
}
|
package/src/bootstrap.ts
ADDED
|
@@ -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
|
+
}
|