@tma.sh/sdk 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.
@@ -0,0 +1,185 @@
1
+ // src/bot/context.ts
2
+ var TELEGRAM_API_BASE = "https://api.telegram.org/bot";
3
+ var createTelegramCaller = (botToken) => {
4
+ const callTelegram = async (method, body) => {
5
+ let response;
6
+ try {
7
+ response = await fetch(`${TELEGRAM_API_BASE}${botToken}/${method}`, {
8
+ method: "POST",
9
+ headers: { "Content-Type": "application/json" },
10
+ body: JSON.stringify(body)
11
+ });
12
+ } catch {
13
+ throw new Error(`Telegram API request failed for method "${method}"`);
14
+ }
15
+ const data = await response.json();
16
+ if (!data.ok) {
17
+ throw new Error(`Telegram API error: ${data.description ?? "unknown"}`);
18
+ }
19
+ return data.result;
20
+ };
21
+ return callTelegram;
22
+ };
23
+ var createKV = (binding) => ({
24
+ get: async (key) => {
25
+ const value = await binding.get(key);
26
+ if (value === null) {
27
+ return null;
28
+ }
29
+ try {
30
+ return JSON.parse(value);
31
+ } catch {
32
+ return value;
33
+ }
34
+ },
35
+ set: async (key, value, ttl) => {
36
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
37
+ await binding.put(
38
+ key,
39
+ serialized,
40
+ ttl ? { expirationTtl: ttl } : void 0
41
+ );
42
+ },
43
+ delete: async (key) => {
44
+ await binding.delete(key);
45
+ },
46
+ list: async (prefix) => {
47
+ const result = await binding.list(prefix ? { prefix } : void 0);
48
+ return result.keys.map((k) => k.name);
49
+ }
50
+ });
51
+ var resolveChatId = (update) => update.message?.chat.id ?? update.callback_query?.message?.chat.id ?? update.inline_query?.from.id;
52
+ var resolveFrom = (update) => update.message?.from ?? update.callback_query?.from ?? update.inline_query?.from ?? update.pre_checkout_query?.from;
53
+ var createBotContext = (update, env) => {
54
+ const callTelegram = createTelegramCaller(env.BOT_TOKEN);
55
+ const chatId = resolveChatId(update);
56
+ const from = resolveFrom(update);
57
+ const kv = createKV(env.KV);
58
+ const api = {
59
+ call: (method, body) => callTelegram(method, body ?? {})
60
+ };
61
+ const requireChatId = () => {
62
+ if (chatId === void 0) {
63
+ throw new Error(
64
+ "reply() requires a chat context (message or callback_query update)"
65
+ );
66
+ }
67
+ return chatId;
68
+ };
69
+ const reply = (text, options) => callTelegram("sendMessage", {
70
+ chat_id: requireChatId(),
71
+ text,
72
+ ...options
73
+ });
74
+ const replyPhoto = (photo, options) => callTelegram("sendPhoto", {
75
+ chat_id: requireChatId(),
76
+ photo,
77
+ ...options
78
+ });
79
+ const answerCallbackQuery = (text, options) => callTelegram("answerCallbackQuery", {
80
+ callback_query_id: update.callback_query?.id,
81
+ ...text !== void 0 ? { text } : {},
82
+ ...options
83
+ });
84
+ const answerInlineQuery = (results, options) => callTelegram("answerInlineQuery", {
85
+ inline_query_id: update.inline_query?.id,
86
+ results,
87
+ ...options
88
+ });
89
+ const answerPreCheckoutQuery = (ok, errorMessage) => callTelegram("answerPreCheckoutQuery", {
90
+ pre_checkout_query_id: update.pre_checkout_query?.id,
91
+ ok,
92
+ ...errorMessage !== void 0 ? { error_message: errorMessage } : {}
93
+ });
94
+ return {
95
+ update,
96
+ message: update.message,
97
+ callbackQuery: update.callback_query,
98
+ inlineQuery: update.inline_query,
99
+ preCheckoutQuery: update.pre_checkout_query,
100
+ chatId,
101
+ from,
102
+ env,
103
+ kv,
104
+ api,
105
+ reply,
106
+ replyPhoto,
107
+ answerCallbackQuery,
108
+ answerInlineQuery,
109
+ answerPreCheckoutQuery
110
+ };
111
+ };
112
+
113
+ // src/bot/runtime.ts
114
+ var composeMiddleware = (middlewares, handler) => {
115
+ return async (ctx) => {
116
+ let index = -1;
117
+ const dispatch = async (i) => {
118
+ if (i <= index) {
119
+ throw new Error("next() called multiple times");
120
+ }
121
+ index = i;
122
+ const middleware = middlewares[i];
123
+ if (middleware) {
124
+ await middleware(ctx, () => dispatch(i + 1));
125
+ } else {
126
+ await handler(ctx);
127
+ }
128
+ };
129
+ await dispatch(0);
130
+ };
131
+ };
132
+ var routeUpdate = async (config, ctx) => {
133
+ if (ctx.message && config.onMessage) {
134
+ await config.onMessage(ctx);
135
+ } else if (ctx.callbackQuery && config.onCallbackQuery) {
136
+ await config.onCallbackQuery(ctx);
137
+ } else if (ctx.inlineQuery && config.onInlineQuery) {
138
+ await config.onInlineQuery(ctx);
139
+ } else if (ctx.preCheckoutQuery && config.onPreCheckoutQuery) {
140
+ await config.onPreCheckoutQuery(ctx);
141
+ } else if (config.onUpdate) {
142
+ await config.onUpdate(ctx);
143
+ }
144
+ };
145
+ var createBotWorker = (config) => {
146
+ const fetch2 = async (request, env, _ctx) => {
147
+ if (request.method !== "POST") {
148
+ return new Response("Method Not Allowed", { status: 405 });
149
+ }
150
+ let update;
151
+ try {
152
+ update = await request.json();
153
+ } catch {
154
+ return new Response("Invalid request body", { status: 400 });
155
+ }
156
+ if (!update || typeof update.update_id !== "number") {
157
+ return new Response("Invalid Telegram update", { status: 400 });
158
+ }
159
+ try {
160
+ const botCtx = createBotContext(update, env);
161
+ const middlewares = config.middleware ?? [];
162
+ const handler = composeMiddleware(
163
+ middlewares,
164
+ (ctx) => routeUpdate(config, ctx)
165
+ );
166
+ await handler(botCtx);
167
+ return new Response("OK", { status: 200 });
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : "Unknown error";
170
+ console.error(`Bot worker error: ${message}`);
171
+ return new Response("Internal Server Error", { status: 500 });
172
+ }
173
+ };
174
+ return { fetch: fetch2 };
175
+ };
176
+
177
+ // src/bot/define-bot.ts
178
+ var defineBot = (config) => {
179
+ const worker = createBotWorker(config);
180
+ return { ...config, ...worker };
181
+ };
182
+ export {
183
+ createKV,
184
+ defineBot
185
+ };
@@ -0,0 +1,120 @@
1
+ // src/client/http.ts
2
+ var TMAError = class extends Error {
3
+ code;
4
+ constructor(message, code = "SDK_ERROR") {
5
+ super(message);
6
+ this.name = "TMAError";
7
+ this.code = code;
8
+ }
9
+ };
10
+ var DEFAULT_API_URL = "https://api.tma.sh";
11
+ var TRAILING_SLASHES = /\/+$/;
12
+ var resolveApiUrl = (apiUrl) => (apiUrl ?? DEFAULT_API_URL).replace(TRAILING_SLASHES, "");
13
+ var makeRequest = async (baseUrl, path, options = {}) => {
14
+ const { method = "GET", body, jwt } = options;
15
+ const url = `${baseUrl}${path}`;
16
+ const headers = {
17
+ "Content-Type": "application/json"
18
+ };
19
+ if (jwt) {
20
+ headers.Authorization = `Bearer ${jwt}`;
21
+ }
22
+ const response = await fetch(url, {
23
+ method,
24
+ headers,
25
+ body: body !== void 0 ? JSON.stringify(body) : void 0
26
+ });
27
+ const json = await response.json();
28
+ if (!json.success) {
29
+ throw new TMAError(json.error, `HTTP_${response.status}`);
30
+ }
31
+ return json.data;
32
+ };
33
+
34
+ // src/client/auth.ts
35
+ var storedJwt = null;
36
+ var setStoredJwt = (jwt) => {
37
+ storedJwt = jwt;
38
+ };
39
+ var getStoredJwt = () => storedJwt;
40
+ var createAuthClient = (config) => {
41
+ const baseUrl = resolveApiUrl(config?.apiUrl);
42
+ return {
43
+ validate: async (initData, projectId) => {
44
+ const result = await makeRequest(
45
+ baseUrl,
46
+ "/sdk/v1/auth/validate",
47
+ {
48
+ method: "POST",
49
+ body: { initData, projectId }
50
+ }
51
+ );
52
+ setStoredJwt(result.jwt);
53
+ return result;
54
+ },
55
+ getJwt: () => storedJwt
56
+ };
57
+ };
58
+
59
+ // src/client/kv.ts
60
+ var requireJwt = () => {
61
+ const jwt = getStoredJwt();
62
+ if (!jwt) {
63
+ throw new TMAError(
64
+ "Authentication required. Call auth.validate() before using KV storage.",
65
+ "AUTH_REQUIRED"
66
+ );
67
+ }
68
+ return jwt;
69
+ };
70
+ var encodeKey = (key) => encodeURIComponent(key);
71
+ var createKVClient = (config) => {
72
+ const baseUrl = resolveApiUrl(config?.apiUrl);
73
+ return {
74
+ set: async (key, value) => {
75
+ const jwt = requireJwt();
76
+ await makeRequest(baseUrl, `/sdk/v1/kv/${encodeKey(key)}`, {
77
+ method: "PUT",
78
+ body: value,
79
+ jwt
80
+ });
81
+ },
82
+ get: async (key) => {
83
+ const jwt = requireJwt();
84
+ try {
85
+ return await makeRequest(baseUrl, `/sdk/v1/kv/${encodeKey(key)}`, {
86
+ jwt
87
+ });
88
+ } catch (error) {
89
+ if (error instanceof TMAError && error.code === "HTTP_404") {
90
+ return null;
91
+ }
92
+ throw error;
93
+ }
94
+ },
95
+ remove: async (key) => {
96
+ const jwt = requireJwt();
97
+ await makeRequest(baseUrl, `/sdk/v1/kv/${encodeKey(key)}`, {
98
+ method: "DELETE",
99
+ jwt
100
+ });
101
+ },
102
+ list: (prefix) => {
103
+ const jwt = requireJwt();
104
+ const query = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
105
+ return makeRequest(baseUrl, `/sdk/v1/kv${query}`, { jwt });
106
+ }
107
+ };
108
+ };
109
+
110
+ // src/client/index.ts
111
+ var createTMA = (config) => ({
112
+ auth: createAuthClient(config),
113
+ kv: createKVClient(config)
114
+ });
115
+
116
+ export {
117
+ TMAError,
118
+ createAuthClient,
119
+ createTMA
120
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ TMAError,
3
+ createTMA
4
+ } from "../chunk-CDN4MZRW.js";
5
+ export {
6
+ TMAError,
7
+ createTMA
8
+ };
@@ -0,0 +1,66 @@
1
+ import {
2
+ createAuthClient,
3
+ createTMA
4
+ } from "../chunk-CDN4MZRW.js";
5
+
6
+ // src/react/index.ts
7
+ import { useEffect, useState } from "react";
8
+ var getInitData = () => window.Telegram?.WebApp?.initData;
9
+ var LOADING_STATE = {
10
+ user: null,
11
+ jwt: null,
12
+ isLoading: true,
13
+ error: null
14
+ };
15
+ var NO_TELEGRAM_STATE = {
16
+ user: null,
17
+ jwt: null,
18
+ isLoading: false,
19
+ error: null
20
+ };
21
+ var makeErrorState = (err) => ({
22
+ user: null,
23
+ jwt: null,
24
+ isLoading: false,
25
+ error: err instanceof Error ? err : new Error(String(err))
26
+ });
27
+ var resolveAuthState = async (config) => {
28
+ const initData = getInitData();
29
+ if (!initData) {
30
+ return NO_TELEGRAM_STATE;
31
+ }
32
+ if (!config?.projectId) {
33
+ return makeErrorState("projectId is required in config");
34
+ }
35
+ const authClient = createAuthClient(config);
36
+ const result = await authClient.validate(initData, config.projectId);
37
+ return {
38
+ user: result.user,
39
+ jwt: result.jwt,
40
+ isLoading: false,
41
+ error: null
42
+ };
43
+ };
44
+ var useTelegramAuth = (config) => {
45
+ const [state, setState] = useState(LOADING_STATE);
46
+ useEffect(() => {
47
+ let cancelled = false;
48
+ resolveAuthState(config).then((resolved) => {
49
+ if (!cancelled) {
50
+ setState(resolved);
51
+ }
52
+ }).catch((err) => {
53
+ if (!cancelled) {
54
+ setState(makeErrorState(err));
55
+ }
56
+ });
57
+ return () => {
58
+ cancelled = true;
59
+ };
60
+ }, [config]);
61
+ return state;
62
+ };
63
+ export {
64
+ createTMA,
65
+ useTelegramAuth
66
+ };
@@ -0,0 +1,144 @@
1
+ // src/server/auth.ts
2
+ import { createRemoteJWKSet, jwtVerify } from "jose";
3
+ import { createMiddleware } from "hono/factory";
4
+ var DEFAULT_JWKS_URL = "https://api.tma.sh/.well-known/jwks.json";
5
+ var requireUser = (options) => {
6
+ const jwks = options?.jwks ?? createRemoteJWKSet(new URL(options?.jwksUrl ?? DEFAULT_JWKS_URL));
7
+ return createMiddleware(async (c, next) => {
8
+ const authHeader = c.req.header("Authorization");
9
+ if (!authHeader?.startsWith("Bearer ")) {
10
+ return c.json({ error: "Unauthorized" }, 401);
11
+ }
12
+ const token = authHeader.slice(7);
13
+ if (token.length === 0) {
14
+ return c.json({ error: "Unauthorized" }, 401);
15
+ }
16
+ try {
17
+ const { payload } = await jwtVerify(token, jwks);
18
+ const user = {
19
+ telegramId: payload.telegramId,
20
+ firstName: payload.firstName,
21
+ lastName: payload.lastName,
22
+ username: payload.username,
23
+ projectId: payload.projectId
24
+ };
25
+ c.set("user", user);
26
+ await next();
27
+ } catch {
28
+ return c.json({ error: "Invalid or expired token" }, 401);
29
+ }
30
+ });
31
+ };
32
+
33
+ // src/server/kv.ts
34
+ var createKV = (binding) => ({
35
+ get: async (key) => {
36
+ const value = await binding.get(key, "json");
37
+ return value;
38
+ },
39
+ set: async (key, value, ttl) => {
40
+ await binding.put(
41
+ key,
42
+ JSON.stringify(value),
43
+ ttl ? { expirationTtl: ttl } : void 0
44
+ );
45
+ },
46
+ delete: async (key) => {
47
+ await binding.delete(key);
48
+ },
49
+ list: async (prefix) => {
50
+ const result = await binding.list({ prefix });
51
+ return result.keys.map((k) => k.name);
52
+ }
53
+ });
54
+
55
+ // src/server/index.ts
56
+ var validateInitData = async (initData, botToken) => {
57
+ const params = new URLSearchParams(initData);
58
+ const hash = params.get("hash");
59
+ if (!hash) {
60
+ return null;
61
+ }
62
+ params.delete("hash");
63
+ const sortedKeys = [];
64
+ params.forEach((_value, key) => {
65
+ sortedKeys.push(key);
66
+ });
67
+ sortedKeys.sort();
68
+ const dataCheckString = sortedKeys.map((key) => `${key}=${params.get(key)}`).join("\n");
69
+ const authDateStr = params.get("auth_date");
70
+ if (!authDateStr) {
71
+ return null;
72
+ }
73
+ const authDate = Number.parseInt(authDateStr, 10);
74
+ const now = Math.floor(Date.now() / 1e3);
75
+ if (Number.isNaN(authDate) || now - authDate > MAX_AUTH_AGE_SECONDS) {
76
+ return null;
77
+ }
78
+ const secretKey = await hmacSha256(
79
+ new TextEncoder().encode("WebAppData"),
80
+ botToken
81
+ );
82
+ const isValid = await verifyHmac(
83
+ new Uint8Array(secretKey),
84
+ dataCheckString,
85
+ hash
86
+ );
87
+ if (!isValid) {
88
+ return null;
89
+ }
90
+ const userJson = params.get("user");
91
+ if (!userJson) {
92
+ return null;
93
+ }
94
+ try {
95
+ const raw = JSON.parse(userJson);
96
+ if (!(raw.id && raw.first_name)) {
97
+ return null;
98
+ }
99
+ return {
100
+ telegramId: raw.id,
101
+ firstName: raw.first_name,
102
+ lastName: raw.last_name,
103
+ username: raw.username,
104
+ authDate
105
+ };
106
+ } catch {
107
+ return null;
108
+ }
109
+ };
110
+ var MAX_AUTH_AGE_SECONDS = 300;
111
+ var hmacSha256 = async (key, data) => {
112
+ const cryptoKey = await crypto.subtle.importKey(
113
+ "raw",
114
+ key,
115
+ { name: "HMAC", hash: "SHA-256" },
116
+ false,
117
+ ["sign"]
118
+ );
119
+ return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(data));
120
+ };
121
+ var verifyHmac = async (key, data, expectedHex) => {
122
+ const cryptoKey = await crypto.subtle.importKey(
123
+ "raw",
124
+ key,
125
+ { name: "HMAC", hash: "SHA-256" },
126
+ false,
127
+ ["verify"]
128
+ );
129
+ const expectedBytes = Uint8Array.from(
130
+ expectedHex.match(/.{2}/g) ?? [],
131
+ (h) => Number.parseInt(h, 16)
132
+ );
133
+ return crypto.subtle.verify(
134
+ "HMAC",
135
+ cryptoKey,
136
+ expectedBytes,
137
+ new TextEncoder().encode(data)
138
+ );
139
+ };
140
+ export {
141
+ createKV,
142
+ requireUser,
143
+ validateInitData
144
+ };
@@ -0,0 +1,61 @@
1
+ import {
2
+ createAuthClient,
3
+ createTMA
4
+ } from "../chunk-CDN4MZRW.js";
5
+
6
+ // src/svelte/index.ts
7
+ import { writable } from "svelte/store";
8
+ var createTelegramAuth = (config) => {
9
+ const store = writable({
10
+ user: null,
11
+ jwt: null,
12
+ isLoading: true,
13
+ error: null
14
+ });
15
+ const authenticate = async () => {
16
+ const initData = typeof window !== "undefined" ? window.Telegram?.WebApp?.initData : void 0;
17
+ if (!initData) {
18
+ store.set({
19
+ user: null,
20
+ jwt: null,
21
+ isLoading: false,
22
+ error: null
23
+ });
24
+ return;
25
+ }
26
+ const projectId = config?.projectId;
27
+ if (!projectId) {
28
+ store.set({
29
+ user: null,
30
+ jwt: null,
31
+ isLoading: false,
32
+ error: new Error("projectId is required in config")
33
+ });
34
+ return;
35
+ }
36
+ try {
37
+ const authClient = createAuthClient(config);
38
+ const result = await authClient.validate(initData, projectId);
39
+ store.set({
40
+ user: result.user,
41
+ jwt: result.jwt,
42
+ isLoading: false,
43
+ error: null
44
+ });
45
+ } catch (err) {
46
+ store.set({
47
+ user: null,
48
+ jwt: null,
49
+ isLoading: false,
50
+ error: err instanceof Error ? err : new Error(String(err))
51
+ });
52
+ }
53
+ };
54
+ authenticate().catch(() => {
55
+ });
56
+ return store;
57
+ };
58
+ export {
59
+ createTMA,
60
+ createTelegramAuth
61
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@tma.sh/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "SDK for Telegram Mini Apps on TMA -- auth, KV storage, and framework bindings",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/client/index.js",
10
+ "types": "./dist/client/index.d.ts"
11
+ },
12
+ "./react": {
13
+ "import": "./dist/react/index.js",
14
+ "types": "./dist/react/index.d.ts"
15
+ },
16
+ "./svelte": {
17
+ "import": "./dist/svelte/index.js",
18
+ "types": "./dist/svelte/index.d.ts"
19
+ },
20
+ "./server": {
21
+ "import": "./dist/server/index.js",
22
+ "types": "./dist/server/index.d.ts"
23
+ },
24
+ "./bot": {
25
+ "import": "./dist/bot/index.js",
26
+ "types": "./dist/bot/index.d.ts"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsup && tsc -p tsconfig.build.json",
34
+ "check-types": "tsc --noEmit",
35
+ "prepublishOnly": "bun run build"
36
+ },
37
+ "dependencies": {
38
+ "jose": "^6.0.11"
39
+ },
40
+ "peerDependencies": {
41
+ "hono": ">=4.0.0",
42
+ "react": ">=18.0.0",
43
+ "svelte": ">=4.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "hono": {
47
+ "optional": true
48
+ },
49
+ "react": {
50
+ "optional": true
51
+ },
52
+ "svelte": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@cloudflare/workers-types": "^4.20250214.0",
58
+ "@types/react": "^19.2.14",
59
+ "hono": "^4.7.10",
60
+ "react": "^19.0.0",
61
+ "svelte": "^5.0.0",
62
+ "tsup": "^8.0.0",
63
+ "typescript": "catalog:"
64
+ }
65
+ }