@www.hyperlinks.space/program-kit 1.2.3

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 (87) hide show
  1. package/README.md +53 -0
  2. package/api/ai.ts +111 -0
  3. package/api/base.ts +117 -0
  4. package/api/blockchain.ts +58 -0
  5. package/api/bot.ts +19 -0
  6. package/api/ping.ts +41 -0
  7. package/api/releases.ts +162 -0
  8. package/api/telegram.ts +65 -0
  9. package/api/tsconfig.json +17 -0
  10. package/app/_layout.tsx +135 -0
  11. package/app/ai.tsx +39 -0
  12. package/app/components/GlobalBottomBar.tsx +447 -0
  13. package/app/components/GlobalBottomBarWeb.tsx +362 -0
  14. package/app/components/GlobalLogoBar.tsx +108 -0
  15. package/app/components/GlobalLogoBarFallback.tsx +66 -0
  16. package/app/components/GlobalLogoBarWithFallback.tsx +24 -0
  17. package/app/components/HyperlinksSpaceLogo.tsx +29 -0
  18. package/app/components/Telegram.tsx +648 -0
  19. package/app/components/telegramWebApp.ts +359 -0
  20. package/app/fonts.ts +12 -0
  21. package/app/index.tsx +102 -0
  22. package/app/theme.ts +117 -0
  23. package/app.json +60 -0
  24. package/assets/icon.ico +0 -0
  25. package/assets/images/favicon.png +0 -0
  26. package/blockchain/coffee.ts +217 -0
  27. package/blockchain/router.ts +44 -0
  28. package/bot/format.ts +143 -0
  29. package/bot/grammy.ts +52 -0
  30. package/bot/responder.ts +620 -0
  31. package/bot/webhook.ts +262 -0
  32. package/database/messages.ts +128 -0
  33. package/database/start.ts +133 -0
  34. package/database/users.ts +46 -0
  35. package/docs/ai_and_search_bar_input.md +94 -0
  36. package/docs/ai_bot_messages.md +124 -0
  37. package/docs/backlogs/medium_term_backlog.md +26 -0
  38. package/docs/backlogs/short_term_backlog.md +39 -0
  39. package/docs/blue_bar_tackling.md +143 -0
  40. package/docs/bot_async_streaming.md +174 -0
  41. package/docs/build_and_install.md +129 -0
  42. package/docs/database_messages.md +34 -0
  43. package/docs/fonts.md +18 -0
  44. package/docs/releases.md +201 -0
  45. package/docs/releases_github_actions.md +188 -0
  46. package/docs/scalability.md +34 -0
  47. package/docs/security_plan_raw.md +244 -0
  48. package/docs/security_raw.md +345 -0
  49. package/docs/timing_raw.md +63 -0
  50. package/docs/tma_logo_bar_jump_investigation.md +69 -0
  51. package/docs/update.md +205 -0
  52. package/docs/wallets_hosting_architecture.md +257 -0
  53. package/eas.json +47 -0
  54. package/eslint.config.js +10 -0
  55. package/fullREADME.md +159 -0
  56. package/global.css +67 -0
  57. package/npmReadMe.md +53 -0
  58. package/package.json +214 -0
  59. package/scripts/load-env.ts +17 -0
  60. package/scripts/migrate-db.ts +16 -0
  61. package/scripts/program-kit-init.cjs +58 -0
  62. package/scripts/run-bot-local.ts +30 -0
  63. package/scripts/set-webhook.ts +67 -0
  64. package/scripts/test-api-base.ts +12 -0
  65. package/telegram/post.ts +328 -0
  66. package/tsconfig.json +17 -0
  67. package/vercel.json +7 -0
  68. package/windows/after-sign-windows-icon.cjs +13 -0
  69. package/windows/build-layout.cjs +72 -0
  70. package/windows/build-with-progress.cjs +88 -0
  71. package/windows/build.cjs +2247 -0
  72. package/windows/cleanup-legacy-appdata-installs.ps1 +91 -0
  73. package/windows/cleanup-legacy-windows-shortcuts.ps1 +46 -0
  74. package/windows/cleanup.cjs +200 -0
  75. package/windows/embed-windows-exe-icon.cjs +55 -0
  76. package/windows/extractAppPackage.nsh +150 -0
  77. package/windows/forge/README.md +41 -0
  78. package/windows/forge/forge.config.js +138 -0
  79. package/windows/forge/make-with-stamp.cjs +65 -0
  80. package/windows/forge-cleanup.cjs +255 -0
  81. package/windows/hsp-app-process.ps1 +63 -0
  82. package/windows/installer-hooks.nsi +373 -0
  83. package/windows/product-brand.cjs +42 -0
  84. package/windows/remove-orphan-uninstall-registry.ps1 +67 -0
  85. package/windows/run-installed-with-icon-debug.cmd +20 -0
  86. package/windows/run-win-electron-builder.cjs +46 -0
  87. package/windows/updater-dialog.html +143 -0
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Program Kit
2
+
3
+ Program Kit is a production-ready cross-platform starter published from `app/`.
4
+ It is built around React Native + Expo and is designed to be quickly tested, scaled,
5
+ and deployed across popular platforms.
6
+
7
+ ## What You Get
8
+
9
+ - Expo + React Native app foundation
10
+ - Telegram bot support (webhook + local bot scripts)
11
+ - Telegram Mini App-ready client structure
12
+ - Android and iOS workflow scripts
13
+ - Windows desktop packaging (`.exe`) with Electron Builder
14
+ - CI-oriented release workflow and deployment helpers
15
+
16
+ ## Install
17
+
18
+ ### npmjs (public)
19
+
20
+ ```bash
21
+ npx @www.hyperlinks.space/program-kit ./my-new-program
22
+ ```
23
+
24
+ ### GitHub Packages
25
+
26
+ ```bash
27
+ npx @hyperlinksspace/program-kit ./my-new-program
28
+ ```
29
+
30
+ If you install from GitHub Packages, configure `.npmrc` with the `@hyperlinksspace`
31
+ registry and token.
32
+
33
+ ## After Scaffold
34
+
35
+ ```bash
36
+ cd my-new-program
37
+ npm install
38
+ npm run start
39
+ ```
40
+
41
+ Then open the project `README.md` for full setup details (env vars, bot setup, build
42
+ and release commands).
43
+
44
+ ## Release Channels
45
+
46
+ - `latest` for stable milestone snapshots
47
+ - `next` for rolling preview snapshots
48
+
49
+ ## Notes
50
+
51
+ - Published directly from the `app/` folder.
52
+ - Package tarball is filtered to include only required project files.
53
+ - **`fullREADME.md`** in the package is the full in-repo developer guide (Expo setup, scripts, and the rest of the project readme).
package/api/ai.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * AI gateway.
3
+ * - GET /api/ai → health check ({ ok: true, ai: true }).
4
+ * - POST /api/ai → AI response from providers (currently OpenAI).
5
+ *
6
+ * Supports both:
7
+ * - Web API style (Request → Response)
8
+ * - Legacy Node style (req, res)
9
+ */
10
+
11
+ import { transmit, type AiRequest } from "../ai/transmitter.js";
12
+
13
+ type NodeRes = {
14
+ setHeader(name: string, value: string): void;
15
+ status(code: number): void;
16
+ end(body?: string): void;
17
+ };
18
+
19
+ function jsonResponse(body: object, status: number): Response {
20
+ return new Response(JSON.stringify(body), {
21
+ status,
22
+ headers: { "Content-Type": "application/json" },
23
+ });
24
+ }
25
+
26
+ async function handler(
27
+ request: Request,
28
+ res?: NodeRes,
29
+ ): Promise<Response | void> {
30
+ const method = (request as any)?.method ?? "GET";
31
+
32
+ if (method === "GET") {
33
+ const body = { ok: true, ai: true };
34
+
35
+ if (res) {
36
+ res.setHeader("Content-Type", "application/json");
37
+ res.status(200);
38
+ res.end(JSON.stringify(body));
39
+ return;
40
+ }
41
+
42
+ return jsonResponse(body, 200);
43
+ }
44
+
45
+ if (method !== "POST") {
46
+ const body = { ok: false, error: "Method not allowed" };
47
+
48
+ if (res) {
49
+ res.setHeader("Content-Type", "application/json");
50
+ res.status(405);
51
+ res.end(JSON.stringify(body));
52
+ return;
53
+ }
54
+
55
+ return jsonResponse(body, 405);
56
+ }
57
+
58
+ let payload: unknown;
59
+ try {
60
+ payload = await request.json();
61
+ } catch {
62
+ const body = { ok: false, error: "Invalid JSON body" };
63
+
64
+ if (res) {
65
+ res.setHeader("Content-Type", "application/json");
66
+ res.status(400);
67
+ res.end(JSON.stringify(body));
68
+ return;
69
+ }
70
+
71
+ return jsonResponse(body, 400);
72
+ }
73
+
74
+ const { input, mode, userId, context } = (payload || {}) as Partial<AiRequest>;
75
+
76
+ if (typeof input !== "string" || input.trim().length === 0) {
77
+ const body = { ok: false, error: "Field 'input' (string) is required." };
78
+
79
+ if (res) {
80
+ res.setHeader("Content-Type", "application/json");
81
+ res.status(400);
82
+ res.end(JSON.stringify(body));
83
+ return;
84
+ }
85
+
86
+ return jsonResponse(body, 400);
87
+ }
88
+
89
+ const result = await transmit({
90
+ input,
91
+ mode,
92
+ userId,
93
+ context,
94
+ });
95
+
96
+ const status = result.ok ? 200 : 500;
97
+
98
+ if (res) {
99
+ res.setHeader("Content-Type", "application/json");
100
+ res.status(status);
101
+ res.end(JSON.stringify(result));
102
+ return;
103
+ }
104
+
105
+ return jsonResponse(result, status);
106
+ }
107
+
108
+ export default handler;
109
+ export const GET = handler;
110
+ export const POST = handler;
111
+
package/api/base.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * API base URL helper used by frontend and bot.
3
+ *
4
+ * Priority:
5
+ * - EXPO_PUBLIC_API_BASE_URL (explicit override for any environment)
6
+ * - React Native / Expo Go dev: derive from dev server host (port 3000)
7
+ * - Browser:
8
+ * - In dev: map localhost/LAN + dev port (8081/19000/19006) → port 3000
9
+ * - In prod: window.location.origin (e.g. https://hsbexpo.vercel.app)
10
+ * - Node (no window): Vercel host if available, otherwise http://localhost:3000
11
+ */
12
+
13
+ function normalizeBase(base: string): string {
14
+ return base.replace(/\/$/, "");
15
+ }
16
+
17
+ function isPrivateOrLocalHost(hostname: string): boolean {
18
+ if (hostname === "localhost" || hostname === "127.0.0.1") return true;
19
+ if (hostname.startsWith("10.")) return true;
20
+ if (hostname.startsWith("192.168.")) return true;
21
+ if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname)) return true;
22
+ return false;
23
+ }
24
+
25
+ function getExpoNativeDevBaseUrl(): string | null {
26
+ // Only attempt this in React Native / Expo Go.
27
+ if (typeof navigator === "undefined" || navigator.product !== "ReactNative") {
28
+ return null;
29
+ }
30
+
31
+ try {
32
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
33
+ const Constants = require("expo-constants").default as any;
34
+ const hostUri: string | undefined =
35
+ Constants?.expoConfig?.hostUri ??
36
+ Constants?.manifest2?.extra?.expoGo?.developer?.hostUri;
37
+
38
+ if (!hostUri || typeof hostUri !== "string") {
39
+ return null;
40
+ }
41
+
42
+ const [hostname] = hostUri.split(":");
43
+ if (!hostname) return null;
44
+
45
+ return `http://${hostname}:3000`;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function getBrowserBaseUrl(): string | null {
52
+ if (typeof window === "undefined" || !window.location?.href) {
53
+ return null;
54
+ }
55
+
56
+ try {
57
+ const url = new URL(window.location.href);
58
+ const { protocol, hostname, port } = url;
59
+
60
+ // In dev, Expo often runs on 8081/19000/19006; map to 3000 for APIs.
61
+ if (
62
+ isPrivateOrLocalHost(hostname) &&
63
+ (port === "8081" || port === "19000" || port === "19006")
64
+ ) {
65
+ return normalizeBase(`${protocol}//${hostname}:3000`);
66
+ }
67
+
68
+ // In production (no explicit dev port), use origin as-is.
69
+ return normalizeBase(url.origin);
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function getNodeBaseUrl(): string {
76
+ const vercelProjectProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
77
+ if (vercelProjectProd) {
78
+ return normalizeBase(`https://${vercelProjectProd}`);
79
+ }
80
+
81
+ const vercelUrl = process.env.VERCEL_URL?.trim();
82
+ if (vercelUrl) {
83
+ return normalizeBase(
84
+ vercelUrl.startsWith("http") ? vercelUrl : `https://${vercelUrl}`,
85
+ );
86
+ }
87
+
88
+ // Local dev fallback (vercel dev on 3000).
89
+ return "http://localhost:3000";
90
+ }
91
+
92
+ export function getApiBaseUrl(): string {
93
+ const envBase = process.env.EXPO_PUBLIC_API_BASE_URL?.trim();
94
+ if (envBase) {
95
+ return normalizeBase(envBase);
96
+ }
97
+
98
+ const expoDev = getExpoNativeDevBaseUrl();
99
+ if (expoDev) {
100
+ return normalizeBase(expoDev);
101
+ }
102
+
103
+ const browserBase = getBrowserBaseUrl();
104
+ if (browserBase) {
105
+ return browserBase;
106
+ }
107
+
108
+ return getNodeBaseUrl();
109
+ }
110
+
111
+ export function buildApiUrl(path: string): string {
112
+ const base = getApiBaseUrl();
113
+ if (!base) {
114
+ return path;
115
+ }
116
+ return `${base}${path}`;
117
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Minimal blockchain gateway.
3
+ * GET /api/blockchain → status of blockchain integrations (swap.coffee, etc.).
4
+ *
5
+ * Supports both:
6
+ * - Web API style (Request → Response)
7
+ * - Legacy Node style (req, res)
8
+ */
9
+
10
+ import { handleBlockchainRequest } from "../blockchain/router.js";
11
+
12
+ type NodeRes = {
13
+ setHeader(name: string, value: string): void;
14
+ status(code: number): void;
15
+ end(body?: string): void;
16
+ };
17
+
18
+ function jsonResponse(body: object, status: number): Response {
19
+ return new Response(JSON.stringify(body), {
20
+ status,
21
+ headers: { "Content-Type": "application/json" },
22
+ });
23
+ }
24
+
25
+ async function handler(
26
+ request: Request,
27
+ res?: NodeRes,
28
+ ): Promise<Response | void> {
29
+ const method = (request as any)?.method ?? "GET";
30
+
31
+ if (method !== "GET") {
32
+ const body = { ok: false, error: "Method not allowed" };
33
+
34
+ if (res) {
35
+ res.setHeader("Content-Type", "application/json");
36
+ res.status(405);
37
+ res.end(JSON.stringify(body));
38
+ return;
39
+ }
40
+
41
+ return jsonResponse(body, 405);
42
+ }
43
+
44
+ const result = await handleBlockchainRequest({ mode: "ping" });
45
+
46
+ if (res) {
47
+ res.setHeader("Content-Type", "application/json");
48
+ res.status(200);
49
+ res.end(JSON.stringify(result));
50
+ return;
51
+ }
52
+
53
+ return jsonResponse(result, 200);
54
+ }
55
+
56
+ export default handler;
57
+ export const GET = handler;
58
+
package/api/bot.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vercel API route: named GET/POST so Telegram webhook POST is handled.
3
+ * Forwards to app/bot/webhook so only this file is a route (avoids 12-function limit).
4
+ */
5
+ import webhookHandler, {
6
+ type NodeReq,
7
+ type NodeRes,
8
+ } from '../bot/webhook.js';
9
+
10
+ async function handler(
11
+ request: Request | NodeReq,
12
+ context?: NodeRes,
13
+ ): Promise<Response | void> {
14
+ return webhookHandler(request, context);
15
+ }
16
+
17
+ export default handler;
18
+ export const GET = handler;
19
+ export const POST = handler;
package/api/ping.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Zero-dependency health check.
3
+ * GET /api/ping → { ok: true, ping: true }
4
+ *
5
+ * Supports both:
6
+ * - Web API style (Request → Response)
7
+ * - Legacy Node style (req, res)
8
+ */
9
+
10
+ type NodeRes = {
11
+ setHeader(name: string, value: string): void;
12
+ status(code: number): void;
13
+ end(body?: string): void;
14
+ };
15
+
16
+ function jsonResponse(body: object, status: number): Response {
17
+ return new Response(JSON.stringify(body), {
18
+ status,
19
+ headers: { 'Content-Type': 'application/json' },
20
+ });
21
+ }
22
+
23
+ async function handler(
24
+ request: Request,
25
+ res?: NodeRes,
26
+ ): Promise<Response | void> {
27
+ const body = { ok: true, ping: true };
28
+
29
+ if (res) {
30
+ res.setHeader('Content-Type', 'application/json');
31
+ res.status(200);
32
+ res.end(JSON.stringify(body));
33
+ return;
34
+ }
35
+
36
+ return jsonResponse(body, 200);
37
+ }
38
+
39
+ export default handler;
40
+ export const GET = handler;
41
+
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Release webhook endpoint.
3
+ * - GET /api/releases -> health check
4
+ * - POST /api/releases -> accepts release publish events from CI
5
+ */
6
+
7
+ type NodeRes = {
8
+ setHeader(name: string, value: string): void;
9
+ status(code: number): void;
10
+ end(body?: string): void;
11
+ };
12
+
13
+ type Asset = {
14
+ name: string;
15
+ url: string;
16
+ sha256?: string;
17
+ };
18
+
19
+ type ReleasePayload = {
20
+ release_id: string;
21
+ version?: string;
22
+ published_at: string;
23
+ platform?: string;
24
+ assets: Asset[];
25
+ github_release_url?: string;
26
+ };
27
+
28
+ const processedReleaseIds = new Set<string>();
29
+ const MAX_REMEMBERED_RELEASE_IDS = 500;
30
+
31
+ function jsonResponse(body: object, status: number): Response {
32
+ return new Response(JSON.stringify(body), {
33
+ status,
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ "Access-Control-Allow-Origin": "*",
37
+ },
38
+ });
39
+ }
40
+
41
+ function setNodeCors(res: NodeRes): void {
42
+ res.setHeader("Access-Control-Allow-Origin", "*");
43
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
44
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, x-release-token");
45
+ res.setHeader("Content-Type", "application/json");
46
+ }
47
+
48
+ function isValidPayload(payload: unknown): payload is ReleasePayload {
49
+ if (!payload || typeof payload !== "object") return false;
50
+ const candidate = payload as Partial<ReleasePayload>;
51
+ if (typeof candidate.release_id !== "string" || candidate.release_id.trim() === "") return false;
52
+ if (typeof candidate.published_at !== "string" || candidate.published_at.trim() === "") return false;
53
+ if (!Array.isArray(candidate.assets)) return false;
54
+ return candidate.assets.every(
55
+ (asset) =>
56
+ asset &&
57
+ typeof asset === "object" &&
58
+ typeof (asset as Asset).name === "string" &&
59
+ typeof (asset as Asset).url === "string",
60
+ );
61
+ }
62
+
63
+ function rememberReleaseId(releaseId: string): void {
64
+ processedReleaseIds.add(releaseId);
65
+ if (processedReleaseIds.size <= MAX_REMEMBERED_RELEASE_IDS) return;
66
+
67
+ // Best-effort bounded memory for warm server instances.
68
+ const oldest = processedReleaseIds.values().next().value as string | undefined;
69
+ if (oldest) processedReleaseIds.delete(oldest);
70
+ }
71
+
72
+ async function notifyReleaseSignal(payload: ReleasePayload): Promise<void> {
73
+ const notifyUrl = process.env.RELEASES_NOTIFY_URL?.trim();
74
+ if (!notifyUrl) return;
75
+
76
+ const token = process.env.RELEASES_NOTIFY_TOKEN?.trim();
77
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
78
+ if (token) headers["x-release-token"] = token;
79
+
80
+ const response = await fetch(notifyUrl, {
81
+ method: "POST",
82
+ headers,
83
+ body: JSON.stringify(payload),
84
+ });
85
+
86
+ if (!response.ok) {
87
+ throw new Error(`notify_failed:${response.status}`);
88
+ }
89
+ }
90
+
91
+ async function readPayload(request: Request): Promise<unknown> {
92
+ try {
93
+ return await request.json();
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ async function handleWebRequest(request: Request): Promise<Response> {
100
+ const method = request.method;
101
+ if (method === "OPTIONS") return jsonResponse({}, 200);
102
+
103
+ if (method === "GET") {
104
+ const tokenConfigured = !!process.env.RELEASE_WEBHOOK_TOKEN?.trim();
105
+ return jsonResponse(
106
+ {
107
+ ok: true,
108
+ service: "releases-webhook",
109
+ token_configured: tokenConfigured,
110
+ auth_mode: tokenConfigured ? "token_required" : "open",
111
+ },
112
+ 200,
113
+ );
114
+ }
115
+
116
+ if (method !== "POST") {
117
+ return jsonResponse({ ok: false, error: "Method not allowed" }, 405);
118
+ }
119
+
120
+ const requiredToken = process.env.RELEASE_WEBHOOK_TOKEN?.trim();
121
+ if (requiredToken) {
122
+ const providedToken = request.headers.get("x-release-token")?.trim();
123
+ if (providedToken !== requiredToken) {
124
+ return jsonResponse({ ok: false, error: "Unauthorized" }, 401);
125
+ }
126
+ } else {
127
+ console.warn("[releases] RELEASE_WEBHOOK_TOKEN is not set; endpoint is open");
128
+ }
129
+
130
+ const payload = await readPayload(request);
131
+ if (!isValidPayload(payload)) {
132
+ return jsonResponse({ ok: false, error: "Invalid payload" }, 400);
133
+ }
134
+
135
+ if (processedReleaseIds.has(payload.release_id)) {
136
+ return jsonResponse({ ok: true, duplicate: true }, 200);
137
+ }
138
+
139
+ rememberReleaseId(payload.release_id);
140
+ try {
141
+ await notifyReleaseSignal(payload);
142
+ } catch (error) {
143
+ console.error("[releases] notify error", error);
144
+ return jsonResponse({ ok: false, error: "Failed to notify downstream service" }, 502);
145
+ }
146
+
147
+ return jsonResponse({ ok: true }, 200);
148
+ }
149
+
150
+ export default async function handler(request: Request, res?: NodeRes): Promise<Response | void> {
151
+ if (!res) {
152
+ return handleWebRequest(request);
153
+ }
154
+
155
+ setNodeCors(res);
156
+ const webResponse = await handleWebRequest(request);
157
+ res.status(webResponse.status);
158
+ res.end(await webResponse.text());
159
+ }
160
+
161
+ export const GET = handler;
162
+ export const POST = handler;
@@ -0,0 +1,65 @@
1
+ const JSON_HEADERS = { 'Content-Type': 'application/json' };
2
+
3
+ function jsonResponse(body: object, status: number): Response {
4
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
5
+ }
6
+
7
+ /** Node res: send body and end so the client gets the response (Vercel uses (req, res) in prod). */
8
+ function sendViaRes(res: { status: (n: number) => void; setHeader: (k: string, v: string) => void; end: (s?: string) => void }, body: object, status: number): void {
9
+ res.status(status);
10
+ res.setHeader('Content-Type', 'application/json');
11
+ res.end(JSON.stringify(body));
12
+ }
13
+
14
+ /**
15
+ * Thin router: GET returns immediately (no heavy deps). POST loads
16
+ * the heavy handler from ./telegram/post so initData verification and
17
+ * DB writes stay out of the fast path. When Vercel passes (req, res),
18
+ * we must send via res so the client gets the response.
19
+ */
20
+ async function handler(
21
+ request: Request,
22
+ res?: { status: (n: number) => void; setHeader: (k: string, v: string) => void; end: (s?: string) => void }
23
+ ): Promise<Response | void> {
24
+ const method = (request as { method?: string }).method ?? request.method;
25
+ if (method === 'GET') {
26
+ const body = { ok: true, endpoint: 'telegram', use: 'POST with initData' };
27
+ if (res) {
28
+ sendViaRes(res, body, 200);
29
+ return;
30
+ }
31
+ return jsonResponse(body, 200);
32
+ }
33
+ if (method !== 'POST') {
34
+ if (res) {
35
+ res.status(405);
36
+ res.end('Method Not Allowed');
37
+ return;
38
+ }
39
+ return new Response('Method Not Allowed', { status: 405 });
40
+ }
41
+ try {
42
+ const { handlePost } = await import('../telegram/post.js');
43
+ const response = await handlePost(request);
44
+ if (res) {
45
+ res.status(response.status);
46
+ response.headers.forEach((v, k) => res.setHeader(k, v));
47
+ res.end(await response.text());
48
+ return;
49
+ }
50
+ return response;
51
+ } catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ console.error('[api/telegram] POST error:', message, err instanceof Error ? err.stack : '');
54
+ const body = { ok: false, error: message || 'internal_error' };
55
+ if (res) {
56
+ sendViaRes(res, body, 500);
57
+ return;
58
+ }
59
+ return jsonResponse(body, 500);
60
+ }
61
+ }
62
+
63
+ export default handler;
64
+ export const GET = handler;
65
+ export const POST = handler;
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "isolatedModules": true
11
+ },
12
+ "include": [
13
+ "**/*.ts",
14
+ "**/*.tsx",
15
+ "../bot/**/*.ts"
16
+ ]
17
+ }