@mt-tl/tl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +47 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/codegen/gen-types.d.ts +10 -0
  8. package/dist/codegen/gen-types.d.ts.map +1 -0
  9. package/dist/codegen/gen-types.js +190 -0
  10. package/dist/codegen/gen-types.js.map +1 -0
  11. package/dist/index.d.ts +13 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +21 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/logger.d.ts +63 -0
  16. package/dist/logger.d.ts.map +1 -0
  17. package/dist/logger.js +147 -0
  18. package/dist/logger.js.map +1 -0
  19. package/dist/migrate.d.ts +37 -0
  20. package/dist/migrate.d.ts.map +1 -0
  21. package/dist/migrate.js +84 -0
  22. package/dist/migrate.js.map +1 -0
  23. package/dist/rpc.d.ts +74 -0
  24. package/dist/rpc.d.ts.map +1 -0
  25. package/dist/rpc.js +2 -0
  26. package/dist/rpc.js.map +1 -0
  27. package/dist/tl/crc32.d.ts +2 -0
  28. package/dist/tl/crc32.d.ts.map +1 -0
  29. package/dist/tl/crc32.js +42 -0
  30. package/dist/tl/crc32.js.map +1 -0
  31. package/dist/tl/ir.d.ts +68 -0
  32. package/dist/tl/ir.d.ts.map +1 -0
  33. package/dist/tl/ir.js +63 -0
  34. package/dist/tl/ir.js.map +1 -0
  35. package/dist/tl/parser.d.ts +22 -0
  36. package/dist/tl/parser.d.ts.map +1 -0
  37. package/dist/tl/parser.js +122 -0
  38. package/dist/tl/parser.js.map +1 -0
  39. package/dist/tl/value.d.ts +26 -0
  40. package/dist/tl/value.d.ts.map +1 -0
  41. package/dist/tl/value.js +44 -0
  42. package/dist/tl/value.js.map +1 -0
  43. package/dist/tools/freeze-layer.d.ts +26 -0
  44. package/dist/tools/freeze-layer.d.ts.map +1 -0
  45. package/dist/tools/freeze-layer.js +86 -0
  46. package/dist/tools/freeze-layer.js.map +1 -0
  47. package/dist/wire.d.ts +12 -0
  48. package/dist/wire.d.ts.map +1 -0
  49. package/dist/wire.js +50 -0
  50. package/dist/wire.js.map +1 -0
  51. package/package.json +76 -0
  52. package/schema/scheme_0_protocol.tl +150 -0
  53. package/src/cli.ts +48 -0
  54. package/src/codegen/gen-types.ts +203 -0
  55. package/src/index.ts +22 -0
  56. package/src/logger.ts +210 -0
  57. package/src/migrate.ts +101 -0
  58. package/src/rpc.ts +70 -0
  59. package/src/tl/crc32.ts +44 -0
  60. package/src/tl/ir.ts +105 -0
  61. package/src/tl/parser.ts +144 -0
  62. package/src/tl/value.ts +60 -0
  63. package/src/tools/freeze-layer.ts +108 -0
  64. package/src/wire.ts +54 -0
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { fileURLToPath } from 'node:url'
2
+
3
+ export * from './tl/ir.js'
4
+ export * from './tl/value.js'
5
+ export * from './tl/parser.js'
6
+ export * from './tl/crc32.js'
7
+ export * from './rpc.js'
8
+ export * from './wire.js'
9
+ export * from './migrate.js'
10
+ // Shared structured logger — the server engine, the handler layer, and your app
11
+ // import it for one consistent log style (see docs/guide/observability.md).
12
+ export * from './logger.js'
13
+ // Codegen + layer tooling — consumers generate TS types and freeze layer
14
+ // snapshots from THEIR `.tl` schema.
15
+ export { generateSchemaTs, writeSchemaTs } from './codegen/gen-types.js'
16
+ export { freezeLayer, type FreezeResult } from './tools/freeze-layer.js'
17
+
18
+ // @mt-tl/tl ships only the fixed MTProto **protocol** schema; business `.tl`
19
+ // lives in the consumer app. This is the protocol-only default; apps pass their
20
+ // own schema dir to the gateway.
21
+ /** Absolute path to the bundled MTProto protocol `.tl` schema directory. */
22
+ export const protocolSchemaDir = fileURLToPath(new URL('../schema', import.meta.url))
package/src/logger.ts ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * A tiny dependency-free structured logger, shared across the MTProto packages
3
+ * (the server engine, the handler layer, and your app — import it for one
4
+ * consistent log style). Levels gate output; fields are key/value context.
5
+ *
6
+ * - `LOG_LEVEL` sets the threshold (`trace`<`debug`<`info`<`warn`<`error`<`silent`).
7
+ * - `LOG_FORMAT=json` emits one JSON object per line (ship to a log pipeline);
8
+ * anything else is a readable line for local dev.
9
+ * - `LOG_ERROR_STACK=true|false` forces whether `Error.stack` is serialized
10
+ * (default: on for `pretty`, off for `json` — prod opts in).
11
+ * - `pretty` output is ANSI-colored when stdout is a TTY (dim keys, colored
12
+ * level) for readability; honors `NO_COLOR` / `FORCE_COLOR`.
13
+ * - Tests default to `silent` (set `LOG_LEVEL` to override).
14
+ *
15
+ * Levels, by convention across this codebase:
16
+ * - `trace` — byte/hex protocol firehose (every recv, framing headers, decrypt).
17
+ * - `debug` — useful protocol events (decoded method, salt re-send, handshake step).
18
+ * - `info` — request/response one-liners + lifecycle links/unlinks (sockets,
19
+ * sessions, auth keys, users) + update deliveries.
20
+ * - `warn` — recoverable anomalies (decode failure, unknown method, integrity
21
+ * rejection, insecure config).
22
+ * - `error` — a request that failed or an update that could not be delivered.
23
+ */
24
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent'
25
+
26
+ const ORDER: Record<LogLevel, number> = {
27
+ trace: 5,
28
+ debug: 10,
29
+ info: 20,
30
+ warn: 30,
31
+ error: 40,
32
+ silent: 99,
33
+ }
34
+
35
+ export type Fields = Record<string, unknown>
36
+
37
+ export interface Logger {
38
+ /** Byte/hex protocol firehose — guard heavy field building with {@link isLevelEnabled}. */
39
+ trace(msg: string, fields?: Fields): void
40
+ debug(msg: string, fields?: Fields): void
41
+ info(msg: string, fields?: Fields): void
42
+ warn(msg: string, fields?: Fields): void
43
+ error(msg: string, fields?: Fields): void
44
+ /** Returns a logger that merges `bindings` into every line (e.g. a scope/conn id). */
45
+ child(bindings: Fields): Logger
46
+ /** True when a message at `level` would be emitted — guard expensive field building. */
47
+ isLevelEnabled(level: LogLevel): boolean
48
+ /** The active threshold level. */
49
+ readonly level: LogLevel
50
+ }
51
+
52
+ export interface LoggerOptions {
53
+ level?: LogLevel
54
+ /** 'json' for machine-readable lines, 'pretty' for humans. */
55
+ format?: 'json' | 'pretty'
56
+ name?: string
57
+ bindings?: Fields
58
+ /**
59
+ * Include `Error.stack` when serializing an Error field. Default: `true` for
60
+ * `pretty` (dev), `false` for `json` (prod opts in). `LOG_ERROR_STACK` overrides.
61
+ */
62
+ errorStack?: boolean
63
+ /**
64
+ * ANSI-color the `pretty` output (dim keys, colored level). Default: auto — on
65
+ * when stdout is a TTY and `NO_COLOR` is unset (off when a custom `write` is
66
+ * given). Never colors `json`.
67
+ */
68
+ color?: boolean
69
+ /** Sink (defaults to stdout/stderr by level). Override in tests. */
70
+ write?: (line: string) => void
71
+ }
72
+
73
+ function defaultLevel(): LogLevel {
74
+ const env = process.env.LOG_LEVEL as LogLevel | undefined
75
+ if (env && env in ORDER) return env
76
+ if (process.env.VITEST || process.env.NODE_ENV === 'test') return 'silent'
77
+ return 'info'
78
+ }
79
+
80
+ function defaultFormat(): 'json' | 'pretty' {
81
+ return process.env.LOG_FORMAT === 'json' ? 'json' : 'pretty'
82
+ }
83
+
84
+ function defaultErrorStack(format: 'json' | 'pretty'): boolean {
85
+ const env = process.env.LOG_ERROR_STACK
86
+ if (env === 'true' || env === '1') return true
87
+ if (env === 'false' || env === '0') return false
88
+ return format === 'pretty'
89
+ }
90
+
91
+ function defaultColor(format: 'json' | 'pretty'): boolean {
92
+ if (format === 'json') return false
93
+ if (process.env.NO_COLOR !== undefined) return false
94
+ if (process.env.FORCE_COLOR) return true
95
+ return !!process.stdout.isTTY
96
+ }
97
+
98
+ // ANSI styling for the pretty (TTY) format.
99
+ const RESET = '\x1b[0m'
100
+ const DIM = '\x1b[2m'
101
+ const BOLD = '\x1b[1m'
102
+ const GRAY = '\x1b[90m'
103
+ const LEVEL_COLOR: Record<Exclude<LogLevel, 'silent'>, string> = {
104
+ trace: '\x1b[90m', // gray
105
+ debug: '\x1b[36m', // cyan
106
+ info: '\x1b[32m', // green
107
+ warn: '\x1b[33m', // yellow
108
+ error: '\x1b[31m', // red
109
+ }
110
+
111
+ function makeSerialize(errorStack: boolean): (value: unknown) => unknown {
112
+ return function serialize(value: unknown): unknown {
113
+ if (value instanceof Error) {
114
+ const base: Fields = { name: value.name, message: value.message }
115
+ if (errorStack && value.stack) base.stack = value.stack
116
+ return base
117
+ }
118
+ if (typeof value === 'bigint') return value.toString()
119
+ return value
120
+ }
121
+ }
122
+
123
+ function prettyFields(fields: Fields, serialize: (v: unknown) => unknown, color: boolean): string {
124
+ const parts: string[] = []
125
+ for (const [k, v] of Object.entries(fields)) {
126
+ const sv = serialize(v)
127
+ const val = typeof sv === 'object' && sv !== null ? JSON.stringify(sv) : String(sv)
128
+ // Dim the key so the eye separates key from value; values stay bright.
129
+ parts.push(color ? `${DIM}${k}=${RESET}${val}` : `${k}=${val}`)
130
+ }
131
+ if (!parts.length) return ''
132
+ // Wider gap between fields when colored (the TTY view we optimize for reading).
133
+ const sep = color ? ' ' : ' '
134
+ return sep + parts.join(sep)
135
+ }
136
+
137
+ export function createLogger(options: LoggerOptions = {}): Logger {
138
+ const level = options.level ?? defaultLevel()
139
+ const format = options.format ?? defaultFormat()
140
+ const errorStack = options.errorStack ?? defaultErrorStack(format)
141
+ // A custom sink (tests/embedders) gets no color unless explicitly asked.
142
+ const color = options.color ?? (options.write ? false : defaultColor(format))
143
+ const name = options.name
144
+ const bindings = options.bindings ?? {}
145
+ const threshold = ORDER[level]
146
+ const serialize = makeSerialize(errorStack)
147
+
148
+ const emit = (lvl: Exclude<LogLevel, 'silent'>, msg: string, fields?: Fields) => {
149
+ if (ORDER[lvl] < threshold) return
150
+ const merged: Fields = { ...bindings, ...(fields ?? {}) }
151
+ const write = options.write ?? (lvl === 'error' || lvl === 'warn' ? errLine : outLine)
152
+ if (format === 'json') {
153
+ const obj: Fields = { time: new Date().toISOString(), level: lvl, ...(name ? { name } : {}), msg }
154
+ for (const [k, v] of Object.entries(merged)) obj[k] = serialize(v)
155
+ write(JSON.stringify(obj))
156
+ } else {
157
+ const ts = new Date().toISOString().slice(11, 23)
158
+ const lvlText = lvl.toUpperCase().padEnd(5)
159
+ if (color) {
160
+ const tag = name ? ` ${DIM}[${name}]${RESET}` : ''
161
+ write(
162
+ `${GRAY}${ts}${RESET} ${LEVEL_COLOR[lvl]}${lvlText}${RESET}${tag} ${BOLD}${msg}${RESET}` +
163
+ prettyFields(merged, serialize, true),
164
+ )
165
+ } else {
166
+ const tag = name ? ` [${name}]` : ''
167
+ write(`${ts} ${lvlText}${tag} ${msg}` + prettyFields(merged, serialize, false))
168
+ }
169
+ }
170
+ }
171
+
172
+ return {
173
+ level,
174
+ trace: (msg, fields) => emit('trace', msg, fields),
175
+ debug: (msg, fields) => emit('debug', msg, fields),
176
+ info: (msg, fields) => emit('info', msg, fields),
177
+ warn: (msg, fields) => emit('warn', msg, fields),
178
+ error: (msg, fields) => emit('error', msg, fields),
179
+ isLevelEnabled: lvl => ORDER[lvl] >= threshold,
180
+ child: extra =>
181
+ createLogger({
182
+ ...options,
183
+ level,
184
+ format,
185
+ errorStack,
186
+ color,
187
+ name,
188
+ bindings: { ...bindings, ...extra },
189
+ }),
190
+ }
191
+ }
192
+
193
+ function outLine(line: string): void {
194
+ process.stdout.write(line + '\n')
195
+ }
196
+ function errLine(line: string): void {
197
+ process.stderr.write(line + '\n')
198
+ }
199
+
200
+ /** A logger that drops everything (explicit opt-out in tests/embedders). */
201
+ export const noopLogger: Logger = {
202
+ level: 'silent',
203
+ trace() {},
204
+ debug() {},
205
+ info() {},
206
+ warn() {},
207
+ error() {},
208
+ isLevelEnabled: () => false,
209
+ child: () => noopLogger,
210
+ }
package/src/migrate.ts ADDED
@@ -0,0 +1,101 @@
1
+ import type { TlObject, TlValue } from './tl/value.js'
2
+
3
+ /**
4
+ * One rung of a predicate's migration ladder. `up` maps THIS version's shape to
5
+ * the NEXT version's; `down` maps the next version back to this one. The newest
6
+ * (canonical) rung has neither.
7
+ */
8
+ export interface MigrationRung {
9
+ /** Layer this shape was introduced. */
10
+ since: number
11
+ up?: (obj: Record<string, unknown>) => Record<string, unknown>
12
+ down?: (obj: Record<string, unknown>) => Record<string, unknown>
13
+ }
14
+
15
+ /**
16
+ * Per-predicate migration ladders. The gateway normalizes inbound values to the
17
+ * canonical (newest) shape (`up`) before forwarding, and renders canonical values
18
+ * down to a client's layer (`down`) before encoding — so workers only ever see
19
+ * the canonical shape. Scales to N versions: one rung per version; adding a
20
+ * version appends a rung and never touches the others.
21
+ *
22
+ * Only non-additively-changed predicates need a ladder; everything else is
23
+ * handled by decode-union + layered-encode with no rungs (identity here).
24
+ */
25
+ export class MigrationRegistry {
26
+ private byPredicate = new Map<string, MigrationRung[]>()
27
+
28
+ /** Register a predicate's ladder (rungs in any order; sorted by `since`). */
29
+ register(predicate: string, rungs: MigrationRung[]): this {
30
+ this.byPredicate.set(
31
+ predicate,
32
+ [...rungs].sort((a, b) => a.since - b.since),
33
+ )
34
+ return this
35
+ }
36
+
37
+ has(predicate: string): boolean {
38
+ return this.byPredicate.has(predicate)
39
+ }
40
+
41
+ get size(): number {
42
+ return this.byPredicate.size
43
+ }
44
+
45
+ /** Normalize a value decoded at `fromLayer` up to the canonical shape. */
46
+ up(value: TlValue, fromLayer: number): TlValue {
47
+ return this.recurse(value, obj => this.upObject(obj, fromLayer))
48
+ }
49
+
50
+ /** Render a canonical value down to `toLayer`'s shape. */
51
+ down(value: TlValue, toLayer: number): TlValue {
52
+ return this.recurse(value, obj => this.downObject(obj, toLayer))
53
+ }
54
+
55
+ // Children-first walk: normalize nested objects, then transform the parent.
56
+ private recurse(value: TlValue, fn: (obj: TlObject) => TlObject): TlValue {
57
+ if (Array.isArray(value)) return value.map(v => this.recurse(v, fn))
58
+ if (value && typeof value === 'object' && !Buffer.isBuffer(value) && '_' in value) {
59
+ const obj = value as TlObject
60
+ const next: TlObject = { _: obj._ }
61
+ for (const [k, v] of Object.entries(obj)) {
62
+ if (k !== '_') next[k] = this.recurse(v as TlValue, fn)
63
+ }
64
+ return fn(next)
65
+ }
66
+ return value
67
+ }
68
+
69
+ private upObject(obj: TlObject, fromLayer: number): TlObject {
70
+ const rungs = this.byPredicate.get(obj._)
71
+ if (!rungs) return obj
72
+ let cur: Record<string, unknown> = obj
73
+ for (let i = startRung(rungs, fromLayer); i <= rungs.length - 2; i++) {
74
+ const up = rungs[i]!.up
75
+ if (up) cur = up(cur)
76
+ }
77
+ return cur as TlObject
78
+ }
79
+
80
+ private downObject(obj: TlObject, toLayer: number): TlObject {
81
+ const rungs = this.byPredicate.get(obj._)
82
+ if (!rungs) return obj
83
+ const target = startRung(rungs, toLayer)
84
+ let cur: Record<string, unknown> = obj
85
+ for (let i = rungs.length - 2; i >= target; i--) {
86
+ const down = rungs[i]!.down
87
+ if (down) cur = down(cur)
88
+ }
89
+ return cur as TlObject
90
+ }
91
+ }
92
+
93
+ /** Index of the rung active at `layer` (the latest with since <= layer; else 0). */
94
+ function startRung(rungs: MigrationRung[], layer: number): number {
95
+ let idx = 0
96
+ for (let i = 0; i < rungs.length; i++) {
97
+ if (rungs[i]!.since <= layer) idx = i
98
+ else break
99
+ }
100
+ return idx
101
+ }
package/src/rpc.ts ADDED
@@ -0,0 +1,70 @@
1
+ import type { JsonValue } from './tl/value.js'
2
+
3
+ /** Per-request connection context the engine attaches to every business call; surfaced as `ctx.request`. */
4
+ export interface RpcContext {
5
+ /** The MTProto session id (hex). */
6
+ sessionId: string
7
+ /** The connection's auth key id (hex). */
8
+ authKeyId: string
9
+ /**
10
+ * The **subject** bound to this auth key — your app's *internal* user id,
11
+ * opaque to the framework and safe to share across your services (e.g. a
12
+ * uuid). Set it via `ctx.login(subject)`; `undefined` for an anonymous key.
13
+ *
14
+ * This is deliberately NOT the wire `user_id` your TL schema exposes to
15
+ * clients (Telegram-style `int`/`long`). The framework only routes/persists
16
+ * by `subject`; mapping `subject ⇄ public user_id` is your app's job (see the
17
+ * demo's `users` module). Keeping them separate lets the public id stay an
18
+ * `int` while the protocol runs entirely on your internal id.
19
+ */
20
+ subject?: string
21
+ /** The client's negotiated TL layer. */
22
+ apiLayer: number
23
+ /** `initConnection.api_id` — the client app id, if reported. */
24
+ apiId?: number
25
+ /** `initConnection.device_model`, if reported. */
26
+ deviceModel?: string
27
+ /** `initConnection.system_version`, if reported. */
28
+ systemVersion?: string
29
+ /** `initConnection.app_version`, if reported. */
30
+ appVersion?: string
31
+ /** `initConnection.lang_code`, if reported. */
32
+ langCode?: string
33
+ /** Client IP (from the carrier / `X-Forwarded-For`), if known. */
34
+ ip?: string
35
+ }
36
+
37
+ /** A decoded business method call handed to the handler layer. */
38
+ export interface RpcRequest {
39
+ /** Derived from the client's reqMsgId. */
40
+ id: string
41
+ /** TL method name, e.g. "dust.getConfig". */
42
+ method: string
43
+ /** Decoded params as tagged JSON (bigint -> {$bigint}, Buffer -> {$bin}). */
44
+ params: JsonValue
45
+ /** The connection context (see {@link RpcContext}). */
46
+ context: RpcContext
47
+ }
48
+
49
+ /**
50
+ * A side-effect a handler records (via `ctx.login`/`logout`/`revoke`) for the
51
+ * engine to apply to auth-key state, returned alongside the normal result/error.
52
+ * Keeps the engine agnostic to your auth scheme: a `signIn` handler returns
53
+ * `bindUser`, and the engine binds the `subject` (your internal user id) to the
54
+ * auth key.
55
+ */
56
+ export type SessionEffect =
57
+ | { type: 'bindUser'; subject: string }
58
+ | { type: 'unbindUser' }
59
+ | { type: 'revokeKey' }
60
+
61
+ /**
62
+ * Response envelope (transport-agnostic). Exactly one of `result` / `error` is
63
+ * set; `effects` may accompany either.
64
+ */
65
+ export interface RpcResponse {
66
+ /** A TL result as tagged JSON ({ _: name, ... }, $bigint/$bin), or a primitive. */
67
+ result?: JsonValue
68
+ error?: { code: number; message: string }
69
+ effects?: SessionEffect[]
70
+ }
@@ -0,0 +1,44 @@
1
+ import CRC32 from 'crc-32'
2
+
3
+ /**
4
+ * Computes a TL constructor id (crc32 of the normalized definition line),
5
+ * mirroring the existing backend's `get-tl-object-crc32.js` exactly so our
6
+ * validation agrees with the schema's declared ids.
7
+ *
8
+ * Normalization (applied repeatedly until stable):
9
+ * :bytes -> :string
10
+ * ?bytes -> ?string
11
+ * #<hex> -> (constructor id removed)
12
+ * name:flags.N?true -> (removed; presence-only flags don't affect the id)
13
+ * < > and {} and ; -> stripped / spaced
14
+ * collapse double / edge spaces
15
+ */
16
+ function normalizeSchemaLine(line: string): string {
17
+ const rules: Array<[RegExp, string]> = [
18
+ [/:bytes /g, ':string '],
19
+ [/\?bytes /g, '?string '],
20
+ [/#[a-f0-9]+ /g, ' '],
21
+ [/ [a-zA-Z0-9_]+:flags\.[0-9]+\?true/g, ''],
22
+ [/</g, ' '],
23
+ [/>/g, ' '],
24
+ [/;/g, ''],
25
+ [/\{/g, ''],
26
+ [/\}/g, ''],
27
+ ]
28
+
29
+ let out = line
30
+ let prev: string
31
+ do {
32
+ prev = out
33
+ for (const [re, to] of rules) out = out.replace(re, to)
34
+ out = out.replace(/ {2}/g, ' ').replace(/^ /, '').replace(/ $/, '')
35
+ } while (out !== prev)
36
+
37
+ return out
38
+ }
39
+
40
+ export function getTlObjectCrc32(line: string): string {
41
+ const hashNum = CRC32.bstr(normalizeSchemaLine(line))
42
+ const unsigned = hashNum < 0 ? hashNum + 0x100000000 : hashNum
43
+ return unsigned.toString(16).padStart(8, '0')
44
+ }
package/src/tl/ir.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Intermediate representation (IR) for the TL schema.
3
+ *
4
+ * A `.tl` file is parsed into a flat list of {@link TlDef} (constructors and
5
+ * methods). Each parameter's textual type is parsed once, at load time, into a
6
+ * structured {@link TlType} so the generic codec can walk it without re-parsing
7
+ * strings on every (de)serialization.
8
+ */
9
+
10
+ export type TlType =
11
+ | { kind: 'int' } // 4 bytes LE -> number
12
+ | { kind: 'long' } // 8 bytes LE -> bigint
13
+ | { kind: 'double' } // 8 bytes IEEE754 LE -> number
14
+ | { kind: 'string' } // length-prefixed utf-8 -> string
15
+ | { kind: 'bytes' } // length-prefixed raw -> Buffer
16
+ | { kind: 'int128' } // 16 raw bytes -> Buffer
17
+ | { kind: 'int256' } // 32 raw bytes -> Buffer
18
+ | { kind: 'bool' } // boxed Bool -> boolean
19
+ | { kind: 'true' } // bare `true`, only as a flag presence marker -> boolean
20
+ | { kind: 'flags' } // the `#` bitmask field
21
+ | { kind: 'flag'; flagsField: string; bit: number; inner: TlType } // conditional field
22
+ | { kind: 'vector'; boxed: boolean; inner: TlType }
23
+ | { kind: 'object' } // `Object` / `!X` / `X` — a nested boxed object (read its ctor id)
24
+ | { kind: 'boxed'; name: string } // a named polymorphic type — read/write with ctor id
25
+ | { kind: 'bare'; name: string } // a named bare constructor (`%X` / lowercase) — no ctor id
26
+
27
+ export interface TlParam {
28
+ name: string
29
+ /** Original textual type, kept for diagnostics / crc. */
30
+ raw: string
31
+ type: TlType
32
+ }
33
+
34
+ export interface TlDef {
35
+ /** 8-char lowercase hex constructor id. */
36
+ id: string
37
+ /** Unsigned 32-bit numeric form of {@link id}. */
38
+ idNum: number
39
+ /** predicate (constructor) or method name, e.g. `dust.getConfig`. */
40
+ name: string
41
+ kind: 'constructor' | 'method'
42
+ params: TlParam[]
43
+ /** Boxed result/return type, e.g. `dust.CalculatedExchange`. */
44
+ type: string
45
+ /** True for the immutable MTProto protocol layer (scheme_0_protocol + core). */
46
+ isProtocol: boolean
47
+ }
48
+
49
+ const PRIMITIVES = new Set(['int', 'long', 'double', 'string', 'bytes', 'int128', 'int256', 'Bool', 'true'])
50
+
51
+ export function parseType(raw: string): TlType {
52
+ const t = raw.trim()
53
+
54
+ if (t === '#') return { kind: 'flags' }
55
+
56
+ // conditional field: <flagsField>.<bit>?<inner>, e.g. api_hash:flags.1?string
57
+ const cond = t.match(/^(\w+)\.(\d+)\?(.+)$/)
58
+ if (cond) {
59
+ return {
60
+ kind: 'flag',
61
+ flagsField: cond[1]!,
62
+ bit: Number(cond[2]),
63
+ inner: parseType(cond[3]!),
64
+ }
65
+ }
66
+
67
+ // Vector<T> (boxed) or vector<T> (bare)
68
+ const vec = t.match(/^([Vv])ector<(.+)>$/)
69
+ if (vec) {
70
+ return { kind: 'vector', boxed: vec[1] === 'V', inner: parseType(vec[2]!) }
71
+ }
72
+
73
+ if (PRIMITIVES.has(t)) {
74
+ switch (t) {
75
+ case 'int':
76
+ return { kind: 'int' }
77
+ case 'long':
78
+ return { kind: 'long' }
79
+ case 'double':
80
+ return { kind: 'double' }
81
+ case 'string':
82
+ return { kind: 'string' }
83
+ case 'bytes':
84
+ return { kind: 'bytes' }
85
+ case 'int128':
86
+ return { kind: 'int128' }
87
+ case 'int256':
88
+ return { kind: 'int256' }
89
+ case 'Bool':
90
+ return { kind: 'bool' }
91
+ case 'true':
92
+ return { kind: 'true' }
93
+ }
94
+ }
95
+
96
+ if (t === 'Object' || t === 'X' || t.startsWith('!')) return { kind: 'object' }
97
+ if (t.startsWith('%')) return { kind: 'bare', name: t.slice(1) }
98
+
99
+ // Named type. Boxed iff the final segment is capitalized (e.g. dust.DustBalance,
100
+ // Currency); otherwise a bare constructor reference (e.g. future_salt).
101
+ const lastSeg = t.includes('.') ? t.slice(t.lastIndexOf('.') + 1) : t
102
+ const first = lastSeg[0] ?? ''
103
+ if (first >= 'A' && first <= 'Z') return { kind: 'boxed', name: t }
104
+ return { kind: 'bare', name: t }
105
+ }