nappup 1.6.0 → 1.7.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +84 -12
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.6.0",
9
+ "version": "1.7.0",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
package/src/index.js CHANGED
@@ -9,15 +9,66 @@ import { NAPP_CATEGORIES } from '#config/napp-categories.js'
9
9
  import { getBlossomServers, healthCheckServers, uploadFilesToBlossom } from '#services/blossom-upload.js'
10
10
  import { uploadBinaryDataChunks, throttledSendEvent } from '#services/irfs-upload.js'
11
11
 
12
- export default async function (...args) {
12
+ // TL;DR
13
+ // import publishApp from 'nappup'
14
+ // await publishApp(
15
+ // fileList,
16
+ // window.nostr,
17
+ // { dTagRaw: 'My app identifier unique to this nsec', onEvent: ({ progress }) => console.log(progress) }
18
+ // )
19
+ //
20
+ // Simple usage -> onEvent: ({ progress, error }) => { if (error) { throw error } else { progressBar.style.width = `${progress}%` } }
21
+ // Geek usage ->
22
+ // onEvent: (event) => {
23
+ // if (event.type === 'file-uploaded') console.log(`Uploaded ${event.filename} via ${event.service}`)
24
+ // if (event.type === 'complete') console.log(`Done! Access at https://44billion.net/${event.naddr}`)
25
+ // if (event.type === 'error') console.error('Error during publishing:', event.error)
26
+ // }
27
+ //
28
+ export default async function (fileList, nostrSigner, opts = {}) {
29
+ const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : null
30
+ let lastProgress = 0
13
31
  try {
14
- return await toApp(...args)
32
+ return await toApp(fileList, nostrSigner, onEvent
33
+ ? {
34
+ ...opts,
35
+ onEvent (event) {
36
+ lastProgress = event.progress ?? lastProgress
37
+ onEvent(event)
38
+ }
39
+ }
40
+ : opts)
41
+ } catch (err) {
42
+ if (onEvent) try { onEvent({ type: 'error', error: err, progress: lastProgress }) } catch (_) {}
43
+ throw err
15
44
  } finally {
16
45
  await nostrRelays.disconnectAll()
17
46
  }
18
47
  }
19
48
 
20
- export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTagRaw, channel = 'main', shouldReupload = false } = {}) {
49
+ /**
50
+ * Publishes an app to Nostr relays and/or blossom servers.
51
+ *
52
+ * The optional `onEvent` callback receives structured progress events.
53
+ * Every event has `type` (string) and `progress` (0–100 integer).
54
+ *
55
+ * Event types:
56
+ * 'init' — { totalFiles, totalSteps, dTag, relayCount, blossomCount }
57
+ * 'icon-uploaded' — { service: 'blossom'|'irfs'|null }
58
+ * 'file-uploaded' — { filename, service: 'blossom'|'irfs' }
59
+ * 'stall-published' — listing metadata published
60
+ * 'bundle-published' — app bundle published
61
+ * 'complete' — { napp } (terminal, progress === 100)
62
+ * 'error' — { error } (terminal, error is rethrown)
63
+ *
64
+ * Terminal events ('complete' or 'error') signal that no more events will follow.
65
+ * The 'error' event is only emitted when using the default export wrapper.
66
+ * Direct `toApp` callers receive the thrown error via normal async/await.
67
+ */
68
+ export async function toApp (fileList, nostrSigner, { log = () => {}, onEvent = () => {}, dTag, dTagRaw, channel = 'main', shouldReupload = false } = {}) {
69
+ let _steps = 0
70
+ let _totalSteps = 1
71
+ const emit = (event) => { try { onEvent({ ...event, progress: event.type === 'complete' ? 100 : Math.round((_steps / _totalSteps) * 100) }) } catch (_) {} }
21
72
  if (!nostrSigner && typeof window !== 'undefined') nostrSigner = window.nostr
22
73
  if (!nostrSigner) throw new Error('No Nostr signer found')
23
74
  if (typeof window !== 'undefined' && nostrSigner === window.nostr) {
@@ -82,6 +133,10 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
82
133
 
83
134
  const useBlossom = healthyBlossomServers.length > 0
84
135
 
136
+ const hasIconUpload = Boolean(nappJson.stallIcon?.[0]?.[0])
137
+ _totalSteps = fileList.length + (hasIconUpload ? 1 : 0) + 2
138
+ emit({ type: 'init', totalFiles: fileList.length, totalSteps: _totalSteps, dTag, relayCount: writeRelays.length, blossomCount: healthyBlossomServers.length })
139
+
85
140
  // Upload icon from napp.json if present
86
141
  if (nappJson.stallIcon?.[0]?.[0]) {
87
142
  try {
@@ -106,7 +161,7 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
106
161
  iconMetadata = {
107
162
  rootHash: uploadedFiles[0].sha256,
108
163
  mimeType,
109
- service: 'blossom'
164
+ service: 'b' // blossom
110
165
  }
111
166
  } else if (failedFiles.length > 0) {
112
167
  log('Blossom icon upload failed, falling back to relay upload')
@@ -127,7 +182,7 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
127
182
  iconMetadata = {
128
183
  rootHash: nmmr.getRoot(),
129
184
  mimeType,
130
- service: 'irfs'
185
+ service: 'i' // relay (irfs)
131
186
  }
132
187
  }
133
188
  }
@@ -136,6 +191,11 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
136
191
  }
137
192
  }
138
193
 
194
+ if (hasIconUpload) {
195
+ _steps++
196
+ emit({ type: 'icon-uploaded', service: iconMetadata?.service === 'b' ? 'blossom' : iconMetadata ? 'irfs' : null })
197
+ }
198
+
139
199
  log(`Processing ${fileList.length} files`)
140
200
 
141
201
  // Files to upload via relay (irfs) — either all files or blossom failures
@@ -155,16 +215,19 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
155
215
  rootHash: uploaded.sha256,
156
216
  filename: uploaded.filename,
157
217
  mimeType: uploaded.mimeType,
158
- service: 'blossom'
218
+ service: 'b'
159
219
  })
