mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
Merge pull request #350 from sepehr-safari/nip98-enhancement
Nip98 enhancement
This commit is contained in:
397
nip98.test.ts
397
nip98.test.ts
@@ -1,76 +1,83 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getToken, unpackEventFromToken, validateEvent, validateToken } from './nip98.ts'
|
||||
import { Event, finalizeEvent } from './pure.ts'
|
||||
import { generateSecretKey, getPublicKey } from './pure.ts'
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
import { utf8Encoder } from './utils.ts'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
import { HTTPAuth } from './kinds.ts'
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
const sk = generateSecretKey()
|
||||
import { HTTPAuth } from './kinds.ts'
|
||||
import {
|
||||
getToken,
|
||||
hashPayload,
|
||||
unpackEventFromToken,
|
||||
validateEvent,
|
||||
validateEventKind,
|
||||
validateEventMethodTag,
|
||||
validateEventPayloadTag,
|
||||
validateEventTimestamp,
|
||||
validateEventUrlTag,
|
||||
validateToken,
|
||||
} from './nip98.ts'
|
||||
import { Event, finalizeEvent, generateSecretKey, getPublicKey } from './pure.ts'
|
||||
import { utf8Encoder } from './utils.ts'
|
||||
|
||||
describe('getToken', () => {
|
||||
test('getToken GET returns without authorization scheme', async () => {
|
||||
let result = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
test('returns without authorization scheme for GET', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
|
||||
const decodedResult: Event = await unpackEventFromToken(result)
|
||||
|
||||
expect(decodedResult.created_at).toBeGreaterThan(0)
|
||||
expect(decodedResult.content).toBe('')
|
||||
expect(decodedResult.kind).toBe(HTTPAuth)
|
||||
expect(decodedResult.pubkey).toBe(getPublicKey(sk))
|
||||
expect(decodedResult.tags).toStrictEqual([
|
||||
expect(unpackedEvent.created_at).toBeGreaterThan(0)
|
||||
expect(unpackedEvent.content).toBe('')
|
||||
expect(unpackedEvent.kind).toBe(HTTPAuth)
|
||||
expect(unpackedEvent.pubkey).toBe(getPublicKey(sk))
|
||||
expect(unpackedEvent.tags).toStrictEqual([
|
||||
['u', 'http://test.com'],
|
||||
['method', 'get'],
|
||||
])
|
||||
})
|
||||
|
||||
test('getToken POST returns token without authorization scheme', async () => {
|
||||
let result = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk))
|
||||
test('returns token without authorization scheme for POST', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk))
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
|
||||
const decodedResult: Event = await unpackEventFromToken(result)
|
||||
|
||||
expect(decodedResult.created_at).toBeGreaterThan(0)
|
||||
expect(decodedResult.content).toBe('')
|
||||
expect(decodedResult.kind).toBe(HTTPAuth)
|
||||
expect(decodedResult.pubkey).toBe(getPublicKey(sk))
|
||||
expect(decodedResult.tags).toStrictEqual([
|
||||
expect(unpackedEvent.created_at).toBeGreaterThan(0)
|
||||
expect(unpackedEvent.content).toBe('')
|
||||
expect(unpackedEvent.kind).toBe(HTTPAuth)
|
||||
expect(unpackedEvent.pubkey).toBe(getPublicKey(sk))
|
||||
expect(unpackedEvent.tags).toStrictEqual([
|
||||
['u', 'http://test.com'],
|
||||
['method', 'post'],
|
||||
])
|
||||
})
|
||||
|
||||
test('getToken GET returns token WITH authorization scheme', async () => {
|
||||
test('returns token WITH authorization scheme for POST', async () => {
|
||||
const authorizationScheme = 'Nostr '
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
|
||||
let result = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true)
|
||||
|
||||
expect(result.startsWith(authorizationScheme)).toBe(true)
|
||||
|
||||
const decodedResult: Event = await unpackEventFromToken(result)
|
||||
|
||||
expect(decodedResult.created_at).toBeGreaterThan(0)
|
||||
expect(decodedResult.content).toBe('')
|
||||
expect(decodedResult.kind).toBe(HTTPAuth)
|
||||
expect(decodedResult.pubkey).toBe(getPublicKey(sk))
|
||||
expect(decodedResult.tags).toStrictEqual([
|
||||
expect(token.startsWith(authorizationScheme)).toBe(true)
|
||||
expect(unpackedEvent.created_at).toBeGreaterThan(0)
|
||||
expect(unpackedEvent.content).toBe('')
|
||||
expect(unpackedEvent.kind).toBe(HTTPAuth)
|
||||
expect(unpackedEvent.pubkey).toBe(getPublicKey(sk))
|
||||
expect(unpackedEvent.tags).toStrictEqual([
|
||||
['u', 'http://test.com'],
|
||||
['method', 'post'],
|
||||
])
|
||||
})
|
||||
|
||||
test('getToken returns token with a valid payload tag when payload is present', async () => {
|
||||
test('returns token with a valid payload tag when payload is present', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const payload = { test: 'payload' }
|
||||
const payloadHash = bytesToHex(sha256(utf8Encoder.encode(JSON.stringify(payload))))
|
||||
let result = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, payload)
|
||||
const payloadHash = hashPayload(payload)
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, payload)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
|
||||
const decodedResult: Event = await unpackEventFromToken(result)
|
||||
|
||||
expect(decodedResult.created_at).toBeGreaterThan(0)
|
||||
expect(decodedResult.content).toBe('')
|
||||
expect(decodedResult.kind).toBe(HTTPAuth)
|
||||
expect(decodedResult.pubkey).toBe(getPublicKey(sk))
|
||||
expect(decodedResult.tags).toStrictEqual([
|
||||
expect(unpackedEvent.created_at).toBeGreaterThan(0)
|
||||
expect(unpackedEvent.content).toBe('')
|
||||
expect(unpackedEvent.kind).toBe(HTTPAuth)
|
||||
expect(unpackedEvent.pubkey).toBe(getPublicKey(sk))
|
||||
expect(unpackedEvent.tags).toStrictEqual([
|
||||
['u', 'http://test.com'],
|
||||
['method', 'post'],
|
||||
['payload', payloadHash],
|
||||
@@ -79,81 +86,265 @@ describe('getToken', () => {
|
||||
})
|
||||
|
||||
describe('validateToken', () => {
|
||||
test('validateToken returns true for valid token without authorization scheme', async () => {
|
||||
const validToken = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
test('returns true for valid token without authorization scheme', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
|
||||
const result = await validateToken(validToken, 'http://test.com', 'get')
|
||||
expect(result).toBe(true)
|
||||
const isTokenValid = await validateToken(token, 'http://test.com', 'get')
|
||||
expect(isTokenValid).toBe(true)
|
||||
})
|
||||
|
||||
test('validateToken returns true for valid token with authorization scheme', async () => {
|
||||
const validToken = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
test('returns true for valid token with authorization scheme', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const isTokenValid = await validateToken(token, 'http://test.com', 'get')
|
||||
|
||||
const result = await validateToken(validToken, 'http://test.com', 'get')
|
||||
expect(result).toBe(true)
|
||||
expect(isTokenValid).toBe(true)
|
||||
})
|
||||
|
||||
test('validateToken throws an error for invalid token', async () => {
|
||||
const result = validateToken('fake', 'http://test.com', 'get')
|
||||
expect(result).rejects.toThrow(Error)
|
||||
test('throws an error for invalid token', async () => {
|
||||
const isTokenValid = validateToken('fake', 'http://test.com', 'get')
|
||||
|
||||
expect(isTokenValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('validateToken throws an error for missing token', async () => {
|
||||
const result = validateToken('', 'http://test.com', 'get')
|
||||
expect(result).rejects.toThrow(Error)
|
||||
test('throws an error for missing token', async () => {
|
||||
const isTokenValid = validateToken('', 'http://test.com', 'get')
|
||||
|
||||
expect(isTokenValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('validateToken throws an error for a wrong url', async () => {
|
||||
const validToken = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
test('throws an error for invalid event kind', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const invalidToken = await getToken('http://test.com', 'get', e => {
|
||||
e.kind = 0
|
||||
return finalizeEvent(e, sk)
|
||||
})
|
||||
const isTokenValid = validateToken(invalidToken, 'http://test.com', 'get')
|
||||
|
||||
const result = validateToken(validToken, 'http://wrong-test.com', 'get')
|
||||
expect(result).rejects.toThrow(Error)
|
||||
expect(isTokenValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('validateToken throws an error for a wrong method', async () => {
|
||||
const validToken = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
test('throws an error for invalid event timestamp', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const invalidToken = await getToken('http://test.com', 'get', e => {
|
||||
e.created_at = 0
|
||||
return finalizeEvent(e, sk)
|
||||
})
|
||||
const isTokenValid = validateToken(invalidToken, 'http://test.com', 'get')
|
||||
|
||||
const result = validateToken(validToken, 'http://test.com', 'post')
|
||||
expect(result).rejects.toThrow(Error)
|
||||
expect(isTokenValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('validateEvent returns true for valid decoded token with authorization scheme', async () => {
|
||||
const validToken = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||
test('throws an error for invalid url', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
const isTokenValid = validateToken(token, 'http://wrong-test.com', 'get')
|
||||
|
||||
const result = await validateEvent(decodedResult, 'http://test.com', 'get')
|
||||
expect(result).toBe(true)
|
||||
expect(isTokenValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('validateEvent throws an error for a wrong url', async () => {
|
||||
const validToken = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||
test('throws an error for invalid method', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
|
||||
const isTokenValid = validateToken(token, 'http://test.com', 'post')
|
||||
|
||||
const result = validateEvent(decodedResult, 'http://wrong-test.com', 'get')
|
||||
expect(result).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('validateEvent throws an error for a wrong method', async () => {
|
||||
const validToken = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||
|
||||
const result = validateEvent(decodedResult, 'http://test.com', 'post')
|
||||
expect(result).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('validateEvent returns true for valid payload tag hash', async () => {
|
||||
const validToken = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, { test: 'payload' })
|
||||
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||
|
||||
const result = await validateEvent(decodedResult, 'http://test.com', 'post', { test: 'payload' })
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test('validateEvent returns false for invalid payload tag hash', async () => {
|
||||
const validToken = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, { test: 'a-payload' })
|
||||
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||
|
||||
const result = validateEvent(decodedResult, 'http://test.com', 'post', { test: 'a-different-payload' })
|
||||
expect(result).rejects.toThrow(Error)
|
||||
expect(isTokenValid).rejects.toThrow(Error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEvent', () => {
|
||||
test('returns true for valid decoded token with authorization scheme', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventValid = await validateEvent(unpackedEvent, 'http://test.com', 'get')
|
||||
|
||||
expect(isEventValid).toBe(true)
|
||||
})
|
||||
|
||||
test('throws an error for invalid event kind', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
unpackedEvent.kind = 0
|
||||
const isEventValid = validateEvent(unpackedEvent, 'http://test.com', 'get')
|
||||
|
||||
expect(isEventValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('throws an error for invalid event timestamp', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
unpackedEvent.created_at = 0
|
||||
const isEventValid = validateEvent(unpackedEvent, 'http://test.com', 'get')
|
||||
|
||||
expect(isEventValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('throws an error for invalid url tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventValid = validateEvent(unpackedEvent, 'http://wrong-test.com', 'get')
|
||||
|
||||
expect(isEventValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('throws an error for invalid method tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventValid = validateEvent(unpackedEvent, 'http://test.com', 'post')
|
||||
|
||||
expect(isEventValid).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
test('returns true for valid payload tag hash', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, { test: 'payload' })
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventValid = await validateEvent(unpackedEvent, 'http://test.com', 'post', { test: 'payload' })
|
||||
|
||||
expect(isEventValid).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for invalid payload tag hash', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, { test: 'a-payload' })
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventValid = validateEvent(unpackedEvent, 'http://test.com', 'post', { test: 'a-different-payload' })
|
||||
|
||||
expect(isEventValid).rejects.toThrow(Error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEventTimestamp', () => {
|
||||
test('returns true for valid timestamp', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventTimestampValid = validateEventTimestamp(unpackedEvent)
|
||||
|
||||
expect(isEventTimestampValid).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for invalid timestamp', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
unpackedEvent.created_at = 0
|
||||
const isEventTimestampValid = validateEventTimestamp(unpackedEvent)
|
||||
|
||||
expect(isEventTimestampValid).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEventKind', () => {
|
||||
test('returns true for valid kind', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventKindValid = validateEventKind(unpackedEvent)
|
||||
|
||||
expect(isEventKindValid).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for invalid kind', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
unpackedEvent.kind = 0
|
||||
const isEventKindValid = validateEventKind(unpackedEvent)
|
||||
|
||||
expect(isEventKindValid).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEventUrlTag', () => {
|
||||
test('returns true for valid url tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventUrlTagValid = validateEventUrlTag(unpackedEvent, 'http://test.com')
|
||||
|
||||
expect(isEventUrlTagValid).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for invalid url tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventUrlTagValid = validateEventUrlTag(unpackedEvent, 'http://wrong-test.com')
|
||||
|
||||
expect(isEventUrlTagValid).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEventMethodTag', () => {
|
||||
test('returns true for valid method tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventMethodTagValid = validateEventMethodTag(unpackedEvent, 'get')
|
||||
|
||||
expect(isEventMethodTagValid).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for invalid method tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventMethodTagValid = validateEventMethodTag(unpackedEvent, 'post')
|
||||
|
||||
expect(isEventMethodTagValid).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEventPayloadTag', () => {
|
||||
test('returns true for valid payload tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, { test: 'payload' })
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventPayloadTagValid = validateEventPayloadTag(unpackedEvent, { test: 'payload' })
|
||||
|
||||
expect(isEventPayloadTagValid).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for invalid payload tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, { test: 'a-payload' })
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventPayloadTagValid = validateEventPayloadTag(unpackedEvent, { test: 'a-different-payload' })
|
||||
|
||||
expect(isEventPayloadTagValid).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for missing payload tag', async () => {
|
||||
const sk = generateSecretKey()
|
||||
const token = await getToken('http://test.com', 'post', e => finalizeEvent(e, sk), true, { test: 'payload' })
|
||||
const unpackedEvent: Event = await unpackEventFromToken(token)
|
||||
const isEventPayloadTagValid = validateEventPayloadTag(unpackedEvent, {})
|
||||
|
||||
expect(isEventPayloadTagValid).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hashPayload', () => {
|
||||
test('returns hash for valid payload', async () => {
|
||||
const payload = { test: 'payload' }
|
||||
const computedPayloadHash = hashPayload(payload)
|
||||
const expectedPayloadHash = bytesToHex(sha256(utf8Encoder.encode(JSON.stringify(payload))))
|
||||
|
||||
expect(computedPayloadHash).toBe(expectedPayloadHash)
|
||||
})
|
||||
|
||||
test('returns hash for empty payload', async () => {
|
||||
const payload = {}
|
||||
const computedPayloadHash = hashPayload(payload)
|
||||
const expectedPayloadHash = bytesToHex(sha256(utf8Encoder.encode(JSON.stringify(payload))))
|
||||
|
||||
expect(computedPayloadHash).toBe(expectedPayloadHash)
|
||||
})
|
||||
})
|
||||
|
||||
141
nip98.ts
141
nip98.ts
@@ -1,17 +1,13 @@
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
import { base64 } from '@scure/base'
|
||||
|
||||
import { HTTPAuth } from './kinds.ts'
|
||||
import { Event, EventTemplate, verifyEvent } from './pure.ts'
|
||||
import { utf8Decoder, utf8Encoder } from './utils.ts'
|
||||
import { HTTPAuth } from './kinds.ts'
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -37,7 +33,7 @@ export async function getToken(
|
||||
}
|
||||
|
||||
if (payload) {
|
||||
event.tags.push(['payload', bytesToHex(sha256(utf8Encoder.encode(JSON.stringify(payload))))])
|
||||
event.tags.push(['payload', hashPayload(payload)])
|
||||
}
|
||||
|
||||
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 => {
|
||||
throw error
|
||||
})
|
||||
|
||||
const valid = await validateEvent(event, url, method).catch(error => {
|
||||
throw error
|
||||
})
|
||||
@@ -63,10 +60,18 @@ export async function validateToken(token: string, url: string, method: string):
|
||||
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> {
|
||||
if (!token) {
|
||||
throw new Error('Missing token')
|
||||
}
|
||||
|
||||
token = token.replace(_authorizationScheme, '')
|
||||
|
||||
const eventB64 = utf8Decoder.decode(base64.decode(token))
|
||||
@@ -79,41 +84,121 @@ export async function unpackEventFromToken(token: string): Promise<Event> {
|
||||
return event
|
||||
}
|
||||
|
||||
export async function validateEvent(event: Event, url: string, method: string, body?: any): Promise<boolean> {
|
||||
if (!event) {
|
||||
throw new Error('Invalid nostr event')
|
||||
/**
|
||||
* Validates the timestamp of an 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)) {
|
||||
throw new Error('Invalid nostr event, signature invalid')
|
||||
}
|
||||
if (event.kind !== HTTPAuth) {
|
||||
|
||||
if (!validateEventKind(event)) {
|
||||
throw new Error('Invalid nostr event, kind invalid')
|
||||
}
|
||||
|
||||
if (!event.created_at) {
|
||||
throw new Error('Invalid nostr event, created_at invalid')
|
||||
if (!validateEventTimestamp(event)) {
|
||||
throw new Error('Invalid nostr event, created_at timestamp invalid')
|
||||
}
|
||||
|
||||
// Event must be less than 60 seconds old
|
||||
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) {
|
||||
if (!validateEventUrlTag(event, url)) {
|
||||
throw new Error('Invalid nostr event, url tag invalid')
|
||||
}
|
||||
|
||||
const methodTag = event.tags.find(t => t[0] === 'method')
|
||||
if (methodTag?.length !== 1 && methodTag?.[1].toLowerCase() !== method.toLowerCase()) {
|
||||
if (!validateEventMethodTag(event, method)) {
|
||||
throw new Error('Invalid nostr event, method tag invalid')
|
||||
}
|
||||
|
||||
if (Boolean(body) && Object.keys(body).length > 0) {
|
||||
const payloadTag = event.tags.find(t => t[0] === 'payload')
|
||||
const payloadHash = bytesToHex(sha256(utf8Encoder.encode(JSON.stringify(body))))
|
||||
if (payloadTag?.[1] !== payloadHash) {
|
||||
throw new Error('Invalid payload tag hash, does not match request body hash')
|
||||
if (Boolean(body) && typeof body === 'object' && Object.keys(body).length > 0) {
|
||||
if (!validateEventPayloadTag(event, body)) {
|
||||
throw new Error('Invalid nostr event, payload tag does not match request body hash')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user