@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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-connection inbound-message guard and state tracker.
|
|
3
|
+
*
|
|
4
|
+
* Implements the client→server half of the MTProto message-id / sequence-number
|
|
5
|
+
* rules (https://core.telegram.org/mtproto/description) and backs the
|
|
6
|
+
* `msgs_state_req` / `msgs_state_info` service messages
|
|
7
|
+
* (https://core.telegram.org/mtproto/service_messages_about_messages).
|
|
8
|
+
*
|
|
9
|
+
* A connection carries a single session's message stream, so one tracker per
|
|
10
|
+
* connection is sufficient. The tracker is purely in-memory: replay protection
|
|
11
|
+
* is per-connection-process, while the time-window check (codes 16/17) bounds
|
|
12
|
+
* cross-process replay since stale msg_ids are rejected everywhere.
|
|
13
|
+
*
|
|
14
|
+
* Sequence-number parity (34/35) and ordering (32) are enforced when `checkSeqNo`
|
|
15
|
+
* / `checkOrder` are set (gated by the `disableSeqNoCheck` config). Code 33 (seqno
|
|
16
|
+
* too high) is unreachable under serial in-order processing; code 64 (invalid
|
|
17
|
+
* container) is raised by the dispatcher. The outer envelope and each
|
|
18
|
+
* container-inner message both run through {@link InboundTracker.accept}. See
|
|
19
|
+
* docs/internals/protocol-compliance.md.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** `bad_msg_notification` error codes. 16–35 come from {@link InboundTracker.accept};
|
|
23
|
+
* 64 (invalid container) is raised by the dispatcher. */
|
|
24
|
+
export type BadMsgCode = 16 | 17 | 18 | 19 | 20 | 32 | 33 | 34 | 35 | 64
|
|
25
|
+
|
|
26
|
+
/** The cached answer (`rpc_result`) to a request, for `msg_detailed_info`. */
|
|
27
|
+
export interface CachedAnswer {
|
|
28
|
+
answerMsgId: bigint
|
|
29
|
+
bytes: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type AcceptResult =
|
|
33
|
+
| { ok: true }
|
|
34
|
+
/** Reject and reply `bad_msg_notification` with this code. */
|
|
35
|
+
| { ok: false; code: BadMsgCode }
|
|
36
|
+
/** Duplicate of an already-answered request — reply `msg_detailed_info`. */
|
|
37
|
+
| { ok: false; detailed: CachedAnswer }
|
|
38
|
+
/** Benign duplicate with no cached answer — drop silently, send no reply. */
|
|
39
|
+
| { ok: false; drop: true }
|
|
40
|
+
|
|
41
|
+
/** Classification of an inbound message, derived from its constructor id. */
|
|
42
|
+
export interface AcceptOptions {
|
|
43
|
+
/** The payload is a `msg_container` (a duplicate of one is a protocol error → 19). */
|
|
44
|
+
isContainer?: boolean
|
|
45
|
+
/** The message requires acknowledgment (RPC queries); odd seqno expected. */
|
|
46
|
+
contentRelated?: boolean
|
|
47
|
+
/** Enforce seqno parity (codes 34/35). */
|
|
48
|
+
checkSeqNo?: boolean
|
|
49
|
+
/** Enforce content-seqno ordering (code 32). Only the top-level stream — inner
|
|
50
|
+
* container messages are skipped, since a resend container carries old seqnos. */
|
|
51
|
+
checkOrder?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface InboundMsg {
|
|
55
|
+
seqNo: number
|
|
56
|
+
/** Odd seqno ⇒ content-related (an RPC query), even ⇒ pure service message. */
|
|
57
|
+
contentRelated: boolean
|
|
58
|
+
/** The reply we generated, once sent — lets a later duplicate get `msg_detailed_info`. */
|
|
59
|
+
answer?: CachedAnswer
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Constructor ids of client→server messages that do NOT require acknowledgment
|
|
63
|
+
// (carry an even seqno). Everything else is content-related (odd seqno).
|
|
64
|
+
const ID_MSG_CONTAINER = 0x73f1f8dc
|
|
65
|
+
const NON_CONTENT_IDS = new Set<number>([
|
|
66
|
+
ID_MSG_CONTAINER, // msg_container
|
|
67
|
+
0x62d6b459, // msgs_ack
|
|
68
|
+
0x7abe77ec, // ping
|
|
69
|
+
0xf3427b8c, // ping_delay_disconnect
|
|
70
|
+
0x9299359f, // http_wait
|
|
71
|
+
0x8cc0d131, // msgs_all_info
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
/** Classify a raw payload by its leading constructor id (used to drive `accept`). */
|
|
75
|
+
export function messageClass(payload: Buffer): { isContainer: boolean; contentRelated: boolean } {
|
|
76
|
+
if (payload.length < 4) return { isContainer: false, contentRelated: true }
|
|
77
|
+
const id = payload.readUInt32LE(0)
|
|
78
|
+
return { isContainer: id === ID_MSG_CONTAINER, contentRelated: !NON_CONTENT_IDS.has(id) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface InboundTrackerOptions {
|
|
82
|
+
/** Clock in milliseconds (default `Date.now`); injectable for tests. */
|
|
83
|
+
nowMs?: () => number
|
|
84
|
+
/** Reject msg_ids whose timestamp is more than this far in the future. */
|
|
85
|
+
futureToleranceSec?: number
|
|
86
|
+
/** Reject msg_ids whose timestamp is more than this far in the past. */
|
|
87
|
+
pastToleranceSec?: number
|
|
88
|
+
/** Size of the recent-msg_id window kept for dedup/state (FIFO eviction). */
|
|
89
|
+
maxTracked?: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Spec tolerances: a client msg_id encodes unix time in its high 32 bits and must
|
|
93
|
+
// be divisible by 4. Telegram ignores ids more than 30s ahead or 300s behind.
|
|
94
|
+
const FUTURE_TOLERANCE_SEC = 30
|
|
95
|
+
const PAST_TOLERANCE_SEC = 300
|
|
96
|
+
const MAX_TRACKED = 1024
|
|
97
|
+
|
|
98
|
+
export class InboundTracker {
|
|
99
|
+
private readonly received = new Map<bigint, InboundMsg>()
|
|
100
|
+
/** Insertion order, for FIFO eviction once the window is full. */
|
|
101
|
+
private readonly order: bigint[] = []
|
|
102
|
+
/** Highest msg_id evicted from the window — anything ≤ this is "too old to verify". */
|
|
103
|
+
private evictedHigh = 0n
|
|
104
|
+
/** Highest msg_id ever accepted (distinguishes "in range" from "too high"). */
|
|
105
|
+
private maxReceived = 0n
|
|
106
|
+
/** Highest odd seqno of a content-related message seen (for ordering code 32). */
|
|
107
|
+
private lastContentSeqNo = -1
|
|
108
|
+
|
|
109
|
+
private readonly now: () => number
|
|
110
|
+
private readonly futureTolerance: number
|
|
111
|
+
private readonly pastTolerance: number
|
|
112
|
+
private readonly maxTracked: number
|
|
113
|
+
|
|
114
|
+
constructor(opts: InboundTrackerOptions = {}) {
|
|
115
|
+
this.now = opts.nowMs ?? (() => Date.now())
|
|
116
|
+
this.futureTolerance = opts.futureToleranceSec ?? FUTURE_TOLERANCE_SEC
|
|
117
|
+
this.pastTolerance = opts.pastToleranceSec ?? PAST_TOLERANCE_SEC
|
|
118
|
+
this.maxTracked = Math.max(1, opts.maxTracked ?? MAX_TRACKED)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate and record an inbound message. Returns the `bad_msg_notification`
|
|
123
|
+
* error code if the message must be rejected, `{ drop: true }` for a benign
|
|
124
|
+
* duplicate to ignore silently, or `{ ok: true }` if it should be processed. A
|
|
125
|
+
* rejected message is NOT recorded, so the client may correct and resend it
|
|
126
|
+
* (e.g. after a `bad_server_salt`, which re-uses the same msg_id).
|
|
127
|
+
*/
|
|
128
|
+
accept(msgId: bigint, seqNo: number, opts: AcceptOptions = {}): AcceptResult {
|
|
129
|
+
const { isContainer = false, contentRelated = true, checkSeqNo = false, checkOrder = false } = opts
|
|
130
|
+
|
|
131
|
+
// 18: the two low bits of a client msg_id must be 0 (divisible by 4).
|
|
132
|
+
if ((msgId & 3n) !== 0n) return { ok: false, code: 18 }
|
|
133
|
+
|
|
134
|
+
const nowSec = Math.floor(this.now() / 1000)
|
|
135
|
+
const msgSec = Number(msgId >> 32n)
|
|
136
|
+
// 16 / 17: client clock skew beyond tolerance.
|
|
137
|
+
if (msgSec < nowSec - this.pastTolerance) return { ok: false, code: 16 }
|
|
138
|
+
if (msgSec > nowSec + this.futureTolerance) return { ok: false, code: 17 }
|
|
139
|
+
|
|
140
|
+
const dup = this.received.get(msgId)
|
|
141
|
+
if (dup) {
|
|
142
|
+
// A duplicate container msg_id is a protocol error (19). For a duplicate
|
|
143
|
+
// regular message: reply `msg_detailed_info` if its answer is still cached,
|
|
144
|
+
// else drop silently — re-processing is unsafe and a bad_msg reply would
|
|
145
|
+
// wrongly make the client resync its clock/salt.
|
|
146
|
+
if (isContainer) return { ok: false, code: 19 }
|
|
147
|
+
return dup.answer ? { ok: false, detailed: dup.answer } : { ok: false, drop: true }
|
|
148
|
+
}
|
|
149
|
+
// 20: older than anything we still remember — can't verify it's not a replay.
|
|
150
|
+
if (msgId <= this.evictedHigh) return { ok: false, code: 20 }
|
|
151
|
+
|
|
152
|
+
if (checkSeqNo) {
|
|
153
|
+
const odd = seqNo % 2 === 1
|
|
154
|
+
// 34/35: content-related messages carry an odd seqno, pure service ones even.
|
|
155
|
+
if (contentRelated && !odd) return { ok: false, code: 35 }
|
|
156
|
+
if (!contentRelated && odd) return { ok: false, code: 34 }
|
|
157
|
+
}
|
|
158
|
+
if (checkOrder && contentRelated) {
|
|
159
|
+
// 32: a content-related seqno must exceed every earlier one (they arrive in
|
|
160
|
+
// msg_id order). Code 33 (too high) can't occur under serial processing.
|
|
161
|
+
if (seqNo <= this.lastContentSeqNo) return { ok: false, code: 32 }
|
|
162
|
+
this.lastContentSeqNo = seqNo
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.note(msgId, seqNo)
|
|
166
|
+
return { ok: true }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Record the reply (`rpc_result`) generated for a request, so a later duplicate
|
|
171
|
+
* of that request can be answered with `msg_detailed_info` instead of dropped.
|
|
172
|
+
* No-op if the request id is no longer tracked.
|
|
173
|
+
*/
|
|
174
|
+
recordAnswer(reqMsgId: bigint, answerMsgId: bigint, bytes: number): void {
|
|
175
|
+
const e = this.received.get(reqMsgId)
|
|
176
|
+
if (e) e.answer = { answerMsgId, bytes }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Record a received msg_id without validation. Used for messages observed
|
|
181
|
+
* inside a container (their ids are not individually validated), so that
|
|
182
|
+
* `msgs_state_req` can still report them as received.
|
|
183
|
+
*/
|
|
184
|
+
note(msgId: bigint, seqNo: number): void {
|
|
185
|
+
if (this.received.has(msgId)) return
|
|
186
|
+
this.received.set(msgId, { seqNo, contentRelated: seqNo % 2 === 1 })
|
|
187
|
+
this.order.push(msgId)
|
|
188
|
+
if (msgId > this.maxReceived) this.maxReceived = msgId
|
|
189
|
+
while (this.order.length > this.maxTracked) {
|
|
190
|
+
const evicted = this.order.shift()!
|
|
191
|
+
this.received.delete(evicted)
|
|
192
|
+
if (evicted > this.evictedHigh) this.evictedHigh = evicted
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Per-id status bytes for `msgs_state_info.info` — one byte per requested id
|
|
198
|
+
* (see the spec's status-byte table). Reports received messages as state 4
|
|
199
|
+
* with the appropriate high bits, and unseen ids as 1/2/3 by position relative
|
|
200
|
+
* to the tracking window.
|
|
201
|
+
*/
|
|
202
|
+
stateOf(ids: bigint[]): Buffer {
|
|
203
|
+
const nowSec = Math.floor(this.now() / 1000)
|
|
204
|
+
return Buffer.from(ids.map(id => this.stateByte(id, nowSec)))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private stateByte(id: bigint, nowSec: number): number {
|
|
208
|
+
const e = this.received.get(id)
|
|
209
|
+
if (e) {
|
|
210
|
+
// 4 = received. Pure service messages don't require an ack (+16);
|
|
211
|
+
// content-related queries are processed and answered (+32 +64).
|
|
212
|
+
return e.contentRelated ? 4 + 32 + 64 : 4 + 16
|
|
213
|
+
}
|
|
214
|
+
const msgSec = Number(id >> 32n)
|
|
215
|
+
// 1 = nothing known (too old / already forgotten); 3 = too high (not yet
|
|
216
|
+
// received); 2 = within the known range but not received.
|
|
217
|
+
if (msgSec < nowSec - this.pastTolerance || id <= this.evictedHigh) return 1
|
|
218
|
+
if (id > this.maxReceived) return 3
|
|
219
|
+
return 2
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MTProto message-id and sequence-number generation, ported from the existing
|
|
5
|
+
* server (`mtproto-tools.generateId` + `generateMessageId`/`generateMessageSeqNo`).
|
|
6
|
+
*
|
|
7
|
+
* A message id encodes a unix timestamp in its high bits; the low 2 bits encode
|
|
8
|
+
* direction/intent: server responses end in 1, notifications in 3. Ids are kept
|
|
9
|
+
* strictly increasing per connection.
|
|
10
|
+
*/
|
|
11
|
+
export interface MsgIdState {
|
|
12
|
+
lastMessageId: bigint | null
|
|
13
|
+
messageSeqNo: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function generateId(): bigint {
|
|
17
|
+
const ticks = Date.now()
|
|
18
|
+
const timeSec = Math.floor(ticks / 1000)
|
|
19
|
+
const timeMSec = ticks % 1000
|
|
20
|
+
const random = randomBytes(2).readUInt16LE(0)
|
|
21
|
+
return (BigInt(timeSec) << 32n) | BigInt((timeMSec << 21) | (random << 3) | 4)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function nextMessageId(state: MsgIdState, isNotification = false): bigint {
|
|
25
|
+
let id = generateId()
|
|
26
|
+
if (state.lastMessageId !== null && id <= state.lastMessageId) {
|
|
27
|
+
id = state.lastMessageId + 1n
|
|
28
|
+
}
|
|
29
|
+
const target = isNotification ? 3n : 1n
|
|
30
|
+
while (id % 4n !== target) id++
|
|
31
|
+
state.lastMessageId = id
|
|
32
|
+
return id
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sequence number for an outgoing message. Content-related messages (rpc_result,
|
|
37
|
+
* updates) consume a slot and get an odd seqno; pure service messages do not.
|
|
38
|
+
*/
|
|
39
|
+
export function nextSeqNo(state: MsgIdState, contentRelated = true): number {
|
|
40
|
+
const seq = state.messageSeqNo
|
|
41
|
+
if (contentRelated) state.messageSeqNo++
|
|
42
|
+
return seq * 2 + (contentRelated ? 1 : 0)
|
|
43
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { toBigIntLE } from '../util/bytes.js'
|
|
3
|
+
import type { SaltRepo, SaltScheduleEntry } from '../storage/types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server-salt scheduler — the spec-faithful core of the salt subsystem
|
|
7
|
+
* (https://core.telegram.org/mtproto/service_messages).
|
|
8
|
+
*
|
|
9
|
+
* Each auth key gets a rolling schedule of 64-bit salts, each valid for a bounded
|
|
10
|
+
* window (~30 min) with overlap: a new salt is minted before the previous expires,
|
|
11
|
+
* so there is always a current salt and a ready successor.
|
|
12
|
+
*
|
|
13
|
+
* Windows lie on a deterministic grid anchored at the first (handshake-derived)
|
|
14
|
+
* salt's `validSince`: window `k` is `[t0 + k·step, t0 + k·step + window)`. Because
|
|
15
|
+
* the anchor is persisted and `step`/`window` are constants, every gateway node
|
|
16
|
+
* derives the same window boundaries and (via the repo's insert-if-absent
|
|
17
|
+
* semantics) converges on one salt per window — so any node validates any salt.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const DEFAULT_WINDOW_SEC = 30 * 60
|
|
21
|
+
const DEFAULT_STEP_SEC = 15 * 60
|
|
22
|
+
|
|
23
|
+
export interface SaltServiceOptions {
|
|
24
|
+
/** Validity length of each salt, seconds (default 1800 = 30 min). */
|
|
25
|
+
windowSec?: number
|
|
26
|
+
/** Spacing between consecutive window starts, seconds. Must be `<= windowSec`
|
|
27
|
+
* for overlap (default 900 = 15 min, giving two concurrently-valid salts). */
|
|
28
|
+
stepSec?: number
|
|
29
|
+
/** Windows to keep minted ahead of the current one (default 1). */
|
|
30
|
+
prefetch?: number
|
|
31
|
+
/** Injectable clock returning unix seconds (default `Date.now()/1000`). */
|
|
32
|
+
nowSec?: () => number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Result of {@link SaltService.resolve}: the salt to advertise + whether the
|
|
36
|
+
* salt the client used is currently valid. */
|
|
37
|
+
export interface SaltCheck {
|
|
38
|
+
current: bigint
|
|
39
|
+
valid: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class SaltService {
|
|
43
|
+
private readonly window: number
|
|
44
|
+
private readonly step: number
|
|
45
|
+
private readonly prefetch: number
|
|
46
|
+
private readonly now: () => number
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
private readonly repo: SaltRepo,
|
|
50
|
+
opts: SaltServiceOptions = {},
|
|
51
|
+
) {
|
|
52
|
+
this.window = opts.windowSec ?? DEFAULT_WINDOW_SEC
|
|
53
|
+
this.step = opts.stepSec ?? DEFAULT_STEP_SEC
|
|
54
|
+
this.prefetch = Math.max(0, opts.prefetch ?? 1)
|
|
55
|
+
this.now = opts.nowSec ?? (() => Math.floor(Date.now() / 1000))
|
|
56
|
+
if (this.step <= 0 || this.step > this.window) {
|
|
57
|
+
throw new Error('SaltService: require 0 < stepSec <= windowSec for overlapping windows')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Seed the schedule's first window from the handshake-derived salt. Idempotent
|
|
63
|
+
* (a no-op if a schedule already exists), and wire-compatible: the first salt
|
|
64
|
+
* keeps its `xor(newNonce, serverNonce)` value.
|
|
65
|
+
*/
|
|
66
|
+
async seed(authKeyId: bigint, firstSalt: bigint): Promise<void> {
|
|
67
|
+
if ((await this.repo.list(authKeyId)).length) return
|
|
68
|
+
const since = this.now()
|
|
69
|
+
await this.repo.append(authKeyId, [
|
|
70
|
+
{ salt: firstSalt, validSince: since, validUntil: since + this.window },
|
|
71
|
+
])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Advertise the current salt and report whether `clientSalt` is valid right
|
|
76
|
+
* now. Mints the current window (and `prefetch` successors) on demand. The
|
|
77
|
+
* decrypt path uses this to drive `bad_server_salt`.
|
|
78
|
+
*/
|
|
79
|
+
async resolve(authKeyId: bigint, clientSalt: bigint): Promise<SaltCheck> {
|
|
80
|
+
const now = this.now()
|
|
81
|
+
const list = await this.ensure(authKeyId, now, this.prefetch)
|
|
82
|
+
return {
|
|
83
|
+
current: pickCurrent(list, now).salt,
|
|
84
|
+
valid: list.some(e => covers(e, now) && e.salt === clientSalt),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The next `num` scheduled salts starting from the current window, minting
|
|
90
|
+
* more if the schedule is short. Backs `get_future_salts(num)`.
|
|
91
|
+
*/
|
|
92
|
+
async future(authKeyId: bigint, num: number): Promise<SaltScheduleEntry[]> {
|
|
93
|
+
const n = Math.max(1, num)
|
|
94
|
+
const now = this.now()
|
|
95
|
+
const list = await this.ensure(authKeyId, now, n - 1)
|
|
96
|
+
const { t0, kNow } = grid(list, now, this.step)
|
|
97
|
+
const out: SaltScheduleEntry[] = []
|
|
98
|
+
for (let k = kNow; k < kNow + n; k++) {
|
|
99
|
+
const since = t0 + k * this.step
|
|
100
|
+
const e = list.find(x => x.validSince === since)
|
|
101
|
+
if (e) out.push(e)
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Ensure the schedule contains the window covering `now` plus `ahead` further
|
|
108
|
+
* grid windows; return the refreshed, ascending schedule. Opportunistically
|
|
109
|
+
* prunes windows that expired more than one window ago.
|
|
110
|
+
*/
|
|
111
|
+
private async ensure(authKeyId: bigint, now: number, ahead: number): Promise<SaltScheduleEntry[]> {
|
|
112
|
+
let list = await this.repo.list(authKeyId)
|
|
113
|
+
if (!list.length) {
|
|
114
|
+
// Defensive: no handshake seed (e.g. get_future_salts on a key with no
|
|
115
|
+
// persisted schedule). Anchor a fresh grid at now.
|
|
116
|
+
await this.repo.append(authKeyId, [this.windowAt(now, randomSalt())])
|
|
117
|
+
list = await this.repo.list(authKeyId)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { t0, kNow } = grid(list, now, this.step)
|
|
121
|
+
const missing: SaltScheduleEntry[] = []
|
|
122
|
+
for (let k = kNow; k <= kNow + ahead; k++) {
|
|
123
|
+
const since = t0 + k * this.step
|
|
124
|
+
if (!list.some(e => e.validSince === since)) missing.push(this.windowAt(since, randomSalt()))
|
|
125
|
+
}
|
|
126
|
+
if (missing.length) {
|
|
127
|
+
await this.repo.append(authKeyId, missing)
|
|
128
|
+
list = await this.repo.list(authKeyId)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Keep one window of expired history (covers messages still in flight).
|
|
132
|
+
await this.repo.prune(authKeyId, now - this.window).catch(() => {})
|
|
133
|
+
return list
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private windowAt(since: number, salt: bigint): SaltScheduleEntry {
|
|
137
|
+
return { salt, validSince: since, validUntil: since + this.window }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function covers(e: SaltScheduleEntry, now: number): boolean {
|
|
142
|
+
return e.validSince <= now && now < e.validUntil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The grid anchor `t0` (first window start) and the index `kNow` of the window
|
|
146
|
+
* covering `now`. All windows sit on `t0 + k·step`, so any surviving entry is a
|
|
147
|
+
* valid anchor — pruning the first one keeps the grid aligned. */
|
|
148
|
+
function grid(list: SaltScheduleEntry[], now: number, step: number): { t0: number; kNow: number } {
|
|
149
|
+
const t0 = list[0]!.validSince
|
|
150
|
+
return { t0, kNow: Math.max(0, Math.floor((now - t0) / step)) }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Newest window covering `now`; falls back to the latest entry if a gap. */
|
|
154
|
+
function pickCurrent(list: SaltScheduleEntry[], now: number): SaltScheduleEntry {
|
|
155
|
+
let best: SaltScheduleEntry | undefined
|
|
156
|
+
for (const e of list) if (covers(e, now) && (!best || e.validSince > best.validSince)) best = e
|
|
157
|
+
return best ?? list[list.length - 1]!
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function randomSalt(): bigint {
|
|
161
|
+
return toBigIntLE(randomBytes(8))
|
|
162
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { noopLogger, type Logger } from '@mt-tl/tl'
|
|
2
|
+
import type { Connection } from '../transport/connection.js'
|
|
3
|
+
import type { Storage } from '../storage/index.js'
|
|
4
|
+
import type { Responder } from '../dispatch/types.js'
|
|
5
|
+
import { randomBigInt } from '../crypto/hashes.js'
|
|
6
|
+
|
|
7
|
+
export interface SessionInfo {
|
|
8
|
+
sessionId: bigint
|
|
9
|
+
authKeyId: bigint
|
|
10
|
+
firstMsgId: bigint
|
|
11
|
+
subject?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensures a persisted session exists for the connection. On the first message
|
|
16
|
+
* of a new session, persists it and emits `new_session_created`. Ported from
|
|
17
|
+
* the existing `handleSessionMessage`, but the session is durable (storage),
|
|
18
|
+
* not an in-memory-only Map.
|
|
19
|
+
*/
|
|
20
|
+
export async function ensureSession(
|
|
21
|
+
storage: Storage,
|
|
22
|
+
responder: Responder,
|
|
23
|
+
conn: Connection,
|
|
24
|
+
info: SessionInfo,
|
|
25
|
+
log: Logger = noopLogger,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const existing = await storage.sessions.get(info.sessionId)
|
|
28
|
+
|
|
29
|
+
if (existing && existing.authKeyId === info.authKeyId) {
|
|
30
|
+
await storage.sessions.touch(info.sessionId)
|
|
31
|
+
conn.ctx.uniqueId = existing.uniqueId
|
|
32
|
+
conn.ctx.apiLayer = existing.apiLayer
|
|
33
|
+
conn.ctx.subject = existing.subject
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (existing) await storage.sessions.delete(info.sessionId)
|
|
37
|
+
|
|
38
|
+
const uniqueId = randomBigInt(64)
|
|
39
|
+
conn.ctx.uniqueId = uniqueId
|
|
40
|
+
conn.ctx.subject = info.subject
|
|
41
|
+
|
|
42
|
+
await storage.sessions.save({
|
|
43
|
+
sessionId: info.sessionId,
|
|
44
|
+
authKeyId: info.authKeyId,
|
|
45
|
+
uniqueId,
|
|
46
|
+
apiLayer: conn.ctx.apiLayer,
|
|
47
|
+
subject: info.subject,
|
|
48
|
+
lastActivity: Date.now(),
|
|
49
|
+
})
|
|
50
|
+
log.info('session.new', {
|
|
51
|
+
sessionId: info.sessionId,
|
|
52
|
+
authKeyId: info.authKeyId,
|
|
53
|
+
subject: info.subject,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
responder.sendEncrypted(
|
|
57
|
+
conn,
|
|
58
|
+
{
|
|
59
|
+
_: 'new_session_created',
|
|
60
|
+
first_msg_id: info.firstMsgId,
|
|
61
|
+
unique_id: uniqueId,
|
|
62
|
+
server_salt: conn.ctx.serverSalt ?? 0n,
|
|
63
|
+
},
|
|
64
|
+
{ isNotification: true, contentRelated: false },
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Storage } from './types.js'
|
|
2
|
+
import { createMemoryStorage } from './memory.js'
|
|
3
|
+
|
|
4
|
+
export type { Storage } from './types.js'
|
|
5
|
+
export type StorageBackend = 'memory' | 'mongo'
|
|
6
|
+
|
|
7
|
+
export interface StorageConfig {
|
|
8
|
+
backend: StorageBackend
|
|
9
|
+
mongoUrl?: string
|
|
10
|
+
mongoDb?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds the configured storage backend. `memory` (default) needs no external
|
|
15
|
+
* services; `mongo` lazily imports the driver so memory mode stays dependency-free.
|
|
16
|
+
*/
|
|
17
|
+
export async function createStorage(config: StorageConfig): Promise<Storage> {
|
|
18
|
+
if (config.backend === 'mongo') {
|
|
19
|
+
if (!config.mongoUrl || !config.mongoDb) {
|
|
20
|
+
throw new Error('STORAGE_BACKEND=mongo requires MONGO_URL and MONGO_DB')
|
|
21
|
+
}
|
|
22
|
+
const { createMongoStorage } = await import('./mongo.js')
|
|
23
|
+
return createMongoStorage(config.mongoUrl, config.mongoDb)
|
|
24
|
+
}
|
|
25
|
+
return createMemoryStorage()
|
|
26
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthKeyMeta,
|
|
3
|
+
AuthKeyRecord,
|
|
4
|
+
AuthKeyRepo,
|
|
5
|
+
SaltRepo,
|
|
6
|
+
SaltScheduleEntry,
|
|
7
|
+
SessionRecord,
|
|
8
|
+
SessionRepo,
|
|
9
|
+
Storage,
|
|
10
|
+
} from './types.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* In-memory storage. The default backend so the gateway runs with no external
|
|
14
|
+
* services (dev, tests). Auth keys/sessions do not survive a restart — use the
|
|
15
|
+
* Mongo backend for that.
|
|
16
|
+
*/
|
|
17
|
+
class MemoryAuthKeyRepo implements AuthKeyRepo {
|
|
18
|
+
private map = new Map<string, AuthKeyRecord>()
|
|
19
|
+
|
|
20
|
+
async create(rec: AuthKeyRecord): Promise<void> {
|
|
21
|
+
this.map.set(rec.id.toString(), { ...rec })
|
|
22
|
+
}
|
|
23
|
+
async getById(id: bigint): Promise<AuthKeyRecord | null> {
|
|
24
|
+
return this.map.get(id.toString()) ?? null
|
|
25
|
+
}
|
|
26
|
+
async setBlocked(id: bigint, blocked: boolean): Promise<void> {
|
|
27
|
+
const rec = this.map.get(id.toString())
|
|
28
|
+
if (rec) rec.isBlocked = blocked
|
|
29
|
+
}
|
|
30
|
+
async bindUser(id: bigint, subject: string | null): Promise<void> {
|
|
31
|
+
const rec = this.map.get(id.toString())
|
|
32
|
+
if (rec) rec.subject = subject
|
|
33
|
+
}
|
|
34
|
+
async updateMeta(id: bigint, patch: AuthKeyMeta): Promise<void> {
|
|
35
|
+
const rec = this.map.get(id.toString())
|
|
36
|
+
if (!rec) return
|
|
37
|
+
const meta = (rec.meta ??= {})
|
|
38
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
39
|
+
if (v !== undefined) (meta as Record<string, unknown>)[k] = v
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class MemorySaltRepo implements SaltRepo {
|
|
45
|
+
private map = new Map<string, SaltScheduleEntry[]>()
|
|
46
|
+
async append(authKeyId: bigint, entries: SaltScheduleEntry[]): Promise<void> {
|
|
47
|
+
const k = authKeyId.toString()
|
|
48
|
+
const list = this.map.get(k) ?? []
|
|
49
|
+
for (const e of entries) {
|
|
50
|
+
// Insert-if-absent by window start; never overwrite an existing salt.
|
|
51
|
+
if (!list.some(x => x.validSince === e.validSince)) list.push({ ...e })
|
|
52
|
+
}
|
|
53
|
+
list.sort((a, b) => a.validSince - b.validSince)
|
|
54
|
+
this.map.set(k, list)
|
|
55
|
+
}
|
|
56
|
+
async list(authKeyId: bigint): Promise<SaltScheduleEntry[]> {
|
|
57
|
+
return (this.map.get(authKeyId.toString()) ?? []).map(e => ({ ...e }))
|
|
58
|
+
}
|
|
59
|
+
async prune(authKeyId: bigint, before: number): Promise<void> {
|
|
60
|
+
const k = authKeyId.toString()
|
|
61
|
+
const list = this.map.get(k)
|
|
62
|
+
if (list)
|
|
63
|
+
this.map.set(
|
|
64
|
+
k,
|
|
65
|
+
list.filter(e => e.validUntil > before),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class MemorySessionRepo implements SessionRepo {
|
|
71
|
+
private map = new Map<string, SessionRecord>()
|
|
72
|
+
async get(sessionId: bigint): Promise<SessionRecord | null> {
|
|
73
|
+
const r = this.map.get(sessionId.toString())
|
|
74
|
+
return r ? { ...r } : null
|
|
75
|
+
}
|
|
76
|
+
async save(rec: SessionRecord): Promise<void> {
|
|
77
|
+
this.map.set(rec.sessionId.toString(), { ...rec })
|
|
78
|
+
}
|
|
79
|
+
async update(sessionId: bigint, patch: Partial<SessionRecord>): Promise<void> {
|
|
80
|
+
const r = this.map.get(sessionId.toString())
|
|
81
|
+
if (r) this.map.set(sessionId.toString(), { ...r, ...patch })
|
|
82
|
+
}
|
|
83
|
+
async delete(sessionId: bigint): Promise<void> {
|
|
84
|
+
this.map.delete(sessionId.toString())
|
|
85
|
+
}
|
|
86
|
+
async touch(sessionId: bigint): Promise<boolean> {
|
|
87
|
+
const r = this.map.get(sessionId.toString())
|
|
88
|
+
if (!r) return false
|
|
89
|
+
r.lastActivity = Date.now()
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createMemoryStorage(): Storage {
|
|
95
|
+
return {
|
|
96
|
+
authKeys: new MemoryAuthKeyRepo(),
|
|
97
|
+
salts: new MemorySaltRepo(),
|
|
98
|
+
sessions: new MemorySessionRepo(),
|
|
99
|
+
async close() {},
|
|
100
|
+
}
|
|
101
|
+
}
|