diff --git a/index.ts b/index.ts index 8f45843..6fbe9e2 100644 --- a/index.ts +++ b/index.ts @@ -13,6 +13,7 @@ export * as nip13 from './nip13' export * as nip19 from './nip19' export * as nip26 from './nip26' export * as nip39 from './nip39' +export * as nip42 from './nip42' export * as nip57 from './nip57' export * as fj from './fakejson' diff --git a/nip42.test.js b/nip42.test.js new file mode 100644 index 0000000..4cedbc8 --- /dev/null +++ b/nip42.test.js @@ -0,0 +1,27 @@ +/* eslint-env jest */ + +require('websocket-polyfill') +const { + relayInit, + generatePrivateKey, + finishEvent, + nip42 +} = require('./lib/nostr.cjs') + +test('auth flow', done => { + const relay = relayInit('wss://nostr.kollider.xyz') + relay.connect() + const sk = generatePrivateKey() + + relay.on('auth', async challenge => { + await expect( + nip42.authenticate({ + challenge, + relay, + sign: e => finishEvent(e, sk) + }) + ).rejects.toBeTruthy() + relay.close() + done() + }) +}) diff --git a/nip42.ts b/nip42.ts new file mode 100644 index 0000000..3df325a --- /dev/null +++ b/nip42.ts @@ -0,0 +1,42 @@ +import {EventTemplate, Event, Kind} from './event' +import {Relay} from './relay' + +/** + * Authenticate via NIP-42 flow. + * + * @example + * const sign = window.nostr.signEvent + * relay.on('auth', challenge => + * authenticate({ relay, sign, challenge }) + * ) + */ +export const authenticate = async ({ + challenge, + relay, + sign +}: { + challenge: string + relay: Relay + sign: (e: EventTemplate) => Promise +}): Promise => { + const e: EventTemplate = { + kind: Kind.ClientAuth, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['relay', relay.url], + ['challenge', challenge] + ], + content: '' + } + const sub = relay.publish(await sign(e), 'AUTH') + return new Promise((resolve, reject) => { + sub.on('ok', function ok() { + sub.off('ok', ok) + resolve() + }) + sub.on('failed', function fail(reason: string) { + sub.off('failed', fail) + reject(reason) + }) + }) +} diff --git a/relay.ts b/relay.ts index 06dd9ce..68265a4 100644 --- a/relay.ts +++ b/relay.ts @@ -4,11 +4,13 @@ import {Event, verifySignature, validateEvent} from './event' import {Filter, matchFilters} from './filter' import {getHex64, getSubscriptionId} from './fakejson' +type OutgoingEventType = 'EVENT' | 'AUTH' type RelayEvent = { connect: () => void | Promise disconnect: () => void | Promise error: () => void | Promise notice: (msg: string) => void | Promise + auth: (challenge: string) => void | Promise } type CountPayload = { count: number @@ -26,8 +28,11 @@ export type Relay = { sub: (filters: Filter[], opts?: SubscriptionOptions) => Sub list: (filters: Filter[], opts?: SubscriptionOptions) => Promise get: (filter: Filter, opts?: SubscriptionOptions) => Promise - count: (filters: Filter[], opts?: SubscriptionOptions) => Promise - publish: (event: Event) => Pub + count: ( + filters: Filter[], + opts?: SubscriptionOptions + ) => Promise + publish: (event: Event, type?: OutgoingEventType) => Pub off: ( event: T, listener: U @@ -61,6 +66,14 @@ export type SubscriptionOptions = { alreadyHaveEvent?: null | ((id: string, relay: string) => boolean) } +const newListeners = (): {[TK in keyof RelayEvent]: RelayEvent[TK][]} => ({ + connect: [], + disconnect: [], + error: [], + notice: [], + auth: [] +}) + export function relayInit( url: string, options: { @@ -73,12 +86,7 @@ export function relayInit( var ws: WebSocket var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {} - var listeners: {[TK in keyof RelayEvent]: RelayEvent[TK][]} = { - connect: [], - disconnect: [], - error: [], - notice: [] - } + var listeners = newListeners() var subListeners: { [subid: string]: {[TK in keyof SubEvent]: SubEvent[TK][]} } = {} @@ -198,6 +206,11 @@ export function relayInit( let notice = data[1] listeners.notice.forEach(cb => cb(notice)) return + case 'AUTH': { + let challenge = data[1] + listeners.auth?.forEach(cb => cb(challenge)) + return + } } } catch (err) { return @@ -351,11 +364,11 @@ export function relayInit( resolve(event) }) }), - publish(event: Event): Pub { + publish(event, type = 'EVENT'): Pub { if (!event.id) throw new Error(`event ${event} has no id`) let id = event.id - trySend(['EVENT', event]) + trySend([type, event]) return { on: (type: 'ok' | 'failed', cb: any) => { @@ -375,7 +388,7 @@ export function relayInit( }, connect, close(): void { - listeners = {connect: [], disconnect: [], error: [], notice: []} + listeners = newListeners() subListeners = {} pubListeners = {} if (ws.readyState === WebSocket.OPEN) {