nappup 1.8.3 → 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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "MIT",
9
- "version": "1.8.3",
9
+ "version": "1.8.4",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
@@ -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 using the `check` method with a random sha256 hash.
31
- * A server is considered healthy if the check call completes without a network-level error.
32
- * The check is expected to fail with a 404 (blob not found), which is fine — it means the server is up.
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
- const client = new BlossomClient(serverUrl, signer)
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 (client, file, fileHash, mimeType, { shouldReupload, log, maxRetries = 5 }) {
88
+ async function uploadFileToServer (serverUrl, signer, file, fileHash, mimeType, { shouldReupload, log, maxRetries = 5 }) {
89
89
  // Check if already uploaded
90
90
  if (!shouldReupload) {
91
- try {
92
- await client.check(fileHash)
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,21 +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 ${client.mediaserver} (attempt ${attempt + 1}/${maxRetries + 1})`)
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 descriptor = await client.httpCall(
109
- 'PUT',
110
- 'upload',
111
- mimeType,
112
- () => client.authorizationHeader((evt) => {
113
- evt.tags.push(['t', 'upload'])
114
- evt.tags.push(['x', fileHash])
115
- }),
116
- file.stream(),
117
- {}
118
- )
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()
119
120
  return { success: true, descriptor }
120
121
  } catch (err) {
121
122
  if (attempt === maxRetries) {
@@ -160,24 +161,24 @@ export async function uploadFilesToBlossom ({
160
161
  const fileServerResults = fileInfos.map(() => ({ successCount: 0, errors: [] }))
161
162
 
162
163
  // Upload to each server in parallel, but within a server, upload files sequentially
163
- const serverTasks = servers.map(async (serverUrl) => {
164
- const client = new BlossomClient(serverUrl, signer)
164
+ const serverTasks = servers.map(async (server) => {
165
+ const serverUrl = normalizeServerUrl(server)
165
166
 
166
167
  for (let i = 0; i < fileInfos.length; i++) {
167
168
  const info = fileInfos[i]
168
- log(`Uploading ${info.filename} to ${serverUrl}`)
169
- const result = await uploadFileToServer(client, info.file, info.sha256, info.mimeType, { shouldReupload, log, maxRetries })
169
+ log(`Uploading ${info.filename} to ${server}`)
170
+ const result = await uploadFileToServer(serverUrl, signer, info.file, info.sha256, info.mimeType, { shouldReupload, log, maxRetries })
170
171
 
171
172
  if (result.success) {
172
173
  fileServerResults[i].successCount++
173
174
  if (result.alreadyExists) {
174
- log(`${info.filename}: Already exists on ${serverUrl}`)
175
+ log(`${info.filename}: Already exists on ${server}`)
175
176
  } else {
176
- log(`${info.filename}: Uploaded to ${serverUrl}`)
177
+ log(`${info.filename}: Uploaded to ${server}`)
177
178
  }
178
179
  } else {
179
- fileServerResults[i].errors.push({ server: serverUrl, error: result.error })
180
- log(`${info.filename}: Failed to upload to ${serverUrl}: ${result.error?.message ?? result.error}`)
180
+ fileServerResults[i].errors.push({ server, error: result.error })
181
+ log(`${info.filename}: Failed to upload to ${server}: ${result.error?.message ?? result.error}`)
181
182
  }
182
183
  }
183
184
  })