nip46 big implementation adapted from ignition.
This commit is contained in:
parent
943cc4fb48
commit
d14830a8ff
42
nip05.ts
42
nip05.ts
|
@ -38,46 +38,16 @@ export async function queryProfile(fullname: string): Promise<ProfilePointer | n
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `https://${domain}/.well-known/nostr.json?name=${name}`
|
const url = `https://${domain}/.well-known/nostr.json?name=${name}`
|
||||||
const res = await _fetch(url, { redirect: 'error' })
|
const res = await (await _fetch(url, { redirect: 'error' })).json()
|
||||||
const { names, relays } = parseNIP05Result(await res.json())
|
|
||||||
|
|
||||||
const pubkey = names[name]
|
let pubkey = res.names[name]
|
||||||
return pubkey ? { pubkey, relays: relays?.[pubkey] } : null
|
return pubkey ? { pubkey, relays: res.relays?.[pubkey] } : null
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** nostr.json result. */
|
export async function isValid(pubkey: string, nip05: string): Promise<boolean> {
|
||||||
export interface NIP05Result {
|
let res = await queryProfile(nip05)
|
||||||
names: {
|
return res ? res.pubkey === pubkey : false
|
||||||
[name: string]: string
|
|
||||||
}
|
|
||||||
relays?: {
|
|
||||||
[pubkey: string]: string[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse the nostr.json and throw if it's not valid. */
|
|
||||||
function parseNIP05Result(json: any): NIP05Result {
|
|
||||||
const result: NIP05Result = {
|
|
||||||
names: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [name, pubkey] of Object.entries(json.names)) {
|
|
||||||
if (typeof name === 'string' && typeof pubkey === 'string') {
|
|
||||||
result.names[name] = pubkey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.relays) {
|
|
||||||
result.relays = {}
|
|
||||||
for (const [pubkey, relays] of Object.entries(json.relays)) {
|
|
||||||
if (typeof pubkey === 'string' && Array.isArray(relays)) {
|
|
||||||
result.relays[pubkey] = relays.filter((relay: unknown) => typeof relay === 'string')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
243
nip46.ts
243
nip46.ts
|
@ -1,4 +1,10 @@
|
||||||
|
import { NostrEvent, UnsignedEvent, VerifiedEvent } from './core.ts'
|
||||||
|
import { generateSecretKey, finalizeEvent, getPublicKey, verifyEvent } from './pure.ts'
|
||||||
|
import { AbstractSimplePool, SubCloser } from './abstract-pool.ts'
|
||||||
|
import { decrypt, encrypt } from './nip04.ts'
|
||||||
import { NIP05_REGEX } from './nip05.ts'
|
import { NIP05_REGEX } from './nip05.ts'
|
||||||
|
import { SimplePool } from './pool.ts'
|
||||||
|
import { Handlerinformation, NostrConnect } from './kinds.ts'
|
||||||
|
|
||||||
var _fetch: any
|
var _fetch: any
|
||||||
|
|
||||||
|
@ -11,8 +17,9 @@ export function useFetchImplementation(fetchImplementation: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BUNKER_REGEX = /^bunker:\/\/[0-9a-f]{64}\??[?\/\w:.=&%]*$/
|
export const BUNKER_REGEX = /^bunker:\/\/[0-9a-f]{64}\??[?\/\w:.=&%]*$/
|
||||||
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
|
||||||
type BunkerPointer = {
|
export type BunkerPointer = {
|
||||||
relays: string[]
|
relays: string[]
|
||||||
pubkey: string
|
pubkey: string
|
||||||
secret: null | string
|
secret: null | string
|
||||||
|
@ -35,7 +42,11 @@ export async function parseBunkerInput(input: string): Promise<BunkerPointer | n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match = input.match(NIP05_REGEX)
|
return queryBunkerProfile(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryBunkerProfile(nip05: string): Promise<BunkerPointer | null> {
|
||||||
|
const match = nip05.match(NIP05_REGEX)
|
||||||
if (!match) return null
|
if (!match) return null
|
||||||
|
|
||||||
const [_, name = '_', domain] = match
|
const [_, name = '_', domain] = match
|
||||||
|
@ -52,3 +63,231 @@ export async function parseBunkerInput(input: string): Promise<BunkerPointer | n
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BunkerSigner {
|
||||||
|
private pool: AbstractSimplePool
|
||||||
|
private subCloser: SubCloser
|
||||||
|
private relays: string[]
|
||||||
|
private isOpen: boolean
|
||||||
|
private serial: number
|
||||||
|
private idPrefix: string
|
||||||
|
private listeners: {
|
||||||
|
[id: string]: {
|
||||||
|
resolve: (_: string) => void
|
||||||
|
reject: (_: string) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private secretKey: Uint8Array
|
||||||
|
private connectionSecret: string
|
||||||
|
public remotePubkey: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the Nip46 class.
|
||||||
|
* @param relays - An array of relay addresses.
|
||||||
|
* @param remotePubkey - An optional remote public key. This is the key you want to sign as.
|
||||||
|
* @param secretKey - An optional key pair.
|
||||||
|
*/
|
||||||
|
public constructor(clientSecretKey: Uint8Array, bp: BunkerPointer) {
|
||||||
|
this.pool = new SimplePool()
|
||||||
|
this.secretKey = clientSecretKey
|
||||||
|
this.relays = bp.relays
|
||||||
|
this.remotePubkey = bp.pubkey
|
||||||
|
this.connectionSecret = bp.secret || ''
|
||||||
|
this.isOpen = false
|
||||||
|
this.idPrefix = Math.random().toString(36).substring(7)
|
||||||
|
this.serial = 0
|
||||||
|
this.listeners = {}
|
||||||
|
|
||||||
|
const listeners = this.listeners
|
||||||
|
|
||||||
|
this.subCloser = this.pool.subscribeMany(
|
||||||
|
this.relays,
|
||||||
|
[{ kinds: [NostrConnect, 24134], '#p': [getPublicKey(this.secretKey)] }],
|
||||||
|
{
|
||||||
|
async onevent(event: NostrEvent) {
|
||||||
|
const decryptedContent = await decrypt(clientSecretKey, event.pubkey, event.content)
|
||||||
|
const parsedContent = JSON.parse(decryptedContent)
|
||||||
|
const { id, result, error } = parsedContent
|
||||||
|
|
||||||
|
let handler = listeners[id]
|
||||||
|
if (handler) {
|
||||||
|
if (error) handler.reject(error)
|
||||||
|
else if (result) handler.resolve(result)
|
||||||
|
delete listeners[id]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
this.isOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// closes the subscription -- this object can't be used anymore after this
|
||||||
|
async close() {
|
||||||
|
this.isOpen = false
|
||||||
|
this.subCloser.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendRequest(method: string, params: string[]): Promise<string> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (!this.isOpen) throw new Error('this signer is not open anymore, create a new one')
|
||||||
|
this.serial++
|
||||||
|
const id = `${this.idPrefix}-${this.serial}`
|
||||||
|
|
||||||
|
const encryptedContent = await encrypt(
|
||||||
|
this.secretKey,
|
||||||
|
this.remotePubkey,
|
||||||
|
JSON.stringify({ id, method, params }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// the request event
|
||||||
|
const verifiedEvent: VerifiedEvent = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: method === 'create_account' ? 24134 : NostrConnect,
|
||||||
|
tags: [['p', this.remotePubkey]],
|
||||||
|
content: encryptedContent,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
},
|
||||||
|
this.secretKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
// setup callback listener
|
||||||
|
this.listeners[id] = { resolve, reject }
|
||||||
|
|
||||||
|
// Build auth_url handler
|
||||||
|
// const authHandler = (response: Response) => {
|
||||||
|
// if (response.result) {
|
||||||
|
// this.emit('authChallengeSuccess', response)
|
||||||
|
// } else {
|
||||||
|
// this.emit('authChallengeError', response.error)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// publish the event
|
||||||
|
await Promise.any(this.pool.publish(this.relays, verifiedEvent))
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a ping request to the remote server.
|
||||||
|
* Requires permission/access rights to bunker.
|
||||||
|
* @returns "Pong" if successful. The promise will reject if the response is not "pong".
|
||||||
|
*/
|
||||||
|
async ping(): Promise<void> {
|
||||||
|
let resp = await this.sendRequest('ping', [])
|
||||||
|
if (resp !== 'pong') throw new Error(`result is not pong: ${resp}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to a remote server using the provided keys and remote public key.
|
||||||
|
* Optionally, a secret can be provided for additional authentication.
|
||||||
|
*
|
||||||
|
* @param remotePubkey - Optional the remote public key to connect to.
|
||||||
|
* @param secret - Optional secret for additional authentication.
|
||||||
|
* @throws {Error} If no keys are found or no remote public key is found.
|
||||||
|
* @returns "ack" if successful. The promise will reject if the response is not "ack".
|
||||||
|
*/
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
await this.sendRequest('connect', [getPublicKey(this.secretKey), this.connectionSecret])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs an event using the remote private key.
|
||||||
|
* @param event - The event to sign.
|
||||||
|
* @throws {Error} If no keys are found or no remote public key is found.
|
||||||
|
* @returns A Promise that resolves to the signed event.
|
||||||
|
*/
|
||||||
|
async signEvent(event: UnsignedEvent): Promise<VerifiedEvent> {
|
||||||
|
let resp = await this.sendRequest('sign_event', [JSON.stringify(event)])
|
||||||
|
let signed: NostrEvent = JSON.parse(resp)
|
||||||
|
if (signed.pubkey === getPublicKey(this.secretKey) && verifyEvent(signed)) {
|
||||||
|
return signed
|
||||||
|
} else {
|
||||||
|
throw new Error(`event returned from bunker is improperly signed: ${signed}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an account with the specified username, domain, and optional email.
|
||||||
|
* @param bunkerPubkey - The public key of the bunker to use for the create_account call.
|
||||||
|
* @param username - The username for the account.
|
||||||
|
* @param domain - The domain for the account.
|
||||||
|
* @param email - The optional email for the account.
|
||||||
|
* @throws Error if no keys are found, no remote public key is found, or the email is present but invalid.
|
||||||
|
* @returns A Promise that resolves to the auth_url that the client should follow to create an account.
|
||||||
|
*/
|
||||||
|
export async function createAccount(
|
||||||
|
bunker: BunkerProfile,
|
||||||
|
username: string,
|
||||||
|
domain: string,
|
||||||
|
email?: string,
|
||||||
|
): Promise<BunkerSigner> {
|
||||||
|
if (email && !EMAIL_REGEX.test(email)) throw new Error('Invalid email')
|
||||||
|
|
||||||
|
let sk = generateSecretKey()
|
||||||
|
let rpc = new BunkerSigner(sk, bunker.bunkerPointer)
|
||||||
|
|
||||||
|
let pubkey = await rpc.sendRequest('create_account', [username, domain, email || ''])
|
||||||
|
|
||||||
|
// once we get the newly created pubkey back, we hijack this signer instance
|
||||||
|
// and turn it into the main instance for this newly created pubkey
|
||||||
|
rpc.remotePubkey = pubkey
|
||||||
|
await rpc.connect()
|
||||||
|
|
||||||
|
return rpc
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches info on available signers (nsecbunkers) using NIP-89 events.
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to an array of available bunker objects.
|
||||||
|
*/
|
||||||
|
export async function fetchCustodialbunkers(pool: AbstractSimplePool, relays: string[]): Promise<BunkerProfile[]> {
|
||||||
|
const events = await pool.querySync(relays, { kinds: [Handlerinformation] })
|
||||||
|
// filter for events that handle the connect event kind
|
||||||
|
const filteredEvents = events.filter(event =>
|
||||||
|
event.tags.some(tag => tag[0] === 'k' && tag[1] === NostrConnect.toString()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate bunkers by checking their NIP-05 and pubkey
|
||||||
|
// Map to a more useful object
|
||||||
|
const validatedBunkers = await Promise.all(
|
||||||
|
filteredEvents.map(async event => {
|
||||||
|
try {
|
||||||
|
const content = JSON.parse(event.content)
|
||||||
|
const bp = await queryBunkerProfile(content.nip05)
|
||||||
|
if (bp && bp.pubkey === event.pubkey) {
|
||||||
|
return {
|
||||||
|
bunkerPointer: bp,
|
||||||
|
nip05: content.nip05,
|
||||||
|
domain: content.nip05.split('@')[1],
|
||||||
|
name: content.name || content.display_name,
|
||||||
|
picture: content.picture,
|
||||||
|
about: content.about,
|
||||||
|
website: content.website,
|
||||||
|
local: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return validatedBunkers.filter(b => b !== undefined) as BunkerProfile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BunkerProfile = {
|
||||||
|
bunkerPointer: BunkerPointer
|
||||||
|
domain: string
|
||||||
|
nip05: string
|
||||||
|
name: string
|
||||||
|
picture: string
|
||||||
|
about: string
|
||||||
|
website: string
|
||||||
|
local: boolean
|
||||||
|
}
|
||||||
|
|
10
package.json
10
package.json
|
@ -20,6 +20,11 @@
|
||||||
"require": "./lib/cjs/index.js",
|
"require": "./lib/cjs/index.js",
|
||||||
"types": "./lib/types/index.d.ts"
|
"types": "./lib/types/index.d.ts"
|
||||||
},
|
},
|
||||||
|
"./core": {
|
||||||
|
"import": "./lib/esm/core.js",
|
||||||
|
"require": "./lib/cjs/core.js",
|
||||||
|
"types": "./lib/types/core.d.ts"
|
||||||
|
},
|
||||||
"./pure": {
|
"./pure": {
|
||||||
"import": "./lib/esm/pure.js",
|
"import": "./lib/esm/pure.js",
|
||||||
"require": "./lib/cjs/pure.js",
|
"require": "./lib/cjs/pure.js",
|
||||||
|
@ -150,6 +155,11 @@
|
||||||
"require": "./lib/cjs/nip44.js",
|
"require": "./lib/cjs/nip44.js",
|
||||||
"types": "./lib/types/nip44.d.ts"
|
"types": "./lib/types/nip44.d.ts"
|
||||||
},
|
},
|
||||||
|
"./nip46": {
|
||||||
|
"import": "./lib/esm/nip46.js",
|
||||||
|
"require": "./lib/cjs/nip46.js",
|
||||||
|
"types": "./lib/types/nip46.d.ts"
|
||||||
|
},
|
||||||
"./nip49": {
|
"./nip49": {
|
||||||
"import": "./lib/esm/nip49.js",
|
"import": "./lib/esm/nip49.js",
|
||||||
"require": "./lib/cjs/nip49.js",
|
"require": "./lib/cjs/nip49.js",
|
||||||
|
|
Loading…
Reference in New Issue