story-research-zapwall/lib/hashIdGenerator.ts
2026-01-06 00:26:31 +01:00

200 lines
5.5 KiB
TypeScript

/**
* 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, unknown>): 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) {
continue // Skip undefined/null values
}
if (typeof value === 'object' && !Array.isArray(value)) {
// Recursively canonicalize nested objects
parts.push(`${key}:${canonicalizeObject(value as Record<string, unknown>)}`)
} 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<string, unknown>): Promise<string> {
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<string> {
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<string> {
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<string> {
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<string> {
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<string> {
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<string> {
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<string> {
return generateHashId({
type: 'sponsoring',
payerPubkey: sponsoringData.payerPubkey,
authorPubkey: sponsoringData.authorPubkey,
seriesId: sponsoringData.seriesId ?? '',
articleId: sponsoringData.articleId ?? '',
amount: sponsoringData.amount,
paymentHash: sponsoringData.paymentHash,
})
}