telstore 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,19 +1,20 @@
1
- # telark
1
+ # telstore
2
2
 
3
3
  Split large files into 1.8GB chunks, store them on Telegram, and restore them intact.
4
4
 
5
5
  ## Quick start
6
6
 
7
7
  ```bash
8
- npx telark login # once only
9
- npx telark config chat @my_backups # where backups go, from now on
10
- npx telark data.tar # split it and send it there
11
- npx telark data.tar --to @somewhere # somewhere else, this run only
12
- npx telark config # every setting and where its value comes from
13
- npx telark status # account, destination, unfinished backups
14
- npx telark list # what is already stored in the destination
15
- npx telark restore telark-20260905-7f3a91
16
- npx telark delete telark-20260905-7f3a91 # take it back out of the chat, for good
8
+ npx telstore login # once only
9
+ npx telstore config chat @my_backups # where backups go, from now on
10
+ npx telstore data.tar # split it and send it there
11
+ npx telstore data.tar --to @somewhere # somewhere else, this run only
12
+ npx telstore config # every setting and where its value comes from
13
+ npx telstore status # account, destination, unfinished backups
14
+ npx telstore list # what is already stored in the destination
15
+ npx telstore restore telstore-20260905-7f3a91
16
+ npx telstore delete telstore-20260905-7f3a91 # take it back out of the chat, for good
17
+ npx telstore token # a session token for a machine you do not trust
17
18
  ```
18
19
 
19
20
  ## What you need
@@ -21,26 +22,27 @@ npx telark delete telark-20260905-7f3a91 # take it back out of the chat, for g
21
22
  - Node.js 18 or newer.
22
23
  - An `api_id` and `api_hash` from <https://my.telegram.org> → API development tools. The `login` command asks for both, plus your phone number and the verification code.
23
24
 
24
- telark signs in with your own Telegram account (MTProto), not a bot. That is a hard requirement: the Bot API caps uploads at 50MB per file, while a user account gets 2GB.
25
+ telstore signs in with your own Telegram account (MTProto), not a bot. That is a hard requirement: the Bot API caps uploads at 50MB per file, while a user account gets 2GB.
25
26
 
26
27
  ## Settings and flags
27
28
 
28
- There are two ways to say what telark should do, and they never overlap. **`config` writes;
29
+ There are two ways to say what telstore should do, and they never overlap. **`config` writes;
29
30
  flags do not.** A flag applies to the run you typed it on and changes nothing on disk, so
30
31
  `--to @elsewhere` sends one backup elsewhere without moving the destination for the next one.
31
32
 
32
33
  ```bash
33
- npx telark config # everything, and whether it is yours or a default
34
- npx telark config chat # one value, bare, ready to pipe
35
- npx telark config chunkSize 500MB # change it for good
36
- npx telark config chunkSize --unset # back to the default
34
+ npx telstore config # everything, and whether it is yours or a default
35
+ npx telstore config chat # one value, bare, ready to pipe
36
+ npx telstore config chunkSize 500MB # change it for good
37
+ npx telstore config chunkSize --unset # back to the default
37
38
  ```
38
39
 
39
40
  | Setting | Flag | Default | Meaning |
40
41
  |---|---|---|---|
41
42
  | `chat` | `--to` | none | `@username`, `-100123…`, or `me`. A negative channel id works with a space or an `=`, as a flag or as a config value. |
42
43
  | `chunkSize` | `--chunk-size` | `1800MB` | e.g. `1.8GB`, `500MB`. Hard ceiling 1950MB. An unfinished backup keeps the size it started with. |
43
- | `concurrency` | `--concurrency` | `8` | 512KB parts sent in parallel. An integer from 1 to 64. Upload only — `restore` downloads through its own fixed pool of workers. |
44
+ | `uploadConcurrency` | `--upload-concurrency` | `32` | 512KB parts sent in parallel while uploading. An integer from 1 to 64. |
45
+ | `downloadConcurrency` | `--download-concurrency` | `8` | 8MB slices fetched in parallel while restoring. An integer from 1 to 64. |
44
46
  | `limit` | `--limit` | `20` | How many backups `list` shows, newest first |
45
47
  | `verbose` | `--verbose` | off | Show the Telegram client's own connection logs, hidden by default so they do not break up the progress bar |
46
48
 
@@ -63,24 +65,24 @@ follows carries a summary card:
63
65
  🗄 data.tar
