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.
@@ -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.')
@@ -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
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "MIT",
9
- "version": "1.8.2",
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,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 ${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.uploadBlob(file, mimeType)
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 (serverUrl) => {
154
- const client = new BlossomClient(serverUrl, signer)
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 ${serverUrl}`)
159
- 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 })
160
171
 
161
172
  if (result.success) {
162
173
  fileServerResults[i].successCount++
163
174
  if (result.alreadyExists) {
164
- log(`${info.filename}: Already exists on ${serverUrl}`)
175
+ log(`${info.filename}: Already exists on ${server}`)
165
176
  } else {
166
- log(`${info.filename}: Uploaded to ${serverUrl}`)
177
+ log(`${info.filename}: Uploaded to ${server}`)
167
178
  }
168
179
  } else {
169
- fileServerResults[i].errors.push({ server: serverUrl, error: result.error })
170
- 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}`)
171
182
  }
172
183
  }
173
184
  })