nappup 1.8.2 → 1.8.4
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/bin/nappup/helpers.js +1 -1
- package/bin/nappup/index.js +4 -2
- package/package.json +1 -1
- package/src/services/blossom-upload.js +49 -38
package/bin/nappup/helpers.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function confirmArgs (args) {
|
|
|
55
55
|
return new Promise(resolve => rl.question(query, resolve))
|
|
56
56
|
}
|
|
57
57
|
const answer = await askQuestion(
|
|
58
|
-
`Publish app from '${args.dir}' to the ${args.channel} release channel? (y/n) `
|
|
58
|
+
`Publish app from '${args.dir}' as '${args.dTag}' to the ${args.channel} release channel? (y/n) `
|
|
59
59
|
)
|
|
60
60
|
if (answer.toLowerCase() !== 'y') {
|
|
61
61
|
console.log('Operation cancelled by user.')
|
package/bin/nappup/index.js
CHANGED
|
@@ -11,10 +11,9 @@ import {
|
|
|
11
11
|
import toApp from '#index.js'
|
|
12
12
|
|
|
13
13
|
const args = parseArgs(process.argv.slice(2))
|
|
14
|
-
await confirmArgs(args)
|
|
15
14
|
|
|
16
|
-
const { dir, sk, channel, shouldReupload } = args
|
|
17
15
|
let { dTag } = args
|
|
16
|
+
const { dir, sk, channel, shouldReupload } = args
|
|
18
17
|
|
|
19
18
|
if (!dTag) {
|
|
20
19
|
let folderName = path.basename(dir)
|
|
@@ -29,6 +28,9 @@ if (!dTag) {
|
|
|
29
28
|
}
|
|
30
29
|
dTag = folderName
|
|
31
30
|
}
|
|
31
|
+
args.dTag = dTag
|
|
32
|
+
|
|
33
|
+
await confirmArgs(args)
|
|
32
34
|
|
|
33
35
|
const fileList = await toFileList(getFiles(dir), dir)
|
|
34
36
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
|
-
import { BlossomClient } from 'nostr-tools/nipb7'
|
|
3
2
|
import nostrRelays from '#services/nostr-relays.js'
|
|
4
3
|
import { bytesToBase16 } from '#helpers/base16.js'
|
|
5
4
|
|
|
5
|
+
function normalizeServerUrl (url) {
|
|
6
|
+
if (!url.startsWith('http')) url = 'https://' + url
|
|
7
|
+
return url.replace(/\/$/, '') + '/'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function createAuthHeader (signer, modify) {
|
|
11
|
+
const now = Math.floor(Date.now() / 1000)
|
|
12
|
+
const event = {
|
|
13
|
+
created_at: now,
|
|
14
|
+
kind: 24242,
|
|
15
|
+
content: 'blossom stuff',
|
|
16
|
+
tags: [['expiration', String(now + 60)]]
|
|
17
|
+
}
|
|
18
|
+
if (modify) modify(event)
|
|
19
|
+
const signedEvent = await signer.signEvent(event)
|
|
20
|
+
return 'Nostr ' + btoa(JSON.stringify(signedEvent))
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* Fetches the user's blossom server list from their kind 10063 event.
|
|
8
25
|
* Returns an array of server URLs, or empty array if none configured.
|
|
@@ -27,31 +44,14 @@ export async function getBlossomServers (signer, writeRelays) {
|
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
/**
|
|
30
|
-
* Health-checks blossom servers
|
|
31
|
-
* A server is considered healthy if
|
|
32
|
-
*
|
|
33
|
-
* Returns the subset of servers that are reachable.
|
|
47
|
+
* Health-checks blossom servers with a simple HEAD request.
|
|
48
|
+
* A server is considered healthy if fetch resolves (any HTTP status).
|
|
49
|
+
* Only network-level errors mark a server as unreachable.
|
|
34
50
|
*/
|
|
35
51
|
export async function healthCheckServers (servers, signer, { log = () => {} } = {}) {
|
|
36
|
-
const randomBytes = crypto.getRandomValues(new Uint8Array(32))
|
|
37
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', randomBytes)
|
|
38
|
-
const randomHash = bytesToBase16(new Uint8Array(hashBuffer))
|
|
39
|
-
|
|
40
52
|
const results = await Promise.allSettled(
|
|
41
53
|
servers.map(async (serverUrl) => {
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
await client.check(randomHash)
|
|
45
|
-
} catch (err) {
|
|
46
|
-
// check() throws on non-2xx. A 404 means the server is up but blob doesn't exist — that's fine.
|
|
47
|
-
// We only want to filter out servers that are truly unreachable (network errors).
|
|
48
|
-
const message = err?.message ?? ''
|
|
49
|
-
if (message.includes('returned an error')) {
|
|
50
|
-
// Server responded with an HTTP error — it's reachable
|
|
51
|
-
return serverUrl
|
|
52
|
-
}
|
|
53
|
-
throw err
|
|
54
|
-
}
|
|
54
|
+
await fetch(normalizeServerUrl(serverUrl), { method: 'HEAD' })
|
|
55
55
|
return serverUrl
|
|
56
56
|
})
|
|
57
57
|
)
|
|
@@ -85,15 +85,12 @@ export async function computeFileHash (file) {
|
|
|
85
85
|
* Uploads a single file to a single blossom server with retry+backoff.
|
|
86
86
|
* Returns { success: true, descriptor } or { success: false, error }.
|
|
87
87
|
*/
|
|
88
|
-
async function uploadFileToServer (
|
|
88
|
+
async function uploadFileToServer (serverUrl, signer, file, fileHash, mimeType, { shouldReupload, log, maxRetries = 5 }) {
|
|
89
89
|
// Check if already uploaded
|
|
90
90
|
if (!shouldReupload) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// File already exists on this server
|
|
91
|
+
const checkResponse = await fetch(serverUrl + fileHash, { method: 'HEAD' })
|
|
92
|
+
if (checkResponse.ok) {
|
|
94
93
|
return { success: true, alreadyExists: true }
|
|
95
|
-
} catch {
|
|
96
|
-
// Not found — proceed to upload
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
|
|
@@ -101,11 +98,25 @@ async function uploadFileToServer (client, file, fileHash, mimeType, { shouldReu
|
|
|
101
98
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
102
99
|
try {
|
|
103
100
|
if (attempt > 0) {
|
|
104
|
-
log(`Retrying upload to ${
|
|
101
|
+
log(`Retrying upload to ${serverUrl} (attempt ${attempt + 1}/${maxRetries + 1})`)
|
|
105
102
|
await new Promise(resolve => setTimeout(resolve, pause))
|
|
106
103
|
pause += 2000
|
|
107
104
|
}
|
|
108
|
-
const
|
|
105
|
+
const authorization = await createAuthHeader(signer, (evt) => {
|
|
106
|
+
evt.tags.push(['t', 'upload'])
|
|
107
|
+
evt.tags.push(['x', fileHash])
|
|
108
|
+
})
|
|
109
|
+
const response = await fetch(serverUrl + 'upload', {
|
|
110
|
+
method: 'PUT',
|
|
111
|
+
headers: { 'Content-Type': mimeType, Authorization: authorization },
|
|
112
|
+
body: file.stream(),
|
|
113
|
+
duplex: 'half'
|
|
114
|
+
})
|
|
115
|
+
if (response.status >= 300) {
|
|
116
|
+
const reason = response.headers.get('X-Reason') || response.statusText
|
|
117
|
+
throw new Error(`upload returned an error (${response.status}): ${reason}`)
|
|
118
|
+
}
|
|
119
|
+
const descriptor = await response.json()
|
|
109
120
|
return { success: true, descriptor }
|
|
110
121
|
} catch (err) {
|
|
111
122
|
if (attempt === maxRetries) {
|
|
@@ -150,24 +161,24 @@ export async function uploadFilesToBlossom ({
|
|
|
150
161
|
const fileServerResults = fileInfos.map(() => ({ successCount: 0, errors: [] }))
|
|
151
162
|
|
|
152
163
|
// Upload to each server in parallel, but within a server, upload files sequentially
|
|
153
|
-
const serverTasks = servers.map(async (
|
|
154
|
-
const
|
|
164
|
+
const serverTasks = servers.map(async (server) => {
|
|
165
|
+
const serverUrl = normalizeServerUrl(server)
|
|
155
166
|
|
|
156
167
|
for (let i = 0; i < fileInfos.length; i++) {
|
|
157
168
|
const info = fileInfos[i]
|
|
158
|
-
log(`Uploading ${info.filename} to ${
|
|
159
|
-
const result = await uploadFileToServer(
|
|
169
|
+
log(`Uploading ${info.filename} to ${server}`)
|
|
170
|
+
const result = await uploadFileToServer(serverUrl, signer, info.file, info.sha256, info.mimeType, { shouldReupload, log, maxRetries })
|
|
160
171
|
|
|
161
172
|
if (result.success) {
|
|
162
173
|
fileServerResults[i].successCount++
|
|
163
174
|
if (result.alreadyExists) {
|
|
164
|
-
log(`${info.filename}: Already exists on ${
|
|
175
|
+
log(`${info.filename}: Already exists on ${server}`)
|
|
165
176
|
} else {
|
|
166
|
-
log(`${info.filename}: Uploaded to ${
|
|
177
|
+
log(`${info.filename}: Uploaded to ${server}`)
|
|
167
178
|
}
|
|
168
179
|
} else {
|
|
169
|
-
fileServerResults[i].errors.push({ server
|
|
170
|
-
log(`${info.filename}: Failed to upload to ${
|
|
180
|
+
fileServerResults[i].errors.push({ server, error: result.error })
|
|
181
|
+
log(`${info.filename}: Failed to upload to ${server}: ${result.error?.message ?? result.error}`)
|
|
171
182
|
}
|
|
172
183
|
}
|
|
173
184
|
})
|