64
66
  ━━━━━━━━━━━━━━━
65
67
  💾 21.4 GB · 12 chunks
66
- 🆔 telark-20260905-7f3a91
68
+ 🆔 telstore-20260905-7f3a91
67
69
  📅 2026-09-05 16:40 UTC
68
70
 
69
- ↩ npx telark restore telark-20260905-7f3a91
70
- #telark
71
+ ↩ npx telstore restore telstore-20260905-7f3a91
72
+ #telstore
71
73
  ```
72
74
 
73
- `npx telark list` reads those cards straight out of the chat — one search, no downloads —
75
+ `npx telstore list` reads those cards straight out of the chat — one search, no downloads —
74
76
  and lays them out as a table:
75
77
 
76
78
  ```
77
79
  Destination https://web.telegram.org/k/#@my_backups
78
80
 
79
- BACKUP ID FILE SIZE CHUNKS CREATED
80
- telark-20260905-7f3a91 data.tar 21.4 GB 12 2026-09-05
81
- telark-20260901-9de447 photos.zip 940.3 MB 1 2026-09-01
81
+ BACKUP ID FILE SIZE CHUNKS CREATED
82
+ telstore-20260905-7f3a91 data.tar 21.4 GB 12 2026-09-05
83
+ telstore-20260901-9de447 photos.zip 940.3 MB 1 2026-09-01
82
84
 
83
- 2 backups. Restore with: npx telark restore <backup-id>
85
+ 2 backups. Restore with: npx telstore restore <backup-id>
84
86
  ```
85
87
 
86
88
  A backup uploaded before the card existed still gets a row, with dashes where the caption
@@ -88,22 +90,22 @@ says nothing — `list` reports what the chat holds and never fills gaps with gu
88
90
 
89
91
  ## How it works
90
92
 
91
- Every run mints a `backupId`. The file is read directly by offset — no temporary copies — and uploaded as documents named `<backupId>.partNNNN`. Once every chunk is up, telark sends a JSON manifest listing the message id and sha256 of each one. Restore needs only the `backupId`: it finds the manifest in the chat, downloads each chunk to its exact position in a `.partial` file, checks every chunk's sha256 and size, and renames it to the real file only after *all* chunks match.
93
+ Every run mints a `backupId`. The file is read directly by offset — no temporary copies — and uploaded as documents named `<backupId>.partNNNN`. Once every chunk is up, telstore sends a JSON manifest listing the message id and sha256 of each one. Restore needs only the `backupId`: it finds the manifest in the chat, downloads each chunk to its exact position in a `.partial` file, checks every chunk's sha256 and size, and renames it to the real file only after *all* chunks match.
92
94
 
93
- If the connection drops during an **upload**, just run the same command again — progress lives in `~/.telark/state/` and finished chunks are skipped, keeping the same `backupId`. Two things to know about rerunning:
95
+ If the connection drops during an **upload**, just run the same command again — progress lives in `~/.telstore/state/` and finished chunks are skipped, keeping the same `backupId`. Two things to know about rerunning:
94
96
 
95
- - Running again against a destination that differs from the one in the unfinished progress makes telark **refuse to run** rather than silently redirect — one backup cannot be split across two destinations. The error names the chat to pass as `--to` to carry on, or the state file to delete to start a new backup. It reads the same whether the mismatch came from a flag or from your configured `chat`.
97
+ - Running again against a destination that differs from the one in the unfinished progress makes telstore **refuse to run** rather than silently redirect — one backup cannot be split across two destinations. The error names the chat to pass as `--to` to carry on, or the state file to delete to start a new backup. It reads the same whether the mismatch came from a flag or from your configured `chat`.
96
98
  - Running again **without** `--chunk-size` resumes at the size the backup started with, whatever your configured `chunkSize` says today. A setting is what to use when nobody asks for anything; it is not somebody asking.
97
- - Running again **with** a `--chunk-size` that differs from that size makes telark **refuse to run**: the chunks already in the chat were cut that way and cannot be re-cut. Drop the flag to carry on, or delete the state file to start a new backup — which leaves the chunks already sent in the chat with nothing pointing at them.
99
+ - Running again **with** a `--chunk-size` that differs from that size makes telstore **refuse to run**: the chunks already in the chat were cut that way and cannot be re-cut. Drop the flag to carry on, or delete the state file to start a new backup — which leaves the chunks already sent in the chat with nothing pointing at them.
98
100
 
99
- `Ctrl-C` during an upload names the backup it was working on, so `telark status` and a later `restore` have something to go on. telark keeps the **20 most recent** unfinished backups in `~/.telark/state/`; starting a new one past that drops the oldest record and says which id it dropped. Only the local record goes — the chunks that backup sent stay in the chat, searchable by that id, but it can no longer be resumed.
101
+ `Ctrl-C` during an upload names the backup it was working on, so `telstore status` and a later `restore` have something to go on. telstore keeps the **20 most recent** unfinished backups in `~/.telstore/state/`; starting a new one past that drops the oldest record and says which id it dropped. Only the local record goes — the chunks that backup sent stay in the chat, searchable by that id, but it can no longer be resumed.
100
102
 
101
103
  **Restore keeps no state to resume from.** Pressing `Ctrl-C` mid-restore saves nothing — running again starts over.
102
104
 
103
105
  ## Deleting a backup
104
106
 
105
107
  ```bash
