Add fromPromenade method to Nip46Bunker

This commit is contained in:
Jon Staab
2025-11-26 14:42:41 -08:00
parent 2381c29799
commit 36997c127f
6 changed files with 336 additions and 31 deletions
+5 -3
View File
@@ -22,7 +22,7 @@ Sessions are stored in local storage and can be:
The simplest type of login is NIP 01, although it's generally a bad idea to be handling user keys. NIP 46, 44, or 07 login are preferable. However, NIP 01 can be useful for supporting signup, local profiles, or ephemeral keys. The simplest type of login is NIP 01, although it's generally a bad idea to be handling user keys. NIP 46, 44, or 07 login are preferable. However, NIP 01 can be useful for supporting signup, local profiles, or ephemeral keys.
```typescript ```typescript
import {makeSecret} from '@welshman/signer' import {makeSecret} from '@welshman/util'
import {loginWithNip01} from '@welshman/app' import {loginWithNip01} from '@welshman/app'
loginWithNip01(makeSecret()) loginWithNip01(makeSecret())
@@ -54,7 +54,8 @@ The best default signing scheme is [NIP 46](https://github.com/nostr-protocol/ni
The simpler `bunker://` handshake is done by asking the user to provide a bunker URL, either by QR code, or by pasting it manually into your application. The simpler `bunker://` handshake is done by asking the user to provide a bunker URL, either by QR code, or by pasting it manually into your application.
```typescript ```typescript
import {Nip46Broker, makeSecret} from "@welshman/signer" import {makeSecret} from "@welshman/util"
import {Nip46Broker} from "@welshman/signer"
import {loginWithNip46, nip46Perms} from "@welshman/app" import {loginWithNip46, nip46Perms} from "@welshman/app"
import {isKeyValid} from "src/util/nostr" import {isKeyValid} from "src/util/nostr"
@@ -96,7 +97,8 @@ if (!isKeyValid(signerPubkey)) {
Alternatively, you can provide the user with a `nostrconnect://` URL which they can copy or scan with their signer. This is a better UX for users using a signer on their mobile phone. Alternatively, you can provide the user with a `nostrconnect://` URL which they can copy or scan with their signer. This is a better UX for users using a signer on their mobile phone.
```typescript ```typescript
import {Nip46Broker, makeSecret} from "@welshman/signer" import {makeSecret} from "@welshman/util"
import {Nip46Broker} from "@welshman/signer"
import {loginWithNip46, nip46Perms} from "@welshman/app" import {loginWithNip46, nip46Perms} from "@welshman/app"
// Create a client secret // Create a client secret
+2 -2
View File
@@ -16,8 +16,8 @@ A Nostr signer implementation that supports multiple authentication methods and
## Quick Example ## Quick Example
```typescript ```typescript
import { makeEvent } from '@welshman/util' import { makeEvent, makeSecret } from '@welshman/util'
import { ISigner, Nip01Signer, makeSecret } from '@welshman/signer' import { ISigner, Nip01Signer } from '@welshman/signer'
const signer: ISigner = new Nip01Signer(makeSecret()) const signer: ISigner = new Nip01Signer(makeSecret())
const options = { const options = {
+4 -3
View File
@@ -20,12 +20,13 @@
"prepublishOnly": "pnpm run build" "prepublishOnly": "pnpm run build"
}, },
"dependencies": { "dependencies": {
"@noble/curves": "^1.7.0", "@jsr/fiatjaf__promenade-trusted-dealer": "^0.4.1",
"@noble/hashes": "^1.6.1", "@noble/curves": "^1.9.7",
"@noble/hashes": "^2.0.1",
"@welshman/lib": "workspace:*", "@welshman/lib": "workspace:*",
"@welshman/net": "workspace:*", "@welshman/net": "workspace:*",
"@welshman/util": "workspace:*", "@welshman/util": "workspace:*",
"nostr-tools": "^2.14.2" "nostr-tools": "^2.18.2"
}, },
"devDependencies": { "devDependencies": {
"@capacitor/core": "^7.2.0", "@capacitor/core": "^7.2.0",
+248 -16
View File
@@ -1,14 +1,36 @@
import {Emitter, throttle, makePromise, defer, sleep, tryCatch, randomId} from "@welshman/lib" import {trustedKeyDeal, hexShard, hexPubShard, KeyShard} from "@jsr/fiatjaf__promenade-trusted-dealer"
import {bytesToHex, hexToBytes, numberToBytesBE} from "@noble/curves/abstract/utils"
import type {AffinePoint} from "@noble/curves/abstract/curve"
import { import {
Emitter,
uniq,
spec,
inc,
throttle,
makePromise,
defer,
sleep,
tryCatch,
randomId,
MaybeAsync,
shuffle,
} from "@welshman/lib"
import {
getPubkey,
HashedEvent,
makeEvent, makeEvent,
makeSecret,
normalizeRelayUrl, normalizeRelayUrl,
TrustedEvent,
StampedEvent,
NOSTR_CONNECT, NOSTR_CONNECT,
hash, prep,
own, PROMENADE_REGISTER_ACCOUNT,
PROMENADE_SHARD_ACK,
PROMENADE_SHARD_SHARE,
RelayMode,
StampedEvent,
TrustedEvent,
} from "@welshman/util" } from "@welshman/util"
import {publish, request, AdapterContext} from "@welshman/net" import {publish, request, PublishStatus, AdapterContext} from "@welshman/net"
import {ISigner, EncryptionImplementation, signWithOptions, SignOptions, decrypt} from "../util.js" import {ISigner, EncryptionImplementation, signWithOptions, SignOptions, decrypt} from "../util.js"
import {Nip01Signer} from "./nip01.js" import {Nip01Signer} from "./nip01.js"
@@ -20,6 +42,12 @@ export const nip46Context = {
debug: false, debug: false,
} }
const nip46Log = (...args: any[]) => {
if (nip46Context.debug) {
console.log(...args)
}
}
export type Nip46Algorithm = "nip04" | "nip44" export type Nip46Algorithm = "nip04" | "nip44"
export enum Nip46Event { export enum Nip46Event {
@@ -58,6 +86,68 @@ export type Nip46ResponseWithError = {
error: string error: string
} }
export type PromenadeOptions = {
secret: string
policy: [number, number]
coordinatorUrl: string
signerPubkeys: string[]
onProgress?: (progress: number) => void
generatePow: (event: HashedEvent, difficulty: number) => MaybeAsync<HashedEvent>
getPubkeyRelays: (pubkey: string, mode: RelayMode) => MaybeAsync<string[]>
}
/*
const secret = 'fd8a80772a55d82ed963305b0f55299ac07e2fdf06d341f9e7c7223ec7bf57b0'
const pubkey = getPubkey(secret)
const coordinatorUrl = 'wss://promenade.coracle.social/'
const event = prep(
makeEvent(10002, {
tags: [["r", "wss://bucket.coracle.social/"], ["r", "wss://relay.damus.io/"], ["r", "wss://nos.lol/"]],
}),
pubkey,
)
event.sig = getSig(event, secret)
repository.publish(event)
nip46Context.debug = true
publish({event, relays: ["wss://purplepag.es/", "wss://indexer.coracle.social/"]}).then(async () => {
console.log("Published outbox relays")
const broker = await Nip46Broker.fromPromenade({
secret,
policy: [2, 3],
coordinatorUrl,
signerPubkeys: [
// '4440e4f93c9dcb0a5521f0bf949a1222698b72a1b1e3534b10537100fc94f97f',
// '23a3ff76766f5ffc852fa6f2fc5058c1306ee25927632e0f8e213af11a5b8de5',
'aa4f53d8041b88adee44cefb62fb49fdeb85d151d1a346e655850c213508ed2e',
// 'ad1c6fa1daca939685d34ab541fc9e7b450ef6295aa273addafee74a579d57fb',
// '3fcd012e970d9dfba4bc638ae9b6420e2ceca76f3b8e31d0ee3f408023a7c5fd',
// '4be49a6175734b43c7083ceac11e47bf684ffe65bd021c949bea1702409c119a',
'290238f7811a50b2b3ded97e42695f906b039fb3f5e2e2e3f77fd5a0b0c9a027',
'c66bebe38406a0b57593fcd8c893762dd9af8e488664c6d1a4eb3868b1f65526',
],
onProgress: p => console.log('progress', p),
generatePow: (e, d) => makePow(e, d).result,
getPubkeyRelays: async (k, m) => {
await forceLoadRelayList(k)
return getPubkeyRelays(k, m)
},
})
console.log('connect', await broker.connect(broker.params.connectSecret))
console.log('sign', await broker.signEvent(makeEvent(1)))
})
*/
export class PromenadeShardError extends Error {
constructor(
message: string,
readonly errorsBySignerPubkey: Map<string, string>,
) {
super(message)
}
}
const popupManager = (() => { const popupManager = (() => {
let pendingUrl = "" let pendingUrl = ""
let pendingSince = 0 let pendingSince = 0
@@ -188,9 +278,7 @@ export class Nip46Sender extends Emitter {
try { try {
await this.send(request) await this.send(request)
} catch (error: any) { } catch (error: any) {
if (nip46Context.debug) { nip46Log("nip46 error:", error, request)
console.log("nip46 error:", error, request)
}
} }
} }
} finally { } finally {
@@ -286,6 +374,154 @@ export class Nip46Broker extends Emitter {
return {relays, signerPubkey, connectSecret} return {relays, signerPubkey, connectSecret}
} }
static fromBunkerUrl = (url: string) => {
const clientSecret = makeSecret()
const {relays, signerPubkey, connectSecret} = Nip46Broker.parseBunkerUrl(url)
return new Nip46Broker({
relays,
clientSecret,
signerPubkey,
connectSecret,
})
}
static fromPromenade = async (options: PromenadeOptions) => {
const [m, n] = options.policy
if (options.signerPubkeys.length < n) {
throw new Error("Not enough signers to create all shards")
}
const deal = trustedKeyDeal(BigInt("0x" + options.secret), m, n)
// Add the VSS commits to each shard
// for (const shard of deal.shards) {
// shard.pubShard.vssCommit = deal.commits
// }
// Use the pubkey and adjusted secret from the deal (BIP-340 adjusted if needed)
const signer = Nip01Signer.fromSecret(options.secret)
const ourPubkey = await signer.getPubkey()
const ackRelays = await options.getPubkeyRelays(ourPubkey, RelayMode.Read)
const remainingSignerPubkeys = shuffle(uniq(options.signerPubkeys))
const errorsBySignerPubkey = new Map<string, string>()
const shardsBySignerPubkey = new Map<string, KeyShard>()
if (ackRelays.length === 0) {
throw new Error("No read relays returned for user pubkey")
}
nip46Log(`generated promenade shards for user ${ourPubkey}`, deal)
await Promise.all(
deal.shards.map(async (shard, i) => {
while (remainingSignerPubkeys.length > 0) {
const signerPubkey = remainingSignerPubkeys.shift()!
nip46Log(`generating proof of work for shard ${i}`)
const shardTemplate = makeEvent(PROMENADE_SHARD_SHARE, {
content: await signer.nip44.encrypt(signerPubkey, hexShard(shard)),
tags: [
["p", signerPubkey],
["coordinator", options.coordinatorUrl],
...ackRelays.map(url => ["reply", url]),
],
})
const shardTemplateWithWork = await tryCatch(() =>
options.generatePow(prep(shardTemplate, ourPubkey), 20),
)
if (!shardTemplateWithWork) {
errorsBySignerPubkey.set(signerPubkey, "Failed to generate work")
continue
}
const shardEvent = await signer.sign(shardTemplateWithWork)
const shardRelays = await options.getPubkeyRelays(signerPubkey, RelayMode.Read)
const publishResults = await publish({relays: shardRelays, event: shardEvent})
nip46Log(`published shard ${i} to signer ${signerPubkey}`, shardRelays, publishResults)
if (!Object.values(publishResults).some(spec({status: PublishStatus.Success}))) {
errorsBySignerPubkey.set(signerPubkey, "Failed to publish shard")
continue
}
const controller = new AbortController()
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(30_000)])
await request({
signal,
relays: ackRelays,
filters: [
{
kinds: [PROMENADE_SHARD_ACK],
authors: [signerPubkey],
"#p": [ourPubkey],
"#e": [shardEvent.id],
},
],
onEvent: (event: TrustedEvent, url: string) => {
nip46Log(`received ack for shard ${i} from signer ${signerPubkey} on ${url}`)
shardsBySignerPubkey.set(signerPubkey, shard)
options.onProgress?.(shardsBySignerPubkey.size / inc(n))
controller.abort()
},
})
if (shardsBySignerPubkey.has(signerPubkey)) {
break
} else {
errorsBySignerPubkey.set(signerPubkey, "Failed to receive shard ACK")
nip46Log(`failed to receive ack for shard ${i} from signer ${signerPubkey}`)
}
}
}),
)
if (shardsBySignerPubkey.size < deal.shards.length) {
throw new PromenadeShardError("Failed to publish all shards", errorsBySignerPubkey)
}
const connectSecret = randomId()
const signerSecret = makeSecret()
const signerPubkey = getPubkey(signerSecret)
const tags = [
["h", signerPubkey],
["threshold", String(m)],
["handlersecret", signerSecret],
["profile", "MAIN", connectSecret, ""],
]
for (const [pubkey, shard] of shardsBySignerPubkey) {
tags.push(["p", pubkey, hexPubShard(shard.pubShard)])
}
nip46Log(`registering coordinator account`, tags)
const relays = [options.coordinatorUrl]
const event = await signer.sign(makeEvent(PROMENADE_REGISTER_ACCOUNT, {tags}))
const accountResults = await publish({relays, event})
if (!Object.values(accountResults).some(spec({status: PublishStatus.Success}))) {
throw new Error("Failed to publish accounts to coordinator")
}
nip46Log(`successfully created promenade broker`)
const clientSecret = makeSecret()
return new Nip46Broker({
relays,
clientSecret,
signerPubkey,
connectSecret,
})
}
// Getters for helper objects // Getters for helper objects
makeSigner = () => new Nip01Signer(this.params.clientSecret) makeSigner = () => new Nip01Signer(this.params.clientSecret)
@@ -294,9 +530,7 @@ export class Nip46Broker extends Emitter {
const sender = new Nip46Sender(this.signer, this.params) const sender = new Nip46Sender(this.signer, this.params)
sender.on(Nip46Event.Send, (data: any) => { sender.on(Nip46Event.Send, (data: any) => {
if (nip46Context.debug) { nip46Log("nip46 send:", data)
console.log("nip46 send:", data)
}
}) })
return sender return sender
@@ -306,9 +540,7 @@ export class Nip46Broker extends Emitter {
const receiver = new Nip46Receiver(this.signer, this.params) const receiver = new Nip46Receiver(this.signer, this.params)
receiver.on(Nip46Event.Receive, (data: any) => { receiver.on(Nip46Event.Receive, (data: any) => {
if (nip46Context.debug) { nip46Log("nip46 receive:", data)
console.log("nip46 receive:", data)
}
}) })
return receiver return receiver
@@ -467,7 +699,7 @@ export class Nip46Signer implements ISigner {
sign = (template: StampedEvent, options: SignOptions = {}) => sign = (template: StampedEvent, options: SignOptions = {}) =>
signWithOptions( signWithOptions(
this.getPubkey().then(pubkey => this.broker.signEvent(hash(own(template, pubkey)))), this.getPubkey().then(pubkey => this.broker.signEvent(prep(template, pubkey))),
options, options,
) )
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import {hexToBytes} from "@noble/hashes/utils" import {hexToBytes} from "@noble/curves/abstract/utils"
import * as nt04 from "nostr-tools/nip04" import * as nt04 from "nostr-tools/nip04"
import * as nt44 from "nostr-tools/nip44" import * as nt44 from "nostr-tools/nip44"
import {Emitter, cached} from "@welshman/lib" import {Emitter, cached} from "@welshman/lib"
+76 -6
View File
@@ -383,12 +383,15 @@ importers:
packages/signer: packages/signer:
dependencies: dependencies:
'@jsr/fiatjaf__promenade-trusted-dealer':
specifier: ^0.4.1
version: 0.4.1
'@noble/curves': '@noble/curves':
specifier: ^1.7.0 specifier: ^1.9.7
version: 1.8.1 version: 1.9.7
'@noble/hashes': '@noble/hashes':
specifier: ^1.6.1 specifier: ^2.0.1
version: 1.7.1 version: 2.0.1
'@welshman/lib': '@welshman/lib':
specifier: workspace:* specifier: workspace:*
version: link:../lib version: link:../lib
@@ -399,8 +402,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../util version: link:../util
nostr-tools: nostr-tools:
specifier: ^2.14.2 specifier: ^2.18.2
version: 2.14.2(typescript@5.8.2) version: 2.18.2(typescript@5.8.2)
devDependencies: devDependencies:
'@capacitor/core': '@capacitor/core':
specifier: ^7.2.0 specifier: ^7.2.0
@@ -849,6 +852,15 @@ packages:
'@jridgewell/trace-mapping@0.3.9': '@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@jsr/fiatjaf__promenade-trusted-dealer@0.4.1':
resolution: {integrity: sha512-K9WjpDkQGyLl5gUZBLr3Gb+b5b1r8miZmDOo4+ZlzGQgoXD2TaqT+dkBjL/yLj/pYwBcd1Bschv0xuNpguL2ZQ==, tarball: https://npm.jsr.io/~/11/@jsr/fiatjaf__promenade-trusted-dealer/0.4.1.tgz}
'@jsr/henrygd__semaphore@0.0.2':
resolution: {integrity: sha512-nrwZ8HaqU1Agb2ij8omIxaOCAsKkDHWcwS9hTRumPhZuptwh6/0BJExBd8+eClTYM7jBnZxK+cP4WJRTcHBvCA==, tarball: https://npm.jsr.io/~/11/@jsr/henrygd__semaphore/0.0.2.tgz}
'@jsr/nostr__tools@2.16.2':
resolution: {integrity: sha512-QK1XwHvAnqEwbimD+ywbLQ3T2iI+/qE/zrRgOhmtjoEGlCWgtbPTNJ6Y/MEunXr6H/MnuHV+s400i/Yk4suvGQ==, tarball: https://npm.jsr.io/~/11/@jsr/nostr__tools/2.16.2.tgz}
'@microsoft/api-extractor-model@7.32.1': '@microsoft/api-extractor-model@7.32.1':
resolution: {integrity: sha512-u4yJytMYiUAnhcNQcZDTh/tVtlrzKlyKrQnLOV+4Qr/5gV+cpufWzCYAB1Q23URFqD6z2RoL2UYncM9xJVGNKA==} resolution: {integrity: sha512-u4yJytMYiUAnhcNQcZDTh/tVtlrzKlyKrQnLOV+4Qr/5gV+cpufWzCYAB1Q23URFqD6z2RoL2UYncM9xJVGNKA==}
@@ -875,6 +887,10 @@ packages:
resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==}
engines: {node: ^14.21.3 || >=16} engines: {node: ^14.21.3 || >=16}
'@noble/curves@1.9.7':
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
engines: {node: ^14.21.3 || >=16}
'@noble/hashes@1.3.1': '@noble/hashes@1.3.1':
resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
@@ -887,6 +903,14 @@ packages:
resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==}
engines: {node: ^14.21.3 || >=16} engines: {node: ^14.21.3 || >=16}
'@noble/hashes@1.8.0':
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
'@noble/hashes@2.0.1':
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2331,6 +2355,14 @@ packages:
typescript: typescript:
optional: true optional: true
nostr-tools@2.18.2:
resolution: {integrity: sha512-lUCJQd9YZG3kEvxV5Zgm7qUkBpaeuvFrtqBz4TJLAxHzUn2pE7nmZZRDQmNzp5neEw20tQS3jR16o7XzzF8ncg==}
peerDependencies:
typescript: '>=5.0.0'
peerDependenciesMeta:
typescript:
optional: true
nostr-wasm@0.1.0: nostr-wasm@0.1.0:
resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==}
@@ -3407,6 +3439,24 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@jsr/fiatjaf__promenade-trusted-dealer@0.4.1':
dependencies:
'@jsr/henrygd__semaphore': 0.0.2
'@jsr/nostr__tools': 2.16.2
'@noble/curves': 1.9.7
'@jsr/henrygd__semaphore@0.0.2': {}
'@jsr/nostr__tools@2.16.2':
dependencies:
'@noble/ciphers': 0.5.3
'@noble/curves': 1.2.0
'@noble/hashes': 1.3.1
'@scure/base': 1.1.1
'@scure/bip32': 1.3.1
'@scure/bip39': 1.2.1
nostr-wasm: 0.1.0
'@microsoft/api-extractor-model@7.32.1(@types/node@22.13.17)': '@microsoft/api-extractor-model@7.32.1(@types/node@22.13.17)':
dependencies: dependencies:
'@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc': 0.16.0
@@ -3457,12 +3507,20 @@ snapshots:
dependencies: dependencies:
'@noble/hashes': 1.7.1 '@noble/hashes': 1.7.1
'@noble/curves@1.9.7':
dependencies:
'@noble/hashes': 1.8.0
'@noble/hashes@1.3.1': {} '@noble/hashes@1.3.1': {}
'@noble/hashes@1.3.2': {} '@noble/hashes@1.3.2': {}
'@noble/hashes@1.7.1': {} '@noble/hashes@1.7.1': {}
'@noble/hashes@1.8.0': {}
'@noble/hashes@2.0.1': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -5047,6 +5105,18 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.8.2 typescript: 5.8.2
nostr-tools@2.18.2(typescript@5.8.2):
dependencies:
'@noble/ciphers': 0.5.3
'@noble/curves': 1.2.0
'@noble/hashes': 1.3.1
'@scure/base': 1.1.1
'@scure/bip32': 1.3.1
'@scure/bip39': 1.2.1
nostr-wasm: 0.1.0
optionalDependencies:
typescript: 5.8.2
nostr-wasm@0.1.0: {} nostr-wasm@0.1.0: {}
onchange@7.1.0: onchange@7.1.0: