@www.hyperlinks.space/program-kit 1.2.181818 → 81.81.81

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/.env.example ADDED
@@ -0,0 +1,19 @@
1
+ # Copy to .env and fill in. Used by bot (npm run bot:local) and optionally by Expo.
2
+ # Do not commit .env.
3
+
4
+ # Telegram bot (required for local bot; optional for Expo app)
5
+ # Get token from @BotFather on Telegram
6
+ BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz-EXAMPLE
7
+ # Neon/Postgres connection string (from Neon dashboard)
8
+ DATABASE_URL=postgresql://user:password@ep-xxx-xxx.region.aws.neon.tech/neondb?sslmode=require
9
+ # OpenAI API key for /api/ai (required for AI responses). Get from https://platform.openai.com/api-keys
10
+ OPENAI=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
11
+
12
+ # Optional: base URL for calling APIs in dev (used by app and bot).
13
+ # Example for local Vercel dev: http://localhost:3000
14
+ # EXPO_PUBLIC_API_BASE_URL=http://localhost:3000
15
+
16
+ # Optional: Swap.Coffee API key for TON token info and routing.
17
+ # COFFEE=your-swap-coffee-api-key
18
+ # COFFEE_BASE_URL=https://api.swap.coffee
19
+ # COFFEE_TOKENS_BASE_URL=https://tokens.swap.coffee
@@ -5,7 +5,7 @@
5
5
  * - EXPO_PUBLIC_API_BASE_URL (explicit override for any environment)
6
6
  * - React Native / Expo Go dev: derive from dev server host (port 3000)
7
7
  * - Browser:
