diff --git a/core.ts b/core.ts index 39f3c58..d479da3 100644 --- a/core.ts +++ b/core.ts @@ -1,3 +1,5 @@ +import { isHex32 } from './utils.ts' + export interface Nostr { generateSecretKey(): Uint8Array getPublicKey(secretKey: Uint8Array): string @@ -36,7 +38,7 @@ export function validateEvent(event: T): event is T & UnsignedEvent { if (typeof event.content !== 'string') return false if (typeof event.created_at !== 'number') return false if (typeof event.pubkey !== 'string') return false - if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false + if (!isHex32(event.pubkey)) return false if (!Array.isArray(event.tags)) return false for (let i = 0; i < event.tags.length; i++) { diff --git a/nip10.ts b/nip10.ts index d8b26d1..c19e7a8 100644 --- a/nip10.ts +++ b/nip10.ts @@ -1,7 +1,6 @@ import type { Event } from './core.ts' import type { EventPointer, ProfilePointer } from './nip19.ts' - -const HEX64 = /^[0-9a-fA-F]{64}$/ +import { isHex32 } from './utils.ts' export function parse(event: Pick): { /** @@ -43,7 +42,7 @@ export function parse(event: Pick): { for (let i = event.tags.length - 1; i >= 0; i--) { const tag = event.tags[i] - if (tag[0] === 'e' && tag[1] && HEX64.test(tag[1])) { + if (tag[0] === 'e' && tag[1] && isHex32(tag[1])) { const [_, eTagEventId, eTagRelayUrl, eTagMarker, eTagAuthor] = tag as [ string, string, @@ -55,7 +54,7 @@ export function parse(event: Pick): { const eventPointer: EventPointer = { id: eTagEventId, relays: eTagRelayUrl ? [eTagRelayUrl] : [], - author: eTagAuthor && HEX64.test(eTagAuthor) ? eTagAuthor : undefined, + author: eTagAuthor && isHex32(eTagAuthor) ? eTagAuthor : undefined, } if (eTagMarker === 'root') { @@ -83,7 +82,7 @@ export function parse(event: Pick): { continue } - if (tag[0] === 'q' && tag[1] && HEX64.test(tag[1])) { + if (tag[0] === 'q' && tag[1] && isHex32(tag[1])) { const [_, eTagEventId, eTagRelayUrl] = tag as [string, string, undefined | string] result.quotes.push({ id: eTagEventId, @@ -91,7 +90,7 @@ export function parse(event: Pick): { }) } - if (tag[0] === 'p' && tag[1] && HEX64.test(tag[1])) { + if (tag[0] === 'p' && tag[1] && isHex32(tag[1])) { result.profiles.push({ pubkey: tag[1], relays: tag[2] ? [tag[2]] : [], diff --git a/nip22.ts b/nip22.ts index d2d8a76..c8149f6 100644 --- a/nip22.ts +++ b/nip22.ts @@ -1,7 +1,6 @@ import type { Event } from './core.ts' import type { AddressPointer, EventPointer, ProfilePointer } from './nip19.ts' - -const HEX64 = /^[0-9a-fA-F]{64}$/ +import { isHex32 } from './utils.ts' export type ExternalPointer = { value: string @@ -22,7 +21,7 @@ function parseAddressPointer(value: string, relayUrl?: string): AddressPointer | if (Number.isNaN(kind)) return undefined const pubkey = value.slice(idx + 1, idx2) - if (!HEX64.test(pubkey)) return undefined + if (!isHex32(pubkey)) return undefined return { kind, @@ -36,11 +35,11 @@ function parsePointer(tag: string[]): EventPointer | AddressPointer | ExternalPo switch (tag[0]) { case 'E': case 'e': - if (!tag[1] || !HEX64.test(tag[1])) return undefined + if (!tag[1] || !isHex32(tag[1])) return undefined return { id: tag[1], relays: tag[2] ? [tag[2]] : [], - author: tag[3] && HEX64.test(tag[3]) ? tag[3] : undefined, + author: tag[3] && isHex32(tag[3]) ? tag[3] : undefined, } case 'A': case 'a': @@ -63,12 +62,12 @@ function parseQuote(tag: string[]): EventPointer | AddressPointer | ExternalPoin return parseAddressPointer(tag[1], tag[2]) } - if (!HEX64.test(tag[1])) return undefined + if (!isHex32(tag[1])) return undefined return { id: tag[1], relays: tag[2] ? [tag[2]] : [], - author: tag[3] && HEX64.test(tag[3]) ? tag[3] : undefined, + author: tag[3] && isHex32(tag[3]) ? tag[3] : undefined, } } @@ -179,7 +178,7 @@ export function parse(event: Pick): { continue } - if ((tag[0] === 'P' || tag[0] === 'p') && tag[1] && HEX64.test(tag[1])) { + if ((tag[0] === 'P' || tag[0] === 'p') && tag[1] && isHex32(tag[1])) { result.profiles.push({ pubkey: tag[1], relays: tag[2] ? [tag[2]] : [], diff --git a/nip45.ts b/nip45.ts index 4b441ed..fd3d421 100644 --- a/nip45.ts +++ b/nip45.ts @@ -3,6 +3,7 @@ import { bytesToHex } from '@noble/hashes/utils.js' import type { Event } from './core.ts' import type { Filter } from './filter.ts' +import { isHex32 } from './utils.ts' const M = 256 const HLL_HEX_LENGTH = M * 2 @@ -32,7 +33,7 @@ export function newHll(): Uint8Array { } export function hllDecode(hex: string): Uint8Array | undefined { - if (hex.length !== HLL_HEX_LENGTH || !/^[0-9a-fA-F]+$/.test(hex)) return undefined + if (hex.length !== HLL_HEX_LENGTH || !/^[0-9a-f]+$/.test(hex)) return undefined const registers = new Uint8Array(M) for (let i = 0; i < M; i++) { @@ -54,9 +55,9 @@ export function hllEncode(registers: Uint8Array): string { export function computeOffset(filterFirstTagValue: string): number { let hex = filterFirstTagValue - if (!isHex64(hex)) { + if (!isHex32(hex)) { const parts = hex.split(':') - if (parts.length === 3 && isHex64(parts[1])) { + if (parts.length === 3 && isHex32(parts[1])) { hex = parts[1] } else { hex = bytesToHex(sha256(utf8Encoder.encode(filterFirstTagValue))) @@ -79,7 +80,7 @@ export function getFilterFirstTagValue(filter: Filter): string | undefined { export function feedPubkey(hll: Uint8Array, pubkey: string, offset: number): Uint8Array { if (offset < 0 || offset > 24) throw new Error(`invalid offset ${offset}`) - if (!isHex64(pubkey)) throw new Error('pubkey must be 32-byte hex') + if (!isHex32(pubkey)) throw new Error('pubkey must be 32-byte hex') if (hll.length === 0) hll = newHll() if (hll.length !== M) throw new Error(`invalid number of registers ${hll.length}`) @@ -123,10 +124,6 @@ export function estimateCount(hll: Uint8Array): number { return Math.floor(estimate) } -function isHex64(value: string): boolean { - return /^[0-9a-fA-F]{64}$/.test(value) -} - function countLeadingZeroBitsAfterOffset(pubkey: string, offset: number): number { let zeroBits = 0 for (let i = offset + 1; i < offset + 8; i++) { diff --git a/nip57.ts b/nip57.ts index cbedb0e..5d76521 100644 --- a/nip57.ts +++ b/nip57.ts @@ -1,7 +1,7 @@ import { bech32 } from '@scure/base' import { NostrEvent, validateEvent, verifyEvent, type Event, type EventTemplate } from './pure.ts' -import { utf8Decoder } from './utils.ts' +import { isHex32, utf8Decoder } from './utils.ts' import { isReplaceableKind, isAddressableKind } from './kinds.ts' var _fetch: any @@ -100,10 +100,10 @@ export function validateZapRequest(zapRequestString: string): string | null { let p = zapRequest.tags.find(([t, v]) => t === 'p' && v) if (!p) return "Zap request doesn't have a 'p' tag." - if (!p[1].match(/^[a-f0-9]{64}$/)) return "Zap request 'p' tag is not valid hex." + if (!isHex32(p[1])) return "Zap request 'p' tag is not valid hex." let e = zapRequest.tags.find(([t, v]) => t === 'e' && v) - if (e && !e[1].match(/^[a-f0-9]{64}$/)) return "Zap request 'e' tag is not valid hex." + if (e && !isHex32(e[1])) return "Zap request 'e' tag is not valid hex." let relays = zapRequest.tags.find(([t, v]) => t === 'relays' && v) if (!relays) return "Zap request doesn't have a 'relays' tag." diff --git a/nipb0.ts b/nipb0.ts index 19be7a7..036c47a 100644 --- a/nipb0.ts +++ b/nipb0.ts @@ -3,6 +3,7 @@ import { bytesToHex } from '@noble/hashes/utils.js' import type { EventTemplate } from './core.ts' import type { Signer } from './signer.ts' import { kinds } from './index.ts' +import { isHex32 } from './utils.ts' export type BlobDescriptor = { url: string @@ -27,10 +28,6 @@ export type AuthEventOptions = { expiration?: number } -export function isSha256(str: string): boolean { - return /^[0-9a-f]{64}$/i.test(str) -} - export function getBlobSize(blob: UploadType): number { if ((typeof File !== 'undefined' && blob instanceof File) || blob instanceof Blob) { return blob.size @@ -213,7 +210,7 @@ export function parseBlossomURI(uri: string): BlossomURI { if (dotIndex === -1) throw new Error('Invalid blossom URI: missing file extension') const sha256 = path.slice(0, dotIndex) const ext = path.slice(dotIndex + 1) - if (!isSha256(sha256)) throw new Error('Invalid blossom URI: invalid sha256 hash') + if (!isHex32(sha256)) throw new Error('Invalid blossom URI: invalid sha256 hash') if (!ext) throw new Error('Invalid blossom URI: empty file extension') const params = new URLSearchParams(query) const servers = params.getAll('xs') @@ -250,7 +247,7 @@ export function blossomURIFromURL(url: URL): BlossomURI { if (dotIndex === -1) throw new Error('Invalid blossom URL: missing file extension') const sha256 = path.slice(0, dotIndex) const ext = path.slice(dotIndex + 1) - if (!isSha256(sha256)) throw new Error('Invalid blossom URL: invalid sha256 hash') + if (!isHex32(sha256)) throw new Error('Invalid blossom URL: invalid sha256 hash') if (!ext) throw new Error('Invalid blossom URL: empty file extension') const servers = url.searchParams.getAll('xs') const authors = url.searchParams.getAll('as') @@ -743,7 +740,7 @@ export class BlossomClient { } async check(hash: string): Promise { - if (!isSha256(hash)) throw new Error(`${hash} is not valid 32-byte hex`) + if (!isHex32(hash)) throw new Error(`${hash} is not valid 32-byte hex`) await this.httpCall('HEAD', hash) } diff --git a/nipb7.ts b/nipb7.ts index 94b57de..5753f46 100644 --- a/nipb7.ts +++ b/nipb7.ts @@ -1,7 +1,7 @@ import { sha256 } from '@noble/hashes/sha2.js' import { EventTemplate } from './core.ts' import { Signer } from './signer.ts' -import { bytesToHex } from './utils.ts' +import { bytesToHex, isHex32 } from './utils.ts' export type BlobDescriptor = { url: string @@ -84,12 +84,8 @@ export class BlossomClient { } } - private isValid32ByteHex(hash: string): boolean { - return /^[a-f0-9]{64}$/i.test(hash) - } - async check(hash: string): Promise { - if (!this.isValid32ByteHex(hash)) { + if (!isHex32(hash)) { throw new Error(`${hash} is not a valid 32-byte hex string`) } @@ -125,7 +121,7 @@ export class BlossomClient { } async download(hash: string): Promise { - if (!this.isValid32ByteHex(hash)) { + if (!isHex32(hash)) { throw new Error(`${hash} is not a valid 32-byte hex string`) } @@ -156,7 +152,7 @@ export class BlossomClient { async list(): Promise { const pubkey = await this.signer.getPublicKey() - if (!this.isValid32ByteHex(pubkey)) { + if (!isHex32(pubkey)) { throw new Error(`pubkey ${pubkey} is not valid`) } @@ -179,7 +175,7 @@ export class BlossomClient { } async delete(hash: string): Promise { - if (!this.isValid32ByteHex(hash)) { + if (!isHex32(hash)) { throw new Error(`${hash} is not a valid 32-byte hex string`) } diff --git a/utils.ts b/utils.ts index a6031f8..59a8dec 100644 --- a/utils.ts +++ b/utils.ts @@ -123,3 +123,16 @@ export function mergeReverseSortedLists(list1: NostrEvent[], list2: NostrEvent[] return result } + +/** + * Checks if a string is a 64-char lowercase hex string as most Nostr ids and pubkeys. + */ +export function isHex32(input: string): boolean { + for (let i = 0; i < 64; i++) { + let cc = input.charCodeAt(i) + if (isNaN(cc) || cc < 48 || cc > 102 || (cc > 57 && cc < 97)) { + return false + } + } + return true +}