Nip46 auth with NDK (#1636)
* ndk * fix: remove duplicated zap note event template * don't init Nip07 signer by default * Update wallets/nwc/server.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * nwc protocol parsing workaround * WebSocket polyfill for worker * increase nwc timeout * remove NDKNip46Signer type * fix type annotation * move eslint-disable camelcase to the top * pass event args to the constructor * fix error handling * Update wallets/nwc/index.js Co-authored-by: ekzyis <ek@stacker.news> * nip46 auth * style tweak, remove unmaintained signers from the list * don't use modal * workaround url parsing * use kind 27235 * add kind 27235 metadata * show suggestion after a timeout * Update lib/nostr.js Co-authored-by: ekzyis <ek@stacker.news> * Update components/nostr-auth.js Co-authored-by: ekzyis <ek@stacker.news> * fix unrelated lnauth crash when closing ext prompt * make ui consistent ... * give buttons spacing --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
285203889d
commit
bdd24130f9
|
@ -29,8 +29,8 @@ SLACK_BOT_TOKEN=
|
||||||
SLACK_CHANNEL_ID=
|
SLACK_CHANNEL_ID=
|
||||||
|
|
||||||
# lnurl ... you'll need a tunnel to localhost:3000 for these
|
# lnurl ... you'll need a tunnel to localhost:3000 for these
|
||||||
LNAUTH_URL=
|
LNAUTH_URL=http://localhost:3000/api/lnauth
|
||||||
LNWITH_URL=
|
LNWITH_URL=http://localhost:3000/api/lnwith
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# SNDEV STUFF WE PRESET #
|
# SNDEV STUFF WE PRESET #
|
||||||
|
|
|
@ -36,7 +36,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
||||||
await window.webln.enable()
|
await window.webln.enable()
|
||||||
await window.webln.lnurl(encodedUrl)
|
await window.webln.lnurl(encodedUrl)
|
||||||
}
|
}
|
||||||
effect()
|
effect().catch(console.error)
|
||||||
}, [encodedUrl])
|
}, [encodedUrl])
|
||||||
|
|
||||||
// output pubkey and k1
|
// output pubkey and k1
|
||||||
|
|
|
@ -1,71 +1,78 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from '@apollo/client'
|
||||||
import { signIn } from 'next-auth/react'
|
import { signIn } from 'next-auth/react'
|
||||||
import Container from 'react-bootstrap/Container'
|
|
||||||
import Col from 'react-bootstrap/Col'
|
import Col from 'react-bootstrap/Col'
|
||||||
import Row from 'react-bootstrap/Row'
|
import Row from 'react-bootstrap/Row'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import BackIcon from '@/svgs/arrow-left-line.svg'
|
import BackIcon from '@/svgs/arrow-left-line.svg'
|
||||||
|
import Nostr from '@/lib/nostr'
|
||||||
|
import { NDKNip46Signer } from '@nostr-dev-kit/ndk'
|
||||||
|
import { useToast } from '@/components/toast'
|
||||||
|
import { Button, Container } from 'react-bootstrap'
|
||||||
|
import { Form, Input, SubmitButton } from '@/components/form'
|
||||||
|
import Moon from '@/svgs/moon-fill.svg'
|
||||||
import styles from './lightning-auth.module.css'
|
import styles from './lightning-auth.module.css'
|
||||||
import { callWithTimeout } from '@/lib/time'
|
|
||||||
|
|
||||||
function ExtensionError ({ message, details }) {
|
const sanitizeURL = (s) => {
|
||||||
return (
|
try {
|
||||||
<>
|
const url = new URL(s)
|
||||||
<h4 className='fw-bold text-danger pb-1'>error: {message}</h4>
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error('invalid protocol')
|
||||||
<div className='text-muted pb-4'>{details}</div>
|
return url.href
|
||||||
</>
|
} catch (e) {
|
||||||
)
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function NostrExplainer ({ text }) {
|
function NostrError ({ message }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExtensionError message='nostr extension not found' details='Nostr extensions are the safest way to use your nostr identity on Stacker News.' />
|
<h4 className='fw-bold text-danger pb-1'>error</h4>
|
||||||
<Row className='w-100 text-muted'>
|
<div className='text-muted pb-4'>{message}</div>
|
||||||
<AccordianItem
|
|
||||||
header={`Which extensions can I use to ${(text || 'Login').toLowerCase()} with Nostr?`}
|
|
||||||
show
|
|
||||||
body={
|
|
||||||
<>
|
|
||||||
<Row>
|
|
||||||
<Col>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href='https://getalby.com'>Alby</a><br />
|
|
||||||
available for: chrome, firefox, and safari
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://www.getflamingo.org/'>Flamingo</a><br />
|
|
||||||
available for: chrome
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://github.com/fiatjaf/nos2x'>nos2x</a><br />
|
|
||||||
available for: chrome
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://diegogurpegui.com/nos2x-fox/'>nos2x-fox</a><br />
|
|
||||||
available for: firefox
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://github.com/fiatjaf/horse'>horse</a><br />
|
|
||||||
available for: chrome<br />
|
|
||||||
supports hardware signing
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [status, setStatus] = useState({
|
||||||
|
msg: '',
|
||||||
|
error: false,
|
||||||
|
loading: false,
|
||||||
|
title: undefined,
|
||||||
|
button: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const [suggestion, setSuggestion] = useState(null)
|
||||||
|
const suggestionTimeout = useRef(null)
|
||||||
|
const toaster = useToast()
|
||||||
|
|
||||||
|
const challengeResolver = useCallback(async (challenge) => {
|
||||||
|
const challengeUrl = sanitizeURL(challenge)
|
||||||
|
if (challengeUrl) {
|
||||||
|
setStatus({
|
||||||
|
title: 'Waiting for confirmation',
|
||||||
|
msg: 'Please confirm this action on your remote signer',
|
||||||
|
error: false,
|
||||||
|
loading: true,
|
||||||
|
button: {
|
||||||
|
label: 'open signer',
|
||||||
|
action: () => {
|
||||||
|
window.open(challengeUrl, '_blank')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setStatus({
|
||||||
|
title: 'Waiting for confirmation',
|
||||||
|
msg: challenge,
|
||||||
|
error: false,
|
||||||
|
loading: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// create auth challenge
|
||||||
|
const [createAuth] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
createAuth {
|
createAuth {
|
||||||
k1
|
k1
|
||||||
|
@ -74,83 +81,253 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
||||||
// don't cache this mutation
|
// don't cache this mutation
|
||||||
fetchPolicy: 'no-cache'
|
fetchPolicy: 'no-cache'
|
||||||
})
|
})
|
||||||
const [hasExtension, setHasExtension] = useState(undefined)
|
|
||||||
const [extensionError, setExtensionError] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
// print an error message
|
||||||
createAuth()
|
const setError = useCallback((e) => {
|
||||||
setHasExtension(!!window.nostr)
|
console.error(e)
|
||||||
|
toaster.danger(e.message || e.toString())
|
||||||
|
setStatus({
|
||||||
|
msg: e.message || e.toString(),
|
||||||
|
error: true,
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const k1 = data?.createAuth.k1
|
const clearSuggestionTimer = () => {
|
||||||
|
if (suggestionTimeout.current) clearTimeout(suggestionTimeout.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSuggestionWithTimer = (msg) => {
|
||||||
|
clearSuggestionTimer()
|
||||||
|
suggestionTimeout.current = setTimeout(() => {
|
||||||
|
setSuggestion(msg)
|
||||||
|
}, 10_000)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!k1 || !hasExtension) return
|
return () => {
|
||||||
|
clearSuggestionTimer()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
console.info('nostr extension detected')
|
// authorize user
|
||||||
|
const auth = useCallback(async (nip46token) => {
|
||||||
|
setStatus({
|
||||||
|
msg: 'Waiting for authorization',
|
||||||
|
error: false,
|
||||||
|
loading: true
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const { data, error } = await createAuth()
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
let mounted = true;
|
const k1 = data?.createAuth.k1
|
||||||
(async function () {
|
if (!k1) throw new Error('Error generating challenge') // should never happen
|
||||||
try {
|
|
||||||
// have them sign a message with the challenge
|
|
||||||
let event
|
|
||||||
try {
|
|
||||||
event = await callWithTimeout(() => window.nostr.signEvent({
|
|
||||||
kind: 22242,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [['challenge', k1]],
|
|
||||||
content: 'Stacker News Authentication'
|
|
||||||
}), 5000)
|
|
||||||
if (!event) throw new Error('extension returned empty event')
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === 'window.nostr call already executing' || !mounted) return
|
|
||||||
setExtensionError({ message: 'nostr extension failed to sign event', details: e.message })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign them in
|
const useExtension = !nip46token
|
||||||
try {
|
const signer = Nostr.getSigner({ nip46token, supportNip07: useExtension })
|
||||||
await signIn('nostr', {
|
if (!signer && useExtension) throw new Error('No extension found')
|
||||||
event: JSON.stringify(event),
|
|
||||||
callbackUrl,
|
if (signer instanceof NDKNip46Signer) {
|
||||||
multiAuth
|
signer.once('authUrl', challengeResolver)
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('authorization failed', e)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return
|
|
||||||
console.log('nostr auth error', e)
|
|
||||||
setExtensionError({ message: `${text} failed`, details: e.message })
|
|
||||||
}
|
}
|
||||||
})()
|
|
||||||
return () => { mounted = false }
|
|
||||||
}, [k1, hasExtension])
|
|
||||||
|
|
||||||
if (error) return <div>error</div>
|
setSuggestionWithTimer('Having trouble? Make sure you used a fresh token or valid NIP-05 address')
|
||||||
|
await signer.blockUntilReady()
|
||||||
|
clearSuggestionTimer()
|
||||||
|
|
||||||
|
setStatus({
|
||||||
|
msg: 'Signing in',
|
||||||
|
error: false,
|
||||||
|
loading: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const signedEvent = await Nostr.sign({
|
||||||
|
kind: 27235,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['challenge', k1],
|
||||||
|
['u', process.env.NEXT_PUBLIC_URL],
|
||||||
|
['method', 'GET']
|
||||||
|
],
|
||||||
|
content: 'Stacker News Authentication'
|
||||||
|
}, { signer })
|
||||||
|
|
||||||
|
await signIn('nostr', {
|
||||||
|
event: JSON.stringify(signedEvent),
|
||||||
|
callbackUrl,
|
||||||
|
multiAuth
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
setError(e)
|
||||||
|
} finally {
|
||||||
|
clearSuggestionTimer()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasExtension === false && <NostrExplainer text={text} />}
|
{status.error && <NostrError message={status.msg} />}
|
||||||
{extensionError && <ExtensionError {...extensionError} />}
|
{status.loading
|
||||||
{hasExtension && !extensionError &&
|
? (
|
||||||
<>
|
<>
|
||||||
<h4 className='fw-bold text-success pb-1'>nostr extension found</h4>
|
<div className='text-muted py-4 w-100 line-height-1 d-flex align-items-center gap-2'>
|
||||||
<h6 className='text-muted pb-4'>authorize event signature in extension</h6>
|
<Moon className='spin fill-grey flex-shrink-0' width='30' height='30' />
|
||||||
</>}
|
{status.msg}
|
||||||
|
</div>
|
||||||
|
{status.button && (
|
||||||
|
<Button
|
||||||
|
className='w-100' variant='primary'
|
||||||
|
onClick={() => status.button.action()}
|
||||||
|
>
|
||||||
|
{status.button.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{suggestion && (
|
||||||
|
<div className='text-muted text-center small pt-2'>{suggestion}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<Form
|
||||||
|
initial={{ token: '' }}
|
||||||
|
onSubmit={values => {
|
||||||
|
if (!values.token) {
|
||||||
|
setError(new Error('Token or NIP-05 address is required'))
|
||||||
|
} else {
|
||||||
|
auth(values.token)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label='Connect with token or NIP-05 address'
|
||||||
|
name='token'
|
||||||
|
placeholder='bunker://... or NIP-05 address'
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className='mt-2'>
|
||||||
|
<SubmitButton className='w-100' variant='primary'>
|
||||||
|
{text || 'Login'} with token or NIP-05
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
<div className='text-center text-muted fw-bold my-2'>or</div>
|
||||||
|
<Button
|
||||||
|
variant='nostr'
|
||||||
|
className='w-100'
|
||||||
|
type='submit'
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await auth()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text || 'Login'} with extension
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NostrAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
function NostrExplainer ({ text, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.login}>
|
<div className={styles.login}>
|
||||||
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
||||||
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
<h3 className='w-100 pb-2'>
|
||||||
<NostrAuth text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
{text || 'Login'} with Nostr
|
||||||
|
</h3>
|
||||||
|
<Row className='w-100 text-muted'>
|
||||||
|
<Col className='ps-0 mb-4' md>
|
||||||
|
<AccordianItem
|
||||||
|
header='Which NIP-46 signers can I use?'
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col xs>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href='https://nsec.app/'>Nsec.app</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome, firefox, and safari</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://app.nsecbunker.com/'>nsecBunker</a>
|
||||||
|
<ul>
|
||||||
|
<li>available as: SaaS or self-hosted</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AccordianItem
|
||||||
|
header='Which extensions can I use?'
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href='https://getalby.com'>Alby</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome, firefox, and safari</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://www.getflamingo.org/'>Flamingo</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://github.com/fiatjaf/nos2x'>nos2x</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://diegogurpegui.com/nos2x-fox/'>nos2x-fox</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: firefox</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://github.com/fiatjaf/horse'>horse</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome</li>
|
||||||
|
<li>supports hardware signing</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col md className='mx-auto' style={{ maxWidth: '300px' }}>
|
||||||
|
{children}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NostrAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||||
|
return (
|
||||||
|
<NostrExplainer text={text}>
|
||||||
|
<NostrAuth text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
|
</NostrExplainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
22
lib/nostr.js
22
lib/nostr.js
|
@ -1,6 +1,6 @@
|
||||||
import { bech32 } from 'bech32'
|
import { bech32 } from 'bech32'
|
||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
import NDK, { NDKEvent, NDKRelaySet, NDKPrivateKeySigner, NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
import NDK, { NDKEvent, NDKNip46Signer, NDKRelaySet, NDKPrivateKeySigner, NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||||
|
|
||||||
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
||||||
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
||||||
|
@ -35,14 +35,14 @@ export class Nostr {
|
||||||
*/
|
*/
|
||||||
_ndk = null
|
_ndk = null
|
||||||
|
|
||||||
constructor ({ privKey, defaultSigner, relays, supportNip07 = false, ...ndkOptions } = {}) {
|
constructor ({ privKey, defaultSigner, relays, nip46token, supportNip07 = false, ...ndkOptions } = {}) {
|
||||||
this._ndk = new NDK({
|
this._ndk = new NDK({
|
||||||
explicitRelayUrls: relays,
|
explicitRelayUrls: relays,
|
||||||
blacklistRelayUrls: RELAYS_BLACKLIST,
|
blacklistRelayUrls: RELAYS_BLACKLIST,
|
||||||
autoConnectUserRelays: false,
|
autoConnectUserRelays: false,
|
||||||
autoFetchUserMutelist: false,
|
autoFetchUserMutelist: false,
|
||||||
clientName: 'stacker.news',
|
clientName: 'stacker.news',
|
||||||
signer: defaultSigner ?? this.getSigner({ privKey, supportNip07 }),
|
signer: defaultSigner ?? this.getSigner({ privKey, supportNip07, nip46token }),
|
||||||
...ndkOptions
|
...ndkOptions
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -56,13 +56,15 @@ export class Nostr {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Object} args
|
* @param {Object} param0
|
||||||
* @param {string} [args.privKey] - private key to use for signing
|
* @param {string} [args.privKey] - private key to use for signing
|
||||||
|
* @param {string} [args.nip46token] - NIP-46 token to use for signing
|
||||||
* @param {boolean} [args.supportNip07] - whether to use NIP-07 signer if available
|
* @param {boolean} [args.supportNip07] - whether to use NIP-07 signer if available
|
||||||
* @returns {NDKPrivateKeySigner | NDKNip07Signer | null} - a signer instance
|
* @returns {NDKPrivateKeySigner | NDKNip46Signer | NDKNip07Signer | null} - a signer instance
|
||||||
*/
|
*/
|
||||||
getSigner ({ privKey, supportNip07 = true } = {}) {
|
getSigner ({ privKey, nip46token, supportNip07 = true } = {}) {
|
||||||
if (privKey) return new NDKPrivateKeySigner(privKey)
|
if (privKey) return new NDKPrivateKeySigner(privKey)
|
||||||
|
if (nip46token) return new NDKNip46SignerURLPatch(this.ndk, nip46token)
|
||||||
if (supportNip07 && typeof window !== 'undefined' && window?.nostr) return new NDKNip07Signer()
|
if (supportNip07 && typeof window !== 'undefined' && window?.nostr) return new NDKNip07Signer()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -176,3 +178,11 @@ export function nostrZapDetails (zap) {
|
||||||
|
|
||||||
return { npub, content, note }
|
return { npub, content, note }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// workaround NDK url parsing issue (see https://github.com/stackernews/stacker.news/pull/1636)
|
||||||
|
class NDKNip46SignerURLPatch extends NDKNip46Signer {
|
||||||
|
connectionTokenInit (connectionToken) {
|
||||||
|
connectionToken = connectionToken.replace('bunker://', 'http://')
|
||||||
|
return super.connectionTokenInit(connectionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue