199 lines
5.5 KiB
TypeScript
199 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) {
|
|
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,
|
|
})
|
|
}
|