/** * Hash-based ID generation for Nostr objects * All IDs are SHA-256 hashes of the object's canonical representation */ /** * Generate a canonical string representation of an object for hashing * This ensures that the same object always produces the same hash */ function canonicalizeObject(obj: Record): string { // Sort keys to ensure consistent ordering const sortedKeys = Object.keys(obj).sort() const parts: string[] = [] for (const key of sortedKeys) { const value = obj[key] if (value !== undefined && value !== null) { if (typeof value === 'object' && !Array.isArray(value)) { // Recursively canonicalize nested objects parts.push(`${key}:${canonicalizeObject(value as Record)}`) } else if (Array.isArray(value)) { // Arrays are serialized as comma-separated values parts.push(`${key}:[${value.map((v) => (typeof v === 'object' ? JSON.stringify(v) : String(v))).join(',')}]`) } else { parts.push(`${key}:${String(value)}`) } } } return `{${parts.join('|')}}` } /** * Generate a SHA-256 hash ID from an object using Web Crypto API * The hash is deterministic: the same object always produces the same hash */ export async function generateHashId(obj: Record): Promise { const canonical = canonicalizeObject(obj) const encoder = new TextEncoder() const data = encoder.encode(canonical) const hashBuffer = await crypto.subtle.digest('SHA-256', data) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') } /** * Generate hash ID for an author presentation */ export async function generateAuthorHashId(authorData: { pubkey: string authorName: string presentation: string contentDescription: string mainnetAddress?: string | undefined pictureUrl?: string | undefined category: string }): Promise { return generateHashId({ type: 'author', pubkey: authorData.pubkey, authorName: authorData.authorName, presentation: authorData.presentation, contentDescription: authorData.contentDescription, mainnetAddress: authorData.mainnetAddress ?? '', pictureUrl: authorData.pictureUrl ?? '', category: authorData.category, }) } /** * Generate hash ID for a series */ export async function generateSeriesHashId(seriesData: { pubkey: string title: string description: string category: string coverUrl?: string | undefined }): Promise { return generateHashId({ type: 'series', pubkey: seriesData.pubkey, title: seriesData.title, description: seriesData.description, category: seriesData.category, coverUrl: seriesData.coverUrl ?? '', }) } /** * Generate hash ID for a publication */ export async function generatePublicationHashId(publicationData: { pubkey: string title: string preview: string category: string seriesId?: string | undefined bannerUrl?: string | undefined zapAmount: number }): Promise { return generateHashId({ type: 'publication', pubkey: publicationData.pubkey, title: publicationData.title, preview: publicationData.preview, category: publicationData.category, seriesId: publicationData.seriesId ?? '', bannerUrl: publicationData.bannerUrl ?? '', zapAmount: publicationData.zapAmount, }) } /** * Generate hash ID for a review/quote */ export async function generateReviewHashId(reviewData: { pubkey: string articleId: string reviewerPubkey: string content: string title?: string }): Promise { return generateHashId({ type: 'quote', pubkey: reviewData.pubkey, articleId: reviewData.articleId, reviewerPubkey: reviewData.reviewerPubkey, content: reviewData.content, title: reviewData.title ?? '', }) } /** * Generate hash ID for a purchase (zap receipt kind 9735) */ export async function generatePurchaseHashId(purchaseData: { payerPubkey: string articleId: string authorPubkey: string amount: number paymentHash: string }): Promise { return generateHashId({ type: 'purchase', payerPubkey: purchaseData.payerPubkey, articleId: purchaseData.articleId, authorPubkey: purchaseData.authorPubkey, amount: purchaseData.amount, paymentHash: purchaseData.paymentHash, }) } /** * Generate hash ID for a review tip (zap receipt kind 9735) */ export async function generateReviewTipHashId(tipData: { payerPubkey: string articleId: string reviewId: string reviewerPubkey: string authorPubkey: string amount: number paymentHash: string }): Promise { return generateHashId({ type: 'review_tip', payerPubkey: tipData.payerPubkey, articleId: tipData.articleId, reviewId: tipData.reviewId, reviewerPubkey: tipData.reviewerPubkey, authorPubkey: tipData.authorPubkey, amount: tipData.amount, paymentHash: tipData.paymentHash, }) } /** * Generate hash ID for a sponsoring (zap receipt kind 9735) */ export async function generateSponsoringHashId(sponsoringData: { payerPubkey: string authorPubkey: string seriesId?: string articleId?: string amount: number paymentHash: string }): Promise { return generateHashId({ type: 'sponsoring', payerPubkey: sponsoringData.payerPubkey, authorPubkey: sponsoringData.authorPubkey, seriesId: sponsoringData.seriesId ?? '', articleId: sponsoringData.articleId ?? '', amount: sponsoringData.amount, paymentHash: sponsoringData.paymentHash, }) }