@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
@@ -0,0 +1,217 @@
1
+ import { Configuration, RoutingApi } from "@swap-coffee/sdk";
2
+
3
+ // Aggregator / routing API (swaps, routes, etc.)
4
+ const COFFEE_BASE_URL =
5
+ process.env.COFFEE_BASE_URL?.trim() || "https://api.swap.coffee";
6
+
7
+ // Tokens API (jettons, metadata). Default per Swap.Coffee docs.
8
+ const COFFEE_TOKENS_BASE_URL =
9
+ process.env.COFFEE_TOKENS_BASE_URL?.trim() || "https://tokens.swap.coffee";
10
+
11
+ const COFFEE = process.env.COFFEE?.trim() || "";
12
+
13
+ export type SwapCoffeeStatus = {
14
+ provider: "swap.coffee";
15
+ enabled: boolean;
16
+ };
17
+
18
+ export function getSwapCoffeeStatus(): SwapCoffeeStatus {
19
+ return {
20
+ provider: "swap.coffee",
21
+ enabled: Boolean(COFFEE),
22
+ };
23
+ }
24
+
25
+ // Prepared Routing API client for future swap / routing features.
26
+ const swapCoffeeConfig =
27
+ COFFEE &&
28
+ new Configuration({
29
+ basePath: COFFEE_BASE_URL,
30
+ apiKey: (name: string) => (name === "X-Api-Key" ? COFFEE : ""),
31
+ });
32
+
33
+ export const swapCoffeeRoutingApi = swapCoffeeConfig
34
+ ? new RoutingApi(swapCoffeeConfig)
35
+ : null;
36
+
37
+ export function normalizeSymbol(symbol: string | null | undefined): string {
38
+ if (!symbol) return "";
39
+ const cleaned = symbol.replace(/\$/g, "").replace(/\s+/g, "").trim();
40
+ const upper = cleaned.toUpperCase();
41
+ if (upper.length < 2 || upper.length > 10) return "";
42
+ return upper;
43
+ }
44
+
45
+ type TokenSearchOk = {
46
+ ok: true;
47
+ data: any;
48
+ elapsed_ms: number;
49
+ source: "swap.coffee";
50
+ };
51
+
52
+ type TokenSearchError = {
53
+ ok: false;
54
+ error: string;
55
+ reason?: string;
56
+ status_code?: number;
57
+ response_snippet?: string;
58
+ elapsed_ms: number;
59
+ symbol: string;
60
+ source: "swap.coffee";
61
+ };
62
+
63
+ export type TokenSearchResult = TokenSearchOk | TokenSearchError;
64
+
65
+ export async function getTokenBySymbol(symbol: string): Promise<TokenSearchResult> {
66
+ const normalized = normalizeSymbol(symbol);
67
+ const started = Date.now();
68
+
69
+ if (!normalized) {
70
+ return {
71
+ ok: false,
72
+ error: "invalid_symbol",
73
+ reason: "Symbol must be 2-10 alphanumeric characters.",
74
+ elapsed_ms: Date.now() - started,
75
+ symbol: symbol ?? "",
76
+ source: "swap.coffee",
77
+ };
78
+ }
79
+
80
+ // Native TON special case.
81
+ if (normalized === "TON") {
82
+ return {
83
+ ok: true,
84
+ data: {
85
+ id: "TON",
86
+ type: "native",
87
+ symbol: "TON",
88
+ name: "Toncoin",
89
+ decimals: 9,
90
+ },
91
+ elapsed_ms: Date.now() - started,
92
+ source: "swap.coffee",
93
+ };
94
+ }
95
+
96
+ // Tokens API can be used with or without COFFEE key; send key only when set.
97
+ const url = new URL(
98
+ "/api/v3/jettons",
99
+ COFFEE_TOKENS_BASE_URL.endsWith("/")
100
+ ? COFFEE_TOKENS_BASE_URL
101
+ : `${COFFEE_TOKENS_BASE_URL}/`,
102
+ );
103
+ url.searchParams.set("search", normalized);
104
+ // API expects multiple verification params, not a single comma-separated value
105
+ const verificationList = (
106
+ process.env.TOKENS_VERIFICATION ?? "WHITELISTED,COMMUNITY,UNKNOWN"
107
+ )
108
+ .split(",")
109
+ .map((s) => s.trim())
110
+ .filter(Boolean);
111
+ for (const v of verificationList.length ? verificationList : ["WHITELISTED"]) {
112
+ url.searchParams.append("verification", v);
113
+ }
114
+ url.searchParams.set("size", "10");
115
+
116
+ try {
117
+ const headers: Record<string, string> = {};
118
+ if (COFFEE) headers["X-Api-Key"] = COFFEE;
119
+ const res = await fetch(url.toString(), { headers });
120
+ const elapsed_ms = Date.now() - started;
121
+
122
+ const text = await res.text();
123
+ if (!res.ok) {
124
+ return {
125
+ ok: false,
126
+ error: "unavailable",
127
+ reason: "non_200",
128
+ status_code: res.status,
129
+ response_snippet: text.slice(0, 200),
130
+ elapsed_ms,
131
+ symbol: normalized,
132
+ source: "swap.coffee",
133
+ };
134
+ }
135
+
136
+ let data: unknown;
137
+ try {
138
+ data = JSON.parse(text);
139
+ } catch {
140
+ return {
141
+ ok: false,
142
+ error: "unavailable",
143
+ reason: "json_parse",
144
+ status_code: res.status,
145
+ response_snippet: text.slice(0, 200),
146
+ elapsed_ms,
147
+ symbol: normalized,
148
+ source: "swap.coffee",
149
+ };
150
+ }
151
+
152
+ if (!Array.isArray(data)) {
153
+ return {
154
+ ok: false,
155
+ error: "unavailable",
156
+ reason: "unexpected_payload",
157
+ status_code: res.status,
158
+ response_snippet: text.slice(0, 200),
159
+ elapsed_ms,
160
+ symbol: normalized,
161
+ source: "swap.coffee",
162
+ };
163
+ }
164
+
165
+ if (data.length === 0) {
166
+ return {
167
+ ok: false,
168
+ error: "not_found",
169
+ elapsed_ms,
170
+ symbol: normalized,
171
+ source: "swap.coffee",
172
+ };
173
+ }
174
+
175
+ const exact = data.find(
176
+ (item: any) =>
177
+ typeof item?.symbol === "string" &&
178
+ normalizeSymbol(item.symbol) === normalized,
179
+ );
180
+
181
+ return {
182
+ ok: true,
183
+ data: exact ?? data[0],
184
+ elapsed_ms,
185
+ source: "swap.coffee",
186
+ };
187
+ } catch (err) {
188
+ return {
189
+ ok: false,
190
+ error: "unavailable",
191
+ reason: "connection",
192
+ elapsed_ms: Date.now() - started,
193
+ symbol: normalized,
194
+ source: "swap.coffee",
195
+ };
196
+ }
197
+ }
198
+
199
+ export async function getJettonByAddress(
200
+ address: string,
201
+ ): Promise<unknown> {
202
+ const base =
203
+ COFFEE_TOKENS_BASE_URL.endsWith("/")
204
+ ? COFFEE_TOKENS_BASE_URL
205
+ : `${COFFEE_TOKENS_BASE_URL}/`;
206
+ const url = `${base}api/v3/jettons/${encodeURIComponent(address)}`;
207
+
208
+ const headers: Record<string, string> = {};
209
+ if (COFFEE) headers["X-Api-Key"] = COFFEE;
210
+ const res = await fetch(url, { headers });
211
+
212
+ if (!res.ok) {
213
+ throw new Error(`Swap.Coffee error: ${res.status} ${res.statusText}`);
214
+ }
215
+
216
+ return res.json();
217
+ }
@@ -0,0 +1,44 @@
1
+ import { getSwapCoffeeStatus } from "./coffee.js";
2
+
3
+ export type BlockchainMode = "ping";
4
+
5
+ export type BlockchainRequest = {
6
+ mode?: BlockchainMode;
7
+ };
8
+
9
+ export type BlockchainResponse = {
10
+ ok: boolean;
11
+ mode: BlockchainMode;
12
+ provider: "swap.coffee";
13
+ status: {
14
+ swapCoffee: ReturnType<typeof getSwapCoffeeStatus>;
15
+ };
16
+ error?: string;
17
+ };
18
+
19
+ export async function handleBlockchainRequest(
20
+ request: BlockchainRequest = {},
21
+ ): Promise<BlockchainResponse> {
22
+ const mode: BlockchainMode = request.mode ?? "ping";
23
+
24
+ if (mode === "ping") {
25
+ return {
26
+ ok: true,
27
+ mode,
28
+ provider: "swap.coffee",
29
+ status: {
30
+ swapCoffee: getSwapCoffeeStatus(),
31
+ },
32
+ };
33
+ }
34
+
35
+ return {
36
+ ok: false,
37
+ mode,
38
+ provider: "swap.coffee",
39
+ status: {
40
+ swapCoffee: getSwapCoffeeStatus(),
41
+ },
42
+ error: `Unsupported blockchain mode: ${mode}`,
43
+ };
44
+ }
package/bot/format.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Converts Markdown (as typically produced by the AI) to Telegram HTML.
3
+ * Telegram supports: <b>, <i>, <code>, <pre>, <a href="...">.
4
+ * We escape & < > in text; lists stay as newlines (no list tags in Telegram).
5
+ */
6
+
7
+ /** Escape for Telegram HTML: only &lt; &gt; &amp; &quot; are supported. Unescaped " breaks attribute values (e.g. href). */
8
+ function escapeHtml(s: string): string {
9
+ return s
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;");
14
+ }
15
+
16
+ /**
17
+ * Convert Markdown to Telegram HTML. Safe for AI output (handles **bold**,
18
+ * *italic*, `code`, ```blocks```, [text](url)).
19
+ */
20
+ export function mdToTelegramHtml(md: string): string {
21
+ if (typeof md !== "string" || md.length === 0) return md;
22
+
23
+ let out = md;
24
+
25
+ // 1) Escape HTML so we can safely wrap segments in tags
26
+ out = escapeHtml(out);
27
+
28
+ // 2) Code blocks (must be before inline code so we don't touch content)
29
+ out = out.replace(/```\n?([\s\S]*?)```/g, (_m, code) => `<pre>${code.trimEnd()}</pre>`);
30
+
31
+ // 3) Inline code
32
+ out = out.replace(/`([^`]*)`/g, "<code>$1</code>");
33
+
34
+ // 4) Bold (** or __)
35
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
36
+ out = out.replace(/__([^_]+)__/g, "<b>$1</b>");
37
+
38
+ // 5) Italic (* or _); bold already replaced so **/__ are gone
39
+ out = out.replace(/\*([^*]+)\*/g, "<i>$1</i>");
40
+ out = out.replace(/_([^_]+)_/g, "<i>$1</i>");
41
+
42
+ // 6) Links [text](url) - href and text are already escaped
43
+ out = out.replace(/\[([^\]]*)\]\(([^)]*)\)/g, '<a href="$2">$1</a>');
44
+
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * Remove unpaired **, __, and ` from HTML (output of mdToTelegramHtml). Use for partial
50
+ * content so the bot never shows raw delimiters. Skips content inside <pre> and <code>
51
+ * so we don't strip asterisks/backticks that are part of code.
52
+ */
53
+ export function stripUnpairedMarkdownDelimiters(html: string): string {
54
+ if (typeof html !== "string" || html.length === 0) return html;
55
+ const blocks: string[] = [];
56
+ const placeholder = (i: number) => `\x00B${i}\x00`;
57
+ let out = html.replace(/<pre>[\s\S]*?<\/pre>/gi, (m) => {
58
+ const i = blocks.length;
59
+ blocks.push(m);
60
+ return placeholder(i);
61
+ });
62
+ out = out.replace(/<code>[\s\S]*?<\/code>/gi, (m) => {
63
+ const i = blocks.length;
64
+ blocks.push(m);
65
+ return placeholder(i);
66
+ });
67
+ out = out.replace(/\*\*/g, "").replace(/__/g, "").replace(/`/g, "");
68
+ for (let i = 0; i < blocks.length; i++) {
69
+ out = out.split(placeholder(i)).join(blocks[i]);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ const TELEGRAM_HTML_TAG_RE = /<(?:(\/(?:code|pre|a|b|i))>|((?:code|pre|a|b|i)(?:\s[^>]*)?)>)/gi;
75
+
76
+ /**
77
+ * Make HTML valid for Telegram: remove unexpected (orphan) closing tags, then
78
+ * append closing tags for any unclosed opens. Fixes "Unexpected end tag" errors
79
+ * from overlapping markdown (e.g. *a **b* c**).
80
+ */
81
+ export function sanitizeTelegramHtml(html: string): string {
82
+ if (typeof html !== "string" || html.length === 0) return html;
83
+ const tags: { start: number; end: number; type: "open" | "close"; name: string }[] = [];
84
+ let m: RegExpExecArray | null;
85
+ TELEGRAM_HTML_TAG_RE.lastIndex = 0;
86
+ while ((m = TELEGRAM_HTML_TAG_RE.exec(html)) !== null) {
87
+ if (m[1] !== undefined) {
88
+ const name = m[1].replace(/^\//, "").toLowerCase();
89
+ tags.push({ start: m.index, end: m.index + m[0].length, type: "close", name });
90
+ } else if (m[2] !== undefined) {
91
+ const name = m[2].replace(/\s.*$/, "").toLowerCase();
92
+ tags.push({ start: m.index, end: m.index + m[0].length, type: "open", name });
93
+ }
94
+ }
95
+ const stack: string[] = [];
96
+ const orphanCloseIndices = new Set<number>();
97
+ for (let i = 0; i < tags.length; i++) {
98
+ const t = tags[i];
99
+ if (t.type === "open") {
100
+ stack.push(t.name);
101
+ } else {
102
+ if (stack.length > 0 && stack[stack.length - 1] === t.name) {
103
+ stack.pop();
104
+ } else {
105
+ orphanCloseIndices.add(i);
106
+ }
107
+ }
108
+ }
109
+ let out = "";
110
+ let pos = 0;
111
+ for (let i = 0; i < tags.length; i++) {
112
+ const t = tags[i];
113
+ out += html.slice(pos, t.start);
114
+ if (!orphanCloseIndices.has(i)) out += html.slice(t.start, t.end);
115
+ pos = t.end;
116
+ }
117
+ out += html.slice(pos);
118
+ if (stack.length > 0) {
119
+ out += stack.reverse().map((tag) => `</${tag}>`).join("");
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /**
125
+ * Append closing tags for any unclosed Telegram HTML tags. Use after mdToTelegramHtml
126
+ * on partial content so we never send invalid HTML (avoids reject + flicker).
127
+ */
128
+ export function closeOpenTelegramHtml(html: string): string {
129
+ return sanitizeTelegramHtml(html);
130
+ }
131
+
132
+ /**
133
+ * Truncate HTML to at most maxLen without cutting through a tag, then close any unclosed tags.
134
+ * Use before sending/edit with parse_mode: "HTML" so Telegram never receives invalid HTML
135
+ * (avoids reject and fallback to plain text).
136
+ */
137
+ export function truncateTelegramHtmlSafe(html: string, maxLen: number): string {
138
+ if (typeof html !== "string" || html.length <= maxLen) return html;
139
+ const cut = html.slice(0, maxLen);
140
+ const lastTagEnd = cut.lastIndexOf(">");
141
+ const safe = lastTagEnd >= 0 ? cut.slice(0, lastTagEnd + 1) : cut;
142
+ return closeOpenTelegramHtml(safe);
143
+ }
package/bot/grammy.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared Grammy bot.
3
+ * Used by app/bot/webhook (Vercel) and scripts/run-bot-local.ts (polling).
4
+ */
5
+ import { Bot, type Context } from 'grammy';
6
+ import {
7
+ normalizeUsername,
8
+ upsertUserFromBot,
9
+ } from '../database/users.js';
10
+ import { handleBotAiResponse } from './responder.js';
11
+
12
+ export function createBot(token: string): Bot {
13
+ const bot = new Bot(token);
14
+
15
+ async function handleUserUpsert(ctx: Context): Promise<void> {
16
+ try {
17
+ const from = ctx.from;
18
+ if (!from) return;
19
+
20
+ const telegramUsername = normalizeUsername(from.username);
21
+ if (!telegramUsername) return;
22
+
23
+ const locale =
24
+ typeof from.language_code === 'string' ? from.language_code : null;
25
+
26
+ await upsertUserFromBot({ telegramUsername, locale });
27
+ } catch (err) {
28
+ console.error('[bot] upsert user failed', err);
29
+ }
30
+ }
31
+
32
+ bot.command('start', async (ctx: Context) => {
33
+ await handleUserUpsert(ctx);
34
+ await ctx.reply("That's @HyperlinksSpaceBot, you can use AI in bot and explore the app for more features");
35
+ });
36
+
37
+ bot.on('message:text', async (ctx: Context) => {
38
+ await handleUserUpsert(ctx);
39
+ await handleBotAiResponse(ctx);
40
+ });
41
+
42
+ bot.on('message:caption', async (ctx: Context) => {
43
+ await handleUserUpsert(ctx);
44
+ await handleBotAiResponse(ctx);
45
+ });
46
+
47
+ bot.catch((err) => {
48
+ console.error('[bot]', err);
49
+ });
50
+
51
+ return bot;
52
+ }