@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.
Files changed (51) hide show
  1. package/README.md +15 -118
  2. package/dist/agent.d.ts +9 -29
  3. package/dist/agent.js +90 -213
  4. package/dist/agent.js.map +1 -1
  5. package/dist/client.d.ts +15 -18
  6. package/dist/client.js +18 -28
  7. package/dist/client.js.map +1 -1
  8. package/dist/crypto/digest.d.ts +0 -5
  9. package/dist/crypto/digest.js +0 -10
  10. package/dist/crypto/digest.js.map +1 -1
  11. package/dist/crypto/file.d.ts +0 -12
  12. package/dist/crypto/file.js +0 -48
  13. package/dist/crypto/file.js.map +1 -1
  14. package/dist/crypto/identity.d.ts +0 -1
  15. package/dist/crypto/keys.d.ts +0 -1
  16. package/dist/crypto/padding.d.ts +0 -1
  17. package/dist/crypto/secretbox.d.ts +0 -1
  18. package/dist/download.d.ts +0 -1
  19. package/dist/protocol/address.d.ts +0 -1
  20. package/dist/protocol/chunks.d.ts +0 -1
  21. package/dist/protocol/client.d.ts +0 -1
  22. package/dist/protocol/commands.d.ts +1 -10
  23. package/dist/protocol/commands.js +1 -15
  24. package/dist/protocol/commands.js.map +1 -1
  25. package/dist/protocol/description.d.ts +0 -1
  26. package/dist/protocol/encoding.d.ts +0 -1
  27. package/dist/protocol/handshake.d.ts +0 -1
  28. package/dist/protocol/transmission.d.ts +0 -1
  29. package/dist-web/assets/__vite-browser-external-9wXp6ZBx.js +1 -0
  30. package/dist-web/assets/__vite-browser-external.js +1 -0
  31. package/dist-web/assets/index.css +1 -0
  32. package/dist-web/assets/index.js +1468 -0
  33. package/dist-web/crypto.worker.js +1413 -0
  34. package/dist-web/index.html +15 -0
  35. package/package.json +10 -6
  36. package/src/agent.ts +101 -286
  37. package/src/client.ts +34 -41
  38. package/src/crypto/digest.ts +2 -15
  39. package/src/crypto/file.ts +0 -83
  40. package/src/protocol/commands.ts +2 -22
  41. package/web/crypto-backend.ts +140 -0
  42. package/web/crypto.worker.ts +316 -0
  43. package/web/download.ts +140 -0
  44. package/web/index.html +15 -0
  45. package/web/main.ts +30 -0
  46. package/web/progress.ts +52 -0
  47. package/web/servers.json +18 -0
  48. package/web/servers.ts +12 -0
  49. package/web/style.css +103 -0
  50. package/web/upload.ts +170 -0
  51. 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"