@www.hyperlinks.space/program-kit 18.18.18 → 123.123.123

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 (52) hide show
  1. package/.eas/workflows/create-development-builds.yml +21 -0
  2. package/.eas/workflows/create-draft.yml +15 -0
  3. package/.eas/workflows/deploy-to-production.yml +68 -0
  4. package/.gitattributes +48 -0
  5. package/.gitignore +52 -0
  6. package/.nvmrc +1 -0
  7. package/.vercelignore +6 -0
  8. package/README.md +17 -2
  9. package/ai/openai.ts +202 -0
  10. package/ai/transmitter.ts +367 -0
  11. package/backlogs/medium_term_backlog.md +26 -0
  12. package/backlogs/short_term_backlog.md +42 -0
  13. package/eslint.config.cjs +10 -0
  14. package/npmReadMe.md +17 -2
  15. package/npmrc.example +1 -0
  16. package/package.json +3 -28
  17. package/polyfills/buffer.ts +9 -0
  18. package/research & docs/ai_and_search_bar_input.md +94 -0
  19. package/research & docs/ai_bot_messages.md +124 -0
  20. package/research & docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
  21. package/research & docs/blue_bar_tackling.md +143 -0
  22. package/research & docs/bot_async_streaming.md +174 -0
  23. package/research & docs/build_and_install.md +129 -0
  24. package/research & docs/database_messages.md +34 -0
  25. package/research & docs/fonts.md +18 -0
  26. package/research & docs/github-gitlab-bidirectional-mirroring.md +154 -0
  27. package/research & docs/keys-retrieval-console-scripts.js +131 -0
  28. package/research & docs/npm-release.md +46 -0
  29. package/research & docs/releases.md +201 -0
  30. package/research & docs/releases_github_actions.md +188 -0
  31. package/research & docs/scalability.md +34 -0
  32. package/research & docs/security_plan_raw.md +244 -0
  33. package/research & docs/security_raw.md +354 -0
  34. package/research & docs/storage-availability-console-script.js +152 -0
  35. package/research & docs/storage-lifetime.md +33 -0
  36. package/research & docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
  37. package/research & docs/timing_raw.md +63 -0
  38. package/research & docs/tma_logo_bar_jump_investigation.md +69 -0
  39. package/research & docs/update.md +205 -0
  40. package/research & docs/wallet_telegram_standalone_multichain_proposal.md +192 -0
  41. package/research & docs/wallets_hosting_architecture.md +403 -0
  42. package/services/wallet/tonWallet.ts +73 -0
  43. package/ui/components/GlobalBottomBar.tsx +447 -0
  44. package/ui/components/GlobalBottomBarWeb.tsx +362 -0
  45. package/ui/components/GlobalLogoBar.tsx +108 -0
  46. package/ui/components/GlobalLogoBarFallback.tsx +66 -0
  47. package/ui/components/GlobalLogoBarWithFallback.tsx +24 -0
  48. package/ui/components/HyperlinksSpaceLogo.tsx +29 -0
  49. package/ui/components/Telegram.tsx +677 -0
  50. package/ui/components/telegramWebApp.ts +359 -0
  51. package/ui/fonts.ts +12 -0
  52. package/ui/theme.ts +117 -0