106
- npx telark delete telark-20260905-7f3a91
108
+ npx telstore delete telstore-20260905-7f3a91
107
109
  ```
108
110
 
109
111
  It prints what it is about to destroy, asks once, and then removes every chunk message and
@@ -127,16 +129,77 @@ manifest or local record giving a message id that is not a whole positive number
127
129
  deletes anything at all. A manifest too damaged for `restore` to use *can* still be deleted —
128
130
  that is usually the one you want gone.
129
131
 
132
+ ## Running on a machine you do not trust
133
+
134
+ `login` leaves your Telegram session on the machine you run it on, in plain text. When that
135
+ machine is not yours, print a **session token** on one that is and log in with that instead:
136
+
137
+ ```bash
138
+ # on the machine you are already logged in on
139
+ npx telstore token # asks for a passphrase, twice
140
+ # prints one line: tls1.…
141
+
142
+ # on the other machine
143
+ npx telstore login --token # paste the token, then type the passphrase
144
+ npx telstore restore telstore-20260905-7f3a91
145
+ npx telstore logout # when you are done
146
+ ```
147
+
148
+ The token holds your `api_id`, `api_hash`, session and settings, encrypted with the
149
+ passphrase (AES-256-GCM, key derived with scrypt). It is one line of base64url, so it
150
+ survives a chat message, an email or a QR code — and a copy that leaks without the
151
+ passphrase is not enough to use your account.
152
+
153
+ `login --token` stores the token exactly as it arrived, so **the session never exists on that
154
+ machine in a form anyone can read**. Every command that talks to Telegram asks for the
155
+ passphrase and opens it in memory:
156
+
157
+ ```json
158
+ { "sealed": "tls1.…", "settings": { "chat": "@my_backups" } }
159
+ ```
160
+
161
+ `npx telstore status` shows which of the two you are running:
162
+
163
+ ```
164
+ Session /home/you/.telstore/config.json (sealed — opened with a passphrase)
165
+ Account Sho (@shovity)
166
+ Destination https://web.telegram.org/k/#@my_backups
167
+ Unfinished none
168
+ ```
169
+
170
+ ### What is left behind
171
+
172
+ - **No readable session.** `logout` removes the sealed blob, and with it the `api_hash`,
173
+ which lives inside it.
174
+ - **State files, yes.** An upload writes `~/.telstore/state/<key>.json` so it can be resumed
175
+ after an interruption. It holds the file path, the chunk sizes and the message ids — no
176
+ secret — and losing it would strand chunks in the chat with nothing able to name them.
177
+
178
+ ### Things worth knowing before you use one
179
+
180
+ - **`--token` takes no value, on purpose.** A token written on the command line would sit in
181
+ `ps` for the whole life of the command and stay in that machine's shell history afterwards.
182
+ It is pasted at a prompt that does not echo it.
183
+ - **The passphrase may be left empty**, and then the token says so on its face: it starts
184
+ `tls0.` instead of `tls1.`, `token` warns, and `login` stores the session in plain text
185
+ exactly as an ordinary login would. telstore will not write a file that looks encrypted and
186
+ is not.
187
+ - **telstore cannot take a token back.** There is no expiry and no revocation. Like `logout`,
188
+ it says nothing to Telegram — to end the session for good, open Telegram → Settings →
189
+ Devices (Active sessions) and terminate it. Every copy of the token dies with it.
190
+ - **A wrong passphrase and a damaged token are the same event** to AES-GCM: one failed
191
+ authentication check. telstore names both possibilities rather than guessing which it was.
192
+
130
193
  ## Limits worth knowing
131
194
 
132
- - A chunk cannot exceed 1950MB: Telegram accepts at most 4000 parts of 512KB per file, an arithmetic ceiling of about 1953MB, and telark stops at 1950MB to leave a safety margin.
133
- - The data is **not** encrypted. Don't upload anything you would mind sitting on someone else's infrastructure.
134
- - Deleting a chunk message on Telegram destroys the backup, with no way to recover it. Use `npx telark delete <backup-id>` when that is what you actually want.
195
+ - A chunk cannot exceed 1950MB: Telegram accepts at most 4000 parts of 512KB per file, an arithmetic ceiling of about 1953MB, and telstore stops at 1950MB to leave a safety margin.
196
+ - The data is **not** encrypted. Don't upload anything you would mind sitting on someone else's infrastructure. The one thing telstore does encrypt is a session token, and that protects your login rather than your files.
197
+ - Deleting a chunk message on Telegram destroys the backup, with no way to recover it. Use `npx telstore delete <backup-id>` when that is what you actually want.
135
198
  - Keep the `backupId`. Without it you have to hunt for the manifest in the chat by hand.
136
199
 
137
200
  ## Where the config lives
138
201
 
139
- `~/.telark/config.json` (mode 600) holds `apiId`, `apiHash` and the session at the top level, with everything `config` manages under `settings`:
202
+ `~/.telstore/config.json` (mode 600) holds `apiId`, `apiHash` and the session at the top level, with everything `config` manages under `settings`:
140
203
 
141
204
  ```json
142
205
  {
@@ -147,6 +210,16 @@ that is usually the one you want gone.
147
210
  }
148
211
  ```
149
212
 
213
+ A machine logged in with `npx telstore login --token` holds `sealed` instead of those three
214
+ fields, and nothing else about the account:
215
+
216
+ ```json
217
+ { "sealed": "tls1.…", "settings": { "chat": "@my_backups" } }
218
+ ```
219
+
220
+ A file holding both shapes is refused rather than guessed at — there would be no way to know
221
+ which account was meant.
222
+
150
223
  Editing it by hand is fine, and a value that cannot be used is named on the next run — with the file and the key, never with a flag you did not type.
151
224
 
152
- `npx telark logout` **only deletes the locally stored session** and keeps the rest — the session is still alive on Telegram's side. To revoke access for good, open Telegram → Settings → Devices (Active sessions) and terminate that session.
225
+ `npx telstore logout` **only deletes the locally stored session** and keeps the rest — the session is still alive on Telegram's side. To revoke access for good, open Telegram → Settings → Devices (Active sessions) and terminate that session.
@@ -7,6 +7,7 @@ import { runList } from '../src/commands/list.js'
7
7
  import { runLogout } from '../src/commands/logout.js'
8
8
  import { runRestore } from '../src/commands/restore.js'
9
9
  import { runStatus } from '../src/commands/status.js'
10
+ import { runToken } from '../src/commands/token.js'
10
11
  import { runUpload } from '../src/commands/upload.js'
11
12
 
12
13
  const SIGINT_EXIT_CODE = 130
@@ -18,6 +19,10 @@ let currentCommand = null
18
19
  let currentBackupId = null
19
20
 
20
21
  process.on('SIGINT', () => {
22
+ // A passphrase prompt has stdin in raw mode, and process.exit skips readline's own cleanup.
23
+ // Without this, Ctrl-C hands back a shell that no longer echoes what is typed into it.
24
+ if (process.stdin.isTTY) process.stdin.setRawMode(false)
25
+
21
26
  process.stderr.write(interruptMessage(currentCommand, { backupId: currentBackupId }))
22
27
  process.exit(SIGINT_EXIT_CODE)
23
28
  })
@@ -41,7 +46,11 @@ async function main() {
41
46
  return
42
47
 
43
48
  case 'login':
44
- await runLogin({ verbose: Boolean(parsed.options.verbose) })
49
+ await runLogin({
50
+ args: parsed.args,
51
+ token: Boolean(parsed.options.token),
52
+ verbose: Boolean(parsed.options.verbose),
53
+ })
45
54
  return
46
55
 
47
56
  case 'logout':
@@ -60,6 +69,10 @@ async function main() {
60
69
  await runConfig(parsed.args, parsed.options)
61
70
  return
62
71
 
72
+ case 'token':
73
+ await runToken(parsed.args, parsed.options)
74
+ return
75
+
63
76
  case 'upload':
64
77
  await runUpload(parsed.args[0], parsed.options, {
65
78
  onBackupId: (id) => {
@@ -70,14 +83,14 @@ async function main() {
70
83
 
71
84
  case 'restore':
72
85
  if (!parsed.args[0]) {
73
- throw new Error('Missing backup id. Example: npx telark restore telark-20260905-7f3a91')
86
+ throw new Error('Missing backup id. Example: npx telstore restore telstore-20260905-7f3a91')
74
87
  }
75
88
  await runRestore(parsed.args[0], parsed.options)
76
89
  return
77
90
 
78
91
  case 'delete':
79
92
  if (!parsed.args[0]) {
80
- throw new Error('Missing backup id. Example: npx telark delete telark-20260905-7f3a91')
93
+ throw new Error('Missing backup id. Example: npx telstore delete telstore-20260905-7f3a91')
81
94
  }
82
95
  await runDelete(parsed.args[0], parsed.options)
83
96
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telstore",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Split large files into chunks and store them on Telegram",
5
5
  "keywords": [
6
6
  "telegram",
@@ -11,19 +11,19 @@
11
11
  "cli",
12
12
  "mtproto"
13
13
  ],
14
- "homepage": "https://github.com/shovity/telark#readme",
14
+ "homepage": "https://github.com/shovity/telstore#readme",
15
15
  "bugs": {
16
- "url": "https://github.com/shovity/telark/issues"
16
+ "url": "https://github.com/shovity/telstore/issues"
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/shovity/telark.git"
20
+ "url": "git+https://github.com/shovity/telstore.git"
21
21
  },
22
22
  "type": "module",
23
23
  "license": "MIT",
24
24
  "author": "shovity",
25
25
  "bin": {
26
- "telark": "bin/telark.js"
26
+ "telstore": "bin/telstore.js"
27
27
  },
28
28
  "files": [
29
29
  "bin",
@@ -39,4 +39,4 @@
39
39
  "dependencies": {
40
40
  "telegram": "^2.26.22"
41
41
  }
42
- }
42
+ }
package/src/caption.js CHANGED
@@ -8,7 +8,7 @@ const DIVIDER = '━'.repeat(15)
8
8
 
9
9
  // The hashtag is what `list` searches for, and it lives on the manifest alone: chunk
10
10
  // captions stay out of that search so a twelve-chunk backup is one hit, not thirteen.
11
- export const MANIFEST_TAG = '#telark'
11
+ export const MANIFEST_TAG = '#telstore'
12
12
 
13
13
  // A file name may legally contain a newline or a tab, and either one would push the
14
14
  // rest of the card down a row and take its shape apart.
@@ -32,7 +32,7 @@ export function manifestCaption({ id, name, size, chunks, createdAt }) {
32
32
  `🆔 ${id}`,
33
33
  `📅 ${utcMinutes(createdAt)}`,
34
34
  '',
35
- `↩ npx telark restore ${id}`,
35
+ `↩ npx telstore restore ${id}`,
36
36
  MANIFEST_TAG,
37
37
  ].join('\n')
38
38
  }
package/src/chat.js CHANGED
@@ -1,4 +1,4 @@
1
- // How telark talks about a destination. None of this touches Telegram — it is string
1
+ // How telstore talks about a destination. None of this touches Telegram — it is string
2
2
  // handling around a target the user typed — so it lives apart from the client that does.
3
3
 
4
4
  export function normalizeChatTarget(input) {
@@ -17,7 +17,7 @@ export function normalizeChatTarget(input) {
17
17
 
18
18
  // Telegram's web client addresses a chat by putting the raw target in the fragment, which
19
19
  // covers both a negative channel id and an @username. Saved Messages is the exception: it
20
- // is reached by the account's own id, which telark does not know, so it gets no link
20
+ // is reached by the account's own id, which telstore does not know, so it gets no link
21
21
  // rather than a guessed one that lands somewhere else.
22
22
  export function chatUrl(chat) {
23
23
  const text = String(chat)
package/src/chunking.js CHANGED
@@ -7,7 +7,20 @@ export const MAX_PARTS = 4000
7
7
  export const SLICE_SIZE = 8 * 1024 * 1024
8
8
  export const MAX_CHUNK_SIZE = 1950 * 1024 * 1024
9
9
  export const DEFAULT_CHUNK_SIZE = 1800 * 1024 * 1024
10
- export const DEFAULT_CONCURRENCY = 8
10
+ // Upload counts 512KB parts and download counts 8MB slices, so one number cannot serve both.
11
+ // Measured on a 1Gb/s line against a single 1800MB chunk, so every run transfers for three
12
+ // to five minutes rather than the seconds a burst benchmark samples — the distinction the
13
+ // parallel-download spec insists on, and it matters: measured in 20s bursts the download
14
+ // order came out reversed, with 4 apparently the fastest value.
15
+ // upload 16 -> 7.7 MB/s (234s), 32 -> 9.4 (193s), 64 -> 9.5 (191s)
16
+ // download 4 -> 5.8 MB/s (311s), 8 -> 6.0 (300s), 16 -> 6.0 (302s)
17
+ // Upload takes 32 because 64 measures the same speed (1% apart, inside the noise) at twice
18
+ // the cost: 64 puts 32MB in flight, and a batch's last part then needs 4.4 Mbps to land
19
+ // before the 60s stall deadline, against 2.1 Mbps at 32. A slow link would be told its
20
+ // transfer stalled while it was merely slow, which is the one thing that deadline must not say.
21
+ // Download takes 8 for the same reason from the other side: 16 buys nothing and 4 is slower.
22
+ export const DEFAULT_UPLOAD_CONCURRENCY = 32
23
+ export const DEFAULT_DOWNLOAD_CONCURRENCY = 8
11
24
  export const MAX_CONCURRENCY = 64
12
25
 
13
26
  // Every chunk is one message in the chat and one entry in the manifest, so a plan this long
package/src/cli.js CHANGED
@@ -8,55 +8,68 @@ const SUBCOMMANDS = new Set([
8
8
  'delete',
9
9
  'status',
10
10
  'config',
11
+ 'token',
11
12
  'help',
12
13
  ])
13
14
 
14
15
  const OPTIONS = {
15
16
  to: { type: 'string' },
16
17
  'chunk-size': { type: 'string' },
17
- concurrency: { type: 'string' },
18
+ 'upload-concurrency': { type: 'string' },
19
+ 'download-concurrency': { type: 'string' },
18
20
  out: { type: 'string' },
19
21
  limit: { type: 'string' },
20
22
  verbose: { type: 'boolean' },
21
23
  unset: { type: 'boolean' },
22
24
  yes: { type: 'boolean' },
25
+ token: { type: 'boolean' },
23
26
  help: { type: 'boolean', short: 'h' },
24
27
  }
25
28
 
26
- export const HELP = `telark — split large files into chunks and store them on Telegram
29
+ export const HELP = `telstore — split large files into chunks and store them on Telegram
27
30
 
28
31
  Usage:
29
- npx telark login Log in to Telegram, only needed once
30
- npx telark <file> Split a file and upload it to Telegram
31
- npx telark list List the backups stored in the destination
32
- npx telark restore <backup-id> Download the chunks and reassemble the file
33
- npx telark delete <backup-id> Remove a backup's chunks and manifest from the chat
34
- npx telark status Show the account, the destination and unfinished backups
35
- npx telark config Show every setting and where its value comes from
36
- npx telark logout Remove the saved session
32
+ npx telstore login Log in to Telegram, only needed once
33
+ npx telstore <file> Split a file and upload it to Telegram
34
+ npx telstore list List the backups stored in the destination
35
+ npx telstore restore <backup-id> Download the chunks and reassemble the file
36
+ npx telstore delete <backup-id> Remove a backup's chunks and manifest from the chat
37
+ npx telstore status Show the account, the destination and unfinished backups
38
+ npx telstore config Show every setting and where its value comes from
39
+ npx telstore logout Remove the saved session
40
+
41
+ Running on a machine you do not trust:
42
+ npx telstore token Print a session token for another machine
43
+ npx telstore login --token Log in there by pasting one, session stays sealed
37
44
 
38
45
  Settings:
39
- npx telark config <name> Print one setting's value
40
- npx telark config <name> <value> Change it for good
41
- npx telark config <name> --unset Drop it and fall back to the default
46
+ npx telstore config <name> Print one setting's value
47
+ npx telstore config <name> <value> Change it for good
48
+ npx telstore config <name> --unset Drop it and fall back to the default
42
49
 
43
- chat Where backups go: @username, -100123..., or me. No default.
44
- chunkSize Size of each chunk, default 1800MB. Examples: 1.8GB, 500MB.
45
- concurrency 512KB parts sent in parallel, default 8, max 64. Upload only.
46
- limit How many backups list shows, default 20.
47
- verbose Show Telegram connection logs, default false.
50
+ chat Where backups go: @username, -100123..., or me. No default.
51
+ chunkSize Size of each chunk, default 1800MB. Examples: 1.8GB, 500MB.
52
+ uploadConcurrency 512KB parts sent in parallel, default 32, max 64.
53
+ downloadConcurrency 8MB slices fetched in parallel, default 8, max 64.
54
+ limit How many backups list shows, default 20.
55
+ verbose Show Telegram connection logs, default false.
48
56
 
49
57
  Options apply to one run and are never saved. Use config to change a setting for good.
50
- --to <chat> Destination for this run only.
51
- --chunk-size <n> Chunk size for this run only. An unfinished backup keeps the size
52
- it started with.
53
- --concurrency <n> Parts in parallel for this run only. Upload only — restore always
54
- downloads with its own fixed pool of workers.
55
- --out <path> Where to write the restored file. Defaults to the basename in the manifest.
56
- --limit <n> How many backups list shows this run.
57
- --yes Delete without asking to confirm first.
58
- --verbose Show Telegram connection logs for this run.
59
- -h, --help Show this help.
58
+ --to <chat> Destination for this run only.
59
+ --chunk-size <n> Chunk size for this run only. An unfinished backup keeps the
60
+ size it started with.
61
+ --upload-concurrency <n> 512KB parts in parallel while uploading, this run only.
62
+ --download-concurrency <n> 8MB slices in parallel while restoring, this run only.
63
+ --out <path> Where to write the restored file. Defaults to the basename in
64
+ the manifest.
65
+ --limit <n> How many backups list shows this run.
66
+ --token Log in by pasting a session token. It takes no value on
67
+ purpose: a token written on the command line would sit in
68
+ "ps" for the whole life of the command, and stay in that
69
+ machine's shell history afterwards.
70
+ --yes Delete without asking to confirm first.
71
+ --verbose Show Telegram connection logs for this run.
72
+ -h, --help Show this help.
60
73
  `
61
74
 
62
75
  // What Ctrl-C means depends on the command that was running: upload has written every
@@ -69,7 +82,7 @@ export function interruptMessage(command, { backupId } = {}) {
69
82
 
70
83
  return (
71
84
  `\n${backup} — run the same command again to continue, ` +
72
- 'or "npx telark status" to see what is left.\n'
85
+ 'or "npx telstore status" to see what is left.\n'
73
86
  )
74
87
  }
75
88
 
@@ -138,13 +151,13 @@ export function route(argv) {
138
151
 
139
152
  const [first, ...rest] = positionals
140
153
 
141
- // `telark --to @chan` with no file used to mean "remember this destination". Flags no
154
+ // `telstore --to @chan` with no file used to mean "remember this destination". Flags no
142
155
  // longer write anything, so that line now asks for a run that has nothing to upload —
143
156
  // say where the destination actually lives instead of printing help at someone who was
144
157
  // perfectly clear about what they wanted.
145
158
  if (first === undefined && values.to && !values.help) {
146
159
  throw new Error(
147
- `Nothing to upload. To change the destination for good, run "npx telark config chat ${values.to}". ` +
160
+ `Nothing to upload. To change the destination for good, run "npx telstore config chat ${values.to}". ` +
148
161
  'To use it for one run, pass --to alongside a file or a command.',
149
162
  )
150
163
  }
package/src/client.js CHANGED
@@ -5,6 +5,7 @@ import { StringSession } from 'telegram/sessions/index.js'
5
5
 
6
6
  import { manifestFileName } from './manifest.js'
7
7
  import { withRetry } from './retry.js'
8
+ import { unlockConfig } from './session.js'
8
9
  import { DEFAULT_STALL_MS, withStallTimeout } from './stall.js'
9
10
 
10
11
  // GramJS narrates its version, every connection and every disconnect at info level, and
@@ -22,7 +23,7 @@ export function documentFileName(message) {
22
23
  return named?.fileName ?? null
23
24
  }
24
25
 
25
- // The one place telark searches a chat. Both callers want documents and nothing else,
26
+ // The one place telstore searches a chat. Both callers want documents and nothing else,
26
27
  // and getMessages is preferred over a raw Api.messages.Search because it handles offsets,
27
28
  // hashes and pagination itself, so we don't hand-build easily mistyped fields. The raw
28
29
  // message is kept alongside the flat fields because downloading needs it whole.
@@ -42,9 +43,9 @@ export async function searchDocuments(client, peer, { search, limit }) {
42
43
  }))
43
44
  }
44
45
 
45
- // How telark finds a backup's manifest, in one place because restore and delete must not
46
+ // How telstore finds a backup's manifest, in one place because restore and delete must not
46
47
  // disagree about it. The search is by backup id, but the answer is decided by the file name
47
- // telark itself wrote — a caption is text a person can edit, a file name is not.
48
+ // telstore itself wrote — a caption is text a person can edit, a file name is not.
48
49
  export async function findManifestMessage(client, peer, backupId) {
49
50
  const wanted = manifestFileName(backupId)
50
51
  const found = await searchDocuments(client, peer, { search: backupId, limit: 100 })
@@ -56,7 +57,7 @@ export async function readMessageBytes(client, message) {
56
57
  return await client.downloadMedia(message)
57
58
  }
58
59
 
59
- // The one place telark removes messages from a chat, and the mirror of searchDocuments
60
+ // The one place telstore removes messages from a chat, and the mirror of searchDocuments
60
61
  // above. GramJS has its own deleteMessages, and it is the right thing to call — it resolves
61
62
  // the peer and picks between channels.DeleteMessages and messages.DeleteMessages, which is
62
63
  // exactly the choice a fake client would never catch us getting wrong.
@@ -64,7 +65,7 @@ export async function readMessageBytes(client, message) {
64
65
  // What it does on top of that is the problem: it splits the ids into batches of a hundred
65
66
  // and fires every batch at once through Promise.all. A ten-thousand-chunk backup would put
66
67
  // a hundred requests in flight together, none of them under the retry policy or the stall
67
- // deadline that every other network wait in telark carries. Batching here instead keeps
68
+ // deadline that every other network wait in telstore carries. Batching here instead keeps
68
69
  // one request outstanding at a time, under both.
69
70
  //
70
71
  // Telegram does not complain about an id that is no longer there, so sending a batch twice
@@ -121,16 +122,27 @@ export async function closeQuietly(client, disconnect, onWarn) {
121
122
  }
122
123
  }
123
124
 
125
+ // Two shapes count as logged in: the ordinary one login writes, and the sealed blob that
126
+ // "login --token" leaves, which holds the same three fields behind a passphrase. Checked
127
+ // before anything asks for that passphrase, so somebody who never logged in is told so rather
128
+ // than asked to type a secret for an account that is not there.
124
129
  export function assertLoggedIn(config) {
130
+ if (config.sealed) return
131
+
125
132
  if (!config.session || !config.apiId || !config.apiHash) {
126
- throw new Error('Not logged in — run "npx telark login" first.')
133
+ throw new Error('Not logged in — run "npx telstore login" first.')
127
134
  }
128
135
  }
129
136
 
130
- export async function connect(config, { verbose = false } = {}) {
137
+ // Every command that needs Telegram comes through here, which makes this the one place a
138
+ // sealed session has to be opened. Doing it anywhere else would mean eight places to keep in
139
+ // step, and a ninth command would simply forget.
140
+ export async function connect(config, { verbose = false, unlock = unlockConfig } = {}) {
131
141
  assertLoggedIn(config)
132
142
 
133
- const client = new TelegramClient(new StringSession(config.session), config.apiId, config.apiHash, {
143
+ const { apiId, apiHash, session } = await unlock(config)
144
+
145
+ const client = new TelegramClient(new StringSession(session), apiId, apiHash, {
134
146
  connectionRetries: 5,
135
147
  floodSleepThreshold: 60,
136
148
  baseLogger: createLogger(verbose),
@@ -139,7 +151,7 @@ export async function connect(config, { verbose = false } = {}) {
139
151
  await client.connect()
140
152
 
141
153
  if (!(await client.isUserAuthorized())) {
142
- throw new Error('Session expired — run "npx telark login".')
154
+ throw new Error('Session expired — run "npx telstore login".')
143
155
  }
144
156
 
145
157
  return client