nappup 1.9.1 → 2.0.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.
@@ -134,7 +134,7 @@ async function uploadFileToServer (serverUrl, signer, file, fileHash, mimeType,
134
134
  * Different servers run in parallel.
135
135
  *
136
136
  * Returns { uploadedFiles: [...], failedFiles: [...] }
137
- * where each uploadedFile has { file, filename, sha256, mimeType }
137
+ * where each uploadedFile has { file, filename, sha256, mimeType, size }
138
138
  * and each failedFile has { file, filename, mimeType, errors }.
139
139
  */
140
140
  export async function uploadFilesToBlossom ({
@@ -195,7 +195,8 @@ export async function uploadFilesToBlossom ({
195
195
  file: info.file,
196
196
  filename: info.filename,
197
197
  sha256: info.sha256,
198
- mimeType: info.mimeType
198
+ mimeType: info.mimeType,
199
+ size: info.file.size
199
200
  })
200
201
  } else {
201
202
  failedFiles.push({
@@ -1,13 +1,14 @@
1
1
  import nostrRelays, { nappRelays } from '#services/nostr-relays.js'
2
- import Base93Encoder from '#services/base93-encoder.js'
2
+ import NMMR from 'nmmr'
3
+ import { decode as base93Decode, encode as base93Encode } from 'libp2r2p/base93'
3
4
  import { stringifyEvent } from '#helpers/event.js'
4
5
 
5
6
  /**
6
7
  * Uploads binary data chunks for a file to Nostr relays using the InterRelay File System (IRFS).
7
8
  *
8
9
  * Splits file content via NMMR (Nostr Merkle Mountain Range) into chunks,
9
- * encodes each chunk with Base93, and publishes them as kind 34600 events.
10
- * Supports resume via created_at cursor alignment with previously stored chunks.
10
+ * encodes each chunk with Base93, and publishes them as kind 34601 events.
11
+ * Supports resume by querying the deterministic d tag of each MMR position.
11
12
  *
12
13
  * @param {object} params
13
14
  * @param {object} params.nmmr - NMMR instance with chunks already appended
@@ -16,51 +17,30 @@ import { stringifyEvent } from '#helpers/event.js'
16
17
  * @param {number} params.chunkLength - Total number of chunks
17
18
  * @param {Function} params.log - Logging function
18
19
  * @param {number} [params.pause=0] - Current pause duration in ms (for rate-limit backoff)
19
- * @param {string} params.mimeType - MIME type of the file
20
20
  * @param {boolean} [params.shouldReupload=false] - Whether to force re-upload existing chunks
21
21
  * @returns {Promise<{pause: number}>} Updated pause duration
22
22
  */
23
- export async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, log, pause = 0, mimeType, shouldReupload = false }) {
24
- const pubkey = await signer.getPublicKey()
23
+ export async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, log, pause = 0, shouldReupload = false }) {
25
24
  const writeRelays = (await signer.getRelays()).write
26
25
  const relays = [...new Set([...writeRelays, ...nappRelays].map(r => r.trim().replace(/\/$/, '')))]
27
-
28
- // Find max stored created_at for this file's chunks
29
26
  const rootHash = nmmr.getRoot()
30
- const allCTags = Array.from({ length: chunkLength }, (_, i) => `${rootHash}:${i}`)
31
- let maxStoredCreatedAt = 0
32
-
33
- for (let i = 0; i < allCTags.length; i += 100) {
34
- const batch = allCTags.slice(i, i + 100)
35
- const storedEvents = (await nostrRelays.getEvents({
36
- kinds: [34600],
37
- authors: [pubkey],
38
- '#c': batch,
39
- limit: 1
40
- }, relays)).result
41
-
42
- if (storedEvents.length > 0) {
43
- const batchMaxCreatedAt = storedEvents.reduce((m, e) => Math.max(m, (e && typeof e.created_at === 'number') ? e.created_at : 0), 0)
44
- if (batchMaxCreatedAt > maxStoredCreatedAt) maxStoredCreatedAt = batchMaxCreatedAt
45
- }
46
- }
47
-
48
- // Set initial created_at based on what's higher, maxStoredCreatedAt or current time
49
- let createdAtCursor = (Math.max(maxStoredCreatedAt, Math.floor(Date.now() / 1000)) + chunkLength)
27
+ const dTags = Array.from({ length: chunkLength }, (_, index) => NMMR.deriveChunkId(rootHash, index))
28
+ const { eventsByD } = await getPreviousChunks(dTags, relays, signer)
50
29
 
51
30
  let chunkIndex = 0
52
31
  for await (const chunk of nmmr.getChunks()) {
53
- const dTag = chunk.x
54
- const currentCtag = `${chunk.rootX}:${chunk.index}`
55
- const { otherCtags, hasCurrentCtag, foundEvent, missingRelays } = await getPreviousCtags(dTag, currentCtag, relays, signer)
56
- if (!shouldReupload && hasCurrentCtag) {
57
- // Handling of partial uploads/resumes:
58
- // If we are observing an existing chunk, we use its created_at to re-align our cursor
59
- // for the next chunks (so next chunk will be this_chunk_time - 1)
60
- if (foundEvent) {
61
- createdAtCursor = foundEvent.created_at - 1
62
- }
63
-
32
+ if (chunk.total !== chunkLength || chunk.index !== chunkIndex) {
33
+ throw new Error('NMMR yielded inconsistent chunk metadata')
34
+ }
35
+ const dTag = NMMR.deriveChunkId(rootHash, chunk.index)
36
+ const storedEvents = eventsByD.get(dTag) ?? []
37
+ const validEvents = storedEvents.filter(event => isExpectedChunkEvent(event, { rootHash, index: chunk.index, total: chunk.total, dTag }))
38
+ .sort(compareEventsNewestFirst)
39
+ const foundEvent = validEvents[0]
40
+ const coveredRelays = new Set(validEvents.filter(event => event.id === foundEvent?.id).map(event => event.meta?.relay).filter(Boolean))
41
+ const missingRelays = relays.filter(relay => !coveredRelays.has(relay))
42
+
43
+ if (!shouldReupload && foundEvent) {
64
44
  if (missingRelays.length === 0) {
65
45
  log(`${filename}: Skipping chunk ${++chunkIndex} of ${chunkLength} (already uploaded)`)
66
46
  continue
@@ -70,21 +50,18 @@ export async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLen
70
50
  continue
71
51
  }
72
52
 
73
- const effectiveCreatedAt = createdAtCursor
74
- // The lower chunk index, the higher created_at must be
75
- // for relays to serve chunks in the most efficient order
76
- createdAtCursor--
53
+ const now = Math.floor(Date.now() / 1000)
54
+ const newestStoredCreatedAt = storedEvents.reduce((latest, event) => Math.max(latest, Number.isSafeInteger(event.created_at) ? event.created_at : 0), 0)
55
+ const effectiveCreatedAt = Math.max(now, newestStoredCreatedAt + 1)
56
+ if (effectiveCreatedAt > now + 172800) throw new Error('Existing chunk timestamp is too far in the future to replace safely')
77
57
 
78
58
  const binaryDataChunk = {
79
- kind: 34600,
59
+ kind: 34601,
80
60
  tags: [
81
61
  ['d', dTag],
82
- ...otherCtags,
83
- ['c', currentCtag, chunk.length, ...chunk.proof],
84
- ...(mimeType ? [['m', mimeType]] : [])
62
+ ['mmr', String(chunk.index), String(chunk.total), base93Encode(chunk.proof)]
85
63
  ],
86
- // These chunks already have the expected size of 51000 bytes
87
- content: new Base93Encoder().update(chunk.contentBytes).getEncoded(),
64
+ content: base93Encode(chunk.contentBytes),
88
65
  created_at: effectiveCreatedAt
89
66
  }
90
67
 
@@ -96,6 +73,41 @@ export async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLen
96
73
  return { pause }
97
74
  }
98
75
 
76
+ function compareEventsNewestFirst (a, b) {
77
+ if (a.created_at !== b.created_at) return b.created_at - a.created_at
78
+ return String(a.id).localeCompare(String(b.id))
79
+ }
80
+
81
+ export function parseChunkEvent (event) {
82
+ if (!event || event.kind !== 34601 || !Array.isArray(event.tags)) throw new Error('Wrong chunk event kind or tags')
83
+ const dTags = event.tags.filter(tag => Array.isArray(tag) && tag[0] === 'd')
84
+ const mmrTags = event.tags.filter(tag => Array.isArray(tag) && tag[0] === 'mmr')
85
+ if (dTags.length !== 1 || dTags[0].length !== 2 || !/^[0-9a-f]{64}$/.test(dTags[0][1])) throw new Error('Wrong chunk d tag')
86
+ if (mmrTags.length !== 1 || mmrTags[0].length !== 4) throw new Error('Wrong chunk mmr tag')
87
+
88
+ const [, index, total, encodedProof] = mmrTags[0]
89
+ const contentBytes = base93Decode(event.content)
90
+ const proof = base93Decode(encodedProof)
91
+ const root = NMMR.calculateRoot({ contentBytes, index, total, proof })
92
+ const numericIndex = Number(index)
93
+ const numericTotal = Number(total)
94
+ if (contentBytes.length < 1 || contentBytes.length > 51000 || (numericIndex < numericTotal - 1 && contentBytes.length !== 51000)) {
95
+ throw new Error('Wrong chunk byte length')
96
+ }
97
+ const d = NMMR.deriveChunkId(root, index)
98
+ if (d !== dTags[0][1]) throw new Error('Chunk d tag mismatch')
99
+ return { root, index: numericIndex, total: numericTotal, proof, contentBytes, d }
100
+ }
101
+
102
+ function isExpectedChunkEvent (event, expected) {
103
+ try {
104
+ const parsed = parseChunkEvent(event)
105
+ return parsed.root === expected.rootHash && parsed.index === expected.index && parsed.total === expected.total && parsed.d === expected.dTag
106
+ } catch (_) {
107
+ return false
108
+ }
109
+ }
110
+
99
111
  /**
100
112
  * Sends a signed Nostr event to relays with retry logic and rate-limit backoff.
101
113
  *
@@ -180,62 +192,25 @@ export async function throttledSendEvent (event, relays, {
180
192
  })
181
193
  }
182
194
 
183
- /**
184
- * Checks if a chunk (identified by its d-tag) already exists on relays.
185
- *
186
- * Returns info about existing c-tags on the stored event (for deduplication),
187
- * whether the current c-tag is already present, the found event itself,
188
- * and which relays are missing the event.
189
- *
190
- * @param {string} dTagValue - The d-tag value of the chunk event
191
- * @param {string} currentCtagValue - The current c-tag value (rootHash:index)
192
- * @param {string[]} relays - Array of relay URLs to check
193
- * @param {object} signer - Nostr signer with getPublicKey()
194
- * @returns {Promise<{otherCtags: Array, hasEvent: boolean, hasCurrentCtag: boolean, foundEvent?: object, missingRelays?: string[]}>}
195
- */
196
- export async function getPreviousCtags (dTagValue, currentCtagValue, relays, signer) {
195
+ export async function getPreviousChunks (dTagValues, relays, signer) {
197
196
  const targetRelays = [...new Set([...relays, ...nappRelays].map(r => r.trim().replace(/\/$/, '')))]
198
- const storedEvents = (await nostrRelays.getEvents({
199
- kinds: [34600],
200
- authors: [await signer.getPublicKey()],
201
- '#d': [dTagValue],
202
- limit: 1
203
- }, targetRelays)).result
204
-
205
- let hasCurrentCtag = false
206
- const hasEvent = storedEvents.length > 0
207
- if (!hasEvent) return { otherCtags: [], hasEvent, hasCurrentCtag }
208
-
209
- const cTagValues = { [currentCtagValue]: true }
210
- storedEvents.sort((a, b) => b.created_at - a.created_at)
211
- const bestEvent = storedEvents[0]
212
- const prevTags = bestEvent.tags
213
-
214
- if (!Array.isArray(prevTags)) return { otherCtags: [], hasEvent, hasCurrentCtag }
215
-
216
- hasCurrentCtag = prevTags.some(tag =>
217
- Array.isArray(tag) &&
218
- tag[0] === 'c' &&
219
- tag[1] === currentCtagValue
220
- )
221
-
222
- const otherCtags = prevTags
223
- .filter(v => {
224
- const isCTag =
225
- Array.isArray(v) &&
226
- v[0] === 'c' &&
227
- typeof v[1] === 'string' &&
228
- /^[0-9a-f]{64}:\d+$/.test(v[1])
229
- if (!isCTag) return false
230
-
231
- const isntDuplicate = !cTagValues[v[1]]
232
- cTagValues[v[1]] = true
233
- return isCTag && isntDuplicate
234
- })
235
-
236
- const matchingEvents = storedEvents.filter(e => e.id === bestEvent.id)
237
- const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
238
- const missingRelays = targetRelays.filter(r => !coveredRelays.has(r))
239
-
240
- return { otherCtags, hasEvent, hasCurrentCtag, foundEvent: bestEvent, missingRelays }
197
+ const eventsByD = new Map(dTagValues.map(dTag => [dTag, []]))
198
+ const pubkey = await signer.getPublicKey()
199
+
200
+ for (let offset = 0; offset < dTagValues.length; offset += 100) {
201
+ const batch = dTagValues.slice(offset, offset + 100)
202
+ const storedEvents = (await nostrRelays.getEvents({
203
+ kinds: [34601],
204
+ authors: [pubkey],
205
+ '#d': batch,
206
+ limit: batch.length
207
+ }, targetRelays)).result
208
+
209
+ for (const event of storedEvents) {
210
+ const dTag = event.tags?.find(tag => tag[0] === 'd')?.[1]
211
+ if (eventsByD.has(dTag)) eventsByD.get(dTag).push(event)
212
+ }
213
+ }
214
+
215
+ return { eventsByD, targetRelays }
241
216
  }
@@ -8,7 +8,7 @@ import nostrRelays, { seedRelays, freeRelays } from '#services/nostr-relays.js'
8
8
  import { getRelays } from '#helpers/signer.js'
9
9
  import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
10
10
  import { finalizeEvent } from '#helpers/nip01.js'
11
- import { nsecDecode, nsecEncode } from '#helpers/nip19.js'
11
+ import { nsecDecode, nsecEncode } from 'libp2r2p/nip19'
12
12
  const __dirname = fileURLToPath(new URL('.', import.meta.url))
13
13
 
14
14
  const dotenvPath = process.env.DOTENV_CONFIG_PATH ?? `${__dirname}/../../.env`
@@ -0,0 +1,216 @@
1
+ import { NAPP_CATEGORIES } from '#config/napp-categories.js'
2
+ import nostrRelays, { nappRelays } from '#services/nostr-relays.js'
3
+ import { throttledSendEvent } from '#services/irfs-upload.js'
4
+
5
+ const MANAGED_MANIFEST_TAGS = new Set([
6
+ 'd', 'service', 'path', 'r', 'name', 'summary', 'description', 'self',
7
+ 'c', 'l', 't', 'auto', 'icon', 'key_art', 'screenshot'
8
+ ])
9
+
10
+ export function normalizeManifestPath (value) {
11
+ if (typeof value !== 'string') throw new TypeError('Manifest path must be a string')
12
+ const path = value.startsWith('/') ? value.slice(1) : value
13
+ if (!path || path.includes('\\') || /[\u0000-\u001f\u007f]/.test(path)) {
14
+ throw new Error(`Unsafe manifest path: ${JSON.stringify(value)}`)
15
+ }
16
+ const segments = path.split('/')
17
+ if (segments.some(segment => !segment || segment === '.' || segment === '..')) {
18
+ throw new Error(`Unsafe manifest path: ${JSON.stringify(value)}`)
19
+ }
20
+ return path
21
+ }
22
+
23
+ function validRoot (root) {
24
+ return typeof root === 'string' && /^[0-9a-f]{64}$/.test(root)
25
+ }
26
+
27
+ function decimalSize (size) {
28
+ if (!Number.isSafeInteger(size) || size < 0) {
29
+ throw new Error('Asset size must be a non-negative safe integer')
30
+ }
31
+ return String(size)
32
+ }
33
+
34
+ function addField (fields, type, value) {
35
+ if (typeof value === 'string' && value) fields.push(`${type} ${value}`)
36
+ }
37
+
38
+ function buildReferenceTags (uploadService, files, media) {
39
+ const tags = []
40
+ if (uploadService === 'blossom') {
41
+ for (const file of files) {
42
+ if (!validRoot(file.rootHash)) throw new Error('Invalid Blossom SHA-256 hash')
43
+ tags.push(['path', normalizeManifestPath(file.filename), file.rootHash])
44
+ }
45
+ }
46
+
47
+ const groups = new Map()
48
+ const addToGroup = (asset, fieldType, fieldValue, country) => {
49
+ if (!asset) return
50
+ if (!validRoot(asset.rootHash)) throw new Error('Invalid asset root hash')
51
+ const mimeType = typeof asset.mimeType === 'string' && asset.mimeType
52
+ ? asset.mimeType
53
+ : 'application/octet-stream'
54
+ const size = decimalSize(asset.size)
55
+ const key = `${asset.rootHash}\u0000${mimeType}\u0000${size}`
56
+ let group = groups.get(key)
57
+ if (!group) {
58
+ group = { root: asset.rootHash, mimeType, size, fields: [] }
59
+ groups.set(key, group)
60
+ }
61
+ addField(group.fields, fieldType, fieldValue)
62
+ if (fieldType === 'mark' && country) addField(group.fields, 'country', country)
63
+ }
64
+
65
+ if (uploadService === 'irfs') {
66
+ for (const file of files) addToGroup(file, 'path', normalizeManifestPath(file.filename))
67
+ }
68
+ for (const { asset, mark, country } of media) addToGroup(asset, 'mark', mark, country)
69
+
70
+ for (const group of groups.values()) {
71
+ tags.push(['r', group.root, ...group.fields, `m ${group.mimeType}`, `size ${group.size}`])
72
+ }
73
+ return tags
74
+ }
75
+
76
+ function buildMetadataTags ({
77
+ name, nameLang, isNameAuto, summary, summaryLang, isSummaryAuto,
78
+ isIconAuto, hasIcon, descriptions, self, countries, categories, hashtags
79
+ }) {
80
+ const tags = []
81
+ const trimmedName = typeof name === 'string' ? name.trim() : ''
82
+ const trimmedSummary = typeof summary === 'string' ? summary.trim() : ''
83
+
84
+ if (Array.isArray(countries) && countries.length) {
85
+ for (const country of countries) {
86
+ if (typeof country === 'string' && country) tags.push(['c', country])
87
+ }
88
+ } else {
89
+ tags.push(['c', '*'])
90
+ }
91
+ if (typeof self === 'string' && self) tags.push(['self', self])
92
+
93
+ let categoryCount = 0
94
+ for (const entry of Array.isArray(categories) ? categories : []) {
95
+ if (categoryCount >= 3) break
96
+ if (!Array.isArray(entry)) continue
97
+ const [category, subcategories] = entry
98
+ for (const subcategory of Array.isArray(subcategories) ? subcategories : []) {
99
+ if (categoryCount >= 3) break
100
+ if (NAPP_CATEGORIES[category]?.includes(subcategory)) {
101
+ tags.push(['l', `napp.${category}:${subcategory}`])
102
+ categoryCount++
103
+ }
104
+ }
105
+ }
106
+
107
+ for (const entry of (Array.isArray(hashtags) ? hashtags : []).slice(0, 3)) {
108
+ if (!Array.isArray(entry) || typeof entry[0] !== 'string') continue
109
+ const value = entry[0].replace(/\s/g, '').toLowerCase()
110
+ if (!value) continue
111
+ const tag = ['t', value]
112
+ if (typeof entry[1] === 'string' && entry[1]) tag.push(entry[1])
113
+ tags.push(tag)
114
+ }
115
+
116
+ if (trimmedName) {
117
+ const tag = ['name', trimmedName]
118
+ if (typeof nameLang === 'string' && nameLang) tag.push(nameLang)
119
+ tags.push(tag)
120
+ if (isNameAuto) tags.push(['auto', 'name'])
121
+ }
122
+ if (trimmedSummary) {
123
+ const tag = ['summary', trimmedSummary]
124
+ if (typeof summaryLang === 'string' && summaryLang) tag.push(summaryLang)
125
+ tags.push(tag)
126
+ if (isSummaryAuto) tags.push(['auto', 'summary'])
127
+ }
128
+ for (const entry of Array.isArray(descriptions) ? descriptions : []) {
129
+ if (!Array.isArray(entry) || typeof entry[0] !== 'string' || !entry[0]) continue
130
+ const tag = ['description', entry[0]]
131
+ if (typeof entry[1] === 'string' && entry[1]) tag.push(entry[1])
132
+ tags.push(tag)
133
+ }
134
+ if (isIconAuto && hasIcon) tags.push(['auto', 'icon'])
135
+ return tags
136
+ }
137
+
138
+ export function buildManifestTags ({
139
+ dTag, uploadService, fileMetadata = [], icon, keyArt = [], screenshots = [],
140
+ previousTags = [], ...metadata
141
+ }) {
142
+ if (uploadService !== 'irfs' && uploadService !== 'blossom') {
143
+ throw new Error('Unknown upload service')
144
+ }
145
+ const media = []
146
+ if (icon) media.push({ asset: icon, mark: 'icon' })
147
+ for (const asset of keyArt) media.push({ asset, mark: 'key_art', country: asset.country })
148
+ for (const asset of screenshots) media.push({ asset, mark: 'screenshot', country: asset.country })
149
+
150
+ const unknownTags = (Array.isArray(previousTags) ? previousTags : [])
151
+ .filter(tag => Array.isArray(tag) && !MANAGED_MANIFEST_TAGS.has(tag[0]))
152
+ .slice(0, 10)
153
+ .map(tag => [...tag])
154
+
155
+ return [
156
+ ['d', dTag],
157
+ ...buildReferenceTags(uploadService, fileMetadata, media),
158
+ ['service', uploadService],
159
+ ...buildMetadataTags({ ...metadata, hasIcon: Boolean(icon) }),
160
+ ...unknownTags
161
+ ]
162
+ }
163
+
164
+ function manifestKind (channel) {
165
+ return {
166
+ main: 35128,
167
+ next: 35129,
168
+ draft: 35130
169
+ }[channel] ?? 35128
170
+ }
171
+
172
+ function newestFirst (a, b) {
173
+ if (b.created_at !== a.created_at) return b.created_at - a.created_at
174
+ return String(a.id).localeCompare(String(b.id))
175
+ }
176
+
177
+ export async function uploadSiteManifest ({
178
+ dTag, channel, fileMetadata, uploadService, signer, pause = 0,
179
+ shouldReupload = false, log = () => {}, ...metadata
180
+ }) {
181
+ const kind = manifestKind(channel)
182
+ const relays = [...new Set([...(await signer.getRelays()).write, ...nappRelays]
183
+ .map(relay => relay.trim().replace(/\/$/, '')))]
184
+ const events = (await nostrRelays.getEvents({
185
+ kinds: [kind],
186
+ authors: [await signer.getPublicKey()],
187
+ '#d': [dTag],
188
+ limit: 1
189
+ }, relays)).result
190
+ events.sort(newestFirst)
191
+ const previous = events[0]
192
+ const tags = buildManifestTags({
193
+ dTag, uploadService, fileMetadata, previousTags: previous?.tags, ...metadata
194
+ })
195
+
196
+ if (!shouldReupload && previous && previous.content === '' && JSON.stringify(previous.tags) === JSON.stringify(tags)) {
197
+ const coveredRelays = new Set(events
198
+ .filter(event => event.id === previous.id)
199
+ .map(event => event.meta?.relay)
200
+ .filter(Boolean))
201
+ const missingRelays = relays.filter(relay => !coveredRelays.has(relay))
202
+ if (!missingRelays.length) return previous
203
+ log(`Re-uploading existing site manifest to ${missingRelays.length} missing relays`)
204
+ await throttledSendEvent(previous, missingRelays, {
205
+ pause, trailingPause: true, log, minSuccessfulRelays: 0
206
+ })
207
+ return previous
208
+ }
209
+
210
+ const now = Math.floor(Date.now() / 1000)
211
+ const createdAt = Math.max(now, (previous?.created_at ?? -1) + 1)
212
+ if (createdAt > now + 172800) throw new Error('Existing manifest timestamp is too far in the future to replace safely')
213
+ const event = await signer.signEvent({ kind, tags, content: '', created_at: createdAt })
214
+ await throttledSendEvent(event, relays, { pause, trailingPause: true, log })
215
+ return event
216
+ }
package/bin/GEMINI.md DELETED
@@ -1,6 +0,0 @@
1
- # bin folder
2
-
3
- Place one-off scripts used by package.json's "bin" field.
4
-
5
- Each script lives on its own subdirectory and may have its own helpers.js file
6
- with helper functions.
@@ -1,12 +0,0 @@
1
- # helpers folder
2
-
3
- - Place helper functions. A helper function is a single-step functionality to be re-used through out the code base.
4
- - Each functionality is exported as a single function.
5
- - These functions are not meant to be used within the test folder or sub-folders, except for being unit-tested themselves.
6
-
7
- ## General Instructions:
8
-
9
- - Prefer grouping functions that deal with the same type of variable within the
10
- same file, preferably named after the type name. E.g.: use `helpers/string.js` for
11
- exporting helper functions that handle strings, or e.g.: use helpers/stream.js` to
12
- deal with streams.
@@ -1,131 +0,0 @@
1
- const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
2
-
3
- const ALPHABET_MAP = {}
4
- for (let z = 0; z < ALPHABET.length; z++) {
5
- const x = ALPHABET.charAt(z)
6
- ALPHABET_MAP[x] = z
7
- }
8
-
9
- function polymod (values) {
10
- const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
11
- let chk = 1
12
- for (let p = 0; p < values.length; ++p) {
13
- const top = chk >> 25
14
- chk = (chk & 0x1ffffff) << 5 ^ values[p]
15
- for (let i = 0; i < 5; ++i) {
16
- if ((top >> i) & 1) {
17
- chk ^= GEN[i]
18
- }
19
- }
20
- }
21
- return chk
22
- }
23
-
24
- function hrpExpand (hrp) {
25
- const ret = []
26
- for (let p = 0; p < hrp.length; ++p) {
27
- ret.push(hrp.charCodeAt(p) >> 5)
28
- }
29
- ret.push(0)
30
- for (let p = 0; p < hrp.length; ++p) {
31
- ret.push(hrp.charCodeAt(p) & 31)
32
- }
33
- return ret
34
- }
35
-
36
- function verifyChecksum (hrp, data) {
37
- return polymod(hrpExpand(hrp).concat(data)) === 1
38
- }
39
-
40
- function createChecksum (hrp, data) {
41
- const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0])
42
- const mod = polymod(values) ^ 1
43
- const ret = []
44
- for (let p = 0; p < 6; ++p) {
45
- ret.push((mod >> 5 * (5 - p)) & 31)
46
- }
47
- return ret
48
- }
49
-
50
- export function encode (hrp, data) {
51
- const combined = data.concat(createChecksum(hrp, data))
52
- let ret = hrp + '1'
53
- for (let p = 0; p < combined.length; ++p) {
54
- ret += ALPHABET.charAt(combined[p])
55
- }
56
- return ret
57
- }
58
-
59
- export function decode (bechString, limit = 90) {
60
- let p
61
- let hasLower = false
62
- let hasUpper = false
63
- for (p = 0; p < bechString.length; ++p) {
64
- if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) {
65
- return null
66
- }
67
- if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) {
68
- hasLower = true
69
- }
70
- if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) {
71
- hasUpper = true
72
- }
73
- }
74
- if (hasLower && hasUpper) {
75
- return null
76
- }
77
- bechString = bechString.toLowerCase()
78
- const pos = bechString.lastIndexOf('1')
79
- if (pos < 1 || pos + 7 > bechString.length || bechString.length > limit) {
80
- return null
81
- }
82
- const hrp = bechString.substring(0, pos)
83
- const data = []
84
- for (p = pos + 1; p < bechString.length; ++p) {
85
- const d = ALPHABET_MAP[bechString.charAt(p)]
86
- if (d === undefined) {
87
- return null
88
- }
89
- data.push(d)
90
- }
91
- if (!verifyChecksum(hrp, data)) {
92
- return null
93
- }
94
- return { hrp, data: data.slice(0, data.length - 6) }
95
- }
96
-
97
- export function toWords (data) {
98
- let value = 0
99
- let bits = 0
100
- const maxV = 31
101
- const result = []
102
- for (let i = 0; i < data.length; ++i) {
103
- value = (value << 8) | data[i]
104
- bits += 8
105
- while (bits >= 5) {
106
- bits -= 5
107
- result.push((value >> bits) & maxV)
108
- }
109
- }
110
- if (bits > 0) {
111
- result.push((value << (5 - bits)) & maxV)
112
- }
113
- return result
114
- }
115
-
116
- export function fromWords (data) {
117
- let value = 0
118
- let bits = 0
119
- const maxV = 255
120
- const result = []
121
- for (let i = 0; i < data.length; ++i) {
122
- const element = data[i]
123
- value = (value << 5) | element
124
- bits += 5
125
- while (bits >= 8) {
126
- bits -= 8
127
- result.push((value >> bits) & maxV)
128
- }
129
- }
130
- return result
131
- }