bytifi 0.2.6 → 0.2.8
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/LICENSE +21 -0
- package/README.md +19 -5
- package/bin/bytifi.js +168 -66
- package/lib/api.js +203 -0
- package/lib/base64url.js +10 -2
- package/lib/crypto.js +0 -1
- package/lib/decrypt.js +184 -136
- package/lib/progress.js +50 -0
- package/lib/upload.js +99 -262
- package/lib/url.js +26 -0
- package/package.json +5 -2
package/lib/upload.js
CHANGED
|
@@ -1,228 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import {
|
|
3
|
+
apiFetchWithRetry,
|
|
4
|
+
sleep,
|
|
5
|
+
} from './api.js'
|
|
1
6
|
import {
|
|
2
7
|
MULTIPART_THRESHOLD_BYTES,
|
|
3
8
|
encryptChunkFromFile,
|
|
4
9
|
resolveUploadFile,
|
|
5
10
|
} from './crypto.js'
|
|
6
|
-
import { formatDuration } from './progress.js'
|
|
7
|
-
import fs from 'node:fs/promises'
|
|
8
11
|
|
|
9
|
-
const DEFAULT_BASE_URL = 'https://bytifi.com'
|
|
10
12
|
const DEFAULT_CONCURRENCY = 4
|
|
11
13
|
const POLL_INTERVAL_MS = 1500
|
|
12
14
|
const POLL_TIMEOUT_MS = 30 * 60 * 1000
|
|
13
|
-
const MAX_RETRIES = 3
|
|
14
15
|
const UNLIMITED_RATE_LIMIT_RETRIES = Number.POSITIVE_INFINITY
|
|
15
16
|
|
|
16
|
-
export
|
|
17
|
-
constructor(message, { status = 0, body = null, retryAfterMs = null } = {}) {
|
|
18
|
-
super(message)
|
|
19
|
-
this.name = 'BytifiApiError'
|
|
20
|
-
this.status = status
|
|
21
|
-
this.body = body
|
|
22
|
-
this.retryAfterMs = retryAfterMs
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class BytifiNetworkError extends Error {
|
|
27
|
-
constructor(message, { cause } = {}) {
|
|
28
|
-
super(message)
|
|
29
|
-
this.name = 'BytifiNetworkError'
|
|
30
|
-
this.cause = cause
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function sleep(ms) {
|
|
35
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function normalizeBaseUrl(baseUrl) {
|
|
39
|
-
return String(baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isRetryableError(error) {
|
|
43
|
-
if (error instanceof BytifiNetworkError) return true
|
|
44
|
-
if (error instanceof BytifiApiError) {
|
|
45
|
-
return error.status === 429 || error.status >= 500
|
|
46
|
-
}
|
|
47
|
-
return false
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function parseRetryAfterMs(response) {
|
|
51
|
-
const retryAfter = response.headers.get('Retry-After')
|
|
52
|
-
if (retryAfter) {
|
|
53
|
-
const seconds = Number(retryAfter)
|
|
54
|
-
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
55
|
-
return seconds * 1000
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const dateMs = Date.parse(retryAfter)
|
|
59
|
-
if (Number.isFinite(dateMs)) {
|
|
60
|
-
return Math.max(0, dateMs - Date.now())
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const resetHeader = response.headers.get('RateLimit-Reset')
|
|
65
|
-
if (resetHeader) {
|
|
66
|
-
const resetValue = Number(resetHeader)
|
|
67
|
-
if (Number.isFinite(resetValue)) {
|
|
68
|
-
if (resetValue > 1_000_000_000) {
|
|
69
|
-
return Math.max(0, resetValue * 1000 - Date.now())
|
|
70
|
-
}
|
|
71
|
-
return Math.max(0, resetValue * 1000)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function readResponseBody(response) {
|
|
79
|
-
const text = await response.text()
|
|
80
|
-
if (!text) return null
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
return JSON.parse(text)
|
|
84
|
-
} catch {
|
|
85
|
-
return text
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function apiFetch(baseUrl, path, { apiKey, method = 'GET', headers = {}, body = null, signal, binary = false } = {}) {
|
|
90
|
-
const url = `${normalizeBaseUrl(baseUrl)}${path}`
|
|
91
|
-
const requestHeaders = {
|
|
92
|
-
...headers,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (apiKey) {
|
|
96
|
-
requestHeaders.Authorization = `Bearer ${apiKey}`
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let response
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
response = await fetch(url, {
|
|
103
|
-
method,
|
|
104
|
-
headers: requestHeaders,
|
|
105
|
-
body,
|
|
106
|
-
signal,
|
|
107
|
-
})
|
|
108
|
-
} catch (error) {
|
|
109
|
-
if (signal?.aborted) {
|
|
110
|
-
throw new Error('Upload aborted.')
|
|
111
|
-
}
|
|
112
|
-
throw new BytifiNetworkError(error.message || 'Network request failed.', { cause: error })
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const responseBuffer = Buffer.from(await response.arrayBuffer())
|
|
116
|
-
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
const text = responseBuffer.toString('utf8')
|
|
119
|
-
let payload = text
|
|
120
|
-
try {
|
|
121
|
-
payload = JSON.parse(text)
|
|
122
|
-
} catch {
|
|
123
|
-
// keep text
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const message = typeof payload === 'object' && payload?.error
|
|
127
|
-
? payload.error
|
|
128
|
-
: typeof payload === 'string' && payload
|
|
129
|
-
? payload
|
|
130
|
-
: `Request failed with status ${response.status}.`
|
|
131
|
-
throw new BytifiApiError(message, {
|
|
132
|
-
status: response.status,
|
|
133
|
-
body: payload,
|
|
134
|
-
retryAfterMs: response.status === 429 ? parseRetryAfterMs(response) : null,
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (binary) {
|
|
139
|
-
return responseBuffer
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const text = responseBuffer.toString('utf8')
|
|
143
|
-
if (!text) return null
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
return JSON.parse(text)
|
|
147
|
-
} catch {
|
|
148
|
-
return text
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function apiFetchWithRetry(baseUrl, path, options = {}) {
|
|
153
|
-
const {
|
|
154
|
-
retries = MAX_RETRIES,
|
|
155
|
-
rateLimitRetries = 0,
|
|
156
|
-
maxRateLimitWaitMs = null,
|
|
157
|
-
onStatus,
|
|
158
|
-
signal,
|
|
159
|
-
...fetchOptions
|
|
160
|
-
} = options
|
|
161
|
-
|
|
162
|
-
let attempt = 0
|
|
163
|
-
let rateLimitAttempt = 0
|
|
164
|
-
const maxRateLimitRetries = rateLimitRetries === UNLIMITED_RATE_LIMIT_RETRIES
|
|
165
|
-
? UNLIMITED_RATE_LIMIT_RETRIES
|
|
166
|
-
: (rateLimitRetries || retries)
|
|
167
|
-
|
|
168
|
-
while (true) {
|
|
169
|
-
try {
|
|
170
|
-
return await apiFetch(baseUrl, path, { ...fetchOptions, signal })
|
|
171
|
-
} catch (error) {
|
|
172
|
-
if (signal?.aborted || error.message === 'Upload aborted.' || error.message === 'Decrypt aborted.') {
|
|
173
|
-
throw error
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (error instanceof BytifiApiError && error.status === 429) {
|
|
177
|
-
if (rateLimitAttempt >= maxRateLimitRetries) {
|
|
178
|
-
throw error
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
rateLimitAttempt += 1
|
|
182
|
-
const serverWaitMs = error.retryAfterMs ?? Math.min(600_000, 15_000 * rateLimitAttempt)
|
|
183
|
-
const waitMs = maxRateLimitWaitMs == null
|
|
184
|
-
? serverWaitMs
|
|
185
|
-
: Math.min(serverWaitMs, maxRateLimitWaitMs)
|
|
186
|
-
|
|
187
|
-
onStatus?.({
|
|
188
|
-
stage: 'waiting',
|
|
189
|
-
message: maxRateLimitWaitMs != null && serverWaitMs > waitMs
|
|
190
|
-
? `Rate limited — retrying in ${formatDuration(waitMs)} (part upload continues when server allows)…`
|
|
191
|
-
: `Rate limited — waiting ${formatDuration(waitMs)} before retry (${rateLimitAttempt})…`,
|
|
192
|
-
waitMs,
|
|
193
|
-
retryAttempt: rateLimitAttempt,
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
await sleep(waitMs)
|
|
197
|
-
continue
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (!isRetryableError(error) || attempt >= retries) {
|
|
201
|
-
throw error
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
attempt += 1
|
|
205
|
-
await sleep(Math.min(1000 * (2 ** attempt), 8000))
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export async function publicFetchWithRetry(baseUrl, path, options = {}) {
|
|
211
|
-
return apiFetchWithRetry(baseUrl, path, {
|
|
212
|
-
...options,
|
|
213
|
-
apiKey: null,
|
|
214
|
-
rateLimitRetries: options.rateLimitRetries ?? UNLIMITED_RATE_LIMIT_RETRIES,
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export async function fetchPublicBinaryWithRetry(baseUrl, path, options = {}) {
|
|
219
|
-
return apiFetchWithRetry(baseUrl, path, {
|
|
220
|
-
...options,
|
|
221
|
-
apiKey: null,
|
|
222
|
-
binary: true,
|
|
223
|
-
rateLimitRetries: options.rateLimitRetries ?? UNLIMITED_RATE_LIMIT_RETRIES,
|
|
224
|
-
})
|
|
225
|
-
}
|
|
17
|
+
export { BytifiApiError, BytifiNetworkError, fetchPublicBinaryWithRetry, publicFetchWithRetry } from './api.js'
|
|
226
18
|
|
|
227
19
|
function buildShareUrl(payload, encryptionToken) {
|
|
228
20
|
return `${payload.url}#token=${encodeURIComponent(encryptionToken)}`
|
|
@@ -245,6 +37,11 @@ function buildResult(payload, context, shareUrl) {
|
|
|
245
37
|
}
|
|
246
38
|
}
|
|
247
39
|
|
|
40
|
+
function uploadProgressPercent(completedParts, inFlightParts, totalParts) {
|
|
41
|
+
const weighted = completedParts + inFlightParts * 0.5
|
|
42
|
+
return Math.min(100, Math.round((weighted / totalParts) * 100))
|
|
43
|
+
}
|
|
44
|
+
|
|
248
45
|
async function collectEncryptedBuffer(filePath, context, { onProgress, signal } = {}) {
|
|
249
46
|
const fileHandle = await fs.open(filePath, 'r')
|
|
250
47
|
const encryptedParts = []
|
|
@@ -263,6 +60,11 @@ async function collectEncryptedBuffer(filePath, context, { onProgress, signal }
|
|
|
263
60
|
})
|
|
264
61
|
|
|
265
62
|
encryptedParts.push(await encryptChunkFromFile(fileHandle, chunkIndex, context))
|
|
63
|
+
|
|
64
|
+
if (signal?.aborted) {
|
|
65
|
+
throw new Error('Upload aborted.')
|
|
66
|
+
}
|
|
67
|
+
|
|
266
68
|
onProgress?.({
|
|
267
69
|
stage: 'encrypted',
|
|
268
70
|
part: chunkIndex + 1,
|
|
@@ -289,10 +91,14 @@ async function uploadDirect(context, encryptedBuffer, {
|
|
|
289
91
|
}) {
|
|
290
92
|
const formData = new FormData()
|
|
291
93
|
const blob = new Blob([encryptedBuffer], { type: 'application/octet-stream' })
|
|
94
|
+
const clientEncryptionMeta = {
|
|
95
|
+
...context.meta,
|
|
96
|
+
encryptedSize: encryptedBuffer.length,
|
|
97
|
+
}
|
|
292
98
|
|
|
293
99
|
formData.append('file', blob, context.originalName)
|
|
294
100
|
formData.append('clientEncrypted', 'true')
|
|
295
|
-
formData.append('clientEncryptionMeta', JSON.stringify(
|
|
101
|
+
formData.append('clientEncryptionMeta', JSON.stringify(clientEncryptionMeta))
|
|
296
102
|
formData.append('deleteOnDownload', deleteOnDownload ? 'true' : 'false')
|
|
297
103
|
formData.append('expiresInMinutes', String(expiresInMinutes))
|
|
298
104
|
|
|
@@ -359,6 +165,17 @@ function resolveUploadConcurrency(originalSize, requested) {
|
|
|
359
165
|
return DEFAULT_CONCURRENCY
|
|
360
166
|
}
|
|
361
167
|
|
|
168
|
+
async function abortUploadSession(baseUrl, sessionToken, { apiKey, signal }) {
|
|
169
|
+
await apiFetchWithRetry(baseUrl, '/api/public/upload/abort', {
|
|
170
|
+
apiKey,
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({ sessionToken }),
|
|
174
|
+
signal,
|
|
175
|
+
retries: 1,
|
|
176
|
+
}).catch(() => {})
|
|
177
|
+
}
|
|
178
|
+
|
|
362
179
|
async function uploadMultipartStreaming(filePath, context, {
|
|
363
180
|
apiKey,
|
|
364
181
|
baseUrl,
|
|
@@ -403,8 +220,20 @@ async function uploadMultipartStreaming(filePath, context, {
|
|
|
403
220
|
context.chunkCount,
|
|
404
221
|
)
|
|
405
222
|
const fileHandle = await fs.open(filePath, 'r')
|
|
223
|
+
const workerAbort = new AbortController()
|
|
406
224
|
let nextChunkIndex = 0
|
|
407
225
|
let completedParts = 0
|
|
226
|
+
let inFlightParts = 0
|
|
227
|
+
let sessionAborted = false
|
|
228
|
+
|
|
229
|
+
const combinedSignal = AbortSignal.any?.([signal, workerAbort.signal])
|
|
230
|
+
?? (() => {
|
|
231
|
+
const controller = new AbortController()
|
|
232
|
+
const abort = () => controller.abort()
|
|
233
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
234
|
+
workerAbort.signal.addEventListener('abort', abort, { once: true })
|
|
235
|
+
return controller.signal
|
|
236
|
+
})()
|
|
408
237
|
|
|
409
238
|
onProgress?.({
|
|
410
239
|
stage: 'uploading',
|
|
@@ -412,6 +241,16 @@ async function uploadMultipartStreaming(filePath, context, {
|
|
|
412
241
|
detail: `${workerCount} workers · session ${sessionToken.slice(0, 8)}…`,
|
|
413
242
|
})
|
|
414
243
|
|
|
244
|
+
function reportPartProgress(stage, partNumber, partBytes) {
|
|
245
|
+
onProgress?.({
|
|
246
|
+
stage,
|
|
247
|
+
part: partNumber,
|
|
248
|
+
totalParts: context.chunkCount,
|
|
249
|
+
partBytes,
|
|
250
|
+
percent: uploadProgressPercent(completedParts, inFlightParts, context.chunkCount),
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
415
254
|
function claimChunkIndex() {
|
|
416
255
|
const chunkIndex = nextChunkIndex
|
|
417
256
|
nextChunkIndex += 1
|
|
@@ -420,7 +259,7 @@ async function uploadMultipartStreaming(filePath, context, {
|
|
|
420
259
|
|
|
421
260
|
async function pipelineWorker() {
|
|
422
261
|
while (true) {
|
|
423
|
-
if (
|
|
262
|
+
if (combinedSignal.aborted) {
|
|
424
263
|
throw new Error('Upload aborted.')
|
|
425
264
|
}
|
|
426
265
|
|
|
@@ -430,62 +269,60 @@ async function uploadMultipartStreaming(filePath, context, {
|
|
|
430
269
|
}
|
|
431
270
|
|
|
432
271
|
const partNumber = chunkIndex + 1
|
|
272
|
+
inFlightParts += 1
|
|
433
273
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
part: partNumber,
|
|
437
|
-
totalParts: context.chunkCount,
|
|
438
|
-
percent: Math.round((completedParts / context.chunkCount) * 100),
|
|
439
|
-
})
|
|
274
|
+
try {
|
|
275
|
+
reportPartProgress('encrypting', partNumber)
|
|
440
276
|
|
|
441
|
-
|
|
277
|
+
const encryptedPart = await encryptChunkFromFile(fileHandle, chunkIndex, context)
|
|
442
278
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
totalParts: context.chunkCount,
|
|
447
|
-
partBytes: encryptedPart.length,
|
|
448
|
-
percent: Math.round((completedParts / context.chunkCount) * 100),
|
|
449
|
-
})
|
|
279
|
+
if (combinedSignal.aborted) {
|
|
280
|
+
throw new Error('Upload aborted.')
|
|
281
|
+
}
|
|
450
282
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
283
|
+
reportPartProgress('uploading', partNumber, encryptedPart.length)
|
|
284
|
+
|
|
285
|
+
await apiFetchWithRetry(
|
|
286
|
+
baseUrl,
|
|
287
|
+
`/api/public/upload/part?sessionToken=${encodeURIComponent(sessionToken)}&partNumber=${partNumber}`,
|
|
288
|
+
{
|
|
289
|
+
apiKey,
|
|
290
|
+
method: 'PUT',
|
|
291
|
+
headers: {
|
|
292
|
+
'Content-Type': 'application/octet-stream',
|
|
293
|
+
'X-Upload-Session': sessionToken,
|
|
294
|
+
},
|
|
295
|
+
body: encryptedPart,
|
|
296
|
+
signal: combinedSignal,
|
|
297
|
+
onStatus,
|
|
298
|
+
rateLimitRetries: UNLIMITED_RATE_LIMIT_RETRIES,
|
|
299
|
+
maxRateLimitWaitMs: 15_000,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
completedParts += 1
|
|
304
|
+
onProgress?.({
|
|
305
|
+
stage: 'uploaded',
|
|
306
|
+
part: partNumber,
|
|
307
|
+
totalParts: context.chunkCount,
|
|
308
|
+
partBytes: encryptedPart.length,
|
|
309
|
+
percent: uploadProgressPercent(completedParts, inFlightParts, context.chunkCount),
|
|
310
|
+
detail: `${completedParts}/${context.chunkCount} parts done`,
|
|
311
|
+
})
|
|
312
|
+
} finally {
|
|
313
|
+
inFlightParts -= 1
|
|
314
|
+
}
|
|
475
315
|
}
|
|
476
316
|
}
|
|
477
317
|
|
|
478
318
|
try {
|
|
479
319
|
await Promise.all(Array.from({ length: workerCount }, () => pipelineWorker()))
|
|
480
320
|
} catch (error) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
body: JSON.stringify({ sessionToken }),
|
|
487
|
-
signal,
|
|
488
|
-
}).catch(() => {})
|
|
321
|
+
workerAbort.abort()
|
|
322
|
+
|
|
323
|
+
if (!sessionAborted) {
|
|
324
|
+
sessionAborted = true
|
|
325
|
+
await abortUploadSession(baseUrl, sessionToken, { apiKey, signal })
|
|
489
326
|
}
|
|
490
327
|
|
|
491
328
|
throw error
|
package/lib/url.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const DEFAULT_BASE_URL = 'https://bytifi.com'
|
|
2
|
+
|
|
3
|
+
export function normalizeBaseUrl(baseUrl) {
|
|
4
|
+
return String(baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function validateBaseUrl(url) {
|
|
8
|
+
const normalized = normalizeBaseUrl(url)
|
|
9
|
+
|
|
10
|
+
let parsed
|
|
11
|
+
try {
|
|
12
|
+
parsed = new URL(normalized)
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error(`Invalid base URL: ${url}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
18
|
+
throw new Error(`Invalid base URL protocol: ${parsed.protocol} (use http: or https:)`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!parsed.hostname) {
|
|
22
|
+
throw new Error('Invalid base URL: missing hostname.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return normalized
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bytifi",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Official Bytifi CLI — encrypt, upload, and decrypt files from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"author": "Bytifi",
|
|
6
7
|
"bin": {
|
|
7
8
|
"bytifi": "./bin/bytifi.js"
|
|
8
9
|
},
|
|
@@ -14,10 +15,12 @@
|
|
|
14
15
|
"node": ">=18"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.js",
|
|
17
19
|
"upload": "node bin/bytifi.js upload",
|
|
18
20
|
"decrypt": "node bin/bytifi.js decrypt",
|
|
19
21
|
"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 \"*\""
|
|
22
|
+
"build:win": "npm run build:bundle && pkg dist/bytifi-bundle.cjs --targets node22-win-x64 --output dist/bytifi.exe --compress GZip --public --public-packages \"*\"",
|
|
23
|
+
"prepublishOnly": "npm test"
|
|
21
24
|
},
|
|
22
25
|
"repository": {
|
|
23
26
|
"type": "git",
|