@@ -0,0 +1,403 @@
1
+ # HyperlinksSpace — Wallet Architecture Doc
2
+
3
+ > High-level system design, UI flow principles, and implementation thoughts.
4
+ > Target: TON wallet architecture options for HyperlinksSpace app (Flutter/TMA/Web), including non-custodial and custodial modes.
5
+
6
+ ---
7
+
8
+ ## 1. Guiding Principles
9
+
10
+ - **Non-custodial by default.** The mnemonic (seed phrase) never touches the server. Key generation and signing happen on-device.
11
+ - **Telegram-first identity.** Inside Telegram, the user's Telegram account is the identity anchor. Outside Telegram (Windows desktop, Web), Telegram Login is used as the identity bridge.
12
+ - **Small, working steps.** Each wallet feature ships as an isolated, backward-compatible unit that can be deployed without touching existing bot or app flows.
13
+ - **Stable coin built-in.** The wallet is designed from day one to display and eventually allocate DLLR (or another locked stable) alongside the TON balance.
14
+
15
+ ---
16
+
17
+ ## 2. Wallet Types and Entry Points
18
+
19
+ The app supports two wallet modes:
20
+
21
+ | Mode | Description | Who uses it |
22
+ |---|---|---|
23
+ | **Unhosted (non-custodial)** | Created inside the app, keys live on device | New users with no existing wallet |
24
+ | **TON Connect** | Connects an existing external wallet (Tonkeeper, MyTonWallet, etc.) | Power users with existing wallets |
25
+
26
+ TON Connect is treated as an **additional/advanced feature**, not the default. The primary flow is always the unhosted wallet.
27
+
28
+ ---
29
+
30
+ ## 3. Wallet State Machine
31
+
32
+ Every wallet instance moves through these states:
33
+
34
+ ```
35
+ [ No Wallet ]
36
+
37
+ ├──── "Create wallet" ──────► [ Generating ]
38
+ │ │
39
+ └──── "Restore wallet" ────────────►│
40
+
41
+ [ Created ]
42
+ (address shown instantly, QR + copy)
43
+
44
+
45
+ [ Deploying ]
46
+ (SMC deploy call in progress)
47
+
48
+ ┌─────────┴─────────┐
49
+ ▼ ▼
50
+ [ Ready ] [ Deploy Failed ]
51
+ (fully operational) (retry available)
52
+ ```
53
+
54
+ **State storage key:** `awallet_v1` (encrypted blob in SecureStorage)
55
+ **Metadata stored separately (plaintext):**
56
+ ```
57
+ wallet_address
58
+ created_at
59
+ last_seen_at
60
+ deploy_status ← "pending" | "deployed" | "failed"
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 4. First Launch — Unhosted Wallet
66
+
67
+ **Goal:** User has no wallet. They open the app for the first time (any platform).
68
+
69
+ ### UI Flow
70
+
71
+ 1. **Wallet tab / panel opens** → app checks `awallet_v1` in local SecureStorage.
72
+ 2. Nothing found → show two CTAs:
73
+ - **"Create new wallet"** (primary, prominent)
74
+ - **"Restore wallet"** (secondary, text link — visible only if device has an encrypted blob from a previous session or the user chooses to enter a mnemonic)
75
+ 3. User taps **"Create new wallet"**:
76
+ - Spinner shown: _"Creating wallet…"_
77
+ - App generates a BIP39 mnemonic + TON keypair **entirely in-memory** (no server call at this step).
78
+ - Address is computed and displayed **instantly**.
79
+ - Encrypted blob is written to SecureStorage.
80
+ 4. App shows **"Your wallet is ready"** screen:
81
+ - Address (truncated + copy button)
82
+ - QR code
83
+ - DLLR status: `Allocated / Locked / Available` (placeholders until deploy completes)
84
+ - `"Deploying…"` status badge
85
+ 5. SMC deploy call fires in the background (POST `/wallet/deploy`). When complete, status badge updates to ✅ `"Active"`.
86
+ 6. A **"Back up your seed phrase"** nudge is shown — non-blocking, dismissible, but persistent until the user acknowledges.
87
+
88
+ ### Key decisions
89
+ - Address generation is instant and shown before deploy. This is intentional UX — users can copy the address to receive funds immediately, even before the contract is deployed.
90
+ - Mnemonic is shown **once**, at creation time. After acknowledgement it is never shown again (only recoverable via "Export seed phrase" with auth).
91
+
92
+ ---
93
+
94
+ ## 5. Second Launch — Loading Existing Wallet
95
+
96
+ **Goal:** User returns to the app on the same device. Wallet was created previously.
97
+
98
+ ### UI Flow
99
+
100
+ 1. App reads `awallet_v1` from SecureStorage on startup.
101
+ 2. Encrypted blob found → decrypt using device key (Secure Enclave / Android Keystore).
102
+ 3. Wallet panel loads directly to **Ready state** — no mnemonic prompt needed.
103
+ 4. App polls `/wallet/status?address=...` to refresh balances and deploy status.
104
+ 5. If `deploy_status === "pending"` (e.g., deploy was interrupted), app auto-retries the deploy silently.
105
+
106
+ ### Key decisions
107
+ - The device key (used to decrypt the blob) **never leaves the device**. It is stored in the OS-level secure keystore.
108
+ - No mnemonic entry is needed on the same device — the encrypted blob handles re-authentication transparently.
109
+
110
+ ---
111
+
112
+ ## 6. New Device / Re-connect Scenario (Mnemonic Required)
113
+
114
+ **Goal:** User opens the app on a new phone, new PC, or after clearing app data.
115
+
116
+ ### UI Flow
117
+
118
+ 1. App finds no `awallet_v1` in SecureStorage (it's a fresh install).
119
+ 2. Wallet panel shows:
120
+ - **"Create new wallet"** (primary)
121
+ - **"Restore wallet"** (secondary, prominent here)
122
+ 3. User taps **"Restore wallet"** → input field: _"Enter your 24-word seed phrase"_
123
+ 4. Words entered → app validates mnemonic → re-derives keypair → re-encrypts blob into the new device's SecureStorage.
124
+ 5. App calls `/wallet/restore` to confirm address matches expectation.
125
+ 6. Wallet loads at **Ready state** (deploy was already done on the original device).
126
+
127
+ ### Key decisions
128
+ - The mnemonic is the **single recovery factor**. There is no server-side recovery.
129
+ - Mnemonic input should use a native secure text field (no clipboard suggestions, no autocorrect logging).
130
+ - CloudStorage (Telegram's `telegram.cloudStorage` API) can optionally store the **ciphertext** (not the key), so restore can be assisted without the user typing all 24 words. The device key is still required to decrypt it — CloudStorage alone is not sufficient to reconstruct the wallet.
131
+
132
+ ---
133
+
134
+ ## 7. Telegram Wallet — Inside Telegram (TMA)
135
+
136
+ **Goal:** User accesses the wallet via the Telegram Mini App (TMA).
137
+
138
+ ### How identity and storage work inside TMA
139
+
140
+ - **Identity:** `initData.user.id` (Telegram user ID) is the identity anchor. No separate login needed.
141
+ - **Storage:**
142
+ - `telegram.cloudStorage` is used for the encrypted wallet blob — it is synced across Telegram sessions automatically.
143
+ - The device key (to decrypt the blob) is stored in the browser's IndexedDB / WebCrypto key store scoped to the TMA origin.
144
+ - **First launch in TMA:** Same as Section 4, but blob goes to `cloudStorage` instead of native SecureStorage.
145
+ - **Re-open in TMA on same device:** Blob in `cloudStorage` + device key in IndexedDB → loads wallet silently.
146
+ - **TMA on a new device:** CloudStorage has the ciphertext, but the device key is absent → user is prompted to enter mnemonic once. New device key is generated and stored. From then on, that device works silently.
147
+
148
+ ### Key decisions
149
+ - `telegram.cloudStorage` is **not end-to-end encrypted by Telegram** — it is accessible to the bot's server-side in theory. This is why we store only the **ciphertext** there, not the plaintext key or mnemonic.
150
+ - The split: _ciphertext in CloudStorage, key in device_ ensures neither half alone can reconstruct the wallet.
151
+
152
+ ---
153
+
154
+ ## 8. Outside Telegram — Web / Windows Setup
155
+
156
+ **Goal:** User uses the app via browser or Windows desktop (Electron/Tauri wrapper), outside of Telegram context.
157
+
158
+ ### How identity works outside TMA
159
+
160
+ Since there is no `initData` from Telegram, identity is established via **Telegram Login Widget** (OAuth-style flow):
161
+
162
+ 1. User is shown a "Log in with Telegram" button.
163
+ 2. Telegram sends a signed hash to the app confirming the user's Telegram ID and username.
164
+ 3. App uses the Telegram ID as the identity anchor — same as inside TMA.
165
+
166
+ ### Storage outside Telegram
167
+
168
+ - **Web browser:** Encrypted blob is stored in `localStorage` (or IndexedDB for larger payloads). Device key in WebCrypto non-extractable key store.
169
+ - **Windows desktop:** Encrypted blob stored in the OS credential manager or app's local data directory. Device key in Windows DPAPI or Keychain equivalent.
170
+
171
+ ### First launch on Windows
172
+
173
+ 1. App opens → Telegram Login flow → identity confirmed.
174
+ 2. App checks local storage for `awallet_v1`.
175
+ 3. **No wallet found:** Same "Create / Restore" UI as Section 4.
176
+ 4. **Wallet found (transferred or restored):** Loads to Ready state.
177
+
178
+ ### Re-launch on Windows
179
+
180
+ Same device → blob in local storage + device key still present → silent load, no mnemonic needed.
181
+
182
+ ### Key decisions
183
+ - Windows is primarily a **reading/management surface** — send, receive, view balances. Heavy transaction flows remain on mobile/TMA first.
184
+ - The GitHub auto-updater (workflow-based releases) is already in place for the Windows build. Wallet state must survive app updates — storage paths must not change across versions, or migration logic must be included.
185
+
186
+ ---
187
+
188
+ ## 9. API Contract (Wallet Endpoints)
189
+
190
+ ```
191
+ POST /wallet/create
192
+ Body: { address, encrypted_blob, public_key }
193
+ Response: { ok: true }
194
+
195
+ POST /wallet/deploy
196
+ Body: { address }
197
+ Response: { status: "pending" | "deployed" | "failed" }
198
+
199
+ GET /wallet/status?address=...
200
+ Response: { deployed: bool, dllr_status: {...}, balances: { ton, dllr } }
201
+
202
+ POST /wallet/restore
203
+ Body: { encrypted_blob }
204
+ Response: { address }
205
+ ```
206
+
207
+ The backend is **optional** for the read path — balances can be fetched directly from TON APIs (toncenter/tonapi) on the client side. The backend is primarily needed for:
208
+ - SMC deploy coordination
209
+ - DLLR allocation logic
210
+ - Caching and rate-limit protection for API calls
211
+
212
+ ---
213
+
214
+ ## 10. WalletService Abstraction (Flutter)
215
+
216
+ The Flutter app uses a `WalletService` interface so the UI never depends directly on whether we're in mock, front-only (direct TON API), or backend-connected mode:
217
+
218
+ ```dart
219
+ abstract class WalletService {
220
+ Future<WalletState> loadFromStorage();
221
+ Stream<WalletStatus> watchStatus(String address);
222
+ Future<WalletInfo> createWallet();
223
+ Future<WalletInfo> restoreWallet(String mnemonic);
224
+ }
225
+ ```
226
+
227
+ Swap between implementations via a flag (`kUseMockWalletState` for dev, env-driven for prod). No UI refactor needed when switching providers.
228
+
229
+ ---
230
+
231
+ ## 11. Security Summary
232
+
233
+ | Concern | Approach |
234
+ |---|---|
235
+ | Mnemonic exposure | Shown once at creation, never stored in plaintext |
236
+ | Key storage | OS Secure Enclave / Android Keystore / WebCrypto non-extractable |
237
+ | Server-side secrets | None — server never sees mnemonic or private key |
238
+ | Cross-device restore | Mnemonic re-entry required OR ciphertext from CloudStorage + device key |
239
+ | Telegram identity outside TMA | Telegram Login Widget (signed hash verification) |
240
+ | Duplicate deploy | State machine prevents re-deploy if `deploy_status === "deployed"` |
241
+ | App update survivability | Storage keys versioned (`awallet_v1`), migration path required for `v2` |
242
+
243
+ ---
244
+
245
+ ## 12. Phase Roadmap
246
+
247
+ | Phase | Scope |
248
+ |---|---|
249
+ | **Phase 1** | Unhosted wallet: create, display address, deploy SMC, show TON balance |
250
+ | **Phase 2** | DLLR integration: allocated/locked/available display, stable coin status |
251
+ | **Phase 3** | TON Connect (connect existing wallets as secondary option) |
252
+ | **Phase 4** | Send/receive flows, transaction history |
253
+ | **Phase 5** | Cross-device CloudStorage assist, backup/export UX hardening |
254
+
255
+ ---
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
+
403
+ *Last updated: April 2026. Maintained in repo at `docs/wallets_hosting_architecture.md`.*
@@ -0,0 +1,73 @@
1
+ import { mnemonicNew, mnemonicToPrivateKey } from "@ton/crypto";
2
+ import { WalletContractV4 } from "@ton/ton";
3
+ import { Buffer as BufferPolyfill } from "buffer";
4
+
5
+ if (typeof globalThis !== "undefined" && !(globalThis as { Buffer?: unknown }).Buffer) {
6
+ (globalThis as { Buffer?: unknown }).Buffer = BufferPolyfill;
7
+ }
8
+
9
+ function bytesToBase64(bytes: Uint8Array): string {
10
+ let binary = "";
11
+ for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
12
+ if (typeof btoa === "function") return btoa(binary);
13
+ return BufferPolyfill.from(bytes).toString("base64");
14
+ }
15
+
16
+ async function sha256Bytes(input: string): Promise<Uint8Array> {
17
+ const digest = await crypto.subtle.digest(
18
+ "SHA-256",
19
+ new TextEncoder().encode(input),
20
+ );
21
+ return new Uint8Array(digest);
22
+ }
23
+
24
+ export async function generateMnemonic(words = 24): Promise<string[]> {
25
+ return mnemonicNew(words);
26
+ }
27
+
28
+ export async function deriveAddressFromMnemonic(opts: {
29
+ mnemonic: string[];
30
+ testnet?: boolean;
31
+ workchain?: number;
32
+ }): Promise<string> {
33
+ const { mnemonic, testnet = false, workchain = 0 } = opts;
34
+ const keyPair = await mnemonicToPrivateKey(mnemonic);
35
+ const wallet = WalletContractV4.create({
36
+ workchain,
37
+ publicKey: keyPair.publicKey,
38
+ });
39
+ return wallet.address.toString({
40
+ bounceable: false,
41
+ urlSafe: true,
42
+ testOnly: testnet,
43
+ });
44
+ }
45
+
46
+ export async function deriveMasterKeyFromMnemonic(mnemonic: string[]): Promise<string> {
47
+ const bytes = await sha256Bytes(mnemonic.join(" "));
48
+ return bytesToBase64(bytes);
49
+ }
50
+
51
+ export async function createSeedCipher(
52
+ masterKey: string,
53
+ seed: string,
54
+ ): Promise<string> {
55
+ const keyMaterial = await crypto.subtle.importKey(
56
+ "raw",
57
+ await sha256Bytes(masterKey),
58
+ { name: "AES-GCM" },
59
+ false,
60
+ ["encrypt"],
61
+ );
62
+
63
+ const iv = crypto.getRandomValues(new Uint8Array(12));
64
+ const encrypted = await crypto.subtle.encrypt(
65
+ { name: "AES-GCM", iv },
66
+ keyMaterial,
67
+ new TextEncoder().encode(seed),
68
+ );
69
+
70
+ const cipherBytes = new Uint8Array(encrypted);
71
+ return `v1.${bytesToBase64(iv)}.${bytesToBase64(cipherBytes)}`;
72
+ }
73
+