nappup 2.3.4 → 2.3.6
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/README.md +18 -1
- package/package.json +1 -1
- package/src/errors.js +78 -0
- package/src/helpers/app.js +1 -1
- package/src/index.js +170 -80
- package/src/services/blossom-upload.js +40 -7
package/README.md
CHANGED
|
@@ -137,4 +137,21 @@ await publishApp(fileList, signer, {
|
|
|
137
137
|
|
|
138
138
|
- **`fileList`** — a `FileList` or array of `File` objects (each needs `webkitRelativePath`).
|
|
139
139
|
- **`signer`** — a [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md)-compatible signer. In the browser, `window.nostr` is used automatically if omitted.
|
|
140
|
-
- **`onEvent`** — optional callback that receives progress events with a `type` (`'init'`, `'file-uploaded'`, `'complete'`, `'error'`, …) and `progress` (0–100).
|
|
140
|
+
- **`onEvent`** — optional callback that receives progress events with a `type` (`'services-checking'`, `'init'`, `'file-uploaded'`, `'complete'`, `'error'`, …) and `progress` (0–100).
|
|
141
|
+
|
|
142
|
+
Rejected uploads use `NappupError`, with a stable code from
|
|
143
|
+
`NAPPUP_ERROR_CODES`. The original error is retained as `cause`, and some
|
|
144
|
+
errors include structured `details`, so applications can show their own
|
|
145
|
+
recovery instructions without matching CLI-oriented message text:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import publishApp, { NAPPUP_ERROR_CODES } from 'nappup'
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await publishApp(fileList, signer)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error.code === NAPPUP_ERROR_CODES.GENERIC_FOLDER_NAME) {
|
|
154
|
+
// Ask the user to choose a unique app folder name.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
package/package.json
CHANGED
package/src/errors.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const ERROR_CODE = /^[A-Z][A-Z0-9_]*$/
|
|
2
|
+
|
|
3
|
+
export const NAPPUP_ERROR_CODES = Object.freeze({
|
|
4
|
+
UPLOAD_CANCELLED: 'NAPPUP_UPLOAD_CANCELLED',
|
|
5
|
+
NO_SIGNER: 'NAPPUP_NO_SIGNER',
|
|
6
|
+
EMPTY_FILE_LIST: 'NAPPUP_EMPTY_FILE_LIST',
|
|
7
|
+
RELAY_LOOKUP_FAILED: 'NAPPUP_RELAY_LOOKUP_FAILED',
|
|
8
|
+
NO_OUTBOX_RELAYS: 'NAPPUP_NO_OUTBOX_RELAYS',
|
|
9
|
+
INVALID_D_TAG: 'NAPPUP_INVALID_D_TAG',
|
|
10
|
+
GENERIC_FOLDER_NAME: 'NAPPUP_GENERIC_FOLDER_NAME',
|
|
11
|
+
INVALID_FOLDER_NAME: 'NAPPUP_INVALID_FOLDER_NAME',
|
|
12
|
+
BLOSSOM_UPLOAD_FAILED: 'NAPPUP_BLOSSOM_UPLOAD_FAILED',
|
|
13
|
+
IRFS_UPLOAD_FAILED: 'NAPPUP_IRFS_UPLOAD_FAILED',
|
|
14
|
+
MANIFEST_UPLOAD_FAILED: 'NAPPUP_MANIFEST_UPLOAD_FAILED',
|
|
15
|
+
UPLOAD_FAILED: 'NAPPUP_UPLOAD_FAILED'
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Carries a stable machine-readable code while retaining technical context.
|
|
19
|
+
export class NappupError extends Error {
|
|
20
|
+
constructor (code, messageOrOptions = code, causeOrOptions) {
|
|
21
|
+
if (typeof code !== 'string' || !ERROR_CODE.test(code)) {
|
|
22
|
+
throw new TypeError('Nappup error code should be uppercase snake case')
|
|
23
|
+
}
|
|
24
|
+
const objectOptions = messageOrOptions && typeof messageOrOptions === 'object'
|
|
25
|
+
? messageOrOptions
|
|
26
|
+
: null
|
|
27
|
+
const trailingOptions = !objectOptions && causeOrOptions && typeof causeOrOptions === 'object' &&
|
|
28
|
+
(Object.hasOwn(causeOrOptions, 'cause') || Object.hasOwn(causeOrOptions, 'details'))
|
|
29
|
+
? causeOrOptions
|
|
30
|
+
: null
|
|
31
|
+
const options = objectOptions || trailingOptions
|
|
32
|
+
const message = objectOptions
|
|
33
|
+
? (objectOptions.message ?? code)
|
|
34
|
+
: (messageOrOptions ?? code)
|
|
35
|
+
const cause = objectOptions
|
|
36
|
+
? objectOptions.cause
|
|
37
|
+
: trailingOptions
|
|
38
|
+
? trailingOptions.cause
|
|
39
|
+
: causeOrOptions
|
|
40
|
+
super(message, cause === undefined ? undefined : { cause })
|
|
41
|
+
Object.defineProperty(this, 'name', {
|
|
42
|
+
configurable: true,
|
|
43
|
+
value: 'NappupError',
|
|
44
|
+
writable: true
|
|
45
|
+
})
|
|
46
|
+
Object.defineProperty(this, 'code', {
|
|
47
|
+
configurable: false,
|
|
48
|
+
enumerable: true,
|
|
49
|
+
value: code,
|
|
50
|
+
writable: false
|
|
51
|
+
})
|
|
52
|
+
if (options?.details !== undefined) {
|
|
53
|
+
Object.defineProperty(this, 'details', {
|
|
54
|
+
configurable: false,
|
|
55
|
+
enumerable: true,
|
|
56
|
+
value: options.details,
|
|
57
|
+
writable: false
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Ensures every error crossing nappup's public API has a documented code.
|
|
64
|
+
export function normalizeNappupError (error) {
|
|
65
|
+
if (typeof error?.code === 'string' && error.code.startsWith('NAPPUP_')) return error
|
|
66
|
+
if (error?.name === 'AbortError' || error?.code === 'ABORT_ERR') {
|
|
67
|
+
return new NappupError(
|
|
68
|
+
NAPPUP_ERROR_CODES.UPLOAD_CANCELLED,
|
|
69
|
+
error?.message || 'Upload cancelled',
|
|
70
|
+
{ cause: error }
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return new NappupError(
|
|
74
|
+
NAPPUP_ERROR_CODES.UPLOAD_FAILED,
|
|
75
|
+
error?.message || 'Upload failed',
|
|
76
|
+
{ cause: error }
|
|
77
|
+
)
|
|
78
|
+
}
|
package/src/helpers/app.js
CHANGED
|
@@ -5,5 +5,5 @@ export const GENERIC_BUILD_FOLDER_NAMES = new Set([
|
|
|
5
5
|
])
|
|
6
6
|
|
|
7
7
|
export function isNostrAppDTagSafe (string) {
|
|
8
|
-
return typeof string === 'string' && string.length <= NOSTR_APP_D_TAG_MAX_LENGTH
|
|
8
|
+
return typeof string === 'string' && string.length > 0 && string.length <= NOSTR_APP_D_TAG_MAX_LENGTH
|
|
9
9
|
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,9 @@ import { extractHtmlMetadata, findAppIcon, findIndexFile } from '#helpers/app-me
|
|
|
8
8
|
import { getBlossomServers, healthCheckServers, uploadFilesToBlossom } from '#services/blossom-upload.js'
|
|
9
9
|
import { uploadBinaryDataChunks } from '#services/irfs-upload.js'
|
|
10
10
|
import { uploadSiteManifest } from '#services/site-manifest.js'
|
|
11
|
+
import { NappupError, NAPPUP_ERROR_CODES, normalizeNappupError } from '#errors.js'
|
|
12
|
+
|
|
13
|
+
export { NappupError, NAPPUP_ERROR_CODES } from '#errors.js'
|
|
11
14
|
|
|
12
15
|
// TL;DR
|
|
13
16
|
// import publishApp from 'nappup'
|
|
@@ -26,21 +29,8 @@ import { uploadSiteManifest } from '#services/site-manifest.js'
|
|
|
26
29
|
// }
|
|
27
30
|
//
|
|
28
31
|
export default async function (fileList, nostrSigner, opts = {}) {
|
|
29
|
-
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : null
|
|
30
|
-
let lastProgress = 0
|
|
31
32
|
try {
|
|
32
|
-
return await toApp(fileList, nostrSigner,
|
|
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
|
|
33
|
+
return await toApp(fileList, nostrSigner, opts)
|
|
44
34
|
} finally {
|
|
45
35
|
await nostrRelays.disconnectAll()
|
|
46
36
|
}
|
|
@@ -53,14 +43,41 @@ export default async function (fileList, nostrSigner, opts = {}) {
|
|
|
53
43
|
* Every event has `type` (string) and `progress` (0–100 integer).
|
|
54
44
|
*
|
|
55
45
|
* Event types:
|
|
46
|
+
* 'services-checking' — Blossom preferences and server health are being checked
|
|
56
47
|
* 'init' — { totalFiles, totalSteps, dTag, relayCount, blossomCount }
|
|
57
48
|
* 'media-uploaded' — { mediaType: 'icon'|'key_art'|'screenshot', service: 'blossom'|'irfs'|null }
|
|
58
49
|
* 'file-uploaded' — { filename, service: 'blossom'|'irfs' }
|
|
59
50
|
* 'manifest-published' — unified site manifest and app metadata published
|
|
60
51
|
* 'complete' — { napp } (terminal, progress === 100)
|
|
61
52
|
* 'error' — { error } (terminal, error is rethrown)
|
|
53
|
+
*
|
|
54
|
+
* Every rejected error has a stable `NAPPUP_*` code and may retain its cause.
|
|
62
55
|
*/
|
|
63
|
-
export async function toApp (fileList, nostrSigner, {
|
|
56
|
+
export async function toApp (fileList, nostrSigner, opts = {}) {
|
|
57
|
+
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : null
|
|
58
|
+
let lastProgress = 0
|
|
59
|
+
const publishOptions = onEvent
|
|
60
|
+
? {
|
|
61
|
+
...opts,
|
|
62
|
+
onEvent (event) {
|
|
63
|
+
lastProgress = event.progress ?? lastProgress
|
|
64
|
+
onEvent(event)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
: opts
|
|
68
|
+
try {
|
|
69
|
+
return await publishApp(fileList, nostrSigner, publishOptions)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const normalized = normalizeNappupError(error)
|
|
72
|
+
if (onEvent) {
|
|
73
|
+
try { onEvent({ type: 'error', error: normalized, progress: lastProgress }) } catch (_) {}
|
|
74
|
+
}
|
|
75
|
+
throw normalized
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Implements publishing after the public boundary has installed error normalization.
|
|
80
|
+
async function publishApp (fileList, nostrSigner, {
|
|
64
81
|
log = () => {}, onEvent = () => {}, dTag, channel = 'main', shouldReupload = false
|
|
65
82
|
} = {}) {
|
|
66
83
|
let steps = 0
|
|
@@ -74,27 +91,66 @@ export async function toApp (fileList, nostrSigner, {
|
|
|
74
91
|
} catch (_) {}
|
|
75
92
|
}
|
|
76
93
|
if (!nostrSigner && typeof window !== 'undefined') nostrSigner = window.nostr
|
|
77
|
-
if (!nostrSigner)
|
|
94
|
+
if (!nostrSigner) {
|
|
95
|
+
throw new NappupError(NAPPUP_ERROR_CODES.NO_SIGNER, 'No Nostr signer found')
|
|
96
|
+
}
|
|
78
97
|
if (typeof window !== 'undefined' && nostrSigner === window.nostr) nostrSigner.getRelays = getRelays
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
fileList = Array.from(fileList || [])
|
|
100
|
+
if (!fileList.length) {
|
|
101
|
+
throw new NappupError(NAPPUP_ERROR_CODES.EMPTY_FILE_LIST, 'No app files were provided')
|
|
102
|
+
}
|
|
84
103
|
|
|
85
|
-
if (
|
|
86
|
-
if (!isNostrAppDTagSafe(dTag))
|
|
104
|
+
if (dTag !== undefined && dTag !== null) {
|
|
105
|
+
if (!isNostrAppDTagSafe(dTag)) {
|
|
106
|
+
throw new NappupError(
|
|
107
|
+
NAPPUP_ERROR_CODES.INVALID_D_TAG,
|
|
108
|
+
'dTag must be a non-empty string with at most 260 characters',
|
|
109
|
+
{ details: { dTag } }
|
|
110
|
+
)
|
|
111
|
+
}
|
|
87
112
|
} else {
|
|
88
|
-
const
|
|
113
|
+
const relativePath = typeof fileList[0]?.webkitRelativePath === 'string'
|
|
114
|
+
? fileList[0].webkitRelativePath
|
|
115
|
+
: ''
|
|
116
|
+
const folderName = relativePath.split('/')[0].trim()
|
|
89
117
|
if (GENERIC_BUILD_FOLDER_NAMES.has(folderName.toLowerCase())) {
|
|
90
|
-
throw new
|
|
118
|
+
throw new NappupError(
|
|
119
|
+
NAPPUP_ERROR_CODES.GENERIC_FOLDER_NAME,
|
|
120
|
+
`Folder name "${folderName}" is a generic build folder. Please provide a d tag with the -d flag.`,
|
|
121
|
+
{ details: { folderName } }
|
|
122
|
+
)
|
|
91
123
|
}
|
|
92
124
|
dTag = folderName
|
|
93
125
|
if (!isNostrAppDTagSafe(dTag)) {
|
|
94
|
-
throw new
|
|
126
|
+
throw new NappupError(
|
|
127
|
+
NAPPUP_ERROR_CODES.INVALID_FOLDER_NAME,
|
|
128
|
+
'Could not derive a valid d tag from the folder name. Please provide one with the -d flag.',
|
|
129
|
+
{ details: { folderName } }
|
|
130
|
+
)
|
|
95
131
|
}
|
|
96
132
|
}
|
|
97
133
|
|
|
134
|
+
let signerRelays
|
|
135
|
+
try {
|
|
136
|
+
signerRelays = await nostrSigner.getRelays()
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new NappupError(
|
|
139
|
+
NAPPUP_ERROR_CODES.RELAY_LOOKUP_FAILED,
|
|
140
|
+
'Could not read the signer outbox relays',
|
|
141
|
+
{ cause: error }
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
const writeRelays = [...new Set((Array.isArray(signerRelays?.write) ? signerRelays.write : [])
|
|
145
|
+
.flatMap(relay => typeof relay === 'string' && relay.trim()
|
|
146
|
+
? [relay.trim().replace(/\/$/, '')]
|
|
147
|
+
: []
|
|
148
|
+
))]
|
|
149
|
+
log(`Found ${writeRelays.length} outbox relays:\n${writeRelays.join(', ')}`)
|
|
150
|
+
if (!writeRelays.length) {
|
|
151
|
+
throw new NappupError(NAPPUP_ERROR_CODES.NO_OUTBOX_RELAYS, 'No outbox relays found')
|
|
152
|
+
}
|
|
153
|
+
|
|
98
154
|
const fileMetadata = []
|
|
99
155
|
const nappJsonFile = fileList.find(file => file.webkitRelativePath.split('/').slice(1).join('/') === '.well-known/napp.json')
|
|
100
156
|
let nappJson = {}
|
|
@@ -127,8 +183,14 @@ export async function toApp (fileList, nostrSigner, {
|
|
|
127
183
|
let isExplicitIcon = false
|
|
128
184
|
let pause = 1000
|
|
129
185
|
|
|
186
|
+
emit({ type: 'services-checking' })
|
|
130
187
|
log('Checking for blossom servers...')
|
|
131
|
-
|
|
188
|
+
let blossomServerUrls = []
|
|
189
|
+
try {
|
|
190
|
+
blossomServerUrls = await getBlossomServers(nostrSigner, writeRelays)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
log('Could not read Blossom server preferences; using relay-based upload instead', error)
|
|
193
|
+
}
|
|
132
194
|
let healthyBlossomServers = []
|
|
133
195
|
if (blossomServerUrls.length) {
|
|
134
196
|
log(`Found ${blossomServerUrls.length} blossom servers: ${blossomServerUrls.join(', ')}`)
|
|
@@ -238,7 +300,18 @@ export async function toApp (fileList, nostrSigner, {
|
|
|
238
300
|
shouldReupload,
|
|
239
301
|
log
|
|
240
302
|
})
|
|
241
|
-
if (failedFiles.length)
|
|
303
|
+
if (failedFiles.length) {
|
|
304
|
+
throw new NappupError(
|
|
305
|
+
NAPPUP_ERROR_CODES.BLOSSOM_UPLOAD_FAILED,
|
|
306
|
+
`${failedFiles.length} file(s) failed to upload to Blossom`,
|
|
307
|
+
{
|
|
308
|
+
details: {
|
|
309
|
+
failedFileCount: failedFiles.length,
|
|
310
|
+
filenames: failedFiles.map(failed => failed.filename).filter(Boolean)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
}
|
|
242
315
|
|
|
243
316
|
for (const uploaded of uploadedFiles) {
|
|
244
317
|
const metadata = {
|
|
@@ -254,65 +327,82 @@ export async function toApp (fileList, nostrSigner, {
|
|
|
254
327
|
}
|
|
255
328
|
} else {
|
|
256
329
|
for (const file of fileList) {
|
|
257
|
-
const nmmr = new NMMR()
|
|
258
|
-
let chunkLength = 0
|
|
259
|
-
for await (const chunk of streamToChunks(file.stream(), 51000)) {
|
|
260
|
-
chunkLength++
|
|
261
|
-
await nmmr.append(chunk)
|
|
262
|
-
}
|
|
263
|
-
// Empty IRFS blobs deliberately have no chunks and no manifest reference.
|
|
264
|
-
if (!chunkLength) {
|
|
265
|
-
steps++
|
|
266
|
-
emit({ type: 'file-uploaded', filename: file.webkitRelativePath.split('/').slice(1).join('/'), service: 'irfs' })
|
|
267
|
-
continue
|
|
268
|
-
}
|
|
269
|
-
|
|
270
330
|
const filename = file.webkitRelativePath.split('/').slice(1).join('/')
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
331
|
+
try {
|
|
332
|
+
const nmmr = new NMMR()
|
|
333
|
+
let chunkLength = 0
|
|
334
|
+
for await (const chunk of streamToChunks(file.stream(), 51000)) {
|
|
335
|
+
chunkLength++
|
|
336
|
+
await nmmr.append(chunk)
|
|
337
|
+
}
|
|
338
|
+
// Empty IRFS blobs deliberately have no chunks and no manifest reference.
|
|
339
|
+
if (!chunkLength) {
|
|
340
|
+
steps++
|
|
341
|
+
emit({ type: 'file-uploaded', filename, service: 'irfs' })
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
log(`Uploading ${chunkLength} file parts of ${filename}`)
|
|
346
|
+
;({ pause } = await uploadBinaryDataChunks({
|
|
347
|
+
nmmr, signer: nostrSigner, filename, chunkLength, log, pause, shouldReupload
|
|
348
|
+
}))
|
|
349
|
+
const metadata = {
|
|
350
|
+
rootHash: nmmr.getRoot(),
|
|
351
|
+
filename,
|
|
352
|
+
mimeType: file.type || 'application/octet-stream',
|
|
353
|
+
size: file.size
|
|
354
|
+
}
|
|
355
|
+
fileMetadata.push(metadata)
|
|
356
|
+
if (!iconMetadata && faviconFile && file === faviconFile) iconMetadata = { ...metadata }
|
|
357
|
+
steps++
|
|
358
|
+
emit({ type: 'file-uploaded', filename, service: 'irfs' })
|
|
359
|
+
} catch (error) {
|
|
360
|
+
throw new NappupError(
|
|
361
|
+
NAPPUP_ERROR_CODES.IRFS_UPLOAD_FAILED,
|
|
362
|
+
`Failed to upload "${filename}" to Nostr relays`,
|
|
363
|
+
{ cause: error, details: { filename } }
|
|
364
|
+
)
|
|
280
365
|
}
|
|
281
|
-
fileMetadata.push(metadata)
|
|
282
|
-
if (!iconMetadata && faviconFile && file === faviconFile) iconMetadata = { ...metadata }
|
|
283
|
-
steps++
|
|
284
|
-
emit({ type: 'file-uploaded', filename, service: 'irfs' })
|
|
285
366
|
}
|
|
286
367
|
}
|
|
287
368
|
|
|
288
369
|
log(`Uploading unified site manifest ${dTag}`)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
370
|
+
let manifest
|
|
371
|
+
try {
|
|
372
|
+
manifest = await uploadSiteManifest({
|
|
373
|
+
dTag,
|
|
374
|
+
channel,
|
|
375
|
+
fileMetadata,
|
|
376
|
+
name: manifestName,
|
|
377
|
+
nameLang: nappJson.name?.[0]?.[1],
|
|
378
|
+
isNameAuto: !nappJson.name?.[0]?.[0],
|
|
379
|
+
summary: manifestSummary,
|
|
380
|
+
summaryLang: nappJson.summary?.[0]?.[1],
|
|
381
|
+
isSummaryAuto: !nappJson.summary?.[0]?.[0],
|
|
382
|
+
icon: iconMetadata,
|
|
383
|
+
isIconAuto: !isExplicitIcon,
|
|
384
|
+
descriptions: nappJson.description,
|
|
385
|
+
keyArt: keyArtMetadata,
|
|
386
|
+
screenshots: screenshotMetadata,
|
|
387
|
+
uploadService,
|
|
388
|
+
sourceRelays: writeRelays,
|
|
389
|
+
blossomServers: healthyBlossomServers,
|
|
390
|
+
signer: nostrSigner,
|
|
391
|
+
log,
|
|
392
|
+
pause,
|
|
393
|
+
shouldReupload,
|
|
394
|
+
self: nappJson.self?.[0]?.[0],
|
|
395
|
+
countries: nappJson.country,
|
|
396
|
+
categories: nappJson.category,
|
|
397
|
+
hashtags: nappJson.hashtag
|
|
398
|
+
})
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throw new NappupError(
|
|
401
|
+
NAPPUP_ERROR_CODES.MANIFEST_UPLOAD_FAILED,
|
|
402
|
+
'Failed to publish the app manifest to Nostr relays',
|
|
403
|
+
{ cause: error }
|
|
404
|
+
)
|
|
405
|
+
}
|
|
316
406
|
|
|
317
407
|
const appEntity = appEncode({
|
|
318
408
|
dTag: manifest.tags.find(tag => tag[0] === 'd')[1],
|
|
@@ -3,6 +3,27 @@ import nostrRelays from '#services/nostr-relays.js'
|
|
|
3
3
|
import { bytesToBase16 } from '#helpers/base16.js'
|
|
4
4
|
import { normalizeBlossomServerUrl } from 'libp2r2p/url'
|
|
5
5
|
|
|
6
|
+
const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 5000
|
|
7
|
+
const DEFAULT_EXISTENCE_CHECK_TIMEOUT_MS = 5000
|
|
8
|
+
|
|
9
|
+
// Bounds browser fetches whose native network timeout can take minutes.
|
|
10
|
+
async function fetchWithTimeout (url, options, timeoutMs) {
|
|
11
|
+
const controller = new AbortController()
|
|
12
|
+
let timedOut = false
|
|
13
|
+
const timeoutId = setTimeout(() => {
|
|
14
|
+
timedOut = true
|
|
15
|
+
controller.abort()
|
|
16
|
+
}, timeoutMs)
|
|
17
|
+
try {
|
|
18
|
+
return await fetch(url, { ...options, signal: controller.signal })
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (timedOut) throw new Error(`request timed out after ${timeoutMs}ms`)
|
|
21
|
+
throw error
|
|
22
|
+
} finally {
|
|
23
|
+
clearTimeout(timeoutId)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
async function createAuthHeader (signer, modify) {
|
|
7
28
|
const now = Math.floor(Date.now() / 1000)
|
|
8
29
|
const event = {
|
|
@@ -43,13 +64,16 @@ export async function getBlossomServers (signer, writeRelays) {
|
|
|
43
64
|
/**
|
|
44
65
|
* Health-checks blossom servers with a simple HEAD request.
|
|
45
66
|
* A server is considered healthy if fetch resolves (any HTTP status).
|
|
46
|
-
*
|
|
67
|
+
* Network errors and timeouts mark a server as unreachable.
|
|
47
68
|
*/
|
|
48
|
-
export async function healthCheckServers (servers, signer, {
|
|
69
|
+
export async function healthCheckServers (servers, signer, {
|
|
70
|
+
log = () => {},
|
|
71
|
+
timeoutMs = DEFAULT_HEALTH_CHECK_TIMEOUT_MS
|
|
72
|
+
} = {}) {
|
|
49
73
|
const results = await Promise.allSettled(
|
|
50
74
|
servers.map(async (serverUrl) => {
|
|
51
75
|
const normalized = normalizeBlossomServerUrl(serverUrl)
|
|
52
|
-
await
|
|
76
|
+
await fetchWithTimeout(normalized, { method: 'HEAD', mode: 'no-cors' }, timeoutMs)
|
|
53
77
|
return normalized
|
|
54
78
|
})
|
|
55
79
|
)
|
|
@@ -86,9 +110,17 @@ export async function computeFileHash (file) {
|
|
|
86
110
|
async function uploadFileToServer (serverUrl, signer, file, fileHash, mimeType, { shouldReupload, log, maxRetries = 5 }) {
|
|
87
111
|
// Check if already uploaded
|
|
88
112
|
if (!shouldReupload) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
try {
|
|
114
|
+
const checkResponse = await fetchWithTimeout(
|
|
115
|
+
`${serverUrl}/${fileHash}`,
|
|
116
|
+
{ method: 'HEAD' },
|
|
117
|
+
DEFAULT_EXISTENCE_CHECK_TIMEOUT_MS
|
|
118
|
+
)
|
|
119
|
+
if (checkResponse.ok) {
|
|
120
|
+
return { success: true, alreadyExists: true }
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
log(`Could not check whether ${fileHash} exists on ${serverUrl}; uploading it anyway: ${error?.message ?? error}`)
|
|
92
124
|
}
|
|
93
125
|
}
|
|
94
126
|
|
|
@@ -184,7 +216,8 @@ export async function uploadFilesToBlossom ({
|
|
|
184
216
|
}
|
|
185
217
|
})
|
|
186
218
|
|
|
187
|
-
|
|
219
|
+
// Unexpected task errors indicate a programming failure and must reach the caller.
|
|
220
|
+
await Promise.all(serverTasks)
|
|
188
221
|
|
|
189
222
|
const uploadedFiles = []
|
|
190
223
|
const failedFiles = []
|