@mtgo-labs/wasm
v0.4.0
Published
Telegram MTProto client for the browser via WebAssembly — powered by mtgo.
Maintainers
Readme
@mtgo-labs/wasm
flowchart LR
JS[Your JS code] -->|invoke method| W["@mtgo-labs/wasm .wasm"]
W -->|syscall/js| WS[Browser WebSocket]
WS -->|wss://| TG[Telegram DC]
TG --> WS --> W --> JSInstall
npm install @mtgo-labs/wasm
# or
bun add @mtgo-labs/wasm
# or
pnpm add @mtgo-labs/wasmQuick start (Vite / SvelteKit / Next.js)
import { load } from "@mtgo-labs/wasm";
import wasmUrl from "@mtgo-labs/wasm/mtgo-wasm.wasm?url";
import wasmExecUrl from "@mtgo-labs/wasm/wasm_exec.js?url";
const mtgo = await load({ wasmUrl, wasmExecUrl });
const client = mtgo.createClient({
apiID: 12345,
apiHash: "your_api_hash",
botToken: "123:ABCdefGHI", // or sessionString for a user session
});
await client.connect();
console.log("Logged in as", client.me());
// Invoke any TL method by name (snake_case params):
const result = await client.invoke("users.getUsers", {
id: [{ _: "inputUserSelf" }],
});
console.log(result);
await client.disconnect();SSR safety: call
load()insideonMount/useEffectonly — WASM has no server context.
Plain HTML (no bundler)
If you're not using a bundler, use the browser subpath export. You can pull
everything from a CDN — no install needed:
<script src="https://unpkg.com/@mtgo-labs/wasm/wasm_exec.js"></script>
<script type="module">
import { load } from "https://unpkg.com/@mtgo-labs/wasm/browser";
const mtgo = await load("https://unpkg.com/@mtgo-labs/wasm/mtgo-wasm.wasm");
const client = mtgo.createClient({
apiID: 12345,
apiHash: "your_api_hash",
botToken: "123:ABCdefGHI",
});
await client.connect();
console.log(client.me());
</script>Or from node_modules after npm install:
<script src="node_modules/@mtgo-labs/wasm/wasm_exec.js"></script>
<script type="module">
import { load } from "node_modules/@mtgo-labs/wasm/browser";
const mtgo = await load("node_modules/@mtgo-labs/wasm/mtgo-wasm.wasm");
const client = mtgo.createClient({ apiID: 12345, apiHash: "...", botToken: "..." });
await client.connect();
</script>SvelteKit integration
1. Install
npm install @mtgo-labs/wasm2. Use in a component
<!-- src/routes/+page.svelte -->
<script>
import { onMount } from "svelte";
import { load } from "@mtgo-labs/wasm";
import wasmUrl from "@mtgo-labs/wasm/mtgo-wasm.wasm?url";
import wasmExecUrl from "@mtgo-labs/wasm/wasm_exec.js?url";
let client = null;
onMount(async () => {
// onMount only runs in the browser — SSR-safe.
const mtgo = await load({ wasmUrl, wasmExecUrl });
client = mtgo.createClient({
apiID: 12345,
apiHash: "your_hash",
botToken: "123:ABC",
});
await client.connect();
const me = await client.invoke("users.getUsers", { id: [{ _: "inputUserSelf" }] });
console.log(me);
});
</script>Alternative: load wasm_exec.js via app.html
Instead of the wasmExecUrl import, you can load Go's bootstrap script
globally in app.html:
<!-- src/app.html — inside <head> -->
<script src="%sveltekit.assets%/wasm_exec.js"></script>Then copy wasm_exec.js into static/ and omit wasmExecUrl:
cp node_modules/@mtgo-labs/wasm/wasm_exec.js static/const mtgo = await load({ wasmUrl }); // wasmExecUrl not needed — Go is already globalKey points
- SSR safety:
onMount/useEffectonly. Never callload()during SSR. Goglobal:wasm_exec.jssetsglobalThis.Go. Load it once, not per-component.?urlsuffix: Vite resolves@mtgo-labs/wasm/mtgo-wasm.wasm?urlto a served URL automatically — no need to copy files tostatic/.
API
MTGoWasm.createClient(opts) → client
| Option | Type | Required | Description |
|------------------|------------|----------|--------------------------------------------------|
| apiID | number | yes* | Telegram API ID from my.telegram.org |
| apiHash | string | yes* | Telegram API hash |
| botToken | string | no | Bot token — triggers bot auth on connect |
| sessionString | string | no | Pre-authenticated session (Telethon/Pyrogram/etc)|
| phoneNumber | string | no | Phone number for interactive user login |
| codeFunc | function | no | async (phone) => code — OTP provider |
| passwordFunc | function | no | async (hint) => password — 2FA password |
* apiID/apiHash optional only when sessionString carries them.
client.connect() → Promise<void>
Establishes the WebSocket transport, performs the DH key exchange, and authenticates (bot login or session restore). Returns a Promise.
client.invoke(method, params) → Promise<object>
Invokes a Telegram TL method by name. method is the TL function name
(e.g. "messages.sendMessage"). params is a plain JS object using
snake_case keys matching the TL schema. Returns the parsed response.
client.me() → object | null
Returns the authenticated user ({ id, username, first_name, last_name, is_bot }),
or null if not connected.
client.disconnect() → Promise<void>
Closes the transport and releases the session.
TL namespace proxy
The client itself acts as a proxy — any property that isn't a known method
(connect, invoke, me, disconnect) is treated as a TL namespace,
giving you namespace.method(params) access to all Telegram TL methods:
const tg = MTGoWasm.createClient({ apiID: 12345, apiHash: "...", botToken: "..." });
await tg.connect();
// Set your username
await tg.account.updateUsername({ username: "new_name" });
// Update profile
await tg.account.updateProfile({
first_name: "John",
last_name: "Doe",
about: "Hello from WASM!",
});
// Send a message
await tg.messages.sendMessage({
peer: { _: "inputPeerSelf" },
message: "Hello from the browser!",
random_id: Date.now(),
});
// Get your own user info
const result = await tg.users.getUsers({
id: [{ _: "inputUserSelf" }],
});Params use snake_case keys matching the TL schema (same as invoke). Each call
returns a Promise resolving to the TL response.
Package exports
| Subpath | Description |
|------------------------------------|------------------------------------------|
| @mtgo-labs/wasm | Vite/bundler loader (default) |
| @mtgo-labs/wasm/browser | Plain-browser loader (no bundler) |
| @mtgo-labs/wasm/wasm_exec.js | Go's WASM bootstrap (globalThis.Go) |
| @mtgo-labs/wasm/mtgo-wasm.wasm | The compiled .wasm binary |
How it works
mtgo is a pure-Go MTProto client. This package adds two things:
A browser WebSocket transport —
wasm/wsconn.gowraps the browser's nativeWebSocketAPI as a Gonet.Conn. mtgo's obfuscated2 framing layer sits on top, exactly as it does for server-side WebSocket connections.A JS bridge —
wasm/bridge.gousessyscall/jsto exposecreateClient/connect/invoke/disconnectto JavaScript. RPC calls go through mtgo'sInvokeJSON, so every TL method is available without per-method glue code.
The mtgo side needs one hook: Config.WSDialer (landed in mtgo v0.12.1) lets this package inject the browser WebSocket as the transport
without reaching into mtgo internals.
Transport notes
- Traffic flows over
wss://(TLS) to Telegram's WebSocket endpoints (pluto.web.telegram.org/apiws, etc.). WebSocket connections are not bound by fetch CORS rules, so browsers can reach Telegram directly — GramJS and Telegram Web do the same. - All storage is in-memory (
InMemory: true). No filesystem is used.
Cloudflare Workers / Pages
Cloudflare enforces a 25 MiB per-file limit. Ship the pre-compressed
.wasm.gz (5.2 MB) instead of the raw 29.5 MB binary — the JS loaders
decompress it transparently in the browser:
import { load } from "@mtgo-labs/wasm";
import wasmUrl from "@mtgo-labs/wasm/mtgo-wasm.wasm.gz?url";
const mtgo = await load({ wasmUrl });For plain HTML/CDN usage:
const mtgo = await load("https://unpkg.com/@mtgo-labs/wasm/mtgo-wasm.wasm.gz");The loaders detect gzip by magic bytes (0x1f 0x8b) and decompress via
DecompressionStream, so no server-side Content-Encoding: gzip header
is needed.
Building from source
If you need to rebuild the .wasm binary (e.g. after modifying the Go code):
git clone https://github.com/mtgo-labs/wasm.git
cd wasm
make build # produces mtgo-wasm.wasm
make copy-exec # copies Go's wasm_exec.js into lib/
make serve # starts a demo server at http://localhost:8080/Requires Go 1.26+ and the mtgo version that ships
Config.WSDialer + telegram.NewWSDialer.
License
MIT, same as mtgo.