8
- * - In dev: map localhost/LAN + dev port (8081/19000/19006) port 3000
8
+ * - In dev: map localhost/LAN + dev port (8081/19000/19006) -> port 3000
9
9
  * - In prod: window.location.origin (e.g. https://hsbexpo.vercel.app)
10
10
  * - Node (no window): Vercel host if available, otherwise http://localhost:3000
11
11
  */
@@ -0,0 +1,143 @@
1
+ import crypto from 'crypto';
2
+ import { normalizeUsername } from '../../database/users.js';
3
+
4
+ type TelegramUserPayload = {
5
+ username?: string;
6
+ language_code?: string;
7
+ [key: string]: unknown;
8
+ };
9
+
10
+ type VerifiedInitData = {
11
+ user?: TelegramUserPayload;
12
+ [key: string]: unknown;
13
+ };
14
+
15
+ const TELEGRAM_WEBAPP_PUBLIC_KEY_RAW = Buffer.from(
16
+ 'e7bf03a2fa4602af4580703d88dda5bb59f32ed8b02a56c187fe7d34caed242d',
17
+ 'hex',
18
+ );
19
+ const ED25519_SPKI_HEADER = Buffer.from('302a300506032b6570032100', 'hex');
20
+ const TELEGRAM_WEBAPP_PUBLIC_KEY = crypto.createPublicKey({
21
+ key: Buffer.concat([
22
+ ED25519_SPKI_HEADER,
23
+ Buffer.from([0]),
24
+ TELEGRAM_WEBAPP_PUBLIC_KEY_RAW,
25
+ ]),
26
+ format: 'der',
27
+ type: 'spki',
28
+ });
29
+
30
+ function verifyTelegramWebAppInitData(
31
+ initData: string,
32
+ botToken: string,
33
+ maxAgeSeconds: number = 24 * 3600,
34
+ ): VerifiedInitData | null {
35
+ if (!initData || !botToken) return null;
36
+ try {
37
+ const params = new URLSearchParams(initData);
38
+ const data: Record<string, string> = {};
39
+ for (const [key, value] of params.entries()) data[key] = value;
40
+
41
+ const authDateStr = data.auth_date;
42
+ if (authDateStr) {
43
+ const authDate = Number(authDateStr);
44
+ if (!Number.isFinite(authDate)) return null;
45
+ const now = Math.floor(Date.now() / 1000);
46
+ if (authDate > now + 60) return null;
47
+ if (maxAgeSeconds != null && now - authDate > maxAgeSeconds) return null;
48
+ }
49
+
50
+ const receivedHash = data.hash;
51
+ const receivedSignature = data.signature;
52
+
53
+ if (receivedHash) {
54
+ const dataForHash = { ...data };
55
+ delete dataForHash.hash;
56
+ const sorted = Object.keys(dataForHash)
57
+ .sort()
58
+ .map((k) => `${k}=${dataForHash[k]}`)
59
+ .join('\n');
60
+ const dataCheckString = Buffer.from(sorted, 'utf8');
61
+ const secretKey = crypto
62
+ .createHmac('sha256', 'WebAppData')
63
+ .update(botToken)
64
+ .digest();
65
+ const computedHash = crypto
66
+ .createHmac('sha256', secretKey)
67
+ .update(dataCheckString)
68
+ .digest('hex');
69
+ const valid =
70
+ receivedHash.length === computedHash.length &&
71
+ crypto.timingSafeEqual(
72
+ Buffer.from(receivedHash, 'hex'),
73
+ Buffer.from(computedHash, 'hex'),
74
+ );
75
+ if (!valid) return null;
76
+ } else if (receivedSignature) {
77
+ const botId = botToken.split(':')[0];
78
+ if (!botId) return null;
79
+ const dataForSig = { ...data };
80
+ delete dataForSig.hash;
81
+ delete dataForSig.signature;
82
+ const sorted = Object.keys(dataForSig)
83
+ .sort()
84
+ .map((k) => `${k}=${dataForSig[k]}`)
85
+ .join('\n');
86
+ const dataCheckString = `${botId}:WebAppData\n${sorted}`;
87
+ const base64 = receivedSignature.replace(/-/g, '+').replace(/_/g, '/');
88
+ const pad = (4 - (base64.length % 4)) % 4;
89
+ const sigBuffer = Buffer.from(base64 + '='.repeat(pad), 'base64');
90
+ const ok = crypto.verify(
91
+ null,
92
+ Buffer.from(dataCheckString, 'utf8'),
93
+ TELEGRAM_WEBAPP_PUBLIC_KEY,
94
+ sigBuffer,
95
+ );
96
+ if (!ok) return null;
97
+ } else {
98
+ return null;
99
+ }
100
+
101
+ const result: VerifiedInitData = { ...data };
102
+ delete result.hash;
103
+ delete result.signature;
104
+ if (data.user) {
105
+ try {
106
+ result.user = JSON.parse(data.user) as TelegramUserPayload;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ return result;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ export type AuthResult = {
118
+ telegramUsername: string;
119
+ locale: string | null;
120
+ };
121
+
122
+ export function authByInitData(initData: string): AuthResult {
123
+ const botToken = (process.env.BOT_TOKEN || '').trim();
124
+ if (!botToken) {
125
+ throw new Error('bot_token_not_configured');
126
+ }
127
+ const verified = verifyTelegramWebAppInitData(initData, botToken);
128
+ if (!verified) {
129
+ throw new Error('invalid_initdata');
130
+ }
131
+ const user =
132
+ verified.user && typeof verified.user === 'object'
133
+ ? (verified.user as TelegramUserPayload)
134
+ : {};
135
+ const telegramUsername = normalizeUsername(user.username);
136
+ if (!telegramUsername) {
137
+ throw new Error('username_required');
138
+ }
139
+ const locale =
140
+ typeof user.language_code === 'string' ? user.language_code : null;
141
+ return { telegramUsername, locale };
142
+ }
143
+
@@ -0,0 +1,151 @@
1
+ import { registerWallet } from '../../database/wallets.js';
2
+ import { upsertUserFromTma } from '../../database/users.js';
3
+ import { authByInitData } from './_auth.js';
4
+
5
+ const JSON_HEADERS = { 'Content-Type': 'application/json' };
6
+
7
+ function jsonResponse(body: object, status: number): Response {
8
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
9
+ }
10
+
11
+ type RegisterRequestBody = {
12
+ initData?: unknown;
13
+ wallet_address?: unknown;
14
+ wallet_blockchain?: unknown;
15
+ wallet_net?: unknown;
16
+ type?: unknown;
17
+ label?: unknown;
18
+ source?: unknown;
19
+ [key: string]: unknown;
20
+ };
21
+
22
+ async function getBody(
23
+ request:
24
+ | Request
25
+ | {
26
+ json?: () => Promise<unknown>;
27
+ body?: unknown;
28
+ },
29
+ ): Promise<unknown> {
30
+ if (typeof (request as { json?: () => Promise<unknown> }).json === 'function') {
31
+ return (request as Request).json();
32
+ }
33
+ return (request as { body?: unknown }).body ?? null;
34
+ }
35
+
36
+ function hasSensitiveFields(body: RegisterRequestBody): boolean {
37
+ const forbidden = [
38
+ 'mnemonic',
39
+ 'seed',
40
+ 'private_key',
41
+ 'secret',
42
+ 'secret_key',
43
+ 'wallet_master_key',
44
+ 'wallet_seed_cipher',
45
+ ];
46
+ return forbidden.some((key) => key in body);
47
+ }
48
+
49
+ function toTrimmedString(value: unknown): string {
50
+ return typeof value === 'string' ? value.trim() : '';
51
+ }
52
+
53
+ async function handler(request: Request): Promise<Response> {
54
+ const method = (request as { method?: string }).method ?? request.method;
55
+ if (method === 'GET') {
56
+ return jsonResponse(
57
+ {
58
+ ok: true,
59
+ endpoint: 'wallet/register',
60
+ use: 'POST with initData + public wallet fields',
61
+ },
62
+ 200,
63
+ );
64
+ }
65
+ if (method !== 'POST') {
66
+ return new Response('Method Not Allowed', { status: 405 });
67
+ }
68
+
69
+ const rawBody = (await getBody(request)) as RegisterRequestBody | null;
70
+ if (!rawBody || typeof rawBody !== 'object') {
71
+ return jsonResponse({ ok: false, error: 'bad_json' }, 400);
72
+ }
73
+ if (hasSensitiveFields(rawBody)) {
74
+ return jsonResponse(
75
+ { ok: false, error: 'sensitive_fields_not_allowed' },
76
+ 400,
77
+ );
78
+ }
79
+
80
+ const initData = toTrimmedString(rawBody.initData);
81
+ if (!initData) return jsonResponse({ ok: false, error: 'missing_initData' }, 400);
82
+
83
+ try {
84
+ const auth = authByInitData(initData);
85
+ await upsertUserFromTma({
86
+ telegramUsername: auth.telegramUsername,
87
+ locale: auth.locale,
88
+ });
89
+
90
+ const wallet_address = toTrimmedString(rawBody.wallet_address);
91
+ const wallet_blockchain = toTrimmedString(rawBody.wallet_blockchain);
92
+ const wallet_net = toTrimmedString(rawBody.wallet_net);
93
+ const type = toTrimmedString(rawBody.type);
94
+ const label = toTrimmedString(rawBody.label);
95
+ const source = toTrimmedString(rawBody.source);
96
+
97
+ if (!wallet_address || !wallet_blockchain || !wallet_net || !type) {
98
+ return jsonResponse(
99
+ {
100
+ ok: false,
101
+ error:
102
+ 'required_fields_missing (wallet_address, wallet_blockchain, wallet_net, type)',
103
+ },
104
+ 400,
105
+ );
106
+ }
107
+
108
+ const wallet = await registerWallet({
109
+ telegramUsername: auth.telegramUsername,
110
+ walletAddress: wallet_address,
111
+ walletBlockchain: wallet_blockchain,
112
+ walletNet: wallet_net,
113
+ type,
114
+ label: label || null,
115
+ source: source || null,
116
+ isDefault: true,
117
+ });
118
+
119
+ if (!wallet) {
120
+ return jsonResponse({ ok: false, error: 'wallet_register_failed' }, 500);
121
+ }
122
+
123
+ return jsonResponse(
124
+ {
125
+ ok: true,
126
+ telegram_username: auth.telegramUsername,
127
+ has_wallet: true,
128
+ wallet: {
129
+ id: wallet.id,
130
+ wallet_address: wallet.wallet_address,
131
+ wallet_blockchain: wallet.wallet_blockchain,
132
+ wallet_net: wallet.wallet_net,
133
+ type: wallet.type,
134
+ label: wallet.label,
135
+ is_default: wallet.is_default,
136
+ source: wallet.source,
137
+ },
138
+ },
139
+ 200,
140
+ );
141
+ } catch (err) {
142
+ const msg = err instanceof Error ? err.message : 'internal_error';
143
+ const status = msg === 'bot_token_not_configured' ? 500 : msg === 'invalid_initdata' ? 401 : 400;
144
+ return jsonResponse({ ok: false, error: msg }, status);
145
+ }
146
+ }
147
+
148
+ export default handler;
149
+ export const GET = handler;
150
+ export const POST = handler;
151
+
@@ -0,0 +1,89 @@
1
+ import { getDefaultWalletByUsername } from '../../database/wallets.js';
2
+ import { upsertUserFromTma } from '../../database/users.js';
3
+ import { authByInitData } from './_auth.js';
4
+
5
+ const JSON_HEADERS = { 'Content-Type': 'application/json' };
6
+
7
+ function jsonResponse(body: object, status: number): Response {
8
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
9
+ }
10
+
11
+ async function getBody(
12
+ request:
13
+ | Request
14
+ | {
15
+ json?: () => Promise<unknown>;
16
+ body?: unknown;
17
+ },
18
+ ): Promise<unknown> {
19
+ if (typeof (request as { json?: () => Promise<unknown> }).json === 'function') {
20
+ return (request as Request).json();
21
+ }
22
+ return (request as { body?: unknown }).body ?? null;
23
+ }
24
+
25
+ async function handler(request: Request): Promise<Response> {
26
+ const method = (request as { method?: string }).method ?? request.method;
27
+ if (method === 'GET') {
28
+ return jsonResponse(
29
+ { ok: true, endpoint: 'wallet/status', use: 'POST with initData' },
30
+ 200,
31
+ );
32
+ }
33
+ if (method !== 'POST') {
34
+ return new Response('Method Not Allowed', { status: 405 });
35
+ }
36
+
37
+ const body = (await getBody(request)) as { initData?: unknown } | null;
38
+ const initData = typeof body?.initData === 'string' ? body.initData : '';
39
+ if (!initData) return jsonResponse({ ok: false, error: 'missing_initData' }, 400);
40
+
41
+ try {
42
+ const auth = authByInitData(initData);
43
+ await upsertUserFromTma({
44
+ telegramUsername: auth.telegramUsername,
45
+ locale: auth.locale,
46
+ });
47
+
48
+ const wallet = await getDefaultWalletByUsername(auth.telegramUsername);
49
+ if (!wallet) {
50
+ return jsonResponse(
51
+ {
52
+ ok: true,
53
+ telegram_username: auth.telegramUsername,
54
+ has_wallet: false,
55
+ wallet_required: true,
56
+ },
57
+ 200,
58
+ );
59
+ }
60
+
61
+ return jsonResponse(
62
+ {
63
+ ok: true,
64
+ telegram_username: auth.telegramUsername,
65
+ has_wallet: true,
66
+ wallet: {
67
+ id: wallet.id,
68
+ wallet_address: wallet.wallet_address,
69
+ wallet_blockchain: wallet.wallet_blockchain,
70
+ wallet_net: wallet.wallet_net,
71
+ type: wallet.type,
72
+ label: wallet.label,
73
+ is_default: wallet.is_default,
74
+ source: wallet.source,
75
+ },
76
+ },
77
+ 200,
78
+ );
79
+ } catch (err) {
80
+ const msg = err instanceof Error ? err.message : 'internal_error';
81
+ const status = msg === 'bot_token_not_configured' ? 500 : msg === 'invalid_initdata' ? 401 : 400;
82
+ return jsonResponse({ ok: false, error: msg }, status);
83
+ }
84
+ }
85
+
86
+ export default handler;
87
+ export const GET = handler;
88
+ export const POST = handler;
89
+