**Motivations:** - Keep dependencies up to date for security and features - Automate dependency updates in deployment script - Fix compatibility issues with major version updates (React 19, Next.js 16, nostr-tools 2.x) **Root causes:** - Dependencies were outdated - Deployment script did not update dependencies before deploying - Major version updates introduced breaking API changes **Correctifs:** - Updated all dependencies to latest versions using npm-check-updates - Modified deploy.sh to run npm-check-updates before installing dependencies - Fixed nostr-tools 2.x API changes (generatePrivateKey -> generateSecretKey, signEvent -> finalizeEvent, verifySignature -> verifyEvent) - Fixed React 19 ref types to accept null - Fixed JSX namespace issues (JSX.Element -> React.ReactElement) - Added proper types for event callbacks - Fixed SimplePool.sub typing issues with type assertions **Evolutions:** - Deployment script now automatically updates dependencies to latest versions before deploying - All dependencies updated to latest versions (Next.js 14->16, React 18->19, nostr-tools 1->2, etc.) **Pages affectées:** - package.json - deploy.sh - lib/keyManagement.ts - lib/nostr.ts - lib/nostrRemoteSigner.ts - lib/zapVerification.ts - lib/platformTrackingEvents.ts - lib/sponsoringTracking.ts - lib/articlePublisherHelpersVerification.ts - lib/contentDeliveryVerification.ts - lib/paymentPollingZapReceipt.ts - lib/nostrPrivateMessages.ts - lib/nostrSubscription.ts - lib/nostrZapVerification.ts - lib/markdownRenderer.tsx - components/AuthorFilter.tsx - components/AuthorFilterButton.tsx - components/UserArticlesList.tsx - types/nostr-tools-extended.ts
118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
import { Event, validateEvent, verifyEvent } from 'nostr-tools'
|
|
|
|
/**
|
|
* Service for verifying zap receipts and their signatures
|
|
*/
|
|
export class ZapVerificationService {
|
|
/**
|
|
* Verify a zap receipt signature
|
|
*/
|
|
verifyZapReceiptSignature(event: Event): boolean {
|
|
try {
|
|
return validateEvent(event) && verifyEvent(event)
|
|
} catch (error) {
|
|
console.error('Error verifying zap receipt signature:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a zap receipt is valid for a specific article and user
|
|
*/
|
|
verifyZapReceiptForArticle(
|
|
zapReceipt: Event,
|
|
articleId: string,
|
|
articlePubkey: string,
|
|
_userPubkey: string,
|
|
expectedAmount: number
|
|
): boolean {
|
|
if (!this.verifyZapReceiptSignature(zapReceipt)) {
|
|
console.warn('Zap receipt signature verification failed')
|
|
return false
|
|
}
|
|
|
|
return (
|
|
this.isRecipientValid(zapReceipt, articlePubkey) &&
|
|
this.isArticleReferenced(zapReceipt, articleId) &&
|
|
this.isAmountValid(zapReceipt, expectedAmount) &&
|
|
this.isZapKind(zapReceipt)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Extract payment information from a zap receipt
|
|
*/
|
|
extractPaymentInfo(zapReceipt: Event): {
|
|
amount: number // in satoshis
|
|
recipient: string
|
|
articleId: string | null
|
|
payer: string
|
|
} | null {
|
|
try {
|
|
const amountTag = zapReceipt.tags.find((tag) => tag[0] === 'amount')
|
|
const recipientTag = zapReceipt.tags.find((tag) => tag[0] === 'p')
|
|
const eventTag = zapReceipt.tags.find((tag) => tag[0] === 'e')
|
|
|
|
if (!amountTag || !recipientTag) {
|
|
return null
|
|
}
|
|
|
|
const amountInMillisats = parseInt(amountTag[1] ?? '0')
|
|
const amountInSats = Math.floor(amountInMillisats / 1000)
|
|
|
|
return {
|
|
amount: amountInSats,
|
|
recipient: recipientTag?.[1] ?? '',
|
|
articleId: eventTag?.[1] ?? null,
|
|
payer: zapReceipt.pubkey,
|
|
}
|
|
} catch (error) {
|
|
console.error('Error extracting payment info:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
private isRecipientValid(zapReceipt: Event, articlePubkey: string): boolean {
|
|
const recipient = zapReceipt.tags.find((tag) => tag[0] === 'p')?.[1]
|
|
if (recipient !== articlePubkey) {
|
|
console.warn('Zap receipt recipient does not match article author')
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private isArticleReferenced(zapReceipt: Event, articleId: string): boolean {
|
|
const eventIdTag = zapReceipt.tags.find((tag) => tag[0] === 'e')?.[1]
|
|
if (eventIdTag !== articleId) {
|
|
console.warn('Zap receipt does not reference the correct article')
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private isAmountValid(zapReceipt: Event, expectedAmount: number): boolean {
|
|
const amountTag = zapReceipt.tags.find((tag) => tag[0] === 'amount')?.[1]
|
|
if (!amountTag) {
|
|
console.warn('Zap receipt does not contain amount tag')
|
|
return false
|
|
}
|
|
const amountInMillisats = parseInt(amountTag ?? '0')
|
|
const expectedAmountInMillisats = expectedAmount * 1000
|
|
if (amountInMillisats < expectedAmountInMillisats) {
|
|
console.warn(`Zap amount ${amountInMillisats} is less than expected ${expectedAmountInMillisats}`)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private isZapKind(zapReceipt: Event): boolean {
|
|
if (zapReceipt.kind !== 9735) {
|
|
console.warn('Event is not a zap receipt (kind 9735)')
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
export const zapVerificationService = new ZapVerificationService()
|