libp2r2p 0.10.17 → 0.10.18
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 +11 -0
- package/irfs/README.md +35 -0
- package/irfs/index.js +74 -0
- package/nip27/index.js +22 -2
- package/nip94/README.md +40 -0
- package/nip94/index.js +55 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -555,3 +555,14 @@ bytes, while integer mode supports fixed-width identifiers.
|
|
|
555
555
|
In NIP-5A, "no padding" means that no separate padding character such as `=`
|
|
556
556
|
is used. Leading `0` digits are nevertheless required to make every Nsite
|
|
557
557
|
Base36 value exactly 50 characters long.
|
|
558
|
+
|
|
559
|
+
## Files
|
|
560
|
+
|
|
561
|
+
[`libp2r2p/irfs`](irfs/README.md) prepares retryable chunk templates from files,
|
|
562
|
+
with explicit cancellation and resource release. [`libp2r2p/nip94`](nip94/README.md)
|
|
563
|
+
builds/interprets file metadata, including the local IRFS profile. NIP-27 extracts
|
|
564
|
+
`https://nostr.alt/nfile1…?localOnly=1` up to the NIP-19 codec's 5,000-character
|
|
565
|
+
limit, retaining the full URL and exposing decoded `url.nfile` and MIME `url.m`.
|
|
566
|
+
|
|
567
|
+
The NIP-94 extension also carries optional `download` intent; see
|
|
568
|
+
[nip94/README.md](nip94/README.md#download-intent) for event and inline URL forms.
|
package/irfs/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# IRFS preparation
|
|
2
|
+
|
|
3
|
+
`libp2r2p/irfs` prepares an immutable browser `File`/`Blob` or `Uint8Array`.
|
|
4
|
+
It has no signer, publisher, relay pool or launcher dependency.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import { prepareIrfsFile, decodeIrfsChunk } from 'libp2r2p/irfs'
|
|
8
|
+
const prepared = await prepareIrfsFile(file, { signal })
|
|
9
|
+
try {
|
|
10
|
+
const created_at = Math.floor(Date.now() / 1000)
|
|
11
|
+
for await (const template of prepared.chunks({ created_at, signal })) {
|
|
12
|
+
// Sign/store/publish according to the consuming application's policy.
|
|
13
|
+
const verified = decodeIrfsChunk(template)
|
|
14
|
+
}
|
|
15
|
+
// prepared.chunks({ created_at }) may be iterated again for retry.
|
|
16
|
+
} finally {
|
|
17
|
+
prepared.close()
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The result exposes `root` (hex MMR root), `size`, `total`, `chunks()` and
|
|
22
|
+
idempotent `close()`. Blocks contain at most 51,000 bytes; only the final block
|
|
23
|
+
may be smaller. Templates use kind 34601, deterministic `d` identifiers, an
|
|
24
|
+
`mmr` tag containing decimal index/total and Base93 proof, and Base93 content.
|
|
25
|
+
Use a stable timestamp when identical templates are required across retries.
|
|
26
|
+
`decodeIrfsChunk()` validates the proof, identifier, root and block size.
|
|
27
|
+
|
|
28
|
+
Preparation retains the immutable input and an in-memory NMMR hash tree, not
|
|
29
|
+
a second copy of file bytes. It creates **no temporary IndexedDB database**.
|
|
30
|
+
`close()` drops its input/tree references and abort-listener registration;
|
|
31
|
+
abort also closes preparation. In-flight Blob reads finish but cannot yield
|
|
32
|
+
another chunk after cancellation. Consumers own any object URLs they create.
|
|
33
|
+
Hash memory scales with chunk count. `onProgress({ completed, total })` reports
|
|
34
|
+
bytes hashed; preparation yields regularly for input/cancellation.
|
|
35
|
+
Empty input raises `ValidationError('EMPTY_IRFS_FILE')` in this version.
|
package/irfs/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import NMMR, { InMemoryMMR } from 'nmmr'
|
|
2
|
+
import { encode, decode } from '../base93/index.js'
|
|
3
|
+
import { bytesToBase16 } from '../base16/index.js'
|
|
4
|
+
import { ValidationError } from '../error/index.js'
|
|
5
|
+
|
|
6
|
+
export const IRFS_CHUNK_BYTES = 51000
|
|
7
|
+
export const IRFS_CHUNK_KIND = 34601
|
|
8
|
+
|
|
9
|
+
// Blob/File input is immutable and seekable: retain it instead of copying its
|
|
10
|
+
// bytes to temporary IndexedDB. Only tree hashes and leaf positions stay in RAM.
|
|
11
|
+
export async function prepareIrfsFile (input, { signal, onProgress } = {}) {
|
|
12
|
+
let blob = input instanceof Uint8Array ? new Blob([input]) : input
|
|
13
|
+
if (!(blob instanceof Blob)) throw new ValidationError('INVALID_IRFS_FILE')
|
|
14
|
+
if (!blob.size) throw new ValidationError('EMPTY_IRFS_FILE')
|
|
15
|
+
let tree = new InMemoryMMR()
|
|
16
|
+
let positions = []
|
|
17
|
+
const size = blob.size
|
|
18
|
+
const total = Math.ceil(size / IRFS_CHUNK_BYTES)
|
|
19
|
+
let closed = false
|
|
20
|
+
const check = currentSignal => {
|
|
21
|
+
signal?.throwIfAborted()
|
|
22
|
+
currentSignal?.throwIfAborted()
|
|
23
|
+
if (closed) throw new Error('IRFS preparation closed')
|
|
24
|
+
}
|
|
25
|
+
const close = () => {
|
|
26
|
+
closed = true
|
|
27
|
+
blob = tree = positions = null
|
|
28
|
+
signal?.removeEventListener('abort', close)
|
|
29
|
+
}
|
|
30
|
+
signal?.addEventListener('abort', close, { once: true })
|
|
31
|
+
try {
|
|
32
|
+
for (let index = 0; index < total; index++) {
|
|
33
|
+
check()
|
|
34
|
+
const bytes = new Uint8Array(await blob.slice(index * IRFS_CHUNK_BYTES, (index + 1) * IRFS_CHUNK_BYTES).arrayBuffer())
|
|
35
|
+
check()
|
|
36
|
+
positions.push(Number(tree.append(bytes).leafIdx))
|
|
37
|
+
onProgress?.({ completed: Math.min(size, (index + 1) * IRFS_CHUNK_BYTES), total: size })
|
|
38
|
+
// Yield to input and cancellation even when a Blob read resolves immediately.
|
|
39
|
+
if (index % 32 === 31) await new Promise(resolve => setTimeout(resolve, 0))
|
|
40
|
+
}
|
|
41
|
+
const root = bytesToBase16(tree.bagThePeaks())
|
|
42
|
+
return {
|
|
43
|
+
root, size, total, close,
|
|
44
|
+
async * chunks ({ created_at: createdAt = Math.floor(Date.now() / 1000), signal: readSignal } = {}) {
|
|
45
|
+
if (!Number.isSafeInteger(createdAt) || createdAt < 0) throw new ValidationError('INVALID_IRFS_TIMESTAMP')
|
|
46
|
+
for (let index = 0; index < total; index++) {
|
|
47
|
+
check(readSignal)
|
|
48
|
+
const bytes = new Uint8Array(await blob.slice(index * IRFS_CHUNK_BYTES, (index + 1) * IRFS_CHUNK_BYTES).arrayBuffer())
|
|
49
|
+
check(readSignal)
|
|
50
|
+
const hashes = tree.getProofArray(positions[index])
|
|
51
|
+
const proof = new Uint8Array(hashes.length * 32)
|
|
52
|
+
hashes.forEach((hash, offset) => proof.set(hash, offset * 32))
|
|
53
|
+
yield { kind: IRFS_CHUNK_KIND, created_at: createdAt, tags: [['d', NMMR.deriveChunkId(root, index)], ['mmr', String(index), String(total), encode(proof)]], content: encode(bytes) }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) { close(); throw error }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function decodeIrfsChunk (event) {
|
|
61
|
+
try {
|
|
62
|
+
if (event?.kind !== IRFS_CHUNK_KIND || !Array.isArray(event.tags)) throw new Error('Invalid kind or tags')
|
|
63
|
+
const d = event.tags.filter(tag => tag[0] === 'd')
|
|
64
|
+
const mmr = event.tags.filter(tag => tag[0] === 'mmr')
|
|
65
|
+
if (d.length !== 1 || d[0].length !== 2 || mmr.length !== 1 || mmr[0].length !== 4) throw new Error('Invalid chunk tags')
|
|
66
|
+
const [, index, total, encodedProof] = mmr[0]
|
|
67
|
+
const contentBytes = decode(event.content)
|
|
68
|
+
const proof = decode(encodedProof)
|
|
69
|
+
const root = NMMR.calculateRoot({ contentBytes, index, total, proof })
|
|
70
|
+
if (!contentBytes.length || contentBytes.length > IRFS_CHUNK_BYTES || (Number(index) < Number(total) - 1 && contentBytes.length !== IRFS_CHUNK_BYTES)) throw new Error('Invalid chunk length')
|
|
71
|
+
if (NMMR.deriveChunkId(root, index) !== d[0][1]) throw new Error('Invalid chunk ID')
|
|
72
|
+
return { root, index: Number(index), total: Number(total), contentBytes, proof }
|
|
73
|
+
} catch (cause) { throw new ValidationError('INVALID_IRFS_CHUNK', { cause }) }
|
|
74
|
+
}
|
package/nip27/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ValidationError } from '../error/index.js'
|
|
|
2
2
|
import {
|
|
3
3
|
NAPP_ENTITY_REGEX,
|
|
4
4
|
naddrDecode,
|
|
5
|
+
nfileDecode,
|
|
5
6
|
neventDecode,
|
|
6
7
|
noteDecode,
|
|
7
8
|
nrelayDecode
|
|
@@ -75,6 +76,7 @@ function getReferencesRegex (bareNip05) {
|
|
|
75
76
|
...(bareNip05 ? [NIP05_BARE_ROOT, NIP05_BARE_CUSTOM] : [])
|
|
76
77
|
]
|
|
77
78
|
const alternatives = [
|
|
79
|
+
'(?<nfileUrl>https://nostr[.]alt/(?<nfileEntity>nfile1[ac-hj-np-z02-9]{1,4994})(?:[?]localOnly=1)?(?:#[-_+.A-Za-z0-9%=&*]{1,4000})?)',
|
|
78
80
|
URL_SOURCE,
|
|
79
81
|
APP_SOURCE,
|
|
80
82
|
NIP05_STANDARD,
|
|
@@ -203,7 +205,8 @@ const NIP94_TAGS = {
|
|
|
203
205
|
image: ['image'],
|
|
204
206
|
summary: ['summary'],
|
|
205
207
|
alt: ['alt'],
|
|
206
|
-
caption: ['caption']
|
|
208
|
+
caption: ['caption'],
|
|
209
|
+
download: ['download']
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
function validateTagConfigs (extraTags) {
|
|
@@ -236,11 +239,20 @@ export function decodeMediaMetadata (url, { extraTags } = {}) {
|
|
|
236
239
|
|
|
237
240
|
const tags = extraTags ? { ...NIP94_TAGS, ...extraTags } : NIP94_TAGS
|
|
238
241
|
const tagIndexes = {}
|
|
242
|
+
// Validate against the complete fragment before the generic bounded parser:
|
|
243
|
+
// punctuation must not turn an invalid value such as 1/foo into a valid 1.
|
|
244
|
+
const fragment = url.includes('#') ? url.slice(url.indexOf('#') + 1) : ''
|
|
245
|
+
const downloads = fragment.split('&').filter(item => decodeFragmentValue(item.split('=')[0]) === 'download')
|
|
246
|
+
if (downloads.length > 1 || downloads.some(item => {
|
|
247
|
+
const parts = item.split('=')
|
|
248
|
+
return parts.length !== 2 || !['0', '1'].includes(decodeFragmentValue(parts[1]))
|
|
249
|
+
})) throw new ValidationError('INVALID_MEDIA_METADATA_DOWNLOAD')
|
|
239
250
|
const obj = (url.match(/(?<=#)[-_+.A-Za-z0-9%=&*]{1,4000}/)?.[0] || '')
|
|
240
251
|
.split('&')
|
|
241
252
|
.filter(Boolean)
|
|
242
253
|
.reduce((memo, item) => {
|
|
243
|
-
|
|
254
|
+
const parts = item.split('=')
|
|
255
|
+
let [key, value = ''] = parts
|
|
244
256
|
key = decodeFragmentValue(key)
|
|
245
257
|
value = decodeFragmentValue(value)
|
|
246
258
|
const config = tags[key]
|
|
@@ -299,6 +311,14 @@ function getReferenceItem (original, groups, { getMimeType, defaultAppUser }) {
|
|
|
299
311
|
: { key: 'text', text: { value: original } }
|
|
300
312
|
}
|
|
301
313
|
|
|
314
|
+
if (groups.nfileUrl) {
|
|
315
|
+
try {
|
|
316
|
+
const file = nfileDecode(groups.nfileEntity)
|
|
317
|
+
const metadata = tryDecodeMediaMetadata(groups.nfileUrl) || {}
|
|
318
|
+
return { key: 'url', url: { ...metadata, value: groups.nfileUrl, nfile: file, ...(file.mime ? { m: file.mime } : {}) } }
|
|
319
|
+
} catch { return { key: 'text', text: { value: original } } }
|
|
320
|
+
}
|
|
321
|
+
|
|
302
322
|
if (groups.url) {
|
|
303
323
|
const url = `${groups.protocol ? '' : 'https://'}${groups.url}`
|
|
304
324
|
let mediaMetadata = {}
|
package/nip94/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# File metadata
|
|
2
|
+
|
|
3
|
+
`libp2r2p/nip94` exports `createFileMetadata(options)` and
|
|
4
|
+
`decodeFileMetadata(event)`. The former creates an unsigned kind-1063 template;
|
|
5
|
+
the latter reads and validates its tags without parsing the caption as a URL.
|
|
6
|
+
|
|
7
|
+
This is an **adaptation/extension of NIP-94**, not a claim that every emitted
|
|
8
|
+
event meets its SHA-256 requirements. `url` and `mime` are required. Options
|
|
9
|
+
include `caption`, `created_at`, `size`, `width`/`height`, `alt`, `root`, `service`,
|
|
10
|
+
`thumbhash`, `sha256`, `originalSha256`, and additional `tags` (e.g. replies).
|
|
11
|
+
The caption becomes `.content`. `root` maps to `r`, dimensions to `dim`, hashes
|
|
12
|
+
to `x`/`ox`; hashes are optional and never synthesized. ThumbHash is transported
|
|
13
|
+
unchanged as Base64, independently of image decoding. No `blurhash` is generated.
|
|
14
|
+
|
|
15
|
+
For local IRFS files use `service: 'irfs'` and
|
|
16
|
+
`https://nostr.alt/nfile1…?localOnly=1`. Encode the MMR root, MIME and filename
|
|
17
|
+
with `nfileEncode`; relay/author hints are unnecessary. The decoder verifies
|
|
18
|
+
agreement between `r`/`m` and nfile metadata and exposes its `filename`.
|
|
19
|
+
The `r` reference allows storage owners to retain the associated local chunks.
|
|
20
|
+
The module does not encrypt, sign, store or download data.
|
|
21
|
+
|
|
22
|
+
## Download intent
|
|
23
|
+
|
|
24
|
+
`download` is an optional extension expressing the author's intended action:
|
|
25
|
+
`'1'` asks clients to download on activation rather than open/play the media.
|
|
26
|
+
It does not prevent thumbnails or enforce server behavior.
|
|
27
|
+
|
|
28
|
+
For kind 1063, no tag and `['download', '0']` both decode to `download: '0'`;
|
|
29
|
+
`['download']` and `['download', '1']` both decode to `download: '1'`.
|
|
30
|
+
Empty/other values, extra fields and duplicate download tags are invalid.
|
|
31
|
+
`createFileMetadata({ ..., download: '0' | '1' })` emits an explicit value;
|
|
32
|
+
omitting the option emits no download tag. Boolean/numeric options are invalid.
|
|
33
|
+
|
|
34
|
+
For inline URLs, `nip27.decodeMediaMetadata()` requires an explicit
|
|
35
|
+
`#download=0` or `#download=1` (or `&download=...` after other fragment fields).
|
|
36
|
+
A bare/empty/duplicate/invalid value throws `INVALID_MEDIA_METADATA_DOWNLOAD`;
|
|
37
|
+
`tryDecodeMediaMetadata()` returns null. No fragment means no download property.
|
|
38
|
+
`extractMedia()` carries the string flag for ordinary URLs and nfile URLs,
|
|
39
|
+
including long nfile entities and `?localOnly=1`. Metadata fragments do not
|
|
40
|
+
change the bytes or root of a file; clients strip them from download routes.
|
package/nip94/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { FILE_METADATA } from '../kind/index.js'
|
|
2
|
+
import { nfileDecode } from '../nip19/index.js'
|
|
3
|
+
import { ValidationError } from '../error/index.js'
|
|
4
|
+
|
|
5
|
+
// NIP-94 fields plus the IRFS r/service and ThumbHash extensions. SHA-256
|
|
6
|
+
// x/ox are optional in this profile; an MMR root is never substituted for x.
|
|
7
|
+
export function decodeFileMetadata (event) {
|
|
8
|
+
if (event?.kind !== FILE_METADATA || typeof event.content !== 'string' || !Array.isArray(event.tags)) throw new ValidationError('INVALID_FILE_METADATA')
|
|
9
|
+
const field = name => {
|
|
10
|
+
const tags = event.tags.filter(tag => Array.isArray(tag) && tag[0] === name)
|
|
11
|
+
if (tags.length > 1 || (tags.length && (tags[0].length < 2 || typeof tags[0][1] !== 'string'))) throw new ValidationError('INVALID_FILE_METADATA_TAG')
|
|
12
|
+
return tags[0]?.[1]
|
|
13
|
+
}
|
|
14
|
+
const downloadTags = event.tags.filter(tag => Array.isArray(tag) && tag[0] === 'download')
|
|
15
|
+
const download = downloadTags[0]
|
|
16
|
+
if (downloadTags.length > 1 || (download && (download.length > 2 || (download.length === 2 && !['0', '1'].includes(download[1]))))) throw new ValidationError('INVALID_FILE_METADATA_DOWNLOAD')
|
|
17
|
+
const result = { url: field('url'), mime: field('m'), caption: event.content, download: download ? download.length === 1 ? '1' : download[1] : '0' }
|
|
18
|
+
let url
|
|
19
|
+
try { url = new URL(result.url) } catch { throw new ValidationError('INVALID_FILE_METADATA_URL') }
|
|
20
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) throw new ValidationError('INVALID_FILE_METADATA_URL')
|
|
21
|
+
if (typeof result.mime !== 'string' || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(result.mime)) throw new ValidationError('INVALID_FILE_METADATA_MIME')
|
|
22
|
+
for (const [tag, key] of [['r', 'root'], ['service', 'service'], ['thumbhash', 'thumbhash'], ['x', 'sha256'], ['ox', 'originalSha256'], ['alt', 'alt']]) {
|
|
23
|
+
const value = field(tag)
|
|
24
|
+
if (value !== undefined) result[key] = value
|
|
25
|
+
}
|
|
26
|
+
for (const key of ['root', 'sha256', 'originalSha256']) if (result[key] !== undefined && !/^[0-9a-f]{64}$/.test(result[key])) throw new ValidationError('INVALID_FILE_METADATA_HASH')
|
|
27
|
+
const size = field('size')
|
|
28
|
+
if (size !== undefined) {
|
|
29
|
+
if (!/^(0|[1-9][0-9]*)$/.test(size) || !Number.isSafeInteger(Number(size))) throw new ValidationError('INVALID_FILE_METADATA_SIZE')
|
|
30
|
+
result.size = Number(size)
|
|
31
|
+
}
|
|
32
|
+
const dim = field('dim')
|
|
33
|
+
if (dim !== undefined) {
|
|
34
|
+
if (!/^[1-9][0-9]*x[1-9][0-9]*$/.test(dim)) throw new ValidationError('INVALID_FILE_METADATA_DIMENSIONS')
|
|
35
|
+
;[result.width, result.height] = dim.split('x').map(Number)
|
|
36
|
+
if (![result.width, result.height].every(Number.isSafeInteger)) throw new ValidationError('INVALID_FILE_METADATA_DIMENSIONS')
|
|
37
|
+
}
|
|
38
|
+
if (result.thumbhash !== undefined && !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(result.thumbhash)) throw new ValidationError('INVALID_FILE_METADATA_THUMBHASH')
|
|
39
|
+
if (url.origin === 'https://nostr.alt') {
|
|
40
|
+
const file = nfileDecode(url.pathname.slice(1))
|
|
41
|
+
if ((result.root && result.root !== file.root) || (file.mime && file.mime !== result.mime)) throw new ValidationError('FILE_METADATA_NFILE_MISMATCH')
|
|
42
|
+
result.filename = file.filename
|
|
43
|
+
}
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createFileMetadata ({ caption = '', created_at: createdAt = Math.floor(Date.now() / 1000), tags = [], ...metadata }) {
|
|
48
|
+
if (!Number.isSafeInteger(createdAt) || createdAt < 0 || !Array.isArray(tags)) throw new ValidationError('INVALID_FILE_METADATA')
|
|
49
|
+
if (metadata.download !== undefined && !['0', '1'].includes(metadata.download)) throw new ValidationError('INVALID_FILE_METADATA_DOWNLOAD')
|
|
50
|
+
const fields = [['download', metadata.download], ['url', metadata.url], ['m', metadata.mime], ['r', metadata.root], ['size', metadata.size], ['service', metadata.service], ['thumbhash', metadata.thumbhash], ['x', metadata.sha256], ['ox', metadata.originalSha256], ['alt', metadata.alt]]
|
|
51
|
+
if (metadata.width !== undefined || metadata.height !== undefined) fields.push(['dim', `${metadata.width}x${metadata.height}`])
|
|
52
|
+
const event = { kind: FILE_METADATA, created_at: createdAt, content: caption, tags: [...fields.filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]), ...tags.map(tag => [...tag])] }
|
|
53
|
+
decodeFileMetadata(event)
|
|
54
|
+
return event
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libp2r2p",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.18",
|
|
4
4
|
"description": "Peer-to-relay-to-peer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"p2r2p",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"idb",
|
|
26
26
|
"idb-queue",
|
|
27
27
|
"index.js",
|
|
28
|
+
"irfs",
|
|
28
29
|
"key",
|
|
29
30
|
"kind",
|
|
30
31
|
"network",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"nip44",
|
|
36
37
|
"nip44-v3",
|
|
37
38
|
"nip46",
|
|
39
|
+
"nip94",
|
|
38
40
|
"nip96",
|
|
39
41
|
"nip98",
|
|
40
42
|
"nwt",
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
"./event": "./event/index.js",
|
|
67
69
|
"./idb": "./idb/index.js",
|
|
68
70
|
"./idb-queue": "./idb-queue/index.js",
|
|
71
|
+
"./irfs": "./irfs/index.js",
|
|
69
72
|
"./key": "./key/index.js",
|
|
70
73
|
"./kind": "./kind/index.js",
|
|
71
74
|
"./network": "./network/index.js",
|
|
@@ -76,6 +79,7 @@
|
|
|
76
79
|
"./nip44": "./nip44/index.js",
|
|
77
80
|
"./nip44-v3": "./nip44-v3/index.js",
|
|
78
81
|
"./nip46": "./nip46/index.js",
|
|
82
|
+
"./nip94": "./nip94/index.js",
|
|
79
83
|
"./nip96": "./nip96/index.js",
|
|
80
84
|
"./nip98": "./nip98/index.js",
|
|
81
85
|
"./nwt": "./nwt/index.js",
|
|
@@ -92,7 +96,8 @@
|
|
|
92
96
|
"@noble/ciphers": "2.2.0",
|
|
93
97
|
"@noble/curves": "2.2.0",
|
|
94
98
|
"@noble/hashes": "2.2.0",
|
|
95
|
-
"@scure/base": "2.0.0"
|
|
99
|
+
"@scure/base": "2.0.0",
|
|
100
|
+
"nmmr": "2.0.0"
|
|
96
101
|
},
|
|
97
102
|
"devDependencies": {
|
|
98
103
|
"eslint": "^9.39.5",
|