nappup 2.2.1 → 2.3.1
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 +8 -1
- package/bin/nappup/helpers.js +3 -1
- package/package.json +1 -1
- package/src/index.js +3 -2
- package/src/services/site-manifest.js +74 -7
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ nappup [directory] [options]
|
|
|
29
29
|
| `-s <secret_key>` | Your Nostr secret key (hex, nsec, or `bunker://` URL) used to sign the application event. See [Authentication](#authentication) for alternatives. |
|
|
30
30
|
| `-d <d_tag>` | The identifier (`d` tag) for your application. Any UTF-8 text up to 260 characters. If omitted, defaults to the directory name. Avoid generic names like `dist` or `build` - use something unique among your other apps like `mycoolapp`. |
|
|
31
31
|
| `-y` | Skip confirmation prompt. Useful for CI/CD pipelines or automated scripts. |
|
|
32
|
-
| `-r` | Force re-upload. By default, Napp Up! might skip files that haven't changed. Use this flag to ensure everything is pushed fresh. |
|
|
32
|
+
| `-r` | Force re-upload. By default, Napp Up! might skip files that haven't changed. Use this flag to ensure everything is pushed fresh; it does not create a new app version unless a file path or hash changes. |
|
|
33
33
|
| `--main` | Publish to the **main** release channel. This is the default behavior. |
|
|
34
34
|
| `--next` | Publish to the **next** release channel. Ideal for beta testing or staging builds. |
|
|
35
35
|
| `--draft` | Publish to the **draft** release channel. Use this for internal testing or work-in-progress builds. |
|
|
@@ -113,6 +113,13 @@ nappup ~/my-repos/projectx/build/projectx --draft -r
|
|
|
113
113
|
|
|
114
114
|
## Programmatic Usage
|
|
115
115
|
|
|
116
|
+
Each published manifest includes an aggregate `x` tag that identifies the app
|
|
117
|
+
version from its file path/hash mappings. Updating only manifest metadata or
|
|
118
|
+
forcing a re-upload preserves that aggregate. The `published_at` tag records
|
|
119
|
+
the first publication time of that aggregate and is preserved across later
|
|
120
|
+
metadata revisions, while the event's `created_at` records the latest manifest
|
|
121
|
+
revision.
|
|
122
|
+
|
|
116
123
|
Napp Up! also exports a function that works in both Node.js and the browser, so you can integrate app uploads directly into your own tooling:
|
|
117
124
|
|
|
118
125
|
```js
|
package/bin/nappup/helpers.js
CHANGED
|
@@ -188,10 +188,12 @@ export async function toFileList (filesIterator, dir) {
|
|
|
188
188
|
const fileList = []
|
|
189
189
|
for await (const f of filesIterator) {
|
|
190
190
|
const fileType = mime.lookup(f)
|
|
191
|
+
const { size } = await fs.promises.stat(f)
|
|
191
192
|
const file = {
|
|
192
193
|
stream: () => Readable.toWeb(fs.createReadStream(f)),
|
|
193
194
|
webkitRelativePath: path.relative(dir.replace(/\/[^/]*$/, ''), f),
|
|
194
|
-
type: fileType || (await fileTypeFromFile(f))?.mime || ''
|
|
195
|
+
type: fileType || (await fileTypeFromFile(f))?.mime || '',
|
|
196
|
+
size
|
|
195
197
|
}
|
|
196
198
|
fileList.push(file)
|
|
197
199
|
}
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import NMMR from 'nmmr'
|
|
2
|
-
import { appEncode } from 'libp2r2p/nip19'
|
|
2
|
+
import { appEncode, npubEncode } from 'libp2r2p/nip19'
|
|
3
3
|
import nostrRelays from '#services/nostr-relays.js'
|
|
4
4
|
import { getRelays } from '#helpers/signer.js'
|
|
5
5
|
import { streamToChunks, streamToText } from '#helpers/stream.js'
|
|
@@ -79,7 +79,8 @@ export async function toApp (fileList, nostrSigner, {
|
|
|
79
79
|
|
|
80
80
|
const writeRelays = [...new Set((await nostrSigner.getRelays()).write
|
|
81
81
|
.map(relay => relay.trim().replace(/\/$/, '')))]
|
|
82
|
-
|
|
82
|
+
const npub = npubEncode(await nostrSigner.getPublicKey())
|
|
83
|
+
log(`Found ${writeRelays.length} outbox relays for pubkey ${npub}:\n${writeRelays.join(', ')}`)
|
|
83
84
|
if (!writeRelays.length) throw new Error('No outbox relays found')
|
|
84
85
|
|
|
85
86
|
if (typeof dTag === 'string') {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NAPP_CATEGORIES } from '#config/napp-categories.js'
|
|
2
2
|
import nostrRelays, { nappRelays } from '#services/nostr-relays.js'
|
|
3
3
|
import { throttledSendEvent } from '#services/irfs-upload.js'
|
|
4
|
+
import { sha256 } from '@noble/hashes/sha2.js'
|
|
5
|
+
import { bytesToBase16 } from '#helpers/base16.js'
|
|
4
6
|
|
|
5
7
|
const MANAGED_MANIFEST_TAGS = new Set([
|
|
6
8
|
'd', 'service', 'path', 'r', 'name', 'summary', 'description', 'self',
|
|
7
|
-
'c', 'l', 't', 'auto', 'icon', 'key_art', 'screenshot'
|
|
9
|
+
'c', 'l', 't', 'auto', 'icon', 'key_art', 'screenshot', 'x', 'published_at'
|
|
8
10
|
])
|
|
9
11
|
|
|
10
12
|
export function normalizeManifestPath (value) {
|
|
@@ -25,6 +27,37 @@ function validRoot (root) {
|
|
|
25
27
|
return typeof root === 'string' && /^[0-9a-f]{64}$/.test(root)
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
function manifestAggregateLines (manifest) {
|
|
31
|
+
const tags = Array.isArray(manifest?.tags) ? manifest.tags : []
|
|
32
|
+
const service = tags.find(tag => Array.isArray(tag) && tag[0] === 'service')?.[1]
|
|
33
|
+
const lines = []
|
|
34
|
+
|
|
35
|
+
if (service === 'irfs') {
|
|
36
|
+
for (const tag of tags) {
|
|
37
|
+
if (!Array.isArray(tag) || tag[0] !== 'r' || !validRoot(tag[1])) continue
|
|
38
|
+
for (const field of tag.slice(2)) {
|
|
39
|
+
if (typeof field !== 'string' || !field.startsWith('path ')) continue
|
|
40
|
+
const path = normalizeManifestPath(field.slice(5))
|
|
41
|
+
lines.push(`${tag[1]} /${path}\n`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
for (const tag of tags) {
|
|
46
|
+
if (!Array.isArray(tag) || tag[0] !== 'path' || !validRoot(tag[2])) continue
|
|
47
|
+
const path = normalizeManifestPath(tag[1])
|
|
48
|
+
lines.push(`${tag[2]} /${path}\n`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getManifestAggregateHash (manifest) {
|
|
56
|
+
const lines = manifestAggregateLines(manifest)
|
|
57
|
+
if (!lines.length) throw new Error('Site manifest must reference at least one file')
|
|
58
|
+
return bytesToBase16(sha256(new TextEncoder().encode(lines.sort().join(''))))
|
|
59
|
+
}
|
|
60
|
+
|
|
28
61
|
function decimalSize (size) {
|
|
29
62
|
if (!Number.isSafeInteger(size) || size < 0) {
|
|
30
63
|
throw new Error('Asset size must be a non-negative safe integer')
|
|
@@ -138,7 +171,7 @@ function buildMetadataTags ({
|
|
|
138
171
|
|
|
139
172
|
export function buildManifestTags ({
|
|
140
173
|
dTag, uploadService, fileMetadata = [], icon, keyArt = [], screenshots = [],
|
|
141
|
-
previousTags = [], ...metadata
|
|
174
|
+
previousTags = [], publishedAt, ...metadata
|
|
142
175
|
}) {
|
|
143
176
|
if (uploadService !== 'irfs' && uploadService !== 'blossom') {
|
|
144
177
|
throw new Error('Unknown upload service')
|
|
@@ -153,10 +186,21 @@ export function buildManifestTags ({
|
|
|
153
186
|
.slice(0, 10)
|
|
154
187
|
.map(tag => [...tag])
|
|
155
188
|
|
|
189
|
+
if (!Number.isSafeInteger(publishedAt) || publishedAt < 0) {
|
|
190
|
+
throw new Error('published_at must be a non-negative safe integer')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const referenceTags = buildReferenceTags(uploadService, fileMetadata, media)
|
|
194
|
+
const aggregateHash = getManifestAggregateHash({
|
|
195
|
+
tags: [...referenceTags, ['service', uploadService]]
|
|
196
|
+
})
|
|
197
|
+
|
|
156
198
|
return [
|
|
157
199
|
['d', dTag],
|
|
158
|
-
...
|
|
200
|
+
...referenceTags,
|
|
159
201
|
['service', uploadService],
|
|
202
|
+
['x', aggregateHash, 'aggregate'],
|
|
203
|
+
['published_at', String(publishedAt)],
|
|
160
204
|
...buildMetadataTags({ ...metadata, hasIcon: Boolean(icon) }),
|
|
161
205
|
...unknownTags
|
|
162
206
|
]
|
|
@@ -190,8 +234,34 @@ export async function uploadSiteManifest ({
|
|
|
190
234
|
}, relays, { timeoutAfterFirstEose: null })).result
|
|
191
235
|
events.sort(newestFirst)
|
|
192
236
|
const previous = events[0]
|
|
237
|
+
|
|
238
|
+
const now = Math.floor(Date.now() / 1000)
|
|
239
|
+
const createdAt = Math.max(now, (previous?.created_at ?? -1) + 1)
|
|
240
|
+
if (createdAt > now + 172800) throw new Error('Existing manifest timestamp is too far in the future to replace safely')
|
|
241
|
+
|
|
242
|
+
const prospectiveTags = buildManifestTags({
|
|
243
|
+
dTag, uploadService, fileMetadata, previousTags: previous?.tags,
|
|
244
|
+
publishedAt: createdAt, ...metadata
|
|
245
|
+
})
|
|
246
|
+
const aggregateHash = getManifestAggregateHash({ tags: prospectiveTags })
|
|
247
|
+
let previousAggregateHash = null
|
|
248
|
+
try {
|
|
249
|
+
if (previous) previousAggregateHash = getManifestAggregateHash(previous)
|
|
250
|
+
} catch (_) {}
|
|
251
|
+
const isSameVersion = previousAggregateHash === aggregateHash
|
|
252
|
+
const previousPublishedAt = previous?.tags?.find(tag => tag[0] === 'published_at')?.[1]
|
|
253
|
+
const parsedPreviousPublishedAt = typeof previousPublishedAt === 'string' && /^(0|[1-9][0-9]*)$/.test(previousPublishedAt)
|
|
254
|
+
? Number(previousPublishedAt)
|
|
255
|
+
: null
|
|
256
|
+
const fallbackPublishedAt = Number.isSafeInteger(previous?.created_at) && previous.created_at >= 0
|
|
257
|
+
? previous.created_at
|
|
258
|
+
: createdAt
|
|
259
|
+
const publishedAt = isSameVersion && Number.isSafeInteger(parsedPreviousPublishedAt)
|
|
260
|
+
? parsedPreviousPublishedAt
|
|
261
|
+
: (isSameVersion ? fallbackPublishedAt : createdAt)
|
|
193
262
|
const tags = buildManifestTags({
|
|
194
|
-
dTag, uploadService, fileMetadata, previousTags: previous?.tags,
|
|
263
|
+
dTag, uploadService, fileMetadata, previousTags: previous?.tags,
|
|
264
|
+
publishedAt, ...metadata
|
|
195
265
|
})
|
|
196
266
|
|
|
197
267
|
if (!shouldReupload && previous && previous.content === '' && JSON.stringify(previous.tags) === JSON.stringify(tags)) {
|
|
@@ -208,9 +278,6 @@ export async function uploadSiteManifest ({
|
|
|
208
278
|
return previous
|
|
209
279
|
}
|
|
210
280
|
|
|
211
|
-
const now = Math.floor(Date.now() / 1000)
|
|
212
|
-
const createdAt = Math.max(now, (previous?.created_at ?? -1) + 1)
|
|
213
|
-
if (createdAt > now + 172800) throw new Error('Existing manifest timestamp is too far in the future to replace safely')
|
|
214
281
|
const event = await signer.signEvent({ kind, tags, content: '', created_at: createdAt })
|
|
215
282
|
await throttledSendEvent(event, relays, { pause, trailingPause: true, log })
|
|
216
283
|
return event
|