@shhhum/xftp-web 0.4.0 → 0.6.0
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 +15 -118
- package/dist/agent.d.ts +9 -29
- package/dist/agent.js +90 -213
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +15 -18
- package/dist/client.js +18 -28
- package/dist/client.js.map +1 -1
- package/dist/crypto/digest.d.ts +0 -5
- package/dist/crypto/digest.js +0 -10
- package/dist/crypto/digest.js.map +1 -1
- package/dist/crypto/file.d.ts +0 -12
- package/dist/crypto/file.js +0 -48
- package/dist/crypto/file.js.map +1 -1
- package/dist/crypto/identity.d.ts +0 -1
- package/dist/crypto/keys.d.ts +0 -1
- package/dist/crypto/padding.d.ts +0 -1
- package/dist/crypto/secretbox.d.ts +0 -1
- package/dist/download.d.ts +0 -1
- package/dist/protocol/address.d.ts +0 -1
- package/dist/protocol/chunks.d.ts +0 -1
- package/dist/protocol/client.d.ts +0 -1
- package/dist/protocol/commands.d.ts +1 -10
- package/dist/protocol/commands.js +1 -15
- package/dist/protocol/commands.js.map +1 -1
- package/dist/protocol/description.d.ts +0 -1
- package/dist/protocol/encoding.d.ts +0 -1
- package/dist/protocol/handshake.d.ts +0 -1
- package/dist/protocol/transmission.d.ts +0 -1
- package/dist-web/assets/__vite-browser-external-9wXp6ZBx.js +1 -0
- package/dist-web/assets/__vite-browser-external.js +1 -0
- package/dist-web/assets/index.css +1 -0
- package/dist-web/assets/index.js +1468 -0
- package/dist-web/crypto.worker.js +1413 -0
- package/dist-web/index.html +15 -0
- package/package.json +10 -6
- package/src/agent.ts +101 -286
- package/src/client.ts +34 -41
- package/src/crypto/digest.ts +2 -15
- package/src/crypto/file.ts +0 -83
- package/src/protocol/commands.ts +2 -22
- package/web/crypto-backend.ts +140 -0
- package/web/crypto.worker.ts +316 -0
- package/web/download.ts +140 -0
- package/web/index.html +15 -0
- package/web/main.ts +30 -0
- package/web/progress.ts +52 -0
- package/web/servers.json +18 -0
- package/web/servers.ts +12 -0
- package/web/style.css +103 -0
- package/web/upload.ts +170 -0
- package/src/index.ts +0 -4
package/web/upload.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {createCryptoBackend} from './crypto-backend.js'
|
|
2
|
+
import {getServers} from './servers.js'
|
|
3
|
+
import {createProgressRing} from './progress.js'
|
|
4
|
+
import {
|
|
5
|
+
newXFTPAgent, closeXFTPAgent, uploadFile, encodeDescriptionURI,
|
|
6
|
+
type EncryptedFileMetadata
|
|
7
|
+
} from '../src/agent.js'
|
|
8
|
+
import {XFTPPermanentError} from '../src/client.js'
|
|
9
|
+
|
|
10
|
+
const MAX_SIZE = 100 * 1024 * 1024
|
|
11
|
+
|
|
12
|
+
export function initUpload(app: HTMLElement) {
|
|
13
|
+
app.innerHTML = `
|
|
14
|
+
<div class="card">
|
|
15
|
+
<h1>SimpleX File Transfer</h1>
|
|
16
|
+
<div id="drop-zone" class="drop-zone">
|
|
17
|
+
<p>Drag & drop a file here</p>
|
|
18
|
+
<p class="hint">or</p>
|
|
19
|
+
<label class="btn" for="file-input">Choose file</label>
|
|
20
|
+
<input id="file-input" type="file" hidden>
|
|
21
|
+
<p class="hint">Max 100 MB</p>
|
|
22
|
+
</div>
|
|
23
|
+
<div id="upload-progress" class="stage" hidden>
|
|
24
|
+
<div id="progress-container"></div>
|
|
25
|
+
<p id="upload-status">Encrypting…</p>
|
|
26
|
+
<button id="cancel-btn" class="btn btn-secondary">Cancel</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div id="upload-complete" class="stage" hidden>
|
|
29
|
+
<p class="success">File uploaded</p>
|
|
30
|
+
<div class="link-row">
|
|
31
|
+
<input id="share-link" data-testid="share-link" readonly>
|
|
32
|
+
<button id="copy-btn" class="btn">Copy</button>
|
|
33
|
+
</div>
|
|
34
|
+
<p class="hint expiry">Files are typically available for 48 hours.</p>
|
|
35
|
+
<div class="security-note">
|
|
36
|
+
<p>Your file was encrypted in the browser before upload — the server never sees file contents.</p>
|
|
37
|
+
<p>The link contains the decryption key in the hash fragment, which the browser never sends to any server.</p>
|
|
38
|
+
<p>For maximum security, use the <a href="https://simplex.chat" target="_blank" rel="noopener">SimpleX app</a>.</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div id="upload-error" class="stage" hidden>
|
|
42
|
+
<p class="error" id="error-msg"></p>
|
|
43
|
+
<button id="retry-btn" class="btn">Retry</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>`
|
|
46
|
+
|
|
47
|
+
const dropZone = document.getElementById('drop-zone')!
|
|
48
|
+
const fileInput = document.getElementById('file-input') as HTMLInputElement
|
|
49
|
+
const progressStage = document.getElementById('upload-progress')!
|
|
50
|
+
const completeStage = document.getElementById('upload-complete')!
|
|
51
|
+
const errorStage = document.getElementById('upload-error')!
|
|
52
|
+
const progressContainer = document.getElementById('progress-container')!
|
|
53
|
+
const statusText = document.getElementById('upload-status')!
|
|
54
|
+
const cancelBtn = document.getElementById('cancel-btn')!
|
|
55
|
+
const shareLink = document.getElementById('share-link') as HTMLInputElement
|
|
56
|
+
const copyBtn = document.getElementById('copy-btn')!
|
|
57
|
+
const errorMsg = document.getElementById('error-msg')!
|
|
58
|
+
const retryBtn = document.getElementById('retry-btn')!
|
|
59
|
+
|
|
60
|
+
let aborted = false
|
|
61
|
+
let pendingFile: File | null = null
|
|
62
|
+
|
|
63
|
+
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over') })
|
|
64
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'))
|
|
65
|
+
dropZone.addEventListener('drop', e => {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
dropZone.classList.remove('drag-over')
|
|
68
|
+
const f = e.dataTransfer?.files[0]
|
|
69
|
+
if (f) startUpload(f)
|
|
70
|
+
})
|
|
71
|
+
fileInput.addEventListener('change', () => {
|
|
72
|
+
if (fileInput.files?.[0]) startUpload(fileInput.files[0])
|
|
73
|
+
})
|
|
74
|
+
retryBtn.addEventListener('click', () => {
|
|
75
|
+
if (pendingFile) startUpload(pendingFile)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
function showStage(stage: HTMLElement) {
|
|
79
|
+
for (const s of [dropZone, progressStage, completeStage, errorStage]) s.hidden = true
|
|
80
|
+
stage.hidden = false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function showError(msg: string) {
|
|
84
|
+
errorMsg.textContent = msg
|
|
85
|
+
showStage(errorStage)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function startUpload(file: File) {
|
|
89
|
+
pendingFile = file
|
|
90
|
+
aborted = false
|
|
91
|
+
|
|
92
|
+
if (file.size > MAX_SIZE) {
|
|
93
|
+
showError(`File too large (${formatSize(file.size)}). Maximum is 100 MB.`)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (file.size === 0) {
|
|
97
|
+
showError('File is empty.')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
showStage(progressStage)
|
|
102
|
+
const ring = createProgressRing()
|
|
103
|
+
progressContainer.innerHTML = ''
|
|
104
|
+
progressContainer.appendChild(ring.canvas)
|
|
105
|
+
statusText.textContent = 'Encrypting…'
|
|
106
|
+
|
|
107
|
+
const backend = createCryptoBackend()
|
|
108
|
+
const agent = newXFTPAgent()
|
|
109
|
+
|
|
110
|
+
cancelBtn.onclick = () => {
|
|
111
|
+
aborted = true
|
|
112
|
+
backend.cleanup().catch(() => {})
|
|
113
|
+
closeXFTPAgent(agent)
|
|
114
|
+
showStage(dropZone)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const fileData = new Uint8Array(await file.arrayBuffer())
|
|
119
|
+
if (aborted) return
|
|
120
|
+
|
|
121
|
+
const encrypted = await backend.encrypt(fileData, file.name, (done, total) => {
|
|
122
|
+
ring.update(done / total * 0.3)
|
|
123
|
+
})
|
|
124
|
+
if (aborted) return
|
|
125
|
+
|
|
126
|
+
statusText.textContent = 'Uploading…'
|
|
127
|
+
const metadata: EncryptedFileMetadata = {
|
|
128
|
+
digest: encrypted.digest,
|
|
129
|
+
key: encrypted.key,
|
|
130
|
+
nonce: encrypted.nonce,
|
|
131
|
+
chunkSizes: encrypted.chunkSizes
|
|
132
|
+
}
|
|
133
|
+
const servers = getServers()
|
|
134
|
+
const result = await uploadFile(agent, servers, metadata, {
|
|
135
|
+
readChunk: (off, sz) => backend.readChunk(off, sz),
|
|
136
|
+
onProgress: (uploaded, total) => {
|
|
137
|
+
ring.update(0.3 + (uploaded / total) * 0.7)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
if (aborted) return
|
|
141
|
+
|
|
142
|
+
const url = window.location.origin + window.location.pathname + '#' + result.uri
|
|
143
|
+
shareLink.value = url
|
|
144
|
+
showStage(completeStage)
|
|
145
|
+
copyBtn.onclick = () => {
|
|
146
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
147
|
+
copyBtn.textContent = 'Copied!'
|
|
148
|
+
setTimeout(() => { copyBtn.textContent = 'Copy' }, 2000)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
if (!aborted) {
|
|
153
|
+
const msg = err?.message ?? String(err)
|
|
154
|
+
showError(msg)
|
|
155
|
+
// Hide retry button for permanent errors (no point retrying)
|
|
156
|
+
if (err instanceof XFTPPermanentError) retryBtn.hidden = true
|
|
157
|
+
else retryBtn.hidden = false
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
await backend.cleanup().catch(() => {})
|
|
161
|
+
closeXFTPAgent(agent)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function formatSize(bytes: number): string {
|
|
167
|
+
if (bytes < 1024) return bytes + ' B'
|
|
168
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
169
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
170
|
+
}
|
package/src/index.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export * from "./agent.js"
|
|
2
|
-
export {parseXFTPServer, formatXFTPServer, type XFTPServer} from "./protocol/address.js"
|
|
3
|
-
export {decodeFileDescription, encodeFileDescription, validateFileDescription, type FileDescription, type FileChunk, type FileChunkReplica, type FileParty, type RedirectFileInfo} from "./protocol/description.js"
|
|
4
|
-
export {type FileHeader} from "./crypto/file.js"
|