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