@www.hyperlinks.space/program-kit 1.2.181818 → 7.8.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.
- package/.eas/workflows/create-development-builds.yml +21 -0
- package/.eas/workflows/create-draft.yml +15 -0
- package/.eas/workflows/deploy-to-production.yml +68 -0
- package/.env.example +19 -0
- package/.gitattributes +48 -0
- package/.gitignore +52 -0
- package/.nvmrc +1 -0
- package/.vercelignore +6 -0
- package/README.md +7 -2
- package/ai/openai.ts +202 -0
- package/ai/transmitter.ts +367 -0
- 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/backlogs/medium_term_backlog.md +26 -0
- package/backlogs/short_term_backlog.md +42 -0
- package/database/start.ts +0 -1
- package/database/wallets.ts +266 -0
- package/eslint.config.cjs +10 -0
- package/fullREADME.md +142 -71
- package/index.js +3 -0
- package/npmReadMe.md +7 -2
- package/npmrc.example +1 -0
- package/package.json +7 -27
- package/polyfills/buffer.ts +9 -0
- package/research & docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
- package/research & docs/github-gitlab-bidirectional-mirroring.md +154 -0
- package/research & docs/keys-retrieval-console-scripts.js +131 -0
- package/{docs/security_plan_raw.md → research & docs/security_plan_raw.md } +1 -1
- package/{docs/security_raw.md → research & docs/security_raw.md } +22 -13
- package/research & docs/storage-availability-console-script.js +152 -0
- package/research & docs/storage-lifetime.md +33 -0
- package/research & docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
- package/{docs/wallets_hosting_architecture.md → research & docs/wallets_hosting_architecture.md } +147 -1
- package/scripts/test-api-base.ts +2 -2
- package/services/wallet/tonWallet.ts +73 -0
- package/telegram/post.ts +44 -8
- package/ui/components/GlobalBottomBar.tsx +447 -0
- package/ui/components/GlobalBottomBarWeb.tsx +362 -0
- package/ui/components/GlobalLogoBar.tsx +108 -0
- package/ui/components/GlobalLogoBarFallback.tsx +66 -0
- package/ui/components/GlobalLogoBarWithFallback.tsx +24 -0
- package/ui/components/HyperlinksSpaceLogo.tsx +29 -0
- package/ui/components/Telegram.tsx +677 -0
- package/ui/components/telegramWebApp.ts +359 -0
- package/ui/fonts.ts +12 -0
- package/ui/theme.ts +117 -0
- /package/{docs → research & docs}/ai_and_search_bar_input.md +0 -0
- /package/{docs → research & docs}/ai_bot_messages.md +0 -0
- /package/{docs → research & docs}/blue_bar_tackling.md +0 -0
- /package/{docs → research & docs}/bot_async_streaming.md +0 -0
- /package/{docs → research & docs}/build_and_install.md +0 -0
- /package/{docs → research & docs}/database_messages.md +0 -0
- /package/{docs → research & docs}/fonts.md +0 -0
- /package/{docs → research & docs}/npm-release.md +0 -0
- /package/{docs → research & docs}/releases.md +0 -0
- /package/{docs → research & docs}/releases_github_actions.md +0 -0
- /package/{docs → research & docs}/scalability.md +0 -0
- /package/{docs → research & docs}/timing_raw.md +0 -0
- /package/{docs → research & docs}/tma_logo_bar_jump_investigation.md +0 -0
- /package/{docs → research & docs}/update.md +0 -0
- /package/{docs → research & docs}/wallet_telegram_standalone_multichain_proposal.md +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Mini App key retrieval checker (paste into DevTools console).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Inspect where wallet keys are currently stored after app flow runs.
|
|
6
|
+
* - Check these keys across available storages:
|
|
7
|
+
* 1) SecureStorage: wallet_master_key
|
|
8
|
+
* 2) DeviceStorage: wallet_master_key
|
|
9
|
+
* 3) CloudStorage: wallet_seed_cipher
|
|
10
|
+
*
|
|
11
|
+
* What it prints:
|
|
12
|
+
* - Per-storage availability (API object + method presence).
|
|
13
|
+
* - Read result for each key (err/value present/length).
|
|
14
|
+
* - Final quick verdict about expected Desktop fallback model:
|
|
15
|
+
* - secureOnly: master key in SecureStorage
|
|
16
|
+
* - deviceFallback: master key in DeviceStorage
|
|
17
|
+
* - none: master key not found in either local store
|
|
18
|
+
*
|
|
19
|
+
* Notes:
|
|
20
|
+
* - This script does not print full secret values; it shows only existence/length.
|
|
21
|
+
* - In some Telegram Desktop builds, SecureStorage methods exist but return UNSUPPORTED.
|
|
22
|
+
*/
|
|
23
|
+
(async () => {
|
|
24
|
+
const wa = window.Telegram?.WebApp;
|
|
25
|
+
if (!wa) {
|
|
26
|
+
const out = { ok: false, error: "Telegram.WebApp not found" };
|
|
27
|
+
console.log("[keys-retrieval]", out);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const readSecure = (key) =>
|
|
32
|
+
new Promise((resolve) => {
|
|
33
|
+
const storage = wa.SecureStorage;
|
|
34
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
35
|
+
resolve({ present: false, err: "NO_API", value: null, canRestore: null });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
storage.getItem(key, (err, value, canRestore) => {
|
|
39
|
+
resolve({
|
|
40
|
+
present: true,
|
|
41
|
+
err: err ?? null,
|
|
42
|
+
value: value ?? null,
|
|
43
|
+
canRestore: canRestore ?? null,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const readDevice = (key) =>
|
|
49
|
+
new Promise((resolve) => {
|
|
50
|
+
const storage = wa.DeviceStorage;
|
|
51
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
52
|
+
resolve({ present: false, err: "NO_API", value: null });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
storage.getItem(key, (err, value) => {
|
|
56
|
+
resolve({
|
|
57
|
+
present: true,
|
|
58
|
+
err: err ?? null,
|
|
59
|
+
value: value ?? null,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const readCloud = (key) =>
|
|
65
|
+
new Promise((resolve) => {
|
|
66
|
+
const storage = wa.CloudStorage;
|
|
67
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
68
|
+
resolve({ present: false, err: "NO_API", value: null });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
storage.getItem(key, (err, value) => {
|
|
72
|
+
resolve({
|
|
73
|
+
present: true,
|
|
74
|
+
err: err ?? null,
|
|
75
|
+
value: value ?? null,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const [secureMaster, deviceMaster, cloudSeedCipher] = await Promise.all([
|
|
81
|
+
readSecure("wallet_master_key"),
|
|
82
|
+
readDevice("wallet_master_key"),
|
|
83
|
+
readCloud("wallet_seed_cipher"),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const hasSecureMaster = secureMaster.value != null && secureMaster.err == null;
|
|
87
|
+
const hasDeviceMaster = deviceMaster.value != null && deviceMaster.err == null;
|
|
88
|
+
const hasCloudSeedCipher = cloudSeedCipher.value != null && cloudSeedCipher.err == null;
|
|
89
|
+
|
|
90
|
+
const localTier = hasSecureMaster ? "secureOnly" : hasDeviceMaster ? "deviceFallback" : "none";
|
|
91
|
+
|
|
92
|
+
const result = {
|
|
93
|
+
ok: true,
|
|
94
|
+
webApp: {
|
|
95
|
+
platform: wa.platform ?? null,
|
|
96
|
+
version: wa.version ?? null,
|
|
97
|
+
},
|
|
98
|
+
keys: {
|
|
99
|
+
wallet_master_key: {
|
|
100
|
+
secureStorage: {
|
|
101
|
+
available: secureMaster.present,
|
|
102
|
+
err: secureMaster.err,
|
|
103
|
+
exists: hasSecureMaster,
|
|
104
|
+
valueLength: typeof secureMaster.value === "string" ? secureMaster.value.length : 0,
|
|
105
|
+
canRestore: secureMaster.canRestore ?? null,
|
|
106
|
+
},
|
|
107
|
+
deviceStorage: {
|
|
108
|
+
available: deviceMaster.present,
|
|
109
|
+
err: deviceMaster.err,
|
|
110
|
+
exists: hasDeviceMaster,
|
|
111
|
+
valueLength: typeof deviceMaster.value === "string" ? deviceMaster.value.length : 0,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
wallet_seed_cipher: {
|
|
115
|
+
cloudStorage: {
|
|
116
|
+
available: cloudSeedCipher.present,
|
|
117
|
+
err: cloudSeedCipher.err,
|
|
118
|
+
exists: hasCloudSeedCipher,
|
|
119
|
+
valueLength: typeof cloudSeedCipher.value === "string" ? cloudSeedCipher.value.length : 0,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
verdict: {
|
|
124
|
+
localTier,
|
|
125
|
+
modelOkForDesktopFallback: localTier === "deviceFallback" && hasCloudSeedCipher,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
console.log("[keys-retrieval]", result);
|
|
130
|
+
return result;
|
|
131
|
+
})();
|
|
@@ -78,7 +78,7 @@ This file describes a practical implementation plan for the security model defin
|
|
|
78
78
|
- Step 3: Show mnemonic & confirmation quiz.
|
|
79
79
|
- Step 4:
|
|
80
80
|
- Derive wallet master key from mnemonic.
|
|
81
|
-
- Store it via `
|
|
81
|
+
- Store it via `SecureStorage.setItem('wallet_master_key', …)` when supported; if that fails (e.g. Desktop `UNSUPPORTED`), fall back to `DeviceStorage` and warn the user (see `docs/security_raw.md`).
|
|
82
82
|
- Derive/encode wallet seed or root key, encrypt with master key → `seed_cipher`.
|
|
83
83
|
- Store `seed_cipher` via `Telegram.WebApp.CloudStorage.setItem('wallet_seed_cipher', <cipher>)`.
|
|
84
84
|
- Derive `wallet_address` from mnemonic/root key.
|
|
@@ -21,9 +21,20 @@ This document describes the current high-level design; it is not a formal audit.
|
|
|
21
21
|
- **Wallet seed cipher**: Encrypted blob that contains the wallet seed (or a seed-derived secret), usable only together with the wallet master key.
|
|
22
22
|
- **Telegram CloudStorage**: Per-user, per-bot key–value store synced via Telegram servers. Suitable for non-secret data or ciphertext.
|
|
23
23
|
- **Telegram SecureStorage**: Per-device secure storage, backed by iOS Keychain / Android Keystore, intended for small sensitive values like tokens and keys (see [Mini Apps docs](https://core.telegram.org/bots/webapps#securestorage)).
|
|
24
|
+
- **Telegram DeviceStorage**: Persistent **local** key–value storage inside the Telegram client for the bot (up to 5 MB per user). Telegram documents it as conceptually similar to browser `localStorage`, **not** as Keychain/Keystore-backed secure storage (see [DeviceStorage](https://core.telegram.org/bots/webapps#devicestorage)).
|
|
24
25
|
|
|
25
26
|
We **never store raw mnemonics** on our backend or in Telegram CloudStorage.
|
|
26
27
|
|
|
28
|
+
### SecureStorage vs DeviceStorage (product stance)
|
|
29
|
+
|
|
30
|
+
- **SecureStorage** is the preferred place for the wallet master key: encrypted at rest and isolated using OS secure hardware where Telegram implements it.
|
|
31
|
+
- Some clients (notably **Telegram Desktop** in common builds) report `secure_storage_failed` with errors such as **`UNSUPPORTED`**, meaning there is no SecureStorage bridge for that Mini App session.
|
|
32
|
+
- **DeviceStorage fallback** (used only when SecureStorage fails) keeps the same split as the design (master key local, seed as ciphertext in CloudStorage), but the master key is **not** under the same hardware-backed guarantees. It is **stricter than random website `localStorage`** (scoped to the Telegram client and bot), yet **strictly weaker than SecureStorage** against threats that can read Mini App storage (e.g. XSS, compromised WebView, malware with access to Telegram’s local storage).
|
|
33
|
+
|
|
34
|
+
**Alternative architectures** (not the default in this repo) include: **require** SecureStorage and **block** signing until the user opens the Mini App on a supported client, or a **Tonkeeper-style** model (password-encrypted secrets with persistence via CloudStorage only, with security bounded by password strength and KDF choice).
|
|
35
|
+
|
|
36
|
+
The Mini App implementation tries **SecureStorage first**, then **DeviceStorage** for `wallet_master_key`, and surfaces a short in-app notice when the weaker path is used.
|
|
37
|
+
|
|
27
38
|
---
|
|
28
39
|
|
|
29
40
|
## I. Wallet Creation in Telegram
|
|
@@ -36,10 +47,8 @@ Flow when a user creates a wallet for the first time in the Telegram Mini App on
|
|
|
36
47
|
|
|
37
48
|
2. **Derive wallet master key (device-local)**
|
|
38
49
|
- From the mnemonic, the app derives a **wallet master key** (e.g. via a BIP-style KDF / HKDF).
|
|
39
|
-
- This master key is stored
|
|
40
|
-
-
|
|
41
|
-
- Encrypted at rest.
|
|
42
|
-
- Bound to this device + Telegram app.
|
|
50
|
+
- This master key is stored in Telegram **`SecureStorage` when available** (Keychain / Keystore on mobile).
|
|
51
|
+
- If SecureStorage is unavailable (`UNSUPPORTED` or failure), the app **falls back to `DeviceStorage`** and explains the weaker guarantee in the UI (see “SecureStorage vs DeviceStorage” above).
|
|
43
52
|
|
|
44
53
|
3. **Create and store wallet seed cipher (cloud)**
|
|
45
54
|
- The app creates a **wallet seed cipher**:
|
|
@@ -57,12 +66,12 @@ Flow when a user creates a wallet for the first time in the Telegram Mini App on
|
|
|
57
66
|
**Properties**
|
|
58
67
|
|
|
59
68
|
- The **first Telegram device** has everything needed to use the wallet:
|
|
60
|
-
- Master key in `SecureStorage`.
|
|
69
|
+
- Master key in `SecureStorage` or, on some clients, `DeviceStorage`.
|
|
61
70
|
- Seed cipher in `CloudStorage`.
|
|
62
71
|
- If the app is reinstalled on the **same device**, we can:
|
|
63
|
-
- Recover the master key from
|
|
72
|
+
- Recover the master key from the same Telegram local store (if Telegram restores it).
|
|
64
73
|
- Decrypt the seed cipher for a smooth UX without re-entering the mnemonic.
|
|
65
|
-
- If
|
|
74
|
+
- If local storage is wiped, the user must re-enter the mnemonic.
|
|
66
75
|
|
|
67
76
|
---
|
|
68
77
|
|
|
@@ -275,7 +284,7 @@ When the user taps the button in the bot message and opens the Mini App:
|
|
|
275
284
|
On **Confirm** from the Mini App:
|
|
276
285
|
|
|
277
286
|
1. Mini App reconstructs/derives the transaction to be signed using:
|
|
278
|
-
- The locally available wallet (seed/master key in `SecureStorage`).
|
|
287
|
+
- The locally available wallet (seed/master key in Telegram `SecureStorage` or `DeviceStorage` fallback).
|
|
279
288
|
- The payload from `pending_transactions`.
|
|
280
289
|
2. Mini App signs the transaction **locally** using the wallet key.
|
|
281
290
|
3. Mini App sends a request to a serverless endpoint, e.g. `POST /api/tx/<id>/complete` with:
|
|
@@ -311,7 +320,7 @@ For users who click a **web URL** from the bot instead of the Mini App:
|
|
|
311
320
|
- No private keys or mnemonics are ever stored or derived in serverless functions.
|
|
312
321
|
- Bot pushes and confirmation flows are coordinated exclusively via:
|
|
313
322
|
- Telegram Bot API (for notifications),
|
|
314
|
-
- Telegram Mini App (for
|
|
323
|
+
- Telegram Mini App (for signing with local master key in SecureStorage or DeviceStorage fallback),
|
|
315
324
|
- Telegram Login (for identity on web / mobile).
|
|
316
325
|
|
|
317
326
|
This model keeps transaction approvals **user‑driven and key‑local** (inside Telegram or another explicit wallet) while still fitting neatly into a serverless architecture.
|
|
@@ -325,12 +334,12 @@ This model keeps transaction approvals **user‑driven and key‑local** (inside
|
|
|
325
334
|
- Neither our backend nor Telegram can unilaterally move funds without the mnemonic / keys.
|
|
326
335
|
|
|
327
336
|
- **Device-local keys:**
|
|
328
|
-
- Each device has its own **wallet master key** stored in
|
|
337
|
+
- Each device has its own **wallet master key** stored in Telegram **SecureStorage** when supported, else **DeviceStorage** (weaker).
|
|
329
338
|
- Compromise of one device does **not** automatically compromise others.
|
|
330
339
|
|
|
331
340
|
- **Cloud data is ciphertext only:**
|
|
332
|
-
- `Wallet Seed Cipher` in CloudStorage is encrypted with
|
|
333
|
-
- An attacker with only CloudStorage access cannot derive the mnemonic.
|
|
341
|
+
- `Wallet Seed Cipher` in CloudStorage is encrypted with the wallet master key, which should exist only on the user’s Telegram client for that device.
|
|
342
|
+
- An attacker with only CloudStorage access cannot derive the mnemonic without the master key.
|
|
334
343
|
|
|
335
344
|
- **Cross-platform restore requires mnemonic:**
|
|
336
345
|
- Any **new environment** (new Telegram device with empty SecureStorage or any non-Telegram platform) requires the mnemonic once.
|
|
@@ -341,5 +350,5 @@ This model keeps transaction approvals **user‑driven and key‑local** (inside
|
|
|
341
350
|
|
|
342
351
|
- If the **mnemonic is lost**, there is no recovery (by design) – it is the self-custodial root.
|
|
343
352
|
- If a device with a master key is compromised, an attacker can act as the owner from that device until the user moves funds to a new wallet.
|
|
344
|
-
- Telegram `SecureStorage` is documented for **iOS and Android**;
|
|
353
|
+
- Telegram `SecureStorage` is documented for **iOS and Android**; **Desktop and some builds may not support it**, triggering fallback to `DeviceStorage` or leaving the key unsaved until the user uses a supported client or restores with the mnemonic.
|
|
345
354
|
- Telegram Login for Websites is an **authentication mechanism only** – it does not give access to keys or the mnemonic itself, and cannot replace the mnemonic for wallet authorization.
|
|
@@ -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
|
+
|
package/{docs/wallets_hosting_architecture.md → research & docs/wallets_hosting_architecture.md }
RENAMED
|
@@ -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/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");
|