@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.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen/gen-types.d.ts +10 -0
- package/dist/codegen/gen-types.d.ts.map +1 -0
- package/dist/codegen/gen-types.js +190 -0
- package/dist/codegen/gen-types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +63 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +147 -0
- package/dist/logger.js.map +1 -0
- package/dist/migrate.d.ts +37 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +84 -0
- package/dist/migrate.js.map +1 -0
- package/dist/rpc.d.ts +74 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +2 -0
- package/dist/rpc.js.map +1 -0
- package/dist/tl/crc32.d.ts +2 -0
- package/dist/tl/crc32.d.ts.map +1 -0
- package/dist/tl/crc32.js +42 -0
- package/dist/tl/crc32.js.map +1 -0
- package/dist/tl/ir.d.ts +68 -0
- package/dist/tl/ir.d.ts.map +1 -0
- package/dist/tl/ir.js +63 -0
- package/dist/tl/ir.js.map +1 -0
- package/dist/tl/parser.d.ts +22 -0
- package/dist/tl/parser.d.ts.map +1 -0
- package/dist/tl/parser.js +122 -0
- package/dist/tl/parser.js.map +1 -0
- package/dist/tl/value.d.ts +26 -0
- package/dist/tl/value.d.ts.map +1 -0
- package/dist/tl/value.js +44 -0
- package/dist/tl/value.js.map +1 -0
- package/dist/tools/freeze-layer.d.ts +26 -0
- package/dist/tools/freeze-layer.d.ts.map +1 -0
- package/dist/tools/freeze-layer.js +86 -0
- package/dist/tools/freeze-layer.js.map +1 -0
- package/dist/wire.d.ts +12 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +50 -0
- package/dist/wire.js.map +1 -0
- package/package.json +76 -0
- package/schema/scheme_0_protocol.tl +150 -0
- package/src/cli.ts +48 -0
- package/src/codegen/gen-types.ts +203 -0
- package/src/index.ts +22 -0
- package/src/logger.ts +210 -0
- package/src/migrate.ts +101 -0
- package/src/rpc.ts +70 -0
- package/src/tl/crc32.ts +44 -0
- package/src/tl/ir.ts +105 -0
- package/src/tl/parser.ts +144 -0
- package/src/tl/value.ts +60 -0
- package/src/tools/freeze-layer.ts +108 -0
- package/src/wire.ts +54 -0
package/src/tl/parser.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { TlDef, TlParam } from './ir.js'
|
|
4
|
+
import { parseType } from './ir.js'
|
|
5
|
+
import { getTlObjectCrc32 } from './crc32.js'
|
|
6
|
+
|
|
7
|
+
const LINE_RE = /^([\w.]+)#([0-9a-fA-F]+)\s*(.*?)\s*=\s*([\w.]+)\s*;\s*$/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Core MTProto constructors that scheme_0_protocol.tl leaves commented out
|
|
11
|
+
* ("parsed manually"). They still need registry entries for id<->name lookup.
|
|
12
|
+
*/
|
|
13
|
+
const CORE_LINES: Array<{ kind: 'constructor' | 'method'; line: string }> = [
|
|
14
|
+
{ kind: 'constructor', line: 'vector#1cb5c415 = Vector;' },
|
|
15
|
+
{ kind: 'constructor', line: 'rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult;' },
|
|
16
|
+
{ kind: 'constructor', line: 'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;' },
|
|
17
|
+
{
|
|
18
|
+
kind: 'constructor',
|
|
19
|
+
line: 'message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message;',
|
|
20
|
+
},
|
|
21
|
+
{ kind: 'constructor', line: 'msg_copy#e06046b2 orig_message:Message = MessageCopy;' },
|
|
22
|
+
{ kind: 'constructor', line: 'gzip_packed#3072cfa1 packed_data:bytes = Object;' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
function parseParams(paramsStr: string): TlParam[] {
|
|
26
|
+
if (!paramsStr) return []
|
|
27
|
+
return paramsStr
|
|
28
|
+
.split(/\s+/)
|
|
29
|
+
.filter(tok => tok.includes(':') && !tok.includes('{') && !tok.includes('}'))
|
|
30
|
+
.map(tok => {
|
|
31
|
+
const idx = tok.indexOf(':')
|
|
32
|
+
const name = tok.slice(0, idx)
|
|
33
|
+
const raw = tok.slice(idx + 1)
|
|
34
|
+
return { name, raw, type: parseType(raw) }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ParsedLine {
|
|
39
|
+
def: TlDef
|
|
40
|
+
/** declared id differs from the crc32 of the normalized line */
|
|
41
|
+
crcMismatch: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseLine(
|
|
45
|
+
line: string,
|
|
46
|
+
kind: 'constructor' | 'method',
|
|
47
|
+
isProtocol: boolean,
|
|
48
|
+
validateCrc: boolean,
|
|
49
|
+
): ParsedLine | null {
|
|
50
|
+
const m = line.match(LINE_RE)
|
|
51
|
+
if (!m) return null
|
|
52
|
+
const [, name, hashRaw, paramsStr, type] = m
|
|
53
|
+
const id = hashRaw!.toLowerCase().padStart(8, '0')
|
|
54
|
+
const def: TlDef = {
|
|
55
|
+
id,
|
|
56
|
+
idNum: parseInt(id, 16) >>> 0,
|
|
57
|
+
name: name!,
|
|
58
|
+
kind,
|
|
59
|
+
params: parseParams(paramsStr ?? ''),
|
|
60
|
+
type: type!,
|
|
61
|
+
isProtocol,
|
|
62
|
+
}
|
|
63
|
+
const crcMismatch = validateCrc ? getTlObjectCrc32(line.trim()) !== id : false
|
|
64
|
+
return { def, crcMismatch }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ParseResult {
|
|
68
|
+
defs: TlDef[]
|
|
69
|
+
crcMismatches: Array<{ name: string; id: string; computed: string }>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseTlText(text: string, isProtocol: boolean): ParseResult {
|
|
73
|
+
const defs: TlDef[] = []
|
|
74
|
+
const crcMismatches: ParseResult['crcMismatches'] = []
|
|
75
|
+
let kind: 'constructor' | 'method' = 'constructor'
|
|
76
|
+
|
|
77
|
+
for (const rawLine of text.split('\n')) {
|
|
78
|
+
const line = rawLine.trim()
|
|
79
|
+
if (!line || line.startsWith('//') || line.startsWith('*') || line.startsWith('/*')) continue
|
|
80
|
+
if (line.includes('---types---')) {
|
|
81
|
+
kind = 'constructor'
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (line.includes('---functions---')) {
|
|
85
|
+
kind = 'method'
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const parsed = parseLine(line, kind, isProtocol, true)
|
|
89
|
+
if (!parsed) continue
|
|
90
|
+
defs.push(parsed.def)
|
|
91
|
+
if (parsed.crcMismatch) {
|
|
92
|
+
crcMismatches.push({
|
|
93
|
+
name: parsed.def.name,
|
|
94
|
+
id: parsed.def.id,
|
|
95
|
+
computed: getTlObjectCrc32(line),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { defs, crcMismatches }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseCoreLines(): TlDef[] {
|
|
104
|
+
return CORE_LINES.map(({ kind, line }) => {
|
|
105
|
+
const parsed = parseLine(line, kind, true, false)
|
|
106
|
+
if (!parsed) throw new Error(`Failed to parse core line: ${line}`)
|
|
107
|
+
return parsed.def
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Loads every `*.tl` file from a directory into the IR. `scheme_0_protocol.tl`
|
|
113
|
+
* is flagged as protocol; the manually-parsed MTProto core constructors are
|
|
114
|
+
* merged in. Duplicate constructor ids are de-duplicated (first wins).
|
|
115
|
+
*/
|
|
116
|
+
export function parseSchemaDir(dir: string): ParseResult {
|
|
117
|
+
const files = readdirSync(dir)
|
|
118
|
+
.filter(f => f.endsWith('.tl'))
|
|
119
|
+
.sort()
|
|
120
|
+
|
|
121
|
+
const seen = new Set<string>()
|
|
122
|
+
const defs: TlDef[] = []
|
|
123
|
+
const crcMismatches: ParseResult['crcMismatches'] = []
|
|
124
|
+
|
|
125
|
+
for (const def of parseCoreLines()) {
|
|
126
|
+
if (seen.has(def.id)) continue
|
|
127
|
+
seen.add(def.id)
|
|
128
|
+
defs.push(def)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
const isProtocol = file === 'scheme_0_protocol.tl'
|
|
133
|
+
const text = readFileSync(join(dir, file), 'utf-8')
|
|
134
|
+
const res = parseTlText(text, isProtocol)
|
|
135
|
+
crcMismatches.push(...res.crcMismatches)
|
|
136
|
+
for (const def of res.defs) {
|
|
137
|
+
if (seen.has(def.id)) continue
|
|
138
|
+
seen.add(def.id)
|
|
139
|
+
defs.push(def)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { defs, crcMismatches }
|
|
144
|
+
}
|
package/src/tl/value.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-code representation of a decoded TL value.
|
|
3
|
+
*
|
|
4
|
+
* A constructed type is a tagged plain object `{ _: 'predicate.name', ...fields }`
|
|
5
|
+
* (matching the existing `toPrimitiveObject` convention). Wire primitives map to:
|
|
6
|
+
* int -> number, long/flags -> bigint|number, double -> number, string -> string,
|
|
7
|
+
* bytes/int128/int256 -> Buffer, Bool/true -> boolean, vector -> array.
|
|
8
|
+
*/
|
|
9
|
+
export type TlValue = number | bigint | boolean | string | Buffer | TlObject | TlValue[] | null | undefined
|
|
10
|
+
|
|
11
|
+
export interface TlObject {
|
|
12
|
+
_: string
|
|
13
|
+
[field: string]: TlValue
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** JSON-safe encoding so values can cross the JSON-RPC boundary. */
|
|
17
|
+
export type JsonValue =
|
|
18
|
+
| number
|
|
19
|
+
| boolean
|
|
20
|
+
| string
|
|
21
|
+
| null
|
|
22
|
+
| { $bigint: string }
|
|
23
|
+
| { $bin: string }
|
|
24
|
+
| JsonValue[]
|
|
25
|
+
| { [k: string]: JsonValue }
|
|
26
|
+
|
|
27
|
+
function isBuffer(v: unknown): v is Buffer {
|
|
28
|
+
return Buffer.isBuffer(v)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toJson(value: TlValue): JsonValue {
|
|
32
|
+
if (value === null || value === undefined) return null
|
|
33
|
+
if (typeof value === 'bigint') return { $bigint: value.toString() }
|
|
34
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value
|
|
35
|
+
if (isBuffer(value)) return { $bin: value.toString('base64') }
|
|
36
|
+
if (Array.isArray(value)) return value.map(toJson)
|
|
37
|
+
// TlObject
|
|
38
|
+
const out: { [k: string]: JsonValue } = {}
|
|
39
|
+
for (const [k, v] of Object.entries(value)) out[k] = toJson(v as TlValue)
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function fromJson(value: JsonValue): TlValue {
|
|
44
|
+
if (value === null) return null
|
|
45
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value
|
|
46
|
+
if (Array.isArray(value)) return value.map(fromJson)
|
|
47
|
+
if (typeof value === 'object') {
|
|
48
|
+
if ('$bigint' in value && typeof value.$bigint === 'string') return BigInt(value.$bigint)
|
|
49
|
+
if ('$bin' in value && typeof value.$bin === 'string') return Buffer.from(value.$bin, 'base64')
|
|
50
|
+
const out: { [k: string]: TlValue } = {}
|
|
51
|
+
for (const [k, v] of Object.entries(value)) out[k] = fromJson(v as JsonValue)
|
|
52
|
+
return out as TlObject
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pretty JSON string for terminal logging (bigint/Buffer made readable). */
|
|
58
|
+
export function stringify(value: TlValue, indent = 2): string {
|
|
59
|
+
return JSON.stringify(toJson(value), null, indent)
|
|
60
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { parseSchemaDir } from '../tl/parser.js'
|
|
5
|
+
|
|
6
|
+
interface SnapshotEntry {
|
|
7
|
+
id: string
|
|
8
|
+
predicate?: string
|
|
9
|
+
method?: string
|
|
10
|
+
params: Array<{ name: string; type: string }>
|
|
11
|
+
type: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FreezeResult {
|
|
15
|
+
constructors: number
|
|
16
|
+
methods: number
|
|
17
|
+
/** Path of the JSON snapshot the gateway loads. */
|
|
18
|
+
out: string
|
|
19
|
+
/** Path of the human-readable `.tl` mirror (not loaded; for inspection/diffs). */
|
|
20
|
+
tlOut: string
|
|
21
|
+
crcWarnings: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A single `name#id p:type … = ResultType;` line. */
|
|
25
|
+
function tlLine(e: SnapshotEntry): string {
|
|
26
|
+
const name = e.predicate ?? e.method ?? '?'
|
|
27
|
+
const params = e.params.map(p => `${p.name}:${p.type}`).join(' ')
|
|
28
|
+
return `${name}#${e.id}${params ? ' ' + params : ''} = ${e.type};`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Reconstruct a readable `.tl` from the frozen IR (constructors, then methods). */
|
|
32
|
+
function toTlText(constructors: SnapshotEntry[], methods: SnapshotEntry[], layer: number): string {
|
|
33
|
+
const lines = [`// Frozen snapshot of layer ${layer}. Generated by \`freeze\` — do not edit.`, '']
|
|
34
|
+
lines.push(...constructors.map(tlLine))
|
|
35
|
+
if (methods.length) lines.push('', '---functions---', '', ...methods.map(tlLine))
|
|
36
|
+
return lines.join('\n') + '\n'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Freezes the `.tl` schema in `schemaDir` into a per-layer snapshot
|
|
41
|
+
* `<outDir>/scheme_<layer>.json` (loaded by the gateway) plus a human-readable
|
|
42
|
+
* `<outDir>/scheme_<layer>.tl` mirror (for inspection/diffs; not loaded). Run
|
|
43
|
+
* this when you SHIP a layer: the `.tl` you edit is the in-progress newest layer;
|
|
44
|
+
* the frozen snapshot is the historical record the gateway uses to encode for
|
|
45
|
+
* clients on that layer.
|
|
46
|
+
*
|
|
47
|
+
* The snapshot is BUSINESS-only: the immutable MTProto protocol/core types are
|
|
48
|
+
* excluded (they live in @mt-tl/tl and are loaded globally), so a business
|
|
49
|
+
* layer never carries protocol definitions.
|
|
50
|
+
*
|
|
51
|
+
* Schema ownership lives in the consumer app, so paths are explicit — apps wrap
|
|
52
|
+
* this in their own `freeze` script.
|
|
53
|
+
*/
|
|
54
|
+
export function freezeLayer(schemaDir: string, outDir: string, layer: number): FreezeResult {
|
|
55
|
+
const { defs, crcMismatches } = parseSchemaDir(schemaDir)
|
|
56
|
+
|
|
57
|
+
const constructors: SnapshotEntry[] = []
|
|
58
|
+
const methods: SnapshotEntry[] = []
|
|
59
|
+
const seen = new Set<string>()
|
|
60
|
+
for (const d of defs) {
|
|
61
|
+
// Per-layer snapshots are BUSINESS-only. The MTProto protocol layer
|
|
62
|
+
// (scheme_0_protocol + the manually-parsed core ctors `parseSchemaDir`
|
|
63
|
+
// injects) is immutable and layer-independent — it lives in @mt-tl/tl
|
|
64
|
+
// and the gateway always loads it globally, so it must NOT be baked into
|
|
65
|
+
// each business snapshot (mirrors Telegram's split: mtproto_api vs api).
|
|
66
|
+
if (d.isProtocol) continue
|
|
67
|
+
if (seen.has(d.id)) continue
|
|
68
|
+
seen.add(d.id)
|
|
69
|
+
const params = d.params.map(p => ({ name: p.name, type: p.raw }))
|
|
70
|
+
if (d.kind === 'method') methods.push({ id: d.id, method: d.name, params, type: d.type })
|
|
71
|
+
else constructors.push({ id: d.id, predicate: d.name, params, type: d.type })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const byId = (a: SnapshotEntry, b: SnapshotEntry) => parseInt(a.id, 16) - parseInt(b.id, 16)
|
|
75
|
+
constructors.sort(byId)
|
|
76
|
+
methods.sort(byId)
|
|
77
|
+
|
|
78
|
+
mkdirSync(outDir, { recursive: true })
|
|
79
|
+
const out = join(outDir, `scheme_${layer}.json`)
|
|
80
|
+
const tlOut = join(outDir, `scheme_${layer}.tl`)
|
|
81
|
+
writeFileSync(out, JSON.stringify({ constructors, methods }, null, 2))
|
|
82
|
+
writeFileSync(tlOut, toTlText(constructors, methods, layer))
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
constructors: constructors.length,
|
|
86
|
+
methods: methods.length,
|
|
87
|
+
out,
|
|
88
|
+
tlOut,
|
|
89
|
+
crcWarnings: crcMismatches.length,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// CLI: `tsx freeze-layer.ts <layer> <schemaDir> <outDir>`
|
|
94
|
+
// or env: `SCHEMA_DIR=… SCHEMA_LAYERS_DIR=… tsx freeze-layer.ts <layer>`
|
|
95
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
96
|
+
const layer = Number(process.argv[2])
|
|
97
|
+
const schemaDir = process.argv[3] || process.env.SCHEMA_DIR
|
|
98
|
+
const outDir = process.argv[4] || process.env.SCHEMA_LAYERS_DIR
|
|
99
|
+
if (!Number.isInteger(layer) || layer <= 0 || !schemaDir || !outDir) {
|
|
100
|
+
console.error('usage: freeze-layer <layer> <schemaDir> <outDir>')
|
|
101
|
+
process.exit(1)
|
|
102
|
+
}
|
|
103
|
+
const r = freezeLayer(schemaDir, outDir, layer)
|
|
104
|
+
console.log(
|
|
105
|
+
`Froze layer ${layer}: ${r.constructors} constructors, ${r.methods} methods -> ${r.out} (+ ${r.tlOut})` +
|
|
106
|
+
(r.crcWarnings ? ` (${r.crcWarnings} crc warnings)` : ''),
|
|
107
|
+
)
|
|
108
|
+
}
|
package/src/wire.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { JsonValue } from './tl/value.js'
|
|
2
|
+
import type { RpcContext, RpcRequest, RpcResponse, SessionEffect } from './rpc.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wire format for the gateway↔worker RPC, shared so both sides agree:
|
|
6
|
+
* request { jsonrpc, id, method, params, context }
|
|
7
|
+
* response { result? , error?: { code, message }, effects? }
|
|
8
|
+
* Gateway encodes requests / decodes responses; workers do the inverse.
|
|
9
|
+
*/
|
|
10
|
+
export function encodeRequest(req: RpcRequest): unknown {
|
|
11
|
+
return {
|
|
12
|
+
jsonrpc: '2.0',
|
|
13
|
+
id: req.id,
|
|
14
|
+
method: req.method,
|
|
15
|
+
params: req.params,
|
|
16
|
+
context: req.context,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function decodeRequest(json: unknown): RpcRequest | null {
|
|
21
|
+
if (!json || typeof json !== 'object') return null
|
|
22
|
+
const o = json as Record<string, unknown>
|
|
23
|
+
if (typeof o.method !== 'string' || !o.context || typeof o.context !== 'object') return null
|
|
24
|
+
return {
|
|
25
|
+
id: o.id === undefined ? '' : String(o.id),
|
|
26
|
+
method: o.method,
|
|
27
|
+
params: (o.params ?? {}) as JsonValue,
|
|
28
|
+
context: o.context as RpcContext,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function encodeResponse(res: RpcResponse): unknown {
|
|
33
|
+
return res
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function decodeResponse(json: unknown): RpcResponse {
|
|
37
|
+
if (!json || typeof json !== 'object') {
|
|
38
|
+
return { error: { code: 502, message: 'BAD_RESPONSE' } }
|
|
39
|
+
}
|
|
40
|
+
const o = json as Record<string, unknown>
|
|
41
|
+
const effects = Array.isArray(o.effects) ? (o.effects as SessionEffect[]) : undefined
|
|
42
|
+
|
|
43
|
+
if (o.error && typeof o.error === 'object') {
|
|
44
|
+
const e = o.error as Record<string, unknown>
|
|
45
|
+
return {
|
|
46
|
+
error: {
|
|
47
|
+
code: typeof e.code === 'number' ? e.code : 500,
|
|
48
|
+
message: typeof e.message === 'string' ? e.message : 'ERROR',
|
|
49
|
+
},
|
|
50
|
+
effects,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { result: o.result as JsonValue, effects }
|
|
54
|
+
}
|