160
220
 
161
221
  if (faviconFile && uploaded.file === faviconFile) {
162
222
  iconMetadata = {
163
223
  rootHash: uploaded.sha256,
164
224
  mimeType: uploaded.mimeType,
165
- service: 'blossom'
225
+ service: 'b'
166
226
  }
167
227
  }
228
+
229
+ _steps++
230
+ emit({ type: 'file-uploaded', filename: uploaded.filename, service: 'blossom' })
168
231
  }
169
232
 
170
233
  if (failedFiles.length > 0) {
@@ -193,16 +256,19 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
193
256
  rootHash: nmmr.getRoot(),
194
257
  filename,
195
258
  mimeType: file.type || 'application/octet-stream',
196
- service: 'irfs'
259
+ service: 'i'
197
260
  })
198
261
 
199
262
  if (faviconFile && file === faviconFile) {
200
263
  iconMetadata = {
201
264
  rootHash: nmmr.getRoot(),
202
265
  mimeType: file.type || 'application/octet-stream',
203
- service: 'irfs'
266
+ service: 'i'
204
267
  }
205
268
  }
269
+
270
+ _steps++
271
+ emit({ type: 'file-uploaded', filename, service: 'irfs' })
206
272
  }
207
273
  }
208
274
 
@@ -228,6 +294,8 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
228
294
  categories: nappJson.category,
229
295
  hashtags: nappJson.hashtag
230
296
  })))
297
+ _steps++
298
+ emit({ type: 'stall-published' })
231
299
 
232
300
  log(`Uploading bundle ${dTag}`)
233
301
  const bundle = await uploadBundle({ dTag, channel, fileMetadata, signer: nostrSigner, pause, shouldReupload, log })
@@ -238,7 +306,11 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
238
306
  relays: [],
239
307
  kind: bundle.kind
240
308
  })
309
+ _steps++
310
+ emit({ type: 'bundle-published' })
311
+
241
312
  log(`Visit at https://44billion.net/${appEntity}`)
313
+ emit({ type: 'complete', napp: appEntity })
242
314
  }
243
315
 
244
316
  async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, shouldReupload = false, log = () => {} }) {
@@ -248,7 +320,7 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
248
320
  draft: 37450 // vibe coded preview
249
321
  }[channel] ?? 37448
250
322
 
251
- const fileTags = fileMetadata.map(v => ['file', v.rootHash, v.filename, v.mimeType, v.service || 'irfs'])
323
+ const fileTags = fileMetadata.map(v => ['file', v.rootHash, v.filename, v.mimeType, v.service || 'i'])
252
324
  const tags = [
253
325
  ['d', dTag],
254
326
  ...fileTags
@@ -285,7 +357,7 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
285
357
 
286
358
  const isSame = currentFileTags.length === recentFileTags.length && currentFileTags.every((t, i) => {
287
359
  const rt = recentFileTags[i]
288
- return rt.length >= 4 && rt[1] === t[1] && rt[2] === t[2] && rt[3] === t[3] && (rt[4] || 'irfs') === (t[4] || 'irfs')
360
+ return rt.length >= 4 && rt[1] === t[1] && rt[2] === t[2] && rt[3] === t[3] && (rt[4] || 'i') === (t[4] || 'i')
289
361
  })
290
362
 
291
363
  if (isSame) {
@@ -346,7 +418,7 @@ async function maybeUploadStall ({
346
418
  const trimmedSummary = typeof summary === 'string' ? summary.trim() : ''
347
419
  const iconRootHash = icon?.rootHash
348
420
  const iconMimeType = icon?.mimeType
349
- const iconService = icon?.service || 'irfs'
421
+ const iconService = icon?.service || 'i'
350
422
  const hasMetadata = Boolean(trimmedName) || Boolean(trimmedSummary) || Boolean(iconRootHash) ||
351
423
  Boolean(self) || (countries && countries.length > 0) || (categories && categories.length > 0) || (hashtags && hashtags.length > 0)
352
424