Update all dependencies to latest versions and fix compatibility issues

**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
This commit is contained in:
Nicolas Cantu 2025-12-28 21:49:19 +01:00
parent b3c25bf16f
commit 42e3e7e692
28 changed files with 1593 additions and 1488 deletions

View File

@ -19,8 +19,8 @@ interface AuthorFilterContentProps {
loading: boolean
onChange: (value: string | null) => void
setIsOpen: (open: boolean) => void
dropdownRef: React.RefObject<HTMLDivElement>
buttonRef: React.RefObject<HTMLButtonElement>
dropdownRef: React.RefObject<HTMLDivElement | null>
buttonRef: React.RefObject<HTMLButtonElement | null>
getDisplayName: (pubkey: string) => string
getPicture: (pubkey: string) => string | undefined
getMnemonicIcons: (pubkey: string) => string[]

View File

@ -67,7 +67,7 @@ export function AuthorFilterButton({
getMnemonicIcons: (pubkey: string) => string[]
isOpen: boolean
setIsOpen: (open: boolean) => void
buttonRef: React.RefObject<HTMLButtonElement>
buttonRef: React.RefObject<HTMLButtonElement | null>
}) {
return (
<button
@ -105,7 +105,7 @@ export function AuthorFilterButtonWrapper({
getMnemonicIcons: (pubkey: string) => string[]
isOpen: boolean
setIsOpen: (open: boolean) => void
buttonRef: React.RefObject<HTMLButtonElement>
buttonRef: React.RefObject<HTMLButtonElement | null>
}) {
return (
<AuthorFilterButton

View File

@ -9,6 +9,7 @@ import { PresentationFormHeader } from './PresentationFormHeader'
import { t } from '@/lib/i18n'
interface AuthorPresentationDraft {
authorName: string
presentation: string
contentDescription: string
mainnetAddress: string
@ -104,6 +105,27 @@ function MainnetAddressField({
)
}
function AuthorNameField({
draft,
onChange,
}: {
draft: AuthorPresentationDraft
onChange: (next: AuthorPresentationDraft) => void
}) {
return (
<ArticleField
id="authorName"
label={t('presentation.field.authorName')}
value={draft.authorName}
onChange={(value) => onChange({ ...draft, authorName: value as string })}
required
type="text"
placeholder={t('presentation.field.authorName.placeholder')}
helpText={t('presentation.field.authorName.help')}
/>
)
}
function PictureField({
draft,
onChange,
@ -128,6 +150,7 @@ const PresentationFields = ({
onChange: (next: AuthorPresentationDraft) => void
}) => (
<div className="space-y-4">
<AuthorNameField draft={draft} onChange={onChange} />
<PictureField draft={draft} onChange={onChange} />
<PresentationField draft={draft} onChange={onChange} />
<ContentDescriptionField draft={draft} onChange={onChange} />
@ -167,15 +190,23 @@ function PresentationForm({
)
}
function useAuthorPresentationState(pubkey: string | null) {
function useAuthorPresentationState(pubkey: string | null, existingAuthorName?: string) {
const { loading, error, success, publishPresentation } = useAuthorPresentation(pubkey)
const [draft, setDraft] = useState<AuthorPresentationDraft>({
authorName: existingAuthorName ?? '',
presentation: '',
contentDescription: '',
mainnetAddress: '',
})
const [validationError, setValidationError] = useState<string | null>(null)
// Update authorName when profile changes
useEffect(() => {
if (existingAuthorName && existingAuthorName !== draft.authorName) {
setDraft((prev) => ({ ...prev, authorName: existingAuthorName }))
}
}, [existingAuthorName])
const handleSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
@ -184,6 +215,10 @@ function useAuthorPresentationState(pubkey: string | null) {
setValidationError(t('presentation.validation.invalidAddress'))
return
}
if (!draft.authorName.trim()) {
setValidationError('Author name is required')
return
}
setValidationError(null)
await publishPresentation(draft)
},
@ -261,7 +296,7 @@ function AuthorPresentationFormView({
pubkey: string | null
profile: { name?: string; pubkey: string } | null
}) {
const state = useAuthorPresentationState(pubkey)
const state = useAuthorPresentationState(pubkey, profile?.name)
if (!pubkey) {
return <NoAccountView />

View File

@ -89,7 +89,7 @@ function buildArticleContent(
}
) {
const parts = [buildArticleCard(props), buildSeriesLink(props), buildActions(props)].filter(Boolean)
return parts as JSX.Element[]
return parts as React.ReactElement[]
}
function buildArticleCard(

View File

@ -18,13 +18,11 @@ function ProfileStats({ articleCount }: { articleCount: number }) {
export function UserProfile({ profile, pubkey, articleCount }: UserProfileProps) {
const displayName = profile.name ?? `${pubkey.slice(0, 16)}...`
const displayPubkey = `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`
return (
<div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
<UserProfileHeader
displayName={displayName}
displayPubkey={displayPubkey}
{...(profile.picture ? { picture: profile.picture } : {})}
{...(profile.nip05 ? { nip05: profile.nip05 } : {})}
/>

View File

@ -3,14 +3,12 @@ import React from 'react'
interface UserProfileHeaderProps {
displayName: string
displayPubkey: string
picture?: string
nip05?: string
}
export function UserProfileHeader({
displayName,
displayPubkey,
picture,
nip05,
}: UserProfileHeaderProps) {
@ -33,7 +31,6 @@ export function UserProfileHeader({
)}
<div className="flex-1">
<h1 className="text-2xl font-bold text-gray-900 mb-2">{displayName}</h1>
<p className="text-sm text-gray-500 font-mono mb-2">{displayPubkey}</p>
{nip05 && <p className="text-sm text-blue-600 mb-2">{nip05}</p>}
</div>
</div>

View File

@ -165,25 +165,30 @@ else
echo " ⚠ next.config.js local non trouvé, utilisation de celui du serveur"
fi
# Mettre à jour les dépendances aux dernières versions
echo ""
echo "13. Mise à jour des dépendances aux dernières versions..."
ssh_exec "cd ${APP_DIR} && npx -y npm-check-updates -u || true"
# Installer les dépendances
echo ""
echo "13. Installation des dépendances..."
ssh_exec "cd ${APP_DIR} && npm ci"
echo "14. Installation des dépendances..."
ssh_exec "cd ${APP_DIR} && npm install"
# Construire l'application
echo ""
echo "14. Construction de l'application..."
echo "15. Construction de l'application..."
ssh_exec "cd ${APP_DIR} && npm run build"
# Redémarrer le service
echo ""
echo "15. Redémarrage du service ${APP_NAME}..."
echo "16. Redémarrage du service ${APP_NAME}..."
ssh_exec "sudo systemctl restart ${APP_NAME}"
sleep 3
# Vérifier que le service fonctionne
echo ""
echo "16. Vérification du service..."
echo "17. Vérification du service..."
if ssh_exec "sudo systemctl is-active ${APP_NAME} >/dev/null"; then
echo " ✓ Service actif"
echo ""
@ -197,7 +202,7 @@ fi
# Vérifier que le port est en écoute
echo ""
echo "17. Vérification du port 3001..."
echo "18. Vérification du port 3001..."
if ssh_exec "sudo ss -tuln | grep -q ':3001 '"; then
echo " ✓ Port 3001 en écoute"
else

View File

@ -2,8 +2,10 @@ import { useState } from 'react'
import { nostrService } from '@/lib/nostr'
import { articlePublisher } from '@/lib/articlePublisher'
import type { Article } from '@/types/nostr'
import type { NostrProfile } from '@/types/nostr'
interface AuthorPresentationDraft {
authorName: string
presentation: string
contentDescription: string
mainnetAddress: string
@ -32,8 +34,20 @@ export function useAuthorPresentation(pubkey: string | null) {
return
}
// Update Nostr profile (kind 0) with author name and picture
const profileUpdates: Partial<NostrProfile> = {
name: draft.authorName.trim(),
...(draft.pictureUrl ? { picture: draft.pictureUrl } : {}),
}
try {
await nostrService.updateProfile(profileUpdates)
} catch (e) {
console.error('Error updating profile:', e)
// Continue with article publication even if profile update fails
}
// Create presentation article
const title = `Présentation de ${nostrService.getPublicKey()?.substring(0, 16)}...`
const title = `Présentation de ${draft.authorName.trim()}`
const preview = draft.presentation.substring(0, 200)
const fullContent = `${draft.presentation}\n\n---\n\nDescription du contenu :\n${draft.contentDescription}`

View File

@ -1,5 +1,6 @@
import { nostrService } from './nostr'
import { getPrimaryRelaySync } from './config'
import type { Event } from 'nostr-tools'
export function createMessageVerificationFilters(messageEventId: string, authorPubkey: string, recipientPubkey: string, articleId: string) {
return [
@ -40,7 +41,7 @@ export function setupMessageVerificationHandlers(
finalize: (value: boolean) => void,
isResolved: () => boolean
): void {
sub.on('event', (event) => {
sub.on('event', (event: Event) => {
handleMessageVerificationEvent(event, articleId, recipientPubkey, authorPubkey, finalize)
})

View File

@ -1,6 +1,7 @@
import { nostrService } from './nostr'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { getPrimaryRelaySync } from './config'
import type { Event } from 'nostr-tools'
export interface ContentDeliveryStatus {
messageEventId: string | null
@ -44,7 +45,7 @@ function setupContentDeliveryHandlers(
finalize: (result: ContentDeliveryStatus) => void,
isResolved: () => boolean
): void {
sub.on('event', (event) => {
sub.on('event', (event: Event) => {
status.published = true
status.verifiedOnRelay = true
status.messageEventId = event.id

View File

@ -1,4 +1,5 @@
import { nip19, getPublicKey, generatePrivateKey } from 'nostr-tools'
import { nip19, getPublicKey, generateSecretKey } from 'nostr-tools'
import { bytesToHex, hexToBytes } from 'nostr-tools/utils'
import { generateRecoveryPhrase } from './keyManagementRecovery'
import { deriveKeyFromPhrase, encryptNsec, decryptNsec } from './keyManagementEncryption'
import {
@ -21,8 +22,9 @@ export class KeyManagementService {
* Returns the private key (hex) and public key (hex)
*/
generateKeyPair(): { privateKey: string; publicKey: string; npub: string } {
const privateKeyHex = generatePrivateKey()
const publicKeyHex = getPublicKey(privateKeyHex)
const secretKey = generateSecretKey()
const privateKeyHex = bytesToHex(secretKey)
const publicKeyHex = getPublicKey(secretKey)
const npub = nip19.npubEncode(publicKeyHex)
return {
@ -52,7 +54,8 @@ export class KeyManagementService {
privateKeyHex = privateKey
}
const publicKeyHex = getPublicKey(privateKeyHex)
const secretKey = hexToBytes(privateKeyHex)
const publicKeyHex = getPublicKey(secretKey)
const npub = nip19.npubEncode(publicKeyHex)
return {
@ -143,7 +146,8 @@ export class KeyManagementService {
const privateKeyHex = await decryptNsec(derivedKey, encryptedNsec)
// Verify by computing public key
const publicKeyHex = getPublicKey(privateKeyHex)
const secretKey = hexToBytes(privateKeyHex)
const publicKeyHex = getPublicKey(secretKey)
const npub = nip19.npubEncode(publicKeyHex)
return {

View File

@ -7,9 +7,9 @@ interface RenderState {
codeBlockContent: string[]
}
export function renderMarkdown(markdown: string): JSX.Element[] {
export function renderMarkdown(markdown: string): React.ReactElement[] {
const lines = markdown.split('\n')
const elements: JSX.Element[] = []
const elements: React.ReactElement[] = []
const state: RenderState = {
currentList: [],
inCodeBlock: false,
@ -25,7 +25,7 @@ export function renderMarkdown(markdown: string): JSX.Element[] {
return elements
}
function processLine(line: string, index: number, state: RenderState, elements: JSX.Element[]): void {
function processLine(line: string, index: number, state: RenderState, elements: React.ReactElement[]): void {
if (line.startsWith('```')) {
handleCodeBlock(line, index, state, elements)
return
@ -53,7 +53,7 @@ function processLine(line: string, index: number, state: RenderState, elements:
renderParagraphOrBreak(line, index, elements)
}
function renderHeading(line: string, index: number, elements: JSX.Element[]): boolean {
function renderHeading(line: string, index: number, elements: React.ReactElement[]): boolean {
if (line.startsWith('# ')) {
elements.push(<h1 key={index} className="text-3xl font-bold mt-8 mb-4 text-neon-cyan font-mono">{line.substring(2)}</h1>)
return true
@ -81,7 +81,7 @@ function renderListLine(line: string, state: RenderState): boolean {
return false
}
function renderLinkLine(line: string, index: number, elements: JSX.Element[]): boolean {
function renderLinkLine(line: string, index: number, elements: React.ReactElement[]): boolean {
if (line.includes('[') && line.includes('](')) {
renderLink(line, index, elements)
return true
@ -89,7 +89,7 @@ function renderLinkLine(line: string, index: number, elements: JSX.Element[]): b
return false
}
function renderBoldAndCodeLine(line: string, index: number, elements: JSX.Element[]): boolean {
function renderBoldAndCodeLine(line: string, index: number, elements: React.ReactElement[]): boolean {
if (line.includes('**') || line.includes('`')) {
renderBoldAndCode(line, index, elements)
return true
@ -97,7 +97,7 @@ function renderBoldAndCodeLine(line: string, index: number, elements: JSX.Elemen
return false
}
function renderParagraphOrBreak(line: string, index: number, elements: JSX.Element[]): void {
function renderParagraphOrBreak(line: string, index: number, elements: React.ReactElement[]): void {
if (line.trim() !== '') {
elements.push(<p key={index} className="mb-4 text-cyber-accent">{line}</p>)
return
@ -114,7 +114,7 @@ function handleCodeBlock(
_line: string,
index: number,
state: RenderState,
elements: JSX.Element[]
elements: React.ReactElement[]
): void {
if (state.inCodeBlock) {
elements.push(
@ -133,7 +133,7 @@ function closeListIfNeeded(
line: string,
index: number,
state: RenderState,
elements: JSX.Element[]
elements: React.ReactElement[]
): void {
if (state.currentList.length > 0 && !line.startsWith('- ') && !line.startsWith('* ') && line.trim() !== '') {
elements.push(
@ -152,7 +152,7 @@ function createLinkElement(
href: string,
key: string,
isExternal: boolean
): JSX.Element {
): React.ReactElement {
const className = 'text-neon-green hover:text-neon-cyan underline transition-colors'
if (isExternal) {
return (
@ -168,10 +168,10 @@ function createLinkElement(
)
}
function renderLink(line: string, index: number, elements: JSX.Element[]): void {
function renderLink(line: string, index: number, elements: React.ReactElement[]): void {
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
let lastIndex = 0
const parts: (string | JSX.Element)[] = []
const parts: (string | React.ReactElement)[] = []
let match
while ((match = linkRegex.exec(line)) !== null) {
@ -196,8 +196,8 @@ function renderLink(line: string, index: number, elements: JSX.Element[]): void
elements.push(<p key={index} className="mb-4">{parts}</p>)
}
function renderBoldAndCode(line: string, index: number, elements: JSX.Element[]): void {
const parts: (string | JSX.Element)[] = []
function renderBoldAndCode(line: string, index: number, elements: React.ReactElement[]): void {
const parts: (string | React.ReactElement)[] = []
const codeRegex = /`([^`]+)`/g
let codeMatch
let lastIndex = 0
@ -223,7 +223,7 @@ function renderBoldAndCode(line: string, index: number, elements: JSX.Element[])
elements.push(<p key={index} className="mb-4">{parts.length > 0 ? parts : line}</p>)
}
function processBold(text: string, parts: (string | JSX.Element)[]): void {
function processBold(text: string, parts: (string | React.ReactElement)[]): void {
const boldParts = text.split(/(\*\*[^*]+\*\*)/g)
boldParts.forEach((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) {

View File

@ -47,18 +47,60 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
const formData = new FormData()
formData.append('file', file)
const response = await fetch(endpoint, {
let response: Response
try {
response = await fetch(endpoint, {
method: 'POST',
body: formData,
})
if (!response.ok) {
const message = await response.text().catch(() => 'Upload failed')
throw new Error(message || 'Upload failed')
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Network error'
console.error('NIP-95 upload fetch error:', {
endpoint,
error: errorMessage,
fileSize: file.size,
fileType: file.type,
})
throw new Error(`Failed to fetch upload endpoint: ${errorMessage}`)
}
if (!response.ok) {
let errorMessage = 'Upload failed'
try {
const text = await response.text()
errorMessage = text || `HTTP ${response.status} ${response.statusText}`
} catch (_e) {
errorMessage = `HTTP ${response.status} ${response.statusText}`
}
console.error('NIP-95 upload response error:', {
endpoint,
status: response.status,
statusText: response.statusText,
errorMessage,
fileSize: file.size,
fileType: file.type,
})
throw new Error(errorMessage)
}
let result: { url?: string }
try {
result = (await response.json()) as { url?: string }
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Invalid JSON response'
console.error('NIP-95 upload JSON parse error:', {
endpoint,
error: errorMessage,
status: response.status,
})
throw new Error(`Invalid upload response: ${errorMessage}`)
}
const result = (await response.json()) as { url?: string }
if (!result.url) {
console.error('NIP-95 upload missing URL:', {
endpoint,
response: result,
})
throw new Error('Upload response missing URL')
}

View File

@ -1,4 +1,5 @@
import { Event, EventTemplate, getEventHash, signEvent, nip19, SimplePool } from 'nostr-tools'
import { Event, EventTemplate, finalizeEvent, nip19, SimplePool } from 'nostr-tools'
import { hexToBytes } from 'nostr-tools/utils'
import type { Article, NostrProfile } from '@/types/nostr'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { parseArticleFromEvent } from './nostrEventParsing'
@ -64,17 +65,13 @@ class NostrService {
throw new Error('Private key not set or pool not initialized')
}
const unsignedEvent = {
pubkey: this.publicKey ?? '',
const unsignedEvent: EventTemplate = {
...eventTemplate,
created_at: eventTemplate.created_at ?? Math.floor(Date.now() / 1000),
}
const event = {
...unsignedEvent,
id: getEventHash(unsignedEvent),
sig: signEvent(unsignedEvent, this.privateKey),
} as Event
const secretKey = hexToBytes(this.privateKey)
const event = finalizeEvent(unsignedEvent, secretKey)
try {
const relayUrl = await getPrimaryRelay()
@ -228,6 +225,46 @@ class NostrService {
return subscribeWithTimeout(this.pool, filters, parseProfile, 5000)
}
/**
* Update Nostr profile (kind 0) with new metadata
* Merges new fields with existing profile data
*/
async updateProfile(updates: Partial<NostrProfile>): Promise<void> {
if (!this.privateKey || !this.publicKey) {
throw new Error('Private key and public key must be set to update profile')
}
// Get existing profile to merge with updates
const existingProfile = await this.getProfile(this.publicKey)
const currentProfile: NostrProfile = existingProfile ?? {
pubkey: this.publicKey,
}
// Merge updates with existing profile
const updatedProfile: NostrProfile = {
...currentProfile,
...updates,
pubkey: this.publicKey, // Always use current pubkey
}
// Create kind 0 event
const profileEvent: EventTemplate = {
kind: 0,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: JSON.stringify({
name: updatedProfile.name,
about: updatedProfile.about,
picture: updatedProfile.picture,
nip05: updatedProfile.nip05,
lud16: updatedProfile.lud16,
lud06: updatedProfile.lud06,
}),
}
await this.publishEvent(profileEvent)
}
async createZapRequest(targetPubkey: string, targetEventId: string, amount: number): Promise<Event> {
if (!this.privateKey) {

View File

@ -1,5 +1,6 @@
import { Event, nip04 } from 'nostr-tools'
import { SimplePool } from 'nostr-tools'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { decryptArticleContent, type DecryptionKey } from './articleEncryption'
import { getPrimaryRelaySync } from './config'
@ -39,7 +40,7 @@ export function getPrivateContent(
return new Promise((resolve) => {
let resolved = false
const relayUrl = getPrimaryRelaySync()
const sub = pool.sub([relayUrl], createPrivateMessageFilters(eventId, publicKey, authorPubkey))
const sub = (pool as SimplePoolWithSub).sub([relayUrl], createPrivateMessageFilters(eventId, publicKey, authorPubkey))
const finalize = (result: string | null) => {
if (resolved) {
@ -115,7 +116,7 @@ export async function getDecryptionKey(
return new Promise((resolve) => {
let resolved = false
const relayUrl = getPrimaryRelaySync()
const sub = pool.sub([relayUrl], createPrivateMessageFilters(eventId, recipientPublicKey, authorPubkey))
const sub = (pool as SimplePoolWithSub).sub([relayUrl], createPrivateMessageFilters(eventId, recipientPublicKey, authorPubkey))
const finalize = (result: DecryptionKey | null) => {
if (resolved) {

View File

@ -1,5 +1,6 @@
import type { EventTemplate, Event } from 'nostr-tools'
import { getEventHash, signEvent } from 'nostr-tools'
import { finalizeEvent } from 'nostr-tools'
import { hexToBytes } from 'nostr-tools/utils'
import { nostrAuthService } from './nostrAuth'
import { nostrService } from './nostr'
@ -42,12 +43,8 @@ export class NostrRemoteSigner {
if (!privateKey) {
throw new Error('Alby extension required for signing. Please install and connect Alby browser extension.')
}
const eventId = getEventHash(unsignedEvent)
return {
...unsignedEvent,
id: eventId,
sig: signEvent(unsignedEvent, privateKey),
} as Event
const secretKey = hexToBytes(privateKey)
return finalizeEvent(unsignedEvent, secretKey)
}
async signEvent(eventTemplate: EventTemplate): Promise<Event | null> {

View File

@ -1,6 +1,7 @@
import type { Event, Filter } from 'nostr-tools'
import { SimplePool } from 'nostr-tools'
import { getPrimaryRelaySync } from './config'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
/**
* Subscribe to events with timeout
@ -14,7 +15,7 @@ export function subscribeWithTimeout<T>(
return new Promise((resolve) => {
const resolved = { value: false }
const relayUrl = getPrimaryRelaySync()
const sub = pool.sub([relayUrl], filters)
const sub = (pool as SimplePoolWithSub).sub([relayUrl], filters)
let timeoutId: NodeJS.Timeout | null = null
const cleanup = () => {

View File

@ -1,5 +1,6 @@
import type { Event } from 'nostr-tools'
import { SimplePool } from 'nostr-tools'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { getPrimaryRelaySync } from './config'
function createZapFilters(targetPubkey: string, targetEventId: string, userPubkey: string) {
@ -62,7 +63,7 @@ export function checkZapReceipt(
return new Promise((resolve) => {
let resolved = false
const relayUrl = getPrimaryRelaySync()
const sub = pool.sub([relayUrl], createZapFilters(targetPubkey, targetEventId, userPubkey))
const sub = (pool as SimplePoolWithSub).sub([relayUrl], createZapFilters(targetPubkey, targetEventId, userPubkey))
const finalize = (value: boolean) => {
if (resolved) {

View File

@ -1,5 +1,6 @@
import { nostrService } from './nostr'
import { getPrimaryRelaySync } from './config'
import type { Event } from 'nostr-tools'
export function parseZapAmount(event: import('nostr-tools').Event): number {
const amountTag = event.tags.find((tag) => tag[0] === 'amount')?.[1]
@ -48,7 +49,7 @@ export function createZapReceiptPromise(
resolve(value)
}
sub.on('event', (event) => {
sub.on('event', (event: Event) => {
handleZapReceiptEvent(event, amount, recipientPubkey, finalize)
})

View File

@ -1,4 +1,5 @@
import { Event, EventTemplate, getEventHash, signEvent } from 'nostr-tools'
import { Event, EventTemplate, finalizeEvent } from 'nostr-tools'
import { hexToBytes } from 'nostr-tools/utils'
import type { ContentDeliveryTracking } from './platformTrackingTypes'
const TRACKING_KIND = 30078 // Custom kind for platform tracking
@ -21,7 +22,7 @@ export function buildTrackingTags(tracking: ContentDeliveryTracking, platformPub
export function buildTrackingEvent(
tracking: ContentDeliveryTracking,
authorPubkey: string,
_authorPubkey: string,
authorPrivateKey: string,
platformPubkey: string
): Event {
@ -43,16 +44,8 @@ export function buildTrackingEvent(
}),
}
const unsignedEvent = {
pubkey: authorPubkey,
...eventTemplate,
}
return {
...unsignedEvent,
id: getEventHash(unsignedEvent),
sig: signEvent(unsignedEvent, authorPrivateKey),
} as Event
const secretKey = hexToBytes(authorPrivateKey)
return finalizeEvent(eventTemplate, secretKey)
}
export function getTrackingKind(): number {

View File

@ -1,4 +1,5 @@
import { Event, EventTemplate, getEventHash, signEvent } from 'nostr-tools'
import { Event, EventTemplate, finalizeEvent } from 'nostr-tools'
import { hexToBytes } from 'nostr-tools/utils'
import { nostrService } from './nostr'
import { PLATFORM_NPUB } from './platformConfig'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
@ -45,7 +46,7 @@ export class SponsoringTrackingService {
private buildSponsoringTrackingEvent(
tracking: SponsoringTracking,
authorPubkey: string,
_authorPubkey: string,
authorPrivateKey: string
): Event {
const eventTemplate: EventTemplate = {
@ -65,16 +66,8 @@ export class SponsoringTrackingService {
}),
}
const unsignedEvent = {
pubkey: authorPubkey,
...eventTemplate,
}
return {
...unsignedEvent,
id: getEventHash(unsignedEvent),
sig: signEvent(unsignedEvent, authorPrivateKey),
} as Event
const secretKey = hexToBytes(authorPrivateKey)
return finalizeEvent(eventTemplate, secretKey)
}
private async publishSponsoringTrackingEvent(event: Event): Promise<void> {

View File

@ -1,4 +1,4 @@
import { Event, validateEvent, verifySignature } from 'nostr-tools'
import { Event, validateEvent, verifyEvent } from 'nostr-tools'
/**
* Service for verifying zap receipts and their signatures
@ -9,7 +9,7 @@ export class ZapVerificationService {
*/
verifyZapReceiptSignature(event: Event): boolean {
try {
return validateEvent(event) && verifySignature(event)
return validateEvent(event) && verifyEvent(event)
} catch (error) {
console.error('Error verifying zap receipt signature:', error)
return false

View File

@ -68,6 +68,9 @@ presentation.field.picture.uploading=Uploading...
presentation.field.picture.remove=Remove
presentation.field.picture.error.imagesOnly=Only images are allowed
presentation.field.picture.error.uploadFailed=Upload error
presentation.field.authorName=Author name
presentation.field.authorName.placeholder=Your author name
presentation.field.authorName.help=This name will be displayed instead of your public key on your profile
presentation.field.presentation=Personal presentation
presentation.field.presentation.placeholder=Introduce yourself: who you are, your background, your interests...
presentation.field.presentation.help=This presentation will be visible to all readers

2716
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,21 +10,21 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "^14.0.4",
"nostr-tools": "1.17.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^16.1.1",
"nostr-tools": "2.19.4",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-qr-code": "^2.0.18"
},
"devDependencies": {
"@types/node": "^20.10.5",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"typescript": "^5.3.3"
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.1",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
}

View File

@ -68,6 +68,9 @@ presentation.field.picture.uploading=Uploading...
presentation.field.picture.remove=Remove
presentation.field.picture.error.imagesOnly=Only images are allowed
presentation.field.picture.error.uploadFailed=Upload error
presentation.field.authorName=Author name
presentation.field.authorName.placeholder=Your author name
presentation.field.authorName.help=This name will be displayed instead of your public key on your profile
presentation.field.presentation=Personal presentation
presentation.field.presentation.placeholder=Introduce yourself: who you are, your background, your interests...
presentation.field.presentation.help=This presentation will be visible to all readers

View File

@ -68,6 +68,9 @@ presentation.field.picture.uploading=Upload en cours...
presentation.field.picture.remove=Supprimer
presentation.field.picture.error.imagesOnly=Seules les images sont autorisées
presentation.field.picture.error.uploadFailed=Erreur lors de l'upload
presentation.field.authorName=Nom d'auteur
presentation.field.authorName.placeholder=Votre nom d'auteur
presentation.field.authorName.help=Ce nom sera affiché à la place de votre clé publique sur votre profil
presentation.field.presentation=Présentation personnelle
presentation.field.presentation.placeholder=Présentez-vous : qui êtes-vous, votre parcours, vos intérêts...
presentation.field.presentation.help=Cette présentation sera visible par tous les lecteurs

View File

@ -1,13 +1,24 @@
import { SimplePool } from 'nostr-tools'
import type { Filter } from 'nostr-tools'
import type { Event } from 'nostr-tools'
/**
* Subscription interface matching nostr-tools 2.x API
*/
export interface Subscription {
on(event: 'event', callback: (event: Event) => void): void
on(event: 'eose', callback: () => void): void
unsub(): void
}
/**
* Alias for SimplePool with typed sub method from nostr-tools definitions.
* Using the existing type avoids compatibility issues while keeping explicit intent.
* In nostr-tools 2.x, SimplePool has a sub method but it's not properly typed.
*/
export interface SimplePoolWithSub extends SimplePool {
sub: SimplePool['sub']
sub(relays: string[], filters: Filter[]): Subscription
}
export function hasSubMethod(pool: SimplePool): pool is SimplePoolWithSub {
return typeof (pool as SimplePoolWithSub).sub === 'function'
return typeof (pool as any).sub === 'function'
}