@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,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constants,
|
|
3
|
+
createPrivateKey,
|
|
4
|
+
createPublicKey,
|
|
5
|
+
generateKeyPairSync,
|
|
6
|
+
privateDecrypt,
|
|
7
|
+
publicEncrypt,
|
|
8
|
+
type KeyObject,
|
|
9
|
+
} from 'node:crypto'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { TlWriter } from '../tl/writer.js'
|
|
12
|
+
import { sha1 } from './hashes.js'
|
|
13
|
+
import { toBigIntLE } from '../util/bytes.js'
|
|
14
|
+
|
|
15
|
+
export interface RsaKeyPair {
|
|
16
|
+
privateKey: KeyObject
|
|
17
|
+
publicKey: KeyObject
|
|
18
|
+
/** Lower-64-bit key fingerprint advertised to clients in resPQ. */
|
|
19
|
+
fingerprint: bigint
|
|
20
|
+
fingerprintBuf: Buffer
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Telegram RSA fingerprint: take the bare TL serialization of the public key
|
|
25
|
+
* (modulus `n` and exponent `e` as `bytes`), SHA1 it, and use the last 8 bytes.
|
|
26
|
+
*/
|
|
27
|
+
export function computeFingerprint(publicKey: KeyObject): { fingerprint: bigint; buf: Buffer } {
|
|
28
|
+
const jwk = publicKey.export({ format: 'jwk' }) as { n: string; e: string }
|
|
29
|
+
const n = Buffer.from(jwk.n, 'base64url')
|
|
30
|
+
const e = Buffer.from(jwk.e, 'base64url')
|
|
31
|
+
const w = new TlWriter()
|
|
32
|
+
w.writeBytes(n)
|
|
33
|
+
w.writeBytes(e)
|
|
34
|
+
const digest = sha1(w.toBuffer())
|
|
35
|
+
const buf = digest.subarray(12, 20)
|
|
36
|
+
return { fingerprint: toBigIntLE(buf), buf: Buffer.from(buf) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Loads the gateway's RSA key pair. With `pemPath` set, the operator-provided
|
|
41
|
+
* production key is used (its fingerprint must match what clients have pinned).
|
|
42
|
+
* Without it, an ephemeral 2048-bit key is generated for local dev/testing.
|
|
43
|
+
*/
|
|
44
|
+
export function loadRsaKeyPair(pemPath?: string): RsaKeyPair {
|
|
45
|
+
let privateKey: KeyObject
|
|
46
|
+
let publicKey: KeyObject
|
|
47
|
+
|
|
48
|
+
if (pemPath) {
|
|
49
|
+
const pem = readFileSync(pemPath, 'utf-8')
|
|
50
|
+
privateKey = createPrivateKey(pem)
|
|
51
|
+
publicKey = createPublicKey(privateKey)
|
|
52
|
+
} else {
|
|
53
|
+
const pair = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
54
|
+
privateKey = pair.privateKey
|
|
55
|
+
publicKey = pair.publicKey
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { fingerprint, buf } = computeFingerprint(publicKey)
|
|
59
|
+
return { privateKey, publicKey, fingerprint, fingerprintBuf: buf }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** RSA decrypt of the client's `encrypted_data` with no padding (raw 256 bytes). */
|
|
63
|
+
export function rsaDecryptNoPadding(privateKey: KeyObject, encryptedData: Buffer): Buffer {
|
|
64
|
+
return privateDecrypt({ key: privateKey, padding: constants.RSA_NO_PADDING }, encryptedData)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** RSA encrypt with no padding — used only by the test client. */
|
|
68
|
+
export function rsaEncryptNoPadding(publicKey: KeyObject, data: Buffer): Buffer {
|
|
69
|
+
return publicEncrypt({ key: publicKey, padding: constants.RSA_NO_PADDING }, data)
|
|
70
|
+
}
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib'
|
|
2
|
+
import { TlReader } from '../tl/reader.js'
|
|
3
|
+
import type { TlCodec } from '../tl/codec.js'
|
|
4
|
+
import type { TlRegistry } from '../tl/registry.js'
|
|
5
|
+
import { fromJson, toJson, MigrationRegistry, type JsonValue, type TlObject, type TlValue } from '@mt-tl/tl'
|
|
6
|
+
import type { Connection } from '../transport/connection.js'
|
|
7
|
+
import type { Storage } from '../storage/index.js'
|
|
8
|
+
import type { SaltService } from '../session/salts.js'
|
|
9
|
+
import { replyToBadAccept, type MessageContext, type Responder } from './types.js'
|
|
10
|
+
import { messageClass } from '../session/inbound-tracker.js'
|
|
11
|
+
import type { RpcForwarder, RpcContext, SessionEffect } from './rpc-forwarder.js'
|
|
12
|
+
import type { PresenceBinder } from '../updates/presence-binder.js'
|
|
13
|
+
import { NoopPresenceBinder } from '../updates/presence-binder.js'
|
|
14
|
+
import type { UpdateLog } from '../core/updates.js'
|
|
15
|
+
import { noopLogger, type Logger } from '@mt-tl/tl'
|
|
16
|
+
|
|
17
|
+
const ID_GZIP_PACKED = 0x3072cfa1
|
|
18
|
+
const ID_MSG_CONTAINER = 0x73f1f8dc
|
|
19
|
+
/** Spec cap: a container carries at most 1024 messages. */
|
|
20
|
+
const CONTAINER_MAX_MESSAGES = 1024
|
|
21
|
+
/** Managed `updates.getDifference`: max updates per response before slicing. */
|
|
22
|
+
const DIFF_SLICE = 100
|
|
23
|
+
/** Managed `updates.getDifference`: gap beyond which we force a full resync (differenceTooLong). */
|
|
24
|
+
const DIFF_TOO_LONG = 5000
|
|
25
|
+
/** Methods the engine answers itself when `updates.managed` (else forwarded to the app). */
|
|
26
|
+
const MANAGED_UPDATES = new Set(['updates.getState', 'updates.getDifference'])
|
|
27
|
+
|
|
28
|
+
/** Protocol/service predicates handled inside the gateway (never forwarded). */
|
|
29
|
+
const SERVICE = new Set([
|
|
30
|
+
'ping',
|
|
31
|
+
'ping_delay_disconnect',
|
|
32
|
+
'msgs_ack',
|
|
33
|
+
'msgs_state_req',
|
|
34
|
+
'msgs_all_info',
|
|
35
|
+
'msg_resend_req',
|
|
36
|
+
'destroy_session',
|
|
37
|
+
'destroy_auth_key',
|
|
38
|
+
'get_future_salts',
|
|
39
|
+
'rpc_drop_answer',
|
|
40
|
+
'http_wait',
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
const WRAPPERS = new Set([
|
|
44
|
+
'invokeWithLayer',
|
|
45
|
+
'initConnection',
|
|
46
|
+
'invokeWithoutUpdates',
|
|
47
|
+
'invokeAfterMsg',
|
|
48
|
+
'invokeAfterMsgs',
|
|
49
|
+
'invokeWithMessagesRange',
|
|
50
|
+
'invokeWithTakeout',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
export interface DispatcherDeps {
|
|
54
|
+
codec: TlCodec
|
|
55
|
+
registry: TlRegistry
|
|
56
|
+
storage: Storage
|
|
57
|
+
saltService: SaltService
|
|
58
|
+
responder: Responder
|
|
59
|
+
forwarder: RpcForwarder
|
|
60
|
+
/** Presence/registry binder; defaults to a no-op (push disabled). */
|
|
61
|
+
binder?: PresenceBinder
|
|
62
|
+
/** Per-predicate migration ladders; defaults to empty (identity). */
|
|
63
|
+
migrations?: MigrationRegistry
|
|
64
|
+
/** Observability sink; defaults to a no-op logger. */
|
|
65
|
+
logger?: Logger
|
|
66
|
+
/** Disable inbound seqno validation for container-inner messages (mirrors the
|
|
67
|
+
* pipeline's `disableSeqNoCheck`); default enforced. */
|
|
68
|
+
disableSeqNoCheck?: boolean
|
|
69
|
+
/** Durable pts log for protocol-managed `updates.getState`/`getDifference`. */
|
|
70
|
+
updateLog?: UpdateLog
|
|
71
|
+
/** When true (+ `updateLog`), answer getState/getDifference in-engine. */
|
|
72
|
+
managedUpdates?: boolean
|
|
73
|
+
/** Whitelist of accepted `initConnection.api_id`s; omitted → any id is accepted. */
|
|
74
|
+
allowedApiIds?: Iterable<number>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class Dispatcher {
|
|
78
|
+
private readonly binder: PresenceBinder
|
|
79
|
+
private readonly migrations: MigrationRegistry
|
|
80
|
+
private readonly logger: Logger
|
|
81
|
+
private readonly checkSeqNo: boolean
|
|
82
|
+
private readonly managedUpdates: boolean
|
|
83
|
+
/** Built once from `deps.allowedApiIds`; undefined = whitelist disabled. */
|
|
84
|
+
private readonly allowedApiIds?: ReadonlySet<number>
|
|
85
|
+
|
|
86
|
+
constructor(private readonly deps: DispatcherDeps) {
|
|
87
|
+
this.binder = deps.binder ?? new NoopPresenceBinder()
|
|
88
|
+
this.migrations = deps.migrations ?? new MigrationRegistry()
|
|
89
|
+
this.logger = deps.logger ?? noopLogger
|
|
90
|
+
this.checkSeqNo = !deps.disableSeqNoCheck
|
|
91
|
+
this.managedUpdates = !!deps.managedUpdates && !!deps.updateLog
|
|
92
|
+
this.allowedApiIds = deps.allowedApiIds ? new Set(deps.allowedApiIds) : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Entry point: a raw message body (after the encrypted envelope). */
|
|
96
|
+
async dispatchPayload(payload: Buffer, ctx: MessageContext, conn: Connection): Promise<void> {
|
|
97
|
+
if (payload.length < 4) return
|
|
98
|
+
const id = payload.readUInt32LE(0)
|
|
99
|
+
|
|
100
|
+
if (id === ID_GZIP_PACKED) {
|
|
101
|
+
const r = new TlReader(payload)
|
|
102
|
+
r.readUInt32()
|
|
103
|
+
const inflated = gunzipSync(r.readBytes())
|
|
104
|
+
return this.dispatchPayload(inflated, ctx, conn)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (id === ID_MSG_CONTAINER) {
|
|
108
|
+
// Parse the whole container before dispatching anything: a malformed one
|
|
109
|
+
// (bad count / inner length overflow) is rejected atomically with
|
|
110
|
+
// bad_msg_notification code 64 ("invalid container"), nothing processed.
|
|
111
|
+
let inners: Array<{ msgId: bigint; seqNo: number; inner: Buffer }>
|
|
112
|
+
try {
|
|
113
|
+
inners = parseContainer(payload)
|
|
114
|
+
} catch {
|
|
115
|
+
this.logger.warn('container.invalid', {
|
|
116
|
+
authKeyId: conn.ctx.authKeyId,
|
|
117
|
+
layer: conn.ctx.apiLayer,
|
|
118
|
+
bytes: payload.length,
|
|
119
|
+
})
|
|
120
|
+
this.deps.responder.sendEncrypted(
|
|
121
|
+
conn,
|
|
122
|
+
{
|
|
123
|
+
_: 'bad_msg_notification',
|
|
124
|
+
bad_msg_id: ctx.msgId,
|
|
125
|
+
bad_msg_seqno: ctx.seqNo,
|
|
126
|
+
error_code: 64,
|
|
127
|
+
},
|
|
128
|
+
{ contentRelated: false },
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
for (const { msgId, seqNo, inner } of inners) {
|
|
133
|
+
// Validate each inner message like the outer envelope, EXCEPT ordering
|
|
134
|
+
// (code 32): a resend container legitimately carries old seqnos. A bad
|
|
135
|
+
// inner gets its own bad_msg_notification / msg_detailed_info and is
|
|
136
|
+
// skipped; the others still run.
|
|
137
|
+
const check = conn.tracker.accept(msgId, seqNo, {
|
|
138
|
+
...messageClass(inner),
|
|
139
|
+
checkSeqNo: this.checkSeqNo,
|
|
140
|
+
checkOrder: false,
|
|
141
|
+
})
|
|
142
|
+
if (!check.ok) {
|
|
143
|
+
replyToBadAccept(this.deps.responder, conn, check, msgId, seqNo)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
await this.dispatchPayload(inner, { ...ctx, msgId, seqNo }, conn)
|
|
147
|
+
}
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let body: TlObject
|
|
152
|
+
try {
|
|
153
|
+
body = this.deps.codec.decode(payload) as TlObject
|
|
154
|
+
} catch {
|
|
155
|
+
// Unknown/undecodable type — ack-by-silence, but log so it's visible.
|
|
156
|
+
this.logger.warn('decode.fail', {
|
|
157
|
+
id: '0x' + id.toString(16).padStart(8, '0'),
|
|
158
|
+
authKeyId: conn.ctx.authKeyId,
|
|
159
|
+
sessionId: conn.ctx.sessionId,
|
|
160
|
+
layer: conn.ctx.apiLayer,
|
|
161
|
+
bytes: payload.length,
|
|
162
|
+
})
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
await this.dispatchObject(body, ctx, conn)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async dispatchObject(body: TlObject, ctx: MessageContext, conn: Connection): Promise<void> {
|
|
169
|
+
const name = body._
|
|
170
|
+
this.logger.debug('msg', {
|
|
171
|
+
method: name,
|
|
172
|
+
authKeyId: conn.ctx.authKeyId,
|
|
173
|
+
sessionId: conn.ctx.sessionId,
|
|
174
|
+
layer: conn.ctx.apiLayer,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
if (WRAPPERS.has(name)) {
|
|
178
|
+
if (name === 'invokeWithLayer' && typeof body.layer === 'number') {
|
|
179
|
+
conn.ctx.apiLayer = body.layer
|
|
180
|
+
if (conn.ctx.sessionId !== undefined) {
|
|
181
|
+
await this.deps.storage.sessions.update(conn.ctx.sessionId, { apiLayer: body.layer })
|
|
182
|
+
}
|
|
183
|
+
} else if (name === 'initConnection') {
|
|
184
|
+
const rejected = await this.handleInitConnection(body, ctx, conn)
|
|
185
|
+
if (rejected) return
|
|
186
|
+
} else if (name === 'invokeWithoutUpdates') {
|
|
187
|
+
// Client opts this connection out of server-push (PushService skips it).
|
|
188
|
+
conn.ctx.noUpdates = true
|
|
189
|
+
}
|
|
190
|
+
const query = body.query
|
|
191
|
+
if (query && typeof query === 'object' && '_' in query) {
|
|
192
|
+
return this.dispatchObject(query as TlObject, ctx, conn)
|
|
193
|
+
}
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (SERVICE.has(name)) return this.handleService(body, ctx, conn)
|
|
198
|
+
|
|
199
|
+
if (this.managedUpdates && MANAGED_UPDATES.has(name)) {
|
|
200
|
+
return this.handleManagedUpdate(body, ctx, conn)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return this.forwardBusiness(body, ctx, conn)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Process an `initConnection` envelope: optionally enforce the `api_id`
|
|
208
|
+
* whitelist, then capture the device/app fields onto the connection and
|
|
209
|
+
* persist them to the auth key's meta (the per-device source of truth; an
|
|
210
|
+
* auth key is one app install, so this is stable per key, not per session).
|
|
211
|
+
* Returns `true` when the connection was rejected (caller must not dispatch
|
|
212
|
+
* the wrapped query).
|
|
213
|
+
*/
|
|
214
|
+
private async handleInitConnection(
|
|
215
|
+
body: TlObject,
|
|
216
|
+
ctx: MessageContext,
|
|
217
|
+
conn: Connection,
|
|
218
|
+
): Promise<boolean> {
|
|
219
|
+
const apiId = typeof body.api_id === 'number' ? body.api_id : undefined
|
|
220
|
+
if (this.allowedApiIds && (apiId === undefined || !this.allowedApiIds.has(apiId))) {
|
|
221
|
+
this.logger.warn('initConnection.rejected', {
|
|
222
|
+
authKeyId: conn.ctx.authKeyId,
|
|
223
|
+
sessionId: conn.ctx.sessionId,
|
|
224
|
+
apiId,
|
|
225
|
+
})
|
|
226
|
+
this.sendRpcError(conn, ctx.msgId, 400, 'API_ID_INVALID')
|
|
227
|
+
return true
|
|
228
|
+
}
|
|
229
|
+
const meta = {
|
|
230
|
+
apiId,
|
|
231
|
+
deviceModel: asString(body.device_model),
|
|
232
|
+
systemVersion: asString(body.system_version),
|
|
233
|
+
appVersion: asString(body.app_version),
|
|
234
|
+
systemLangCode: asString(body.system_lang_code),
|
|
235
|
+
langCode: asString(body.lang_code),
|
|
236
|
+
}
|
|
237
|
+
conn.ctx.apiId = meta.apiId
|
|
238
|
+
conn.ctx.deviceModel = meta.deviceModel
|
|
239
|
+
conn.ctx.systemVersion = meta.systemVersion
|
|
240
|
+
conn.ctx.appVersion = meta.appVersion
|
|
241
|
+
conn.ctx.systemLangCode = meta.systemLangCode
|
|
242
|
+
conn.ctx.langCode = meta.langCode
|
|
243
|
+
if (conn.ctx.authKeyId !== undefined) {
|
|
244
|
+
await this.deps.storage.authKeys.updateMeta(conn.ctx.authKeyId, meta)
|
|
245
|
+
}
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Engine-owned `updates.getState` / `updates.getDifference` (when
|
|
251
|
+
* `config.updates.managed`). Common pts sequence only — qts/seq/channels are 0.
|
|
252
|
+
* Updates are returned in `other_updates`; the durable {@link UpdateLog}
|
|
253
|
+
* supplies pts. Auth-gated like a normal `auth: true` method.
|
|
254
|
+
*/
|
|
255
|
+
private async handleManagedUpdate(body: TlObject, ctx: MessageContext, conn: Connection): Promise<void> {
|
|
256
|
+
const log = this.deps.updateLog!
|
|
257
|
+
const subject = conn.ctx.subject
|
|
258
|
+
if (subject === undefined) return this.sendRpcError(conn, ctx.msgId, 401, 'AUTH_KEY_UNREGISTERED')
|
|
259
|
+
const date = Math.floor(Date.now() / 1000)
|
|
260
|
+
const state = (pts: number): JsonValue => ({
|
|
261
|
+
_: 'updates.state',
|
|
262
|
+
pts,
|
|
263
|
+
qts: 0,
|
|
264
|
+
date,
|
|
265
|
+
seq: 0,
|
|
266
|
+
unread_count: 0,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
if (body._ === 'updates.getState') {
|
|
270
|
+
return this.sendRpcResult(conn, ctx.msgId, state(await log.currentPts(subject)))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// updates.getDifference
|
|
274
|
+
const sincePts = Number(body.pts ?? 0)
|
|
275
|
+
const current = await log.currentPts(subject)
|
|
276
|
+
if (sincePts >= current) {
|
|
277
|
+
return this.sendRpcResult(conn, ctx.msgId, { _: 'updates.differenceEmpty', date, seq: 0 })
|
|
278
|
+
}
|
|
279
|
+
if (current - sincePts > DIFF_TOO_LONG) {
|
|
280
|
+
return this.sendRpcResult(conn, ctx.msgId, { _: 'updates.differenceTooLong', pts: current })
|
|
281
|
+
}
|
|
282
|
+
const all = await log.since(subject, sincePts)
|
|
283
|
+
const sliced = all.length > DIFF_SLICE
|
|
284
|
+
const page = sliced ? all.slice(0, DIFF_SLICE) : all
|
|
285
|
+
const lastPts = page.at(-1)?.pts ?? current
|
|
286
|
+
const common = {
|
|
287
|
+
new_messages: [],
|
|
288
|
+
new_encrypted_messages: [],
|
|
289
|
+
other_updates: page.map(u => u.update),
|
|
290
|
+
chats: [],
|
|
291
|
+
users: [],
|
|
292
|
+
}
|
|
293
|
+
return this.sendRpcResult(
|
|
294
|
+
conn,
|
|
295
|
+
ctx.msgId,
|
|
296
|
+
sliced
|
|
297
|
+
? { _: 'updates.differenceSlice', ...common, intermediate_state: state(lastPts) }
|
|
298
|
+
: { _: 'updates.difference', ...common, state: state(lastPts) },
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async handleService(body: TlObject, ctx: MessageContext, conn: Connection): Promise<void> {
|
|
303
|
+
const { responder } = this.deps
|
|
304
|
+
switch (body._) {
|
|
305
|
+
case 'ping_delay_disconnect':
|
|
306
|
+
// Close the connection after `disconnect_delay`s of inactivity unless
|
|
307
|
+
// reset; then respond like a normal ping.
|
|
308
|
+
conn.armDisconnect(Number(body.disconnect_delay ?? 0))
|
|
309
|
+
// falls through
|
|
310
|
+
case 'ping':
|
|
311
|
+
responder.sendEncrypted(
|
|
312
|
+
conn,
|
|
313
|
+
{ _: 'pong', msg_id: ctx.msgId, ping_id: body.ping_id as bigint },
|
|
314
|
+
{ contentRelated: false },
|
|
315
|
+
)
|
|
316
|
+
return
|
|
317
|
+
case 'msgs_state_req':
|
|
318
|
+
case 'msg_resend_req': {
|
|
319
|
+
// We keep no sent-message store to re-send, so per spec a msg_resend_req
|
|
320
|
+
// is answered like a msgs_state_req: report each requested id's state.
|
|
321
|
+
const ids = (body.msg_ids as bigint[] | undefined) ?? []
|
|
322
|
+
responder.sendEncrypted(
|
|
323
|
+
conn,
|
|
324
|
+
{ _: 'msgs_state_info', req_msg_id: ctx.msgId, info: conn.tracker.stateOf(ids) },
|
|
325
|
+
{ contentRelated: false },
|
|
326
|
+
)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
case 'destroy_auth_key': {
|
|
330
|
+
// Permanent key destruction (logout). Block the key so any further use
|
|
331
|
+
// is rejected; the response is sent before this message's key is dropped.
|
|
332
|
+
await this.deps.storage.authKeys.setBlocked(ctx.authKeyId, true)
|
|
333
|
+
responder.sendEncrypted(conn, { _: 'destroy_auth_key_ok' }, { contentRelated: false })
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
case 'destroy_session': {
|
|
337
|
+
// Tear down the stored session if it belongs to this auth key; the
|
|
338
|
+
// client uses this to forget another session under the same key.
|
|
339
|
+
const sessionId = body.session_id as bigint
|
|
340
|
+
const existing = await this.deps.storage.sessions.get(sessionId)
|
|
341
|
+
const owned = !!existing && existing.authKeyId === ctx.authKeyId
|
|
342
|
+
if (owned) await this.deps.storage.sessions.delete(sessionId)
|
|
343
|
+
responder.sendEncrypted(
|
|
344
|
+
conn,
|
|
345
|
+
{ _: owned ? 'destroy_session_ok' : 'destroy_session_none', session_id: sessionId },
|
|
346
|
+
{ contentRelated: false },
|
|
347
|
+
)
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
case 'get_future_salts': {
|
|
351
|
+
// Return the next `num` scheduled salts (clamped) with true windows,
|
|
352
|
+
// minting more if the schedule is short.
|
|
353
|
+
const num = Math.min(64, Math.max(1, Number(body.num ?? 1)))
|
|
354
|
+
const authKeyId = conn.ctx.authKeyId
|
|
355
|
+
const scheduled =
|
|
356
|
+
authKeyId !== undefined ? await this.deps.saltService.future(authKeyId, num) : []
|
|
357
|
+
responder.sendEncrypted(
|
|
358
|
+
conn,
|
|
359
|
+
{
|
|
360
|
+
_: 'future_salts',
|
|
361
|
+
req_msg_id: ctx.msgId,
|
|
362
|
+
now: Math.floor(Date.now() / 1000),
|
|
363
|
+
salts: scheduled.map(s => ({
|
|
364
|
+
_: 'future_salt',
|
|
365
|
+
valid_since: s.validSince,
|
|
366
|
+
valid_until: s.validUntil,
|
|
367
|
+
salt: s.salt,
|
|
368
|
+
})),
|
|
369
|
+
},
|
|
370
|
+
{ contentRelated: false },
|
|
371
|
+
)
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
case 'rpc_drop_answer':
|
|
375
|
+
// Packets are processed serially per connection (see transport `pump`)
|
|
376
|
+
// and answers are sent immediately (no outgoing queue to drop from), so
|
|
377
|
+
// by the time a drop arrives its target RPC has already been answered.
|
|
378
|
+
// `rpc_answer_unknown` ("no memory of req_msg_id / already responded")
|
|
379
|
+
// is the spec-correct reply here. The RpcDropAnswer is wrapped in an
|
|
380
|
+
// rpc_result (and acknowledged) like any RPC response — see
|
|
381
|
+
// docs/internals/protocol-compliance.md.
|
|
382
|
+
responder.sendEncrypted(conn, {
|
|
383
|
+
_: 'rpc_result',
|
|
384
|
+
req_msg_id: ctx.msgId,
|
|
385
|
+
result: { _: 'rpc_answer_unknown' },
|
|
386
|
+
})
|
|
387
|
+
return
|
|
388
|
+
case 'msgs_ack':
|
|
389
|
+
// Client acknowledgments of server→client messages. We never retransmit,
|
|
390
|
+
// so there is no resend queue for an ack to clear — nothing to do.
|
|
391
|
+
return
|
|
392
|
+
case 'msgs_all_info':
|
|
393
|
+
// Voluntary status of our messages from the client; informational and
|
|
394
|
+
// not requiring acknowledgment — nothing to do (we don't retransmit).
|
|
395
|
+
return
|
|
396
|
+
case 'http_wait':
|
|
397
|
+
default:
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async forwardBusiness(body: TlObject, ctx: MessageContext, conn: Connection): Promise<void> {
|
|
403
|
+
// Identity carried on every line for this request: reqId (the client's
|
|
404
|
+
// msg_id) + authKeyId/sessionId/subject/layer. Matches the per-request
|
|
405
|
+
// `ctx.log` child the handler layer binds, so engine ⇄ handler lines join.
|
|
406
|
+
// device/ip are added only once known (deviceModel after initConnection) so
|
|
407
|
+
// they don't noise pre-init lines.
|
|
408
|
+
const logBase = {
|
|
409
|
+
reqId: ctx.msgId,
|
|
410
|
+
method: body._,
|
|
411
|
+
subject: conn.ctx.subject,
|
|
412
|
+
authKeyId: ctx.authKeyId,
|
|
413
|
+
sessionId: ctx.sessionId,
|
|
414
|
+
layer: conn.ctx.apiLayer,
|
|
415
|
+
...(conn.ctx.deviceModel ? { deviceModel: conn.ctx.deviceModel } : {}),
|
|
416
|
+
...(conn.ctx.remoteAddress ? { ip: conn.ctx.remoteAddress } : {}),
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const def = this.deps.registry.getByName(body._)
|
|
420
|
+
if (!def || def.kind !== 'method') {
|
|
421
|
+
// Not a known business method — surface as an rpc_error.
|
|
422
|
+
this.logger.warn('rpc.unknown', logBase)
|
|
423
|
+
return this.sendRpcError(conn, ctx.msgId, 400, 'METHOD_NOT_FOUND')
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Normalize older-layer input up to the canonical shape before forwarding.
|
|
427
|
+
const canonical = this.migrations.up(body, conn.ctx.apiLayer) as TlObject
|
|
428
|
+
const params = paramsToJson(canonical, this.deps.registry)
|
|
429
|
+
// Full incoming payload at debug — "what came in" (structured in JSON mode).
|
|
430
|
+
this.logger.debug('rpc.params', { ...logBase, params })
|
|
431
|
+
const rpcCtx: RpcContext = {
|
|
432
|
+
sessionId: ctx.sessionId.toString(),
|
|
433
|
+
authKeyId: ctx.authKeyId.toString(),
|
|
434
|
+
subject: conn.ctx.subject,
|
|
435
|
+
apiLayer: conn.ctx.apiLayer,
|
|
436
|
+
apiId: conn.ctx.apiId,
|
|
437
|
+
deviceModel: conn.ctx.deviceModel,
|
|
438
|
+
systemVersion: conn.ctx.systemVersion,
|
|
439
|
+
appVersion: conn.ctx.appVersion,
|
|
440
|
+
langCode: conn.ctx.langCode,
|
|
441
|
+
ip: conn.ctx.remoteAddress,
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const startedAt = Date.now()
|
|
445
|
+
let res
|
|
446
|
+
try {
|
|
447
|
+
res = await this.deps.forwarder.forward({
|
|
448
|
+
id: ctx.msgId.toString(),
|
|
449
|
+
method: body._,
|
|
450
|
+
params,
|
|
451
|
+
context: rpcCtx,
|
|
452
|
+
})
|
|
453
|
+
} catch (err) {
|
|
454
|
+
this.logger.error('rpc.fail', { ...logBase, ms: Date.now() - startedAt, err })
|
|
455
|
+
return this.sendRpcError(conn, ctx.msgId, 500, 'INTERNAL')
|
|
456
|
+
}
|
|
457
|
+
const ms = Date.now() - startedAt
|
|
458
|
+
|
|
459
|
+
if (res.effects?.length) await this.applyEffects(conn, ctx, res.effects)
|
|
460
|
+
|
|
461
|
+
if (res.error) {
|
|
462
|
+
this.logger.info('rpc', {
|
|
463
|
+
...logBase,
|
|
464
|
+
ms,
|
|
465
|
+
status: 'error',
|
|
466
|
+
code: res.error.code,
|
|
467
|
+
error: res.error.message,
|
|
468
|
+
})
|
|
469
|
+
return this.sendRpcError(conn, ctx.msgId, res.error.code, res.error.message)
|
|
470
|
+
}
|
|
471
|
+
if (res.result !== undefined) {
|
|
472
|
+
this.logger.info('rpc', { ...logBase, ms, status: 'ok' })
|
|
473
|
+
// Full outgoing payload at debug — "what went out".
|
|
474
|
+
this.logger.debug('rpc.result', { ...logBase, result: res.result })
|
|
475
|
+
return this.sendRpcResult(conn, ctx.msgId, res.result)
|
|
476
|
+
}
|
|
477
|
+
// Neither result nor error — malformed envelope.
|
|
478
|
+
this.logger.error('rpc.malformed', { ...logBase, ms })
|
|
479
|
+
return this.sendRpcError(conn, ctx.msgId, 500, 'INTERNAL')
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Apply backend-requested mutations to gateway-owned auth/session state. */
|
|
483
|
+
private async applyEffects(
|
|
484
|
+
conn: Connection,
|
|
485
|
+
ctx: MessageContext,
|
|
486
|
+
effects: SessionEffect[],
|
|
487
|
+
): Promise<void> {
|
|
488
|
+
for (const effect of effects) {
|
|
489
|
+
switch (effect.type) {
|
|
490
|
+
case 'bindUser':
|
|
491
|
+
await this.deps.storage.authKeys.bindUser(ctx.authKeyId, effect.subject)
|
|
492
|
+
conn.ctx.subject = effect.subject
|
|
493
|
+
await this.patchSession(conn, { subject: effect.subject })
|
|
494
|
+
this.binder.bind(conn, effect.subject)
|
|
495
|
+
// A user logged in on this auth key (device login).
|
|
496
|
+
this.logger.info('user.bind', { subject: effect.subject, authKeyId: ctx.authKeyId })
|
|
497
|
+
break
|
|
498
|
+
case 'unbindUser':
|
|
499
|
+
await this.deps.storage.authKeys.bindUser(ctx.authKeyId, null)
|
|
500
|
+
conn.ctx.subject = undefined
|
|
501
|
+
this.binder.unbind(conn)
|
|
502
|
+
this.logger.info('user.unbind', { authKeyId: ctx.authKeyId })
|
|
503
|
+
break
|
|
504
|
+
case 'revokeKey':
|
|
505
|
+
await this.deps.storage.authKeys.setBlocked(ctx.authKeyId, true)
|
|
506
|
+
this.logger.info('authkey.revoke', { authKeyId: ctx.authKeyId })
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private async patchSession(
|
|
513
|
+
conn: Connection,
|
|
514
|
+
patch: { subject?: string; apiLayer?: number },
|
|
515
|
+
): Promise<void> {
|
|
516
|
+
if (conn.ctx.sessionId !== undefined) {
|
|
517
|
+
await this.deps.storage.sessions.update(conn.ctx.sessionId, patch)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private sendRpcResult(conn: Connection, reqMsgId: bigint, result: JsonValue): void {
|
|
522
|
+
// Render the canonical result down to the client's layer before encoding.
|
|
523
|
+
const tl = this.migrations.down(fromJson(result), conn.ctx.apiLayer)
|
|
524
|
+
// rpc_result.result must be a boxed object, Bool, or Vector.
|
|
525
|
+
if (typeof tl !== 'boolean' && !Array.isArray(tl) && !(tl && typeof tl === 'object' && '_' in tl)) {
|
|
526
|
+
return this.sendRpcError(conn, reqMsgId, 500, 'INVALID_RESULT')
|
|
527
|
+
}
|
|
528
|
+
this.deps.responder.sendEncrypted(conn, {
|
|
529
|
+
_: 'rpc_result',
|
|
530
|
+
req_msg_id: reqMsgId,
|
|
531
|
+
result: tl as TlObject | boolean,
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private sendRpcError(conn: Connection, reqMsgId: bigint, code: number, message: string): void {
|
|
536
|
+
this.deps.responder.sendEncrypted(conn, {
|
|
537
|
+
_: 'rpc_result',
|
|
538
|
+
req_msg_id: reqMsgId,
|
|
539
|
+
result: { _: 'rpc_error', error_code: code, error_message: message },
|
|
540
|
+
})
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Build JSON-RPC params from a decoded method: drop `_` and bitmask fields. */
|
|
545
|
+
function paramsToJson(body: TlObject, registry: TlRegistry): JsonValue {
|
|
546
|
+
const def = registry.getByName(body._)
|
|
547
|
+
const omit = new Set<string>(['_'])
|
|
548
|
+
if (def) {
|
|
549
|
+
for (const p of def.params) {
|
|
550
|
+
if (p.type.kind === 'flags') omit.add(p.name)
|
|
551
|
+
else if (p.type.kind === 'flag') omit.add(p.type.flagsField)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const out: Record<string, JsonValue> = {}
|
|
555
|
+
for (const [k, v] of Object.entries(body)) {
|
|
556
|
+
if (!omit.has(k)) out[k] = toJson(v as TlValue)
|
|
557
|
+
}
|
|
558
|
+
return out
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function asString(v: unknown): string | undefined {
|
|
562
|
+
return typeof v === 'string' ? v : undefined
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Parse a `msg_container` body into its inner messages, validating structure.
|
|
567
|
+
* Throws on a malformed container (too many messages, or an inner length that
|
|
568
|
+
* overruns the buffer) — the caller maps that to `bad_msg_notification` code 64.
|
|
569
|
+
*/
|
|
570
|
+
function parseContainer(payload: Buffer): Array<{ msgId: bigint; seqNo: number; inner: Buffer }> {
|
|
571
|
+
const r = new TlReader(payload)
|
|
572
|
+
r.readUInt32() // container constructor id
|
|
573
|
+
const count = r.readUInt32()
|
|
574
|
+
if (count > CONTAINER_MAX_MESSAGES) {
|
|
575
|
+
throw new Error(`container: ${count} messages exceeds ${CONTAINER_MAX_MESSAGES}`)
|
|
576
|
+
}
|
|
577
|
+
const out: Array<{ msgId: bigint; seqNo: number; inner: Buffer }> = []
|
|
578
|
+
for (let i = 0; i < count; i++) {
|
|
579
|
+
const msgId = r.readLong()
|
|
580
|
+
const seqNo = r.readUInt32()
|
|
581
|
+
const bytes = r.readUInt32()
|
|
582
|
+
const inner = r.read(bytes) // throws if `bytes` overruns the buffer
|
|
583
|
+
out.push({ msgId, seqNo, inner })
|
|
584
|
+
}
|
|
585
|
+
return out
|
|
586
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RpcForwarder, RpcRequest, RpcResponse } from '../rpc-forwarder.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Forwarder that calls a handler in the same process — for co-locating the
|
|
5
|
+
* gateway and a worker (no broker), and for end-to-end tests. The handler is
|
|
6
|
+
* typically the worker's `dispatchRpc`.
|
|
7
|
+
*/
|
|
8
|
+
export class InProcessForwarder implements RpcForwarder {
|
|
9
|
+
constructor(private readonly handler: (req: RpcRequest) => Promise<RpcResponse>) {}
|
|
10
|
+
|
|
11
|
+
forward(req: RpcRequest): Promise<RpcResponse> {
|
|
12
|
+
return this.handler(req)
|
|
13
|
+
}
|
|
14
|
+
}
|