fix some bugs, refactor to smaller parts, add docs

This commit is contained in:
Sepehr Safari 2024-01-17 17:50:48 +03:30
parent 677b679c2c
commit 75eb08b170
1 changed files with 113 additions and 28 deletions

141
nip98.ts
View File

@ -1,17 +1,13 @@
import { bytesToHex } from '@noble/hashes/utils'
import { sha256 } from '@noble/hashes/sha256' import { sha256 } from '@noble/hashes/sha256'
import { bytesToHex } from '@noble/hashes/utils'
import { base64 } from '@scure/base' import { base64 } from '@scure/base'
import { HTTPAuth } from './kinds.ts'
import { Event, EventTemplate, verifyEvent } from './pure.ts' import { Event, EventTemplate, verifyEvent } from './pure.ts'
import { utf8Decoder, utf8Encoder } from './utils.ts' import { utf8Decoder, utf8Encoder } from './utils.ts'
import { HTTPAuth } from './kinds.ts'
const _authorizationScheme = 'Nostr ' const _authorizationScheme = 'Nostr '
export function hashPayload(payload: any): string {
const hash = sha256(utf8Encoder.encode(JSON.stringify(payload)))
return bytesToHex(hash)
}
/** /**
* Generate token for NIP-98 flow. * Generate token for NIP-98 flow.
* *
@ -37,7 +33,7 @@ export async function getToken(
} }
if (payload) { if (payload) {
event.tags.push(['payload', bytesToHex(sha256(utf8Encoder.encode(JSON.stringify(payload))))]) event.tags.push(['payload', hashPayload(payload)])
} }
const signedEvent = await sign(event) const signedEvent = await sign(event)
@ -56,6 +52,7 @@ export async function validateToken(token: string, url: string, method: string):
const event = await unpackEventFromToken(token).catch(error => { const event = await unpackEventFromToken(token).catch(error => {
throw error throw error
}) })
const valid = await validateEvent(event, url, method).catch(error => { const valid = await validateEvent(event, url, method).catch(error => {
throw error throw error
}) })
@ -63,10 +60,18 @@ export async function validateToken(token: string, url: string, method: string):
return valid return valid
} }
/**
* Unpacks an event from a token.
*
* @param token - The token to unpack.
* @returns A promise that resolves to the unpacked event.
* @throws {Error} If the token is missing, invalid, or cannot be parsed.
*/
export async function unpackEventFromToken(token: string): Promise<Event> { export async function unpackEventFromToken(token: string): Promise<Event> {
if (!token) { if (!token) {
throw new Error('Missing token') throw new Error('Missing token')
} }
token = token.replace(_authorizationScheme, '') token = token.replace(_authorizationScheme, '')
const eventB64 = utf8Decoder.decode(base64.decode(token)) const eventB64 = utf8Decoder.decode(base64.decode(token))
@ -79,41 +84,121 @@ export async function unpackEventFromToken(token: string): Promise<Event> {
return event return event
} }
export async function validateEvent(event: Event, url: string, method: string, body?: any): Promise<boolean> { /**
if (!event) { * Validates the timestamp of an event.
throw new Error('Invalid nostr event') * @param event - The event object to validate.
* @returns A boolean indicating whether the event timestamp is within the last 60 seconds.
*/
export function validateEventTimestamp(event: Event): boolean {
if (!event.created_at) {
return false
} }
return Math.round(new Date().getTime() / 1000) - event.created_at < 60
}
/**
* Validates the kind of an event.
* @param event The event to validate.
* @returns A boolean indicating whether the event kind is valid.
*/
export function validateEventKind(event: Event): boolean {
return event.kind === HTTPAuth
}
/**
* Validates if the given URL matches the URL tag of the event.
* @param event - The event object.
* @param url - The URL to validate.
* @returns A boolean indicating whether the URL is valid or not.
*/
export function validateEventUrlTag(event: Event, url: string): boolean {
const urlTag = event.tags.find(t => t[0] === 'u')
if (!urlTag) {
return false
}
return urlTag.length > 0 && urlTag[1] === url
}
/**
* Validates if the given event has a method tag that matches the specified method.
* @param event - The event to validate.
* @param method - The method to match against the method tag.
* @returns A boolean indicating whether the event has a matching method tag.
*/
export function validateEventMethodTag(event: Event, method: string): boolean {
const methodTag = event.tags.find(t => t[0] === 'method')
if (!methodTag) {
return false
}
return methodTag.length > 0 && methodTag[1].toLowerCase() === method.toLowerCase()
}
/**
* Calculates the hash of a payload.
* @param payload - The payload to be hashed.
* @returns The hash value as a string.
*/
export function hashPayload(payload: any): string {
const hash = sha256(utf8Encoder.encode(JSON.stringify(payload)))
return bytesToHex(hash)
}
/**
* Validates the event payload tag against the provided payload.
* @param event The event object.
* @param payload The payload to validate.
* @returns A boolean indicating whether the payload tag is valid.
*/
export function validateEventPayloadTag(event: Event, payload: any): boolean {
const payloadTag = event.tags.find(t => t[0] === 'payload')
if (!payloadTag) {
return false
}
const payloadHash = hashPayload(payload)
return payloadTag.length > 0 && payloadTag[1] === payloadHash
}
/**
* Validates a Nostr event for the NIP-98 flow.
*
* @param event - The Nostr event to validate.
* @param url - The URL associated with the event.
* @param method - The HTTP method associated with the event.
* @param body - The request body associated with the event (optional).
* @returns A promise that resolves to a boolean indicating whether the event is valid.
* @throws An error if the event is invalid.
*/
export async function validateEvent(event: Event, url: string, method: string, body?: any): Promise<boolean> {
if (!verifyEvent(event)) { if (!verifyEvent(event)) {
throw new Error('Invalid nostr event, signature invalid') throw new Error('Invalid nostr event, signature invalid')
} }
if (event.kind !== HTTPAuth) {
if (!validateEventKind(event)) {
throw new Error('Invalid nostr event, kind invalid') throw new Error('Invalid nostr event, kind invalid')
} }
if (!event.created_at) { if (!validateEventTimestamp(event)) {
throw new Error('Invalid nostr event, created_at invalid') throw new Error('Invalid nostr event, created_at timestamp invalid')
} }
// Event must be less than 60 seconds old if (!validateEventUrlTag(event, url)) {
if (Math.round(new Date().getTime() / 1000) - event.created_at > 60) {
throw new Error('Invalid nostr event, expired')
}
const urlTag = event.tags.find(t => t[0] === 'u')
if (urlTag?.length !== 1 && urlTag?.[1] !== url) {
throw new Error('Invalid nostr event, url tag invalid') throw new Error('Invalid nostr event, url tag invalid')
} }
const methodTag = event.tags.find(t => t[0] === 'method') if (!validateEventMethodTag(event, method)) {
if (methodTag?.length !== 1 && methodTag?.[1].toLowerCase() !== method.toLowerCase()) {
throw new Error('Invalid nostr event, method tag invalid') throw new Error('Invalid nostr event, method tag invalid')
} }
if (Boolean(body) && Object.keys(body).length > 0) { if (Boolean(body) && typeof body === 'object' && Object.keys(body).length > 0) {
const payloadTag = event.tags.find(t => t[0] === 'payload') if (!validateEventPayloadTag(event, body)) {
const payloadHash = bytesToHex(sha256(utf8Encoder.encode(JSON.stringify(body)))) throw new Error('Invalid nostr event, payload tag does not match request body hash')
if (payloadTag?.[1] !== payloadHash) {
throw new Error('Invalid payload tag hash, does not match request body hash')
} }
} }