bytifi 0.2.0 → 0.2.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 +2 -31
- package/bin/bytifi.js +11 -17
- package/lib/crypto.js +6 -68
- package/lib/upload.js +0 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,11 +55,11 @@ Prefer the environment variable — keys on the command line can appear in shell
|
|
|
55
55
|
bytifi upload ./photo.png
|
|
56
56
|
bytifi upload "./my video (1).mp4"
|
|
57
57
|
bytifi upload ./report.pdf --expires 60 --delete-on-download
|
|
58
|
-
bytifi upload ./logs.txt --
|
|
58
|
+
bytifi upload ./logs.txt --concurrency 8 --json > upload.json
|
|
59
59
|
bytifi upload ./photo.png -q
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Files over **10 MB** use multipart upload automatically
|
|
62
|
+
All uploads are gzip-compressed per chunk before encryption. Files over **10 MB** use multipart upload automatically.
|
|
63
63
|
|
|
64
64
|
Upload accepts **one file at a time**. Quote paths that contain spaces. Avoid shell globs like `**` — your shell may expand them into dozens of paths.
|
|
65
65
|
|
|
@@ -128,7 +128,6 @@ Note: with `npm exec`, put `--` before the file path so npm does not swallow `--
|
|
|
128
128
|
| `-q, --quiet` | Print only the share URL |
|
|
129
129
|
| `--verbose` | Print API error details to stderr |
|
|
130
130
|
| `--mime-type` | Override detected MIME type |
|
|
131
|
-
| `--compress` | Compression mode: `auto`, `gzip`, or `off` (default: `auto`) |
|
|
132
131
|
| `--concurrency` | Parallel encrypt/upload workers, 1–16 (default: `4`) |
|
|
133
132
|
| `--base-url` | API base URL (default: `https://bytifi.com`) |
|
|
134
133
|
|
|
@@ -156,34 +155,6 @@ JSON output for decrypt includes `outputPath`, `originalName`, `size`, `mimeType
|
|
|
156
155
|
|
|
157
156
|
Files over ~100 MB encrypted use multipart upload automatically. Progress prints to stderr unless `--json` or `--quiet` is set.
|
|
158
157
|
|
|
159
|
-
## How it works
|
|
160
|
-
|
|
161
|
-
**Upload**
|
|
162
|
-
|
|
163
|
-
1. Optionally gzip-compress each chunk (`--compress auto|gzip|off`)
|
|
164
|
-
2. Encrypts locally with AES-GCM (same format as the website, meta `version: 2` when compressed)
|
|
165
|
-
3. Uploads encrypted bytes via multipart pipeline for files >10 MB
|
|
166
|
-
4. Prints a share URL including `#token=...`
|
|
167
|
-
|
|
168
|
-
## Compatibility
|
|
169
|
-
|
|
170
|
-
| Scenario | Works? |
|
|
171
|
-
|----------|--------|
|
|
172
|
-
| Old links (no compression) | Yes — everywhere |
|
|
173
|
-
| New CLI upload + new CLI decrypt | Yes |
|
|
174
|
-
| New CLI upload + **updated website** decrypt | Yes — **deploy Bytifi-Website** after upgrading CLI |
|
|
175
|
-
| New CLI upload + old website (not deployed) | **Broken in browser** — garbled download |
|
|
176
|
-
| New CLI upload + old CLI (≤0.1.5) decrypt | **Broken** — upgrade CLI |
|
|
177
|
-
| Website upload (no compression) | Yes — unchanged |
|
|
178
|
-
|
|
179
|
-
Compressed files larger than one chunk use **part-based storage**; decrypt via share link or CLI part download.
|
|
180
|
-
|
|
181
|
-
**Decrypt**
|
|
182
|
-
|
|
183
|
-
1. Reads link metadata from `/api/link/:token`, from `--upload-json`, or from `--meta`
|
|
184
|
-
2. Downloads the encrypted file (unless you pass a local encrypted file)
|
|
185
|
-
3. Decrypts locally and writes the original file to disk
|
|
186
|
-
|
|
187
158
|
## Development
|
|
188
159
|
|
|
189
160
|
```bash
|
package/bin/bytifi.js
CHANGED
|
@@ -8,7 +8,10 @@ import { decryptFile } from '../lib/decrypt.js'
|
|
|
8
8
|
import { BytifiApiError, BytifiNetworkError, uploadFile } from '../lib/upload.js'
|
|
9
9
|
|
|
10
10
|
const require = createRequire(import.meta.url)
|
|
11
|
-
const
|
|
11
|
+
const version =
|
|
12
|
+
typeof __BYTIFI_VERSION__ !== 'undefined'
|
|
13
|
+
? __BYTIFI_VERSION__
|
|
14
|
+
: require('../package.json').version
|
|
12
15
|
|
|
13
16
|
function printHelp() {
|
|
14
17
|
process.stdout.write(`Bytifi CLI v${version} — encrypt, upload, and decrypt files
|
|
@@ -26,7 +29,6 @@ Upload options:
|
|
|
26
29
|
-q, --quiet Print only the share URL
|
|
27
30
|
--verbose Show API error details on stderr
|
|
28
31
|
--mime-type <type> Override detected MIME type
|
|
29
|
-
--compress <mode> Compression: auto|gzip|off (default: auto)
|
|
30
32
|
--concurrency <n> Parallel part workers (default: 4)
|
|
31
33
|
--base-url <url> API base URL (default: https://bytifi.com)
|
|
32
34
|
|
|
@@ -54,16 +56,16 @@ Exit codes:
|
|
|
54
56
|
3 network error
|
|
55
57
|
|
|
56
58
|
Examples:
|
|
59
|
+
bytifi --version
|
|
60
|
+
export BYTIFI_API_KEY=usk_your_key
|
|
61
|
+
|
|
57
62
|
bytifi upload ./photo.png
|
|
58
|
-
bytifi upload ./
|
|
59
|
-
bytifi upload ./logs.
|
|
63
|
+
bytifi upload "./my report.pdf" --expires 60 --delete-on-download
|
|
64
|
+
bytifi upload ./logs.txt --concurrency 8 --json > upload.json
|
|
60
65
|
bytifi upload ./large.iso -q
|
|
61
66
|
|
|
62
|
-
bytifi decrypt 'https://bytifi.com/link?link=
|
|
63
|
-
bytifi decrypt
|
|
64
|
-
|
|
65
|
-
bytifi decrypt ./downloaded.encrypted --upload-json upload.json
|
|
66
|
-
bytifi decrypt "./my file(1).mp4" --link abc --token 'ENCRYPTION_TOKEN'
|
|
67
|
+
bytifi decrypt 'https://bytifi.com/link?link=LINK#token=KEY'
|
|
68
|
+
bytifi decrypt ./downloaded.bin --upload-json upload.json -o ./restored.bin
|
|
67
69
|
`)
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -85,7 +87,6 @@ function parseUploadArgs(argv) {
|
|
|
85
87
|
quiet: false,
|
|
86
88
|
verbose: false,
|
|
87
89
|
mimeType: '',
|
|
88
|
-
compressionMode: 'auto',
|
|
89
90
|
concurrency: 4,
|
|
90
91
|
baseUrl: 'https://bytifi.com',
|
|
91
92
|
help: false,
|
|
@@ -148,12 +149,6 @@ function parseUploadArgs(argv) {
|
|
|
148
149
|
continue
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
if (arg === '--compress') {
|
|
152
|
-
options.compressionMode = readFlagValue(argv, index, arg)
|
|
153
|
-
index += 1
|
|
154
|
-
continue
|
|
155
|
-
}
|
|
156
|
-
|
|
157
152
|
if (arg === '--concurrency') {
|
|
158
153
|
const raw = readFlagValue(argv, index, arg)
|
|
159
154
|
const concurrency = Number(raw)
|
|
@@ -334,7 +329,6 @@ async function runUpload(filePath, options) {
|
|
|
334
329
|
expiresInMinutes: options.expiresInMinutes,
|
|
335
330
|
deleteOnDownload: options.deleteOnDownload,
|
|
336
331
|
mimeType: options.mimeType || undefined,
|
|
337
|
-
compressionMode: options.compressionMode,
|
|
338
332
|
concurrency: options.concurrency,
|
|
339
333
|
signal: abortController.signal,
|
|
340
334
|
onProgress: showProgress
|
package/lib/crypto.js
CHANGED
|
@@ -13,19 +13,6 @@ export const PLAIN_CHUNK_SIZE = ENCRYPTED_PART_SIZE - 16
|
|
|
13
13
|
export const DIRECT_UPLOAD_LIMIT_BYTES = 100 * 1024 * 1024
|
|
14
14
|
export const MULTIPART_THRESHOLD_BYTES = 10 * 1024 * 1024
|
|
15
15
|
|
|
16
|
-
const SKIP_COMPRESS_EXTENSIONS = new Set([
|
|
17
|
-
'.7z', '.avi', '.br', '.bz2', '.gif', '.gz', '.jpeg', '.jpg', '.mkv', '.mov',
|
|
18
|
-
'.mp3', '.mp4', '.png', '.rar', '.webp', '.woff', '.woff2', '.xz', '.zip', '.zst', '.iso',
|
|
19
|
-
])
|
|
20
|
-
|
|
21
|
-
const SKIP_COMPRESS_MIME_EXACT = new Set([
|
|
22
|
-
'application/gzip',
|
|
23
|
-
'application/pdf',
|
|
24
|
-
'application/x-7z-compressed',
|
|
25
|
-
'application/x-rar-compressed',
|
|
26
|
-
'application/zip',
|
|
27
|
-
])
|
|
28
|
-
|
|
29
16
|
export function buildChunkIv(noncePrefix, chunkIndex) {
|
|
30
17
|
const iv = Buffer.alloc(12)
|
|
31
18
|
noncePrefix.copy(iv, 0, 0, 8)
|
|
@@ -33,44 +20,8 @@ export function buildChunkIv(noncePrefix, chunkIndex) {
|
|
|
33
20
|
return iv
|
|
34
21
|
}
|
|
35
22
|
|
|
36
|
-
export function resolveCompressionMode(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (mode === 'off' || mode === 'none') {
|
|
40
|
-
return 'none'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (mode === 'gzip') {
|
|
44
|
-
return 'gzip'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (mode === 'auto') {
|
|
48
|
-
return shouldSkipAutoCompression(mimeType, originalName) ? 'none' : 'gzip'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
throw new Error('compress must be one of: auto, gzip, off')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function shouldSkipAutoCompression(mimeType, originalName) {
|
|
55
|
-
const extension = path.extname(String(originalName || '')).toLowerCase()
|
|
56
|
-
if (SKIP_COMPRESS_EXTENSIONS.has(extension)) {
|
|
57
|
-
return true
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const normalizedMime = String(mimeType || '').toLowerCase()
|
|
61
|
-
if (SKIP_COMPRESS_MIME_EXACT.has(normalizedMime)) {
|
|
62
|
-
return true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (normalizedMime.startsWith('video/') || normalizedMime.startsWith('audio/')) {
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (normalizedMime.startsWith('image/')) {
|
|
70
|
-
return true
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return false
|
|
23
|
+
export function resolveCompressionMode() {
|
|
24
|
+
return 'gzip'
|
|
74
25
|
}
|
|
75
26
|
|
|
76
27
|
export function usesChunkCompression(meta) {
|
|
@@ -112,17 +63,11 @@ export function buildClientEncryptionMeta({
|
|
|
112
63
|
noncePrefix,
|
|
113
64
|
originalSize,
|
|
114
65
|
mimeType,
|
|
115
|
-
compression = 'none',
|
|
116
66
|
}) {
|
|
117
|
-
const compressionMeta = normalizeCompressionMeta({
|
|
118
|
-
algorithm: compression === 'gzip' ? 'gzip' : 'none',
|
|
119
|
-
scope: 'chunk',
|
|
120
|
-
})
|
|
121
|
-
|
|
122
67
|
return {
|
|
123
|
-
version:
|
|
68
|
+
version: 2,
|
|
124
69
|
algorithm: 'AES-GCM',
|
|
125
|
-
compression:
|
|
70
|
+
compression: { algorithm: 'gzip', scope: 'chunk' },
|
|
126
71
|
chunkSize: plainChunkSize,
|
|
127
72
|
chunkCount,
|
|
128
73
|
noncePrefix: toBase64Url(noncePrefix),
|
|
@@ -151,12 +96,10 @@ export function createEncryptionContext({
|
|
|
151
96
|
tokenBytes = crypto.randomBytes(32),
|
|
152
97
|
noncePrefix = crypto.randomBytes(8),
|
|
153
98
|
plainChunkSize = PLAIN_CHUNK_SIZE,
|
|
154
|
-
compressionMode = 'auto',
|
|
155
99
|
}) {
|
|
156
100
|
if (tokenBytes.length !== 32) throw new Error('Encryption token must be 32 bytes.')
|
|
157
101
|
if (noncePrefix.length !== 8) throw new Error('Nonce prefix must be 8 bytes.')
|
|
158
102
|
|
|
159
|
-
const compression = resolveCompressionMode(compressionMode, mimeType, originalName)
|
|
160
103
|
const { chunkCount, encryptedSize } = calculateEncryptedSize(originalSize, plainChunkSize)
|
|
161
104
|
const meta = buildClientEncryptionMeta({
|
|
162
105
|
plainChunkSize,
|
|
@@ -164,7 +107,6 @@ export function createEncryptionContext({
|
|
|
164
107
|
noncePrefix,
|
|
165
108
|
originalSize,
|
|
166
109
|
mimeType,
|
|
167
|
-
compression,
|
|
168
110
|
})
|
|
169
111
|
|
|
170
112
|
return {
|
|
@@ -178,7 +120,7 @@ export function createEncryptionContext({
|
|
|
178
120
|
mimeType,
|
|
179
121
|
originalSize,
|
|
180
122
|
plainChunkSize,
|
|
181
|
-
compression,
|
|
123
|
+
compression: 'gzip',
|
|
182
124
|
}
|
|
183
125
|
}
|
|
184
126
|
|
|
@@ -199,7 +141,6 @@ export async function resolveUploadFile(filePath, options = {}) {
|
|
|
199
141
|
originalSize: stat.size,
|
|
200
142
|
originalName,
|
|
201
143
|
mimeType,
|
|
202
|
-
compressionMode: options.compressionMode,
|
|
203
144
|
}),
|
|
204
145
|
}
|
|
205
146
|
}
|
|
@@ -225,10 +166,7 @@ export async function encryptChunkFromFile(fileHandle, chunkIndex, context) {
|
|
|
225
166
|
context.plainChunkSize,
|
|
226
167
|
)
|
|
227
168
|
|
|
228
|
-
let payload = plainChunk
|
|
229
|
-
if (context.compression === 'gzip') {
|
|
230
|
-
payload = await compressPlainChunk(plainChunk)
|
|
231
|
-
}
|
|
169
|
+
let payload = await compressPlainChunk(plainChunk)
|
|
232
170
|
|
|
233
171
|
return encryptChunk(payload, context.tokenBytes, context.noncePrefix, chunkIndex)
|
|
234
172
|
}
|
package/lib/upload.js
CHANGED
|
@@ -410,7 +410,6 @@ export async function uploadFile(filePath, options) {
|
|
|
410
410
|
|
|
411
411
|
const { absolutePath, context } = await resolveUploadFile(filePath, {
|
|
412
412
|
mimeType: options.mimeType,
|
|
413
|
-
compressionMode: options.compressionMode,
|
|
414
413
|
})
|
|
415
414
|
|
|
416
415
|
if (context.originalSize <= MULTIPART_THRESHOLD_BYTES) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bytifi",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Official Bytifi CLI — encrypt, upload, and decrypt files from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"upload": "node bin/bytifi.js upload",
|
|
18
18
|
"decrypt": "node bin/bytifi.js decrypt",
|
|
19
|
-
"build:bundle": "
|
|
20
|
-
"build:win": "npm run build:bundle && pkg dist/bytifi-bundle.cjs --targets node22-win-x64 --output dist/bytifi.exe --compress GZip"
|
|
19
|
+
"build:bundle": "node scripts/build-bundle.mjs",
|
|
20
|
+
"build:win": "npm run build:bundle && pkg dist/bytifi-bundle.cjs --targets node22-win-x64 --output dist/bytifi.exe --compress GZip --public --public-packages \"*\""
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|