1212 */
1313
1414import { verifyEvent } from 'nostr-tools' ;
15- import {
16- unpackEventFromToken ,
17- validateEventKind ,
18- validateEventTimestamp ,
19- validateEventUrlTag ,
20- validateEventMethodTag ,
21- validateEventPayloadTag
22- } from 'nostr-tools/nip98' ;
15+ import crypto from 'crypto' ;
2316
2417// NIP-98 event kind (references RFC 7235)
2518const HTTP_AUTH_KIND = 27235 ;
@@ -49,6 +42,34 @@ export function extractNostrToken(authHeader) {
4942 return authHeader . slice ( 6 ) . trim ( ) ;
5043}
5144
45+ /**
46+ * Decode NIP-98 event from base64 token
47+ * @param {string } token - Base64 encoded event
48+ * @returns {object|null } Decoded event or null
49+ */
50+ function decodeEvent ( token ) {
51+ try {
52+ const decoded = Buffer . from ( token , 'base64' ) . toString ( 'utf8' ) ;
53+ return JSON . parse ( decoded ) ;
54+ } catch {
55+ return null ;
56+ }
57+ }
58+
59+ /**
60+ * Get tag value from event
61+ * @param {object } event - Nostr event
62+ * @param {string } tagName - Tag name (e.g., 'u', 'method')
63+ * @returns {string|null } Tag value or null
64+ */
65+ function getTagValue ( event , tagName ) {
66+ if ( ! event . tags || ! Array . isArray ( event . tags ) ) {
67+ return null ;
68+ }
69+ const tag = event . tags . find ( t => Array . isArray ( t ) && t [ 0 ] === tagName ) ;
70+ return tag ? tag [ 1 ] : null ;
71+ }
72+
5273/**
5374 * Convert Nostr pubkey to did:nostr URI
5475 * @param {string } pubkey - 64-char hex public key
@@ -70,24 +91,21 @@ export async function verifyNostrAuth(request) {
7091 return { webId : null , error : 'Missing Nostr token' } ;
7192 }
7293
73- let event ;
74- try {
75- event = unpackEventFromToken ( token ) ;
76- } catch ( err ) {
77- return { webId : null , error : 'Invalid token format: ' + err . message } ;
78- }
79-
94+ // Decode the event
95+ const event = decodeEvent ( token ) ;
8096 if ( ! event ) {
81- return { webId : null , error : 'Could not decode event from token ' } ;
97+ return { webId : null , error : 'Invalid token format: could not decode base64 JSON ' } ;
8298 }
8399
84100 // Validate event kind (must be 27235)
85- if ( ! validateEventKind ( event , HTTP_AUTH_KIND ) ) {
101+ if ( event . kind !== HTTP_AUTH_KIND ) {
86102 return { webId : null , error : `Invalid event kind: expected ${ HTTP_AUTH_KIND } , got ${ event . kind } ` } ;
87103 }
88104
89105 // Validate timestamp (within ±60 seconds)
90- if ( ! validateEventTimestamp ( event , TIMESTAMP_TOLERANCE ) ) {
106+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
107+ const eventTime = event . created_at ;
108+ if ( ! eventTime || Math . abs ( now - eventTime ) > TIMESTAMP_TOLERANCE ) {
91109 return { webId : null , error : 'Event timestamp outside acceptable window (±60s)' } ;
92110 }
93111
@@ -97,21 +115,32 @@ export async function verifyNostrAuth(request) {
97115 const fullUrl = `${ protocol } ://${ host } ${ request . url } ` ;
98116
99117 // Validate URL tag matches request URL
100- if ( ! validateEventUrlTag ( event , fullUrl ) ) {
101- // Also try without query string for compatibility
102- const urlWithoutQuery = fullUrl . split ( '?' ) [ 0 ] ;
103- if ( ! validateEventUrlTag ( event , urlWithoutQuery ) ) {
104- return { webId : null , error : `URL mismatch: event URL does not match request URL` } ;
105- }
118+ const eventUrl = getTagValue ( event , 'u' ) ;
119+ if ( ! eventUrl ) {
120+ return { webId : null , error : 'Missing URL tag in event' } ;
121+ }
122+
123+ // Compare URLs (normalize by removing trailing slashes)
124+ const normalizedEventUrl = eventUrl . replace ( / \/ $ / , '' ) ;
125+ const normalizedRequestUrl = fullUrl . replace ( / \/ $ / , '' ) ;
126+ const normalizedRequestUrlNoQuery = fullUrl . split ( '?' ) [ 0 ] . replace ( / \/ $ / , '' ) ;
127+
128+ if ( normalizedEventUrl !== normalizedRequestUrl && normalizedEventUrl !== normalizedRequestUrlNoQuery ) {
129+ return { webId : null , error : `URL mismatch: event URL "${ eventUrl } " does not match request URL "${ fullUrl } "` } ;
106130 }
107131
108132 // Validate method tag matches request method
109- if ( ! validateEventMethodTag ( event , request . method ) ) {
110- return { webId : null , error : `Method mismatch: expected ${ request . method } ` } ;
133+ const eventMethod = getTagValue ( event , 'method' ) ;
134+ if ( ! eventMethod ) {
135+ return { webId : null , error : 'Missing method tag in event' } ;
136+ }
137+ if ( eventMethod . toUpperCase ( ) !== request . method . toUpperCase ( ) ) {
138+ return { webId : null , error : `Method mismatch: expected ${ request . method } , got ${ eventMethod } ` } ;
111139 }
112140
113141 // Validate payload hash if present and request has body
114- if ( request . body && event . tags . some ( t => t [ 0 ] === 'payload' ) ) {
142+ const payloadTag = getTagValue ( event , 'payload' ) ;
143+ if ( payloadTag && request . body ) {
115144 let bodyString ;
116145 if ( typeof request . body === 'string' ) {
117146 bodyString = request . body ;
@@ -121,11 +150,17 @@ export async function verifyNostrAuth(request) {
121150 bodyString = JSON . stringify ( request . body ) ;
122151 }
123152
124- if ( ! validateEventPayloadTag ( event , bodyString ) ) {
153+ const expectedHash = crypto . createHash ( 'sha256' ) . update ( bodyString ) . digest ( 'hex' ) ;
154+ if ( payloadTag . toLowerCase ( ) !== expectedHash . toLowerCase ( ) ) {
125155 return { webId : null , error : 'Payload hash mismatch' } ;
126156 }
127157 }
128158
159+ // Validate pubkey exists
160+ if ( ! event . pubkey || typeof event . pubkey !== 'string' || event . pubkey . length !== 64 ) {
161+ return { webId : null , error : 'Invalid or missing pubkey' } ;
162+ }
163+
129164 // Verify Schnorr signature
130165 const isValid = verifyEvent ( event ) ;
131166 if ( ! isValid ) {
@@ -154,7 +189,7 @@ export async function getNostrPubkey(request) {
154189 }
155190
156191 try {
157- const event = unpackEventFromToken ( token ) ;
192+ const event = decodeEvent ( token ) ;
158193 return event ?. pubkey || null ;
159194 } catch {
160195 return null ;
0 commit comments