@www.hyperlinks.space/program-kit 1.2.91881 → 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 +19 -0
- package/README.md +3 -3
- package/api/{base.ts → _base.ts} +1 -1
- package/api/wallet/_auth.ts +143 -0
- package/api/wallet/register.ts +151 -0
- package/api/wallet/status.ts +89 -0
- package/app/index.tsx +319 -5
- package/assets/images/PreviewImage.png +0 -0
- package/database/start.ts +0 -1
- package/database/wallets.ts +266 -0
- package/docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
- package/docs/keys-retrieval-console-scripts.js +131 -0
- package/docs/security_plan_raw.md +1 -1
- package/docs/security_raw.md +22 -13
- package/docs/storage-availability-console-script.js +152 -0
- package/docs/storage-lifetime.md +33 -0
- package/docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
- package/docs/wallets_hosting_architecture.md +147 -1
- package/fullREADME.md +71 -26
- package/index.js +3 -0
- package/npmReadMe.md +3 -3
- package/package.json +7 -2
- package/scripts/test-api-base.ts +2 -2
- package/telegram/post.ts +44 -8
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Mini App storage availability probe (paste into DevTools console).
|
|
3
|
+
*
|
|
4
|
+
* What this script does:
|
|
5
|
+
* 1) Checks whether Telegram WebApp bridge exists.
|
|
6
|
+
* 2) Probes SecureStorage by real write+read+remove roundtrip.
|
|
7
|
+
* 3) Probes DeviceStorage by real write+read+remove roundtrip.
|
|
8
|
+
* 4) Probes CloudStorage by real write+read+remove roundtrip.
|
|
9
|
+
* 5) Prints a single QA verdict:
|
|
10
|
+
* - "secure" => SecureStorage works.
|
|
11
|
+
* - "device" => SecureStorage fails, but DeviceStorage works.
|
|
12
|
+
* - "none" => neither SecureStorage nor DeviceStorage works.
|
|
13
|
+
*
|
|
14
|
+
* Notes:
|
|
15
|
+
* - "API object exists" does NOT mean storage is supported. Telegram Desktop may expose methods
|
|
16
|
+
* but return UNSUPPORTED at runtime. This script verifies runtime behavior.
|
|
17
|
+
* - Probe keys are deleted at the end.
|
|
18
|
+
*/
|
|
19
|
+
(async () => {
|
|
20
|
+
const wa = window.Telegram?.WebApp;
|
|
21
|
+
if (!wa) {
|
|
22
|
+
const result = {
|
|
23
|
+
verdict: "none",
|
|
24
|
+
webApp: null,
|
|
25
|
+
error: "Telegram.WebApp not found (not running in TMA context).",
|
|
26
|
+
};
|
|
27
|
+
console.log("[storage-probe]", result);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
|
|
33
|
+
const probeSecureOrDevice = async (storage, eventPrefix, includeCanRestore) => {
|
|
34
|
+
const key = `probe_${eventPrefix}_${Date.now()}`;
|
|
35
|
+
const value = "ok";
|
|
36
|
+
let eventFailed = null;
|
|
37
|
+
|
|
38
|
+
const failedEventName = `${eventPrefix}_failed`;
|
|
39
|
+
const savedEventName = `${eventPrefix}_key_saved`;
|
|
40
|
+
|
|
41
|
+
const onFailed = (payload) => { eventFailed = payload; };
|
|
42
|
+
const onSaved = () => {};
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
wa.onEvent?.(failedEventName, onFailed);
|
|
46
|
+
wa.onEvent?.(savedEventName, onSaved);
|
|
47
|
+
} catch {}
|
|
48
|
+
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
try { wa.offEvent?.(failedEventName, onFailed); } catch {}
|
|
51
|
+
try { wa.offEvent?.(savedEventName, onSaved); } catch {}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!storage || typeof storage.setItem !== "function" || typeof storage.getItem !== "function") {
|
|
55
|
+
cleanup();
|
|
56
|
+
return {
|
|
57
|
+
present: false,
|
|
58
|
+
supported: false,
|
|
59
|
+
set: null,
|
|
60
|
+
get: null,
|
|
61
|
+
remove: null,
|
|
62
|
+
eventFailed: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
storage.setItem(key, value, (setErr, stored) => {
|
|
68
|
+
storage.getItem(key, async (getErr, gotValue, canRestore) => {
|
|
69
|
+
await wait(50);
|
|
70
|
+
|
|
71
|
+
const base = {
|
|
72
|
+
present: true,
|
|
73
|
+
supported: setErr == null && getErr == null && gotValue === value,
|
|
74
|
+
set: { err: setErr ?? null, stored: stored ?? null },
|
|
75
|
+
get: includeCanRestore
|
|
76
|
+
? { err: getErr ?? null, value: gotValue ?? null, canRestore: canRestore ?? null }
|
|
77
|
+
: { err: getErr ?? null, value: gotValue ?? null },
|
|
78
|
+
remove: null,
|
|
79
|
+
eventFailed,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (typeof storage.removeItem === "function") {
|
|
83
|
+
storage.removeItem(key, (rmErr, removed) => {
|
|
84
|
+
base.remove = { err: rmErr ?? null, removed: removed ?? null };
|
|
85
|
+
cleanup();
|
|
86
|
+
resolve(base);
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
cleanup();
|
|
90
|
+
resolve(base);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const probeCloudStorage = async () => {
|
|
98
|
+
const cs = wa.CloudStorage;
|
|
99
|
+
const key = `probe_cloud_${Date.now()}`;
|
|
100
|
+
const value = "ok";
|
|
101
|
+
|
|
102
|
+
if (!cs || typeof cs.setItem !== "function" || typeof cs.getItem !== "function") {
|
|
103
|
+
return {
|
|
104
|
+
present: false,
|
|
105
|
+
supported: false,
|
|
106
|
+
set: null,
|
|
107
|
+
get: null,
|
|
108
|
+
remove: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
cs.setItem(key, value, (setErr, stored) => {
|
|
114
|
+
cs.getItem(key, (getErr, gotValue) => {
|
|
115
|
+
const out = {
|
|
116
|
+
present: true,
|
|
117
|
+
supported: setErr == null && getErr == null && gotValue === value,
|
|
118
|
+
set: { err: setErr ?? null, stored: stored ?? null },
|
|
119
|
+
get: { err: getErr ?? null, value: gotValue ?? null },
|
|
120
|
+
remove: null,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (typeof cs.removeItem === "function") {
|
|
124
|
+
cs.removeItem(key, (rmErr, removed) => {
|
|
125
|
+
out.remove = { err: rmErr ?? null, removed: removed ?? null };
|
|
126
|
+
resolve(out);
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
resolve(out);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const secureStorage = await probeSecureOrDevice(wa.SecureStorage, "secure_storage", true);
|
|
137
|
+
const deviceStorage = await probeSecureOrDevice(wa.DeviceStorage, "device_storage", false);
|
|
138
|
+
const cloudStorage = await probeCloudStorage();
|
|
139
|
+
|
|
140
|
+
const verdict = secureStorage.supported ? "secure" : deviceStorage.supported ? "device" : "none";
|
|
141
|
+
|
|
142
|
+
const result = {
|
|
143
|
+
verdict,
|
|
144
|
+
webApp: { platform: wa.platform ?? null, version: wa.version ?? null },
|
|
145
|
+
secureStorage,
|
|
146
|
+
deviceStorage,
|
|
147
|
+
cloudStorage,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
console.log(`[storage-probe] verdict=${verdict}`, result);
|
|
151
|
+
return result;
|
|
152
|
+
})();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Storage Lifetime for Wallet Keys
|
|
2
|
+
|
|
3
|
+
This document summarizes practical storage lifetime expectations for wallet-related secrets.
|
|
4
|
+
|
|
5
|
+
Important: "lifetime" is never absolute. Every layer can be lost due to account loss, uninstall, reset, policy changes, corruption, or compromise.
|
|
6
|
+
|
|
7
|
+
## Quick Matrix
|
|
8
|
+
|
|
9
|
+
| Storage type | Typical lifetime | Main limiting factor | Cross-device | Notes for wallet keys |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| Telegram CloudStorage | Potentially long-lived | Telegram account/app availability and platform behavior | Yes (inside Telegram ecosystem) | Use for ciphertext/public metadata, not raw mnemonic |
|
|
12
|
+
| Telegram SecureStorage | Device-scoped durable | Device/app reinstall/reset and client support | No | Best TMA local tier when supported |
|
|
13
|
+
| Telegram DeviceStorage | Device-scoped durable | Device/app reinstall/reset | No | Weaker than SecureStorage; fallback tier |
|
|
14
|
+
| Session Storage (`sessionStorage`) | Current tab/session only | Tab/window/browser session end | No | Not suitable for persistent keys |
|
|
15
|
+
| Browser Local Storage (`localStorage`) | Potentially long-lived on same browser profile | User clearing data, browser profile loss | No | Use only encrypted blobs if needed |
|
|
16
|
+
| IndexedDB (web) | Potentially long-lived on same browser profile | User/browser data clearing, profile loss | No | Better than localStorage for larger encrypted blobs |
|
|
17
|
+
| Expo Secure Store (iOS/Android) | Durable on device | Uninstall/reset/device change | No | Good native local secret store; still not "forever" |
|
|
18
|
+
| Electron OS secure storage (Windows/macOS) | Durable on device profile | OS profile/device lifetime, reinstall/migration | No | Suitable desktop local secret store |
|
|
19
|
+
| In-memory runtime only | Process/session only | App restart/crash/background termination | No | Best for temporary decrypted material |
|
|
20
|
+
|
|
21
|
+
## Lifetime Statements (practical wording)
|
|
22
|
+
|
|
23
|
+
- **Telegram CloudStorage:** theoretically long-lived, practically bounded by **Telegram account lifetime and platform behavior**.
|
|
24
|
+
- **Device-local storage (SecureStorage/DeviceStorage/Secure Store/OS keychain):** bounded by **device/app/profile lifetime**.
|
|
25
|
+
- **Session storage:** bounded by **session lifetime** (tab/window/app session).
|
|
26
|
+
|
|
27
|
+
## Recommended usage pattern
|
|
28
|
+
|
|
29
|
+
1. Keep **mnemonic as offline backup** (user-controlled, not in backend/cloud plaintext).
|
|
30
|
+
2. Store day-to-day local unlock material in the strongest available **device-local secure store**.
|
|
31
|
+
3. Store only **ciphertext** in cloud/sync layers.
|
|
32
|
+
4. Treat all lifetimes as "best effort durability", not guaranteed permanence.
|
|
33
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Telegram CloudStorage Raw Keys Risks
|
|
2
|
+
|
|
3
|
+
This note clarifies a common misunderstanding:
|
|
4
|
+
|
|
5
|
+
- It is true that Telegram Mini App `CloudStorage` is scoped per bot/per user.
|
|
6
|
+
- It is true that each Telegram user session is strongly protected by Telegram account security controls (session management, device authorization, transport security, etc.).
|
|
7
|
+
- It is also true that endpoint compromise exists in both models (Bitcoin Core local storage and TMA/web clients).
|
|
8
|
+
|
|
9
|
+
The key security difference is not "cross-user direct reads". The key difference is how compromise can scale through the app runtime.
|
|
10
|
+
|
|
11
|
+
In other words: Telegram session protection is a strong baseline and should be acknowledged. The residual risk discussed here is about compromised Mini App code/runtime on an already authenticated user session, not about bypassing Telegram account/session controls.
|
|
12
|
+
|
|
13
|
+
## Corrected comparison
|
|
14
|
+
|
|
15
|
+
Exactly: a PC can be hacked in both cases.
|
|
16
|
+
|
|
17
|
+
- **Bitcoin Core local storage:** an attacker usually needs to compromise that specific machine (or steal wallet backups) to get that user's keys.
|
|
18
|
+
- **Raw mnemonic in Telegram CloudStorage:** per-user storage still applies, but if the Mini App runtime/supply chain is compromised, malicious code can read each currently logged-in user's own CloudStorage during their session and exfiltrate it. This can impact many users over time without a "read all users" API.
|
|
19
|
+
|
|
20
|
+
So both models are vulnerable to endpoint compromise, but web/TMA delivery can add centralized distribution/supply-chain risk that increases aggregate exposure.
|
|
21
|
+
|
|
22
|
+
## Practical differences vs local desktop wallet model
|
|
23
|
+
|
|
24
|
+
- **Runtime surface:**
|
|
25
|
+
- Local desktop wallet (Bitcoin Core style): narrower app distribution/runtime model.
|
|
26
|
+
- Web/TMA app: JavaScript/runtime/supply-chain surface is broader.
|
|
27
|
+
|
|
28
|
+
- **Secret impact on read:**
|
|
29
|
+
- **Raw mnemonic in cloud:** one successful read is immediate takeover.
|
|
30
|
+
- **Encrypted cloud blob:** attacker still needs decrypt capability (password/device key), which adds a barrier.
|
|
31
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# HyperlinksSpace — Wallet Architecture Doc
|
|
2
2
|
|
|
3
3
|
> High-level system design, UI flow principles, and implementation thoughts.
|
|
4
|
-
> Target:
|
|
4
|
+
> Target: TON wallet architecture options for HyperlinksSpace app (Flutter/TMA/Web), including non-custodial and custodial modes.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -254,4 +254,150 @@ Swap between implementations via a flag (`kUseMockWalletState` for dev, env-driv
|
|
|
254
254
|
|
|
255
255
|
---
|
|
256
256
|
|
|
257
|
+
## 13. Custodial Model Extension
|
|
258
|
+
|
|
259
|
+
This section adds a custodial architecture option alongside the existing non-custodial design.
|
|
260
|
+
|
|
261
|
+
### 13.1 What "custodial" means in this doc
|
|
262
|
+
|
|
263
|
+
- Wallet secrets are stored centrally as encrypted data in backend storage.
|
|
264
|
+
- Backend trust boundary can participate in decrypt/sign authorization.
|
|
265
|
+
- User identity (Google/Telegram/GitHub/email OTP) becomes a stronger operational gate.
|
|
266
|
+
|
|
267
|
+
This is a product/trust choice, not just an implementation detail.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### 13.2 Custodial key architecture (envelope encryption)
|
|
272
|
+
|
|
273
|
+
Store per-wallet:
|
|
274
|
+
|
|
275
|
+
- `ciphertext` (wallet secret encrypted by a DEK)
|
|
276
|
+
- `wrapped_dek` (DEK encrypted by KEK)
|
|
277
|
+
- metadata (`key_version`, `algo`, `created_at`, `status`)
|
|
278
|
+
|
|
279
|
+
Keep KEK in KMS/HSM, not in DB/app code.
|
|
280
|
+
|
|
281
|
+
Result:
|
|
282
|
+
- DB theft alone is insufficient.
|
|
283
|
+
- Attacker also needs KMS/HSM access path and permissions.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### 13.3 Identity and auth in custodial mode
|
|
288
|
+
|
|
289
|
+
Identity providers:
|
|
290
|
+
- Google OAuth
|
|
291
|
+
- GitHub OAuth
|
|
292
|
+
- Telegram login bridge
|
|
293
|
+
- Email + OTP (protection code)
|
|
294
|
+
|
|
295
|
+
Account linking maps all providers to one internal `user_id`.
|
|
296
|
+
|
|
297
|
+
Auth session is used to authorize protected operations:
|
|
298
|
+
- key unwrap requests
|
|
299
|
+
- signing requests
|
|
300
|
+
- provider linking/unlinking
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### 13.4 Custodial state machine
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
[ No Wallet Record ]
|
|
308
|
+
│
|
|
309
|
+
├── Create wallet ─► [ Custodial Encrypted ]
|
|
310
|
+
│ │
|
|
311
|
+
│ ├── Unlock request (auth + policy) ─► [ Session Unlocked ]
|
|
312
|
+
│ │ │
|
|
313
|
+
│ │ └── Sign tx ─► [ Ready ]
|
|
314
|
+
│ │
|
|
315
|
+
└── Restore/import ─────► [ Custodial Encrypted ]
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`Session Unlocked` should be short-lived and policy-limited (risk checks, cooldowns, limits).
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### 13.5 Signing variants
|
|
323
|
+
|
|
324
|
+
## Variant A: Backend signing (fully custodial)
|
|
325
|
+
- Backend unwraps DEK and signs transactions server-side.
|
|
326
|
+
- Client receives signed transaction/hash.
|
|
327
|
+
- Simplest UX, highest custodial responsibility.
|
|
328
|
+
|
|
329
|
+
## Variant B: Backend-assisted client resolve (hybrid custodial)
|
|
330
|
+
- Backend authorizes access and returns short-lived decrypt context.
|
|
331
|
+
- Client resolves and signs locally.
|
|
332
|
+
- Better client-side control, still centralized key lifecycle.
|
|
333
|
+
|
|
334
|
+
Choose one variant explicitly in product docs and legal language.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
### 13.6 API extension for custodial mode
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
POST /wallet/custodial/create
|
|
342
|
+
Body: { user_id, wallet_label, encrypted_payload, wrapped_dek, key_version }
|
|
343
|
+
Response: { ok: true, wallet_id }
|
|
344
|
+
|
|
345
|
+
POST /wallet/custodial/unlock
|
|
346
|
+
Body: { wallet_id, auth_context }
|
|
347
|
+
Response: { unlock_token, expires_at } // or server-side unlock only
|
|
348
|
+
|
|
349
|
+
POST /wallet/custodial/sign
|
|
350
|
+
Body: { wallet_id, unlock_token, tx_payload }
|
|
351
|
+
Response: { signed_tx | tx_hash }
|
|
352
|
+
|
|
353
|
+
POST /wallet/custodial/rotate-kek
|
|
354
|
+
Body: { wallet_id? | batch_selector, new_key_version }
|
|
355
|
+
Response: { rewrapped_count }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
All endpoints must be audited and rate-limited.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### 13.7 Security controls required in custodial mode
|
|
363
|
+
|
|
364
|
+
- KMS/HSM-backed KEK, non-exportable where possible
|
|
365
|
+
- Strict IAM separation (runtime vs admin)
|
|
366
|
+
- Row-level access control by `user_id`
|
|
367
|
+
- Risk checks before unlock/sign (IP/device anomaly, velocity limits)
|
|
368
|
+
- Immutable security event log
|
|
369
|
+
- Emergency freeze and key-rotation runbook
|
|
370
|
+
- Signed release pipeline + dependency controls
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### 13.8 UX implications vs non-custodial
|
|
375
|
+
|
|
376
|
+
Benefits:
|
|
377
|
+
- Easier cross-device recovery
|
|
378
|
+
- Less mnemonic friction for mainstream users
|
|
379
|
+
|
|
380
|
+
Tradeoffs:
|
|
381
|
+
- Backend/service compromise has larger blast radius
|
|
382
|
+
- Stronger legal/compliance burden
|
|
383
|
+
- Must clearly disclose custodial trust model
|
|
384
|
+
|
|
385
|
+
Recommended product stance:
|
|
386
|
+
- Keep non-custodial as default for advanced users.
|
|
387
|
+
- Offer custodial mode as explicit opt-in with clear warnings and recovery terms.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### 13.9 Updated roadmap including custodial track
|
|
392
|
+
|
|
393
|
+
| Phase | Non-custodial track | Custodial track |
|
|
394
|
+
|---|---|---|
|
|
395
|
+
| **Phase 1** | Create/restore local wallet, deploy flow | Auth foundation (Google/GitHub/Telegram/email OTP), user linking |
|
|
396
|
+
| **Phase 2** | Device/local storage hardening | Envelope storage (`ciphertext` + `wrapped_dek`) + KMS/HSM integration |
|
|
397
|
+
| **Phase 3** | TMA/Desktop fallback tiers | Unlock/sign endpoints + policy/rate limits |
|
|
398
|
+
| **Phase 4** | Send/receive/history | Custodial signing UX + anomaly protections |
|
|
399
|
+
| **Phase 5** | Backup/export UX hardening | Key rotation, incident runbooks, compliance hardening |
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
257
403
|
*Last updated: April 2026. Maintained in repo at `docs/wallets_hosting_architecture.md`.*
|
package/fullREADME.md
CHANGED
|
@@ -4,23 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
<u>**In progress, contribute!**</u>
|
|
6
6
|
|
|
7
|
-
This program is built upon [React Native](https://reactnative.dev/) by Meta and [Expo](https://expo.dev) multiplatform technologies, Windows build and executable creation achieved with [Electron Builder](https://www.electron.build/) and [Electron Forge](https://www.electronforge.io/), working in Telegram with help of [Telegram Mini Apps React SDK](http://telegram-mini-apps.com/)
|
|
8
|
-
|
|
9
|
-
## Program Kit
|
|
10
|
-
|
|
11
|
-
To give value for other developers we decided to launch an npm package that provides a ready starter for creating a multiplatform program in one command.
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx @www.hyperlinks.space/program-kit ./your-project
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Link to the package: https://www.npmjs.com/package/@www.hyperlinks.space/program-kit
|
|
7
|
+
This program is built upon [React Native](https://reactnative.dev/) by Meta and [Expo](https://expo.dev) multiplatform technologies, Windows build and executable creation achieved with [Electron Builder](https://www.electron.build/) and [Electron Forge](https://www.electronforge.io/), working in Telegram with help of [Telegram Mini Apps React SDK](http://telegram-mini-apps.com/), [Bot API](https://core.telegram.org/bots) and [Grammy](https://grammy.dev/). AI is backed by [OpenAI API](https://openai.com/ru-RU/api/), blockchain info is processed from [Swap.Coffee API](https://docs.swap.coffee/eng/user-guides/welcome). DB for the best user's experience we host on [Neon](https://neon.tech/).
|
|
18
8
|
|
|
19
9
|
## Program design
|
|
20
10
|
|
|
21
11
|
Access [Figma](https://www.figma.com/design/53lDKAD6pRv3e0uef1DP18/TECHSYMBAL-Inc.?node-id=754-71&t=v3tmAlywNgXkTWMd-1) in real time for contributing. Contact [Seva](t.me/sevaaignatyev) in Telegram to discuss and implement.
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
All core materials are available publicly for сгккуте hyperlinks.space team members' instant and easy access worldwide and our project's availability for newcomers' research only.
|
|
14
|
+
|
|
15
|
+
## Structure
|
|
16
|
+
|
|
17
|
+
- [`app`](./app) - Expo/React Telegram Mini App client (web/mobile screens, navigation, UI logic).
|
|
18
|
+
- [`ui`](./ui) - shared UI layer (components, theme tokens, and font configuration used by the app).
|
|
19
|
+
- [`bot`](./bot) - TypeScript Telegram bot service and runtime entrypoints.
|
|
20
|
+
- [`database`](./database) - database startup/migration/service scripts.
|
|
21
|
+
- [`ai`](./ai) - AI assistant service logic and model integration points.
|
|
22
|
+
- [`api`](./api) - backend API handlers and server-side endpoints.
|
|
23
|
+
- [`blockchain`](./blockchain) - TON/blockchain interaction logic and related helpers.
|
|
24
|
+
- [`telegram`](./telegram) - Telegram-specific integration utilities and adapters.
|
|
25
|
+
- [`windows`](./windows) - Electron desktop shell, NSIS installer config, and auto-update flow.
|
|
26
|
+
- [`scripts`](./scripts) - developer/ops scripts (local run, migration, release helpers).
|
|
27
|
+
- [`docs`](./docs) - project and operational documentation.
|
|
28
|
+
- [`backlogs`](./backlogs) - short-term planning notes and prioritized work items.
|
|
29
|
+
- [`assets`](./assets) - static assets used by app, installer, and branding.
|
|
30
|
+
- [`dist`](./dist) - generated web build output (export artifacts).
|
|
24
31
|
|
|
25
32
|
## How to fork and contribute?
|
|
26
33
|
|
|
@@ -74,6 +81,35 @@ git switch -c new-branch-for-next-update # Create and switch to a new feature br
|
|
|
74
81
|
|
|
75
82
|
## Local deploy
|
|
76
83
|
|
|
84
|
+
`npm` package note: `.env.example` is included in the published package so you can use it as a reference for establishing your testing environment with `.env` file.
|
|
85
|
+
|
|
86
|
+
Before local deploy / cloud deploy, prepare these env-backed services:
|
|
87
|
+
|
|
88
|
+
1. **Neon PostgreSQL (`DATABASE_URL`)**
|
|
89
|
+
- Create an account/project at [Neon](https://neon.tech/).
|
|
90
|
+
- Create a database and copy the connection string.
|
|
91
|
+
- Put it into `.env` as `DATABASE_URL=...`.
|
|
92
|
+
2. **OpenAI API (`OPENAI_API_KEY`)**
|
|
93
|
+
- Create an account at [OpenAI Platform](https://platform.openai.com/).
|
|
94
|
+
- Create an API key in the API Keys page.
|
|
95
|
+
- Put it into `.env` as `OPENAI_API_KEY=...`.
|
|
96
|
+
3. **Telegram bot token (`BOT_TOKEN`)**
|
|
97
|
+
- In Telegram, open [@BotFather](https://t.me/BotFather), create a test bot with `/newbot`.
|
|
98
|
+
- Copy the bot token and put it into `.env` as `BOT_TOKEN=...`.
|
|
99
|
+
4. **Vercel project envs (for comfortable deploy/testing)**
|
|
100
|
+
- Create a [Vercel](https://vercel.com/) account and import this repository as a project.
|
|
101
|
+
- In Project Settings -> Environment Variables, set at least:
|
|
102
|
+
- `DATABASE_URL`
|
|
103
|
+
- `OPENAI_API_KEY`
|
|
104
|
+
- `BOT_TOKEN` (or `TELEGRAM_BOT_TOKEN`)
|
|
105
|
+
- Pull envs locally when needed with `vercel env pull .env.local`.
|
|
106
|
+
|
|
107
|
+
Copy env template locally:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cp .env.example .env
|
|
111
|
+
```
|
|
112
|
+
|
|
77
113
|
To start the full local stack, run:
|
|
78
114
|
|
|
79
115
|
```bash
|
|
@@ -82,6 +118,13 @@ npm run start
|
|
|
82
118
|
|
|
83
119
|
This runs Expo dev server, the Telegram bot (polling mode), and local Vercel API (`vercel dev`).
|
|
84
120
|
|
|
121
|
+
After `npm run start`, you can test the app on real phones with Expo Go:
|
|
122
|
+
|
|
123
|
+
- Install **Expo Go** from Google Play (Android) or App Store (iOS).
|
|
124
|
+
- Make sure your phone and development machine are on the same network.
|
|
125
|
+
- Open Expo Go and scan the QR code shown in the terminal/Expo UI.
|
|
126
|
+
- The app will launch on the device and hot-reload on code changes.
|
|
127
|
+
|
|
85
128
|
Isolated/local run options:
|
|
86
129
|
|
|
87
130
|
- Expo only (no bot, no Vercel): `npm run start:expo`
|
|
@@ -113,11 +156,11 @@ The `.env` file is gitignored; do not commit it.
|
|
|
113
156
|
|
|
114
157
|
Current Actions workflows include:
|
|
115
158
|
|
|
116
|
-
- `Vercel Deploy Test`
|
|
117
|
-
- `NPM Package Release`
|
|
118
|
-
- `Electron EXE Release` and `Electron Forge EXE Release` - manual Windows release pipelines.
|
|
119
|
-
- `EXPO Publish`
|
|
120
|
-
- `Lint errors check`
|
|
159
|
+
- [`Vercel Deploy Test`](./.github/workflows/vercel-deploy-test-envs.yml) - manual web deploy to Vercel.
|
|
160
|
+
- [`NPM Package Release`](./.github/workflows/npm-package-release.yml) - npm/GitHub Packages release workflow.
|
|
161
|
+
- [`Electron EXE Release`](./.github/workflows/electron-exe-release.yml) and [`Electron Forge EXE Release`](./.github/workflows/electron-forge-exe-release.yml) - manual Windows release pipelines.
|
|
162
|
+
- [`EXPO Publish`](./.github/workflows/expo-publish.yml) - manual OTA publish with EAS CLI.
|
|
163
|
+
- [`Lint errors check`](./.github/workflows/lint-errors-check.yml) - manual lint check.
|
|
121
164
|
|
|
122
165
|
## Expo Workflows
|
|
123
166
|
|
|
@@ -134,14 +177,6 @@ Run `npm run draft` to [publish a preview update](https://docs.expo.dev/eas/work
|
|
|
134
177
|
|
|
135
178
|
Run `npm run development-builds` to [create a development build](https://docs.expo.dev/eas/workflows/examples/create-development-builds/). Note - you'll need to follow the [Prerequisites](https://docs.expo.dev/eas/workflows/examples/create-development-builds/#prerequisites) to ensure you have the correct emulator setup on your machine.
|
|
136
179
|
|
|
137
|
-
### Production Deployments
|
|
138
|
-
|
|
139
|
-
Run `npm run deploy` to [deploy to production](https://docs.expo.dev/eas/workflows/examples/deploy-to-production/). Note - you'll need to follow the [Prerequisites](https://docs.expo.dev/eas/workflows/examples/deploy-to-production/#prerequisites) to ensure you're set up to submit to the Apple and Google stores.
|
|
140
|
-
|
|
141
|
-
## Hosting
|
|
142
|
-
|
|
143
|
-
Expo offers hosting for websites and API functions via EAS Hosting. See the [Getting Started](https://docs.expo.dev/eas/hosting/get-started/) guide to learn more.
|
|
144
|
-
|
|
145
180
|
### Deploy web build to Vercel
|
|
146
181
|
|
|
147
182
|
From the repository root, deploy the static web build to Vercel production:
|
|
@@ -152,7 +187,7 @@ vercel --prod
|
|
|
152
187
|
|
|
153
188
|
Deploying from repository root makes this folder the project root, so `api/bot` is deployed and no Root Directory setting is needed. The project is configured so Vercel runs `npx expo export -p web` and serves the `dist/` output. Link the project first with `vercel` if needed.
|
|
154
189
|
|
|
155
|
-
## Telegram bot
|
|
190
|
+
## Telegram bot
|
|
156
191
|
|
|
157
192
|
The bot is extended beyond a basic "Hello" and "Start program" responder and now supports AI streaming and threads.
|
|
158
193
|
|
|
@@ -174,6 +209,16 @@ The bot is extended beyond a basic "Hello" and "Start program" responder and now
|
|
|
174
209
|
- Run full local stack (Expo + bot + Vercel): `npm run start`
|
|
175
210
|
- Keep production and local bot tokens separate when possible to avoid webhook/polling conflicts.
|
|
176
211
|
|
|
212
|
+
## Program Kit
|
|
213
|
+
|
|
214
|
+
To make it easier for developers to create multiplatform programs with us, we decided to launch an npm package that provides a ready starter for creating such a program basis in one command.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npx @www.hyperlinks.space/program-kit ./new-program
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Link to the package: https://www.npmjs.com/package/@www.hyperlinks.space/program-kit
|
|
221
|
+
|
|
177
222
|
## Where to discuss the project?
|
|
178
223
|
|
|
179
224
|
This repository has [GitHub Discussions](https://github.com/HyperlinksSpace/HyperlinksSpaceProgram/discussions) opened, as well you can join our [Telegram Chat](https://t.me/HyperlinksSpaceChat) and [Channel](https://t.me/HyperlinksSpace).
|
package/index.js
ADDED
package/npmReadMe.md
CHANGED
|
@@ -19,13 +19,13 @@ and deployed across popular platforms.
|
|
|
19
19
|
### npmjs (public)
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx @www.hyperlinks.space/program-kit ./
|
|
22
|
+
npx @www.hyperlinks.space/program-kit ./new-program
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### GitHub Packages
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npx @hyperlinksspace/program-kit ./
|
|
28
|
+
npx @hyperlinksspace/program-kit ./new-program
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
If you install from GitHub Packages, configure `.npmrc` with the `@hyperlinksspace`
|
|
@@ -34,7 +34,7 @@ registry and token.
|
|
|
34
34
|
## After Scaffold
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
cd
|
|
37
|
+
cd new-program
|
|
38
38
|
npm install
|
|
39
39
|
npm run start
|
|
40
40
|
```
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"program-kit": "scripts/program-kit-init.cjs"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
+
".env.example",
|
|
12
13
|
"README.md",
|
|
13
14
|
"fullREADME.md",
|
|
14
15
|
"npmReadMe.md",
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
"type": "git",
|
|
40
41
|
"url": "https://github.com/HyperlinksSpace/HyperlinksSpaceBot.git"
|
|
41
42
|
},
|
|
42
|
-
"main": "
|
|
43
|
-
"version": "
|
|
43
|
+
"main": "index.js",
|
|
44
|
+
"version": "81.81.81",
|
|
44
45
|
"type": "module",
|
|
45
46
|
"engines": {
|
|
46
47
|
"node": ">=18 <=22"
|
|
@@ -161,6 +162,10 @@
|
|
|
161
162
|
"@react-navigation/native": "^7.1.6",
|
|
162
163
|
"@swap-coffee/sdk": "^1.5.4",
|
|
163
164
|
"@tma.js/sdk-react": "^3.0.16",
|
|
165
|
+
"@ton/core": "^0.63.1",
|
|
166
|
+
"@ton/crypto": "^3.3.0",
|
|
167
|
+
"@ton/ton": "^16.2.3",
|
|
168
|
+
"buffer": "^6.0.3",
|
|
164
169
|
"electron-updater": "^6.8.3",
|
|
165
170
|
"expo": "~54.0.0",
|
|
166
171
|
"expo-blur": "~15.0.8",
|
package/scripts/test-api-base.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Quick check that api/
|
|
2
|
+
* Quick check that api/_base.ts works. Run: npx tsx scripts/test-api-base.ts
|
|
3
3
|
* In Node (no window), getApiBaseUrl() uses Vercel env or falls back to http://localhost:3000.
|
|
4
4
|
*/
|
|
5
|
-
import { getApiBaseUrl, buildApiUrl } from "../api/
|
|
5
|
+
import { getApiBaseUrl, buildApiUrl } from "../api/_base.js";
|
|
6
6
|
|
|
7
7
|
const base = getApiBaseUrl();
|
|
8
8
|
const full = buildApiUrl("/api/telegram");
|
package/telegram/post.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
normalizeUsername,
|
|
8
8
|
upsertUserFromTma,
|
|
9
9
|
} from '../database/users.js';
|
|
10
|
+
import { getDefaultWalletByUsername } from '../database/wallets.js';
|
|
10
11
|
|
|
11
12
|
const LOG_TAG = '[api/telegram]';
|
|
12
13
|
|
|
@@ -302,10 +303,53 @@ export async function handlePost(
|
|
|
302
303
|
const dbStart = Date.now();
|
|
303
304
|
try {
|
|
304
305
|
await upsertUserFromTma({ telegramUsername, locale });
|
|
306
|
+
const wallet = await getDefaultWalletByUsername(telegramUsername);
|
|
305
307
|
log('db_upsert_done', {
|
|
306
308
|
dbMs: Date.now() - dbStart,
|
|
307
309
|
elapsedMs: Date.now() - startMs,
|
|
310
|
+
hasWallet: !!wallet,
|
|
308
311
|
});
|
|
312
|
+
|
|
313
|
+
if (wallet) {
|
|
314
|
+
log('success', {
|
|
315
|
+
telegramUsername,
|
|
316
|
+
hasWallet: true,
|
|
317
|
+
totalMs: Date.now() - startMs,
|
|
318
|
+
});
|
|
319
|
+
return new Response(
|
|
320
|
+
JSON.stringify({
|
|
321
|
+
ok: true,
|
|
322
|
+
telegram_username: telegramUsername,
|
|
323
|
+
has_wallet: true,
|
|
324
|
+
wallet: {
|
|
325
|
+
id: wallet.id,
|
|
326
|
+
wallet_address: wallet.wallet_address,
|
|
327
|
+
wallet_blockchain: wallet.wallet_blockchain,
|
|
328
|
+
wallet_net: wallet.wallet_net,
|
|
329
|
+
type: wallet.type,
|
|
330
|
+
label: wallet.label,
|
|
331
|
+
is_default: wallet.is_default,
|
|
332
|
+
source: wallet.source,
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
log('success', {
|
|
340
|
+
telegramUsername,
|
|
341
|
+
hasWallet: false,
|
|
342
|
+
totalMs: Date.now() - startMs,
|
|
343
|
+
});
|
|
344
|
+
return new Response(
|
|
345
|
+
JSON.stringify({
|
|
346
|
+
ok: true,
|
|
347
|
+
telegram_username: telegramUsername,
|
|
348
|
+
has_wallet: false,
|
|
349
|
+
wallet_required: true,
|
|
350
|
+
}),
|
|
351
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
352
|
+
);
|
|
309
353
|
} catch (e) {
|
|
310
354
|
logErr('db_upsert_failed', e);
|
|
311
355
|
return new Response(
|
|
@@ -317,12 +361,4 @@ export async function handlePost(
|
|
|
317
361
|
);
|
|
318
362
|
}
|
|
319
363
|
|
|
320
|
-
log('success', {
|
|
321
|
-
telegramUsername,
|
|
322
|
-
totalMs: Date.now() - startMs,
|
|
323
|
-
});
|
|
324
|
-
return new Response(
|
|
325
|
-
JSON.stringify({ ok: true, telegram_username: telegramUsername }),
|
|
326
|
-
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
327
|
-
);
|
|
328
364
|
}
|