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 loading: boolean
onChange: (value: string | null) => void onChange: (value: string | null) => void
setIsOpen: (open: boolean) => void setIsOpen: (open: boolean) => void
dropdownRef: React.RefObject<HTMLDivElement> dropdownRef: React.RefObject<HTMLDivElement | null>
buttonRef: React.RefObject<HTMLButtonElement> buttonRef: React.RefObject<HTMLButtonElement | null>
getDisplayName: (pubkey: string) => string getDisplayName: (pubkey: string) => string
getPicture: (pubkey: string) => string | undefined getPicture: (pubkey: string) => string | undefined
getMnemonicIcons: (pubkey: string) => string[] getMnemonicIcons: (pubkey: string) => string[]

View File

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

View File

@ -9,6 +9,7 @@ import { PresentationFormHeader } from './PresentationFormHeader'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
interface AuthorPresentationDraft { interface AuthorPresentationDraft {
authorName: string
presentation: string presentation: string
contentDescription: string contentDescription: string
mainnetAddress: 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({ function PictureField({
draft, draft,
onChange, onChange,
@ -128,6 +150,7 @@ const PresentationFields = ({
onChange: (next: AuthorPresentationDraft) => void onChange: (next: AuthorPresentationDraft) => void
}) => ( }) => (
<div className="space-y-4"> <div className="space-y-4">
<AuthorNameField draft={draft} onChange={onChange} />
<PictureField draft={draft} onChange={onChange} /> <PictureField draft={draft} onChange={onChange} />
<PresentationField draft={draft} onChange={onChange} /> <PresentationField draft={draft} onChange={onChange} />
<ContentDescriptionField 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 { loading, error, success, publishPresentation } = useAuthorPresentation(pubkey)
const [draft, setDraft] = useState<AuthorPresentationDraft>({ const [draft, setDraft] = useState<AuthorPresentationDraft>({
authorName: existingAuthorName ?? '',
presentation: '', presentation: '',
contentDescription: '', contentDescription: '',
mainnetAddress: '', mainnetAddress: '',
}) })
const [validationError, setValidationError] = useState<string | null>(null) 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( const handleSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => { async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
@ -184,6 +215,10 @@ function useAuthorPresentationState(pubkey: string | null) {
setValidationError(t('presentation.validation.invalidAddress')) setValidationError(t('presentation.validation.invalidAddress'))
return return
} }
if (!draft.authorName.trim()) {
setValidationError('Author name is required')
return
}
setValidationError(null) setValidationError(null)
await publishPresentation(draft) await publishPresentation(draft)
}, },
@ -261,7 +296,7 @@ function AuthorPresentationFormView({
pubkey: string | null pubkey: string | null
profile: { name?: string; pubkey: string } | null profile: { name?: string; pubkey: string } | null
}) { }) {
const state = useAuthorPresentationState(pubkey) const state = useAuthorPresentationState(pubkey, profile?.name)
if (!pubkey) { if (!pubkey) {
return <NoAccountView /> return <NoAccountView />

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,10 @@ import { useState } from 'react'
import { nostrService } from '@/lib/nostr' import { nostrService } from '@/lib/nostr'
import { articlePublisher } from '@/lib/articlePublisher' import { articlePublisher } from '@/lib/articlePublisher'
import type { Article } from '@/types/nostr' import type { Article } from '@/types/nostr'
import type { NostrProfile } from '@/types/nostr'
interface AuthorPresentationDraft { interface AuthorPresentationDraft {
authorName: string
presentation: string presentation: string
contentDescription: string contentDescription: string
mainnetAddress: string mainnetAddress: string
@ -32,8 +34,20 @@ export function useAuthorPresentation(pubkey: string | null) {
return 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 // 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 preview = draft.presentation.substring(0, 200)
const fullContent = `${draft.presentation}\n\n---\n\nDescription du contenu :\n${draft.contentDescription}` const fullContent = `${draft.presentation}\n\n---\n\nDescription du contenu :\n${draft.contentDescription}`

View File

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

View File

@ -1,6 +1,7 @@
import { nostrService } from './nostr' import { nostrService } from './nostr'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
import type { Event } from 'nostr-tools'
export interface ContentDeliveryStatus { export interface ContentDeliveryStatus {
messageEventId: string | null messageEventId: string | null
@ -44,7 +45,7 @@ function setupContentDeliveryHandlers(
finalize: (result: ContentDeliveryStatus) => void, finalize: (result: ContentDeliveryStatus) => void,
isResolved: () => boolean isResolved: () => boolean
): void { ): void {
sub.on('event', (event) => { sub.on('event', (event: Event) => {
status.published = true status.published = true
status.verifiedOnRelay = true status.verifiedOnRelay = true
status.messageEventId = event.id 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 { generateRecoveryPhrase } from './keyManagementRecovery'
import { deriveKeyFromPhrase, encryptNsec, decryptNsec } from './keyManagementEncryption' import { deriveKeyFromPhrase, encryptNsec, decryptNsec } from './keyManagementEncryption'
import { import {
@ -21,8 +22,9 @@ export class KeyManagementService {
* Returns the private key (hex) and public key (hex) * Returns the private key (hex) and public key (hex)
*/ */
generateKeyPair(): { privateKey: string; publicKey: string; npub: string } { generateKeyPair(): { privateKey: string; publicKey: string; npub: string } {
const privateKeyHex = generatePrivateKey() const secretKey = generateSecretKey()
const publicKeyHex = getPublicKey(privateKeyHex) const privateKeyHex = bytesToHex(secretKey)
const publicKeyHex = getPublicKey(secretKey)
const npub = nip19.npubEncode(publicKeyHex) const npub = nip19.npubEncode(publicKeyHex)
return { return {
@ -52,7 +54,8 @@ export class KeyManagementService {
privateKeyHex = privateKey privateKeyHex = privateKey
} }
const publicKeyHex = getPublicKey(privateKeyHex) const secretKey = hexToBytes(privateKeyHex)
const publicKeyHex = getPublicKey(secretKey)
const npub = nip19.npubEncode(publicKeyHex) const npub = nip19.npubEncode(publicKeyHex)
return { return {
@ -143,7 +146,8 @@ export class KeyManagementService {
const privateKeyHex = await decryptNsec(derivedKey, encryptedNsec) const privateKeyHex = await decryptNsec(derivedKey, encryptedNsec)
// Verify by computing public key // Verify by computing public key
const publicKeyHex = getPublicKey(privateKeyHex) const secretKey = hexToBytes(privateKeyHex)
const publicKeyHex = getPublicKey(secretKey)
const npub = nip19.npubEncode(publicKeyHex) const npub = nip19.npubEncode(publicKeyHex)
return { return {

View File

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

View File

@ -47,18 +47,60 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
const response = await fetch(endpoint, { let response: Response
method: 'POST', try {
body: formData, response = await fetch(endpoint, {
}) method: 'POST',
body: formData,
if (!response.ok) { })
const message = await response.text().catch(() => 'Upload failed') } catch (e) {
throw new Error(message || 'Upload failed') 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) { if (!result.url) {
console.error('NIP-95 upload missing URL:', {
endpoint,
response: result,
})
throw new Error('Upload response missing URL') 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 { Article, NostrProfile } from '@/types/nostr'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { parseArticleFromEvent } from './nostrEventParsing' import { parseArticleFromEvent } from './nostrEventParsing'
@ -64,17 +65,13 @@ class NostrService {
throw new Error('Private key not set or pool not initialized') throw new Error('Private key not set or pool not initialized')
} }
const unsignedEvent = { const unsignedEvent: EventTemplate = {
pubkey: this.publicKey ?? '',
...eventTemplate, ...eventTemplate,
created_at: eventTemplate.created_at ?? Math.floor(Date.now() / 1000), created_at: eventTemplate.created_at ?? Math.floor(Date.now() / 1000),
} }
const event = { const secretKey = hexToBytes(this.privateKey)
...unsignedEvent, const event = finalizeEvent(unsignedEvent, secretKey)
id: getEventHash(unsignedEvent),
sig: signEvent(unsignedEvent, this.privateKey),
} as Event
try { try {
const relayUrl = await getPrimaryRelay() const relayUrl = await getPrimaryRelay()
@ -228,6 +225,46 @@ class NostrService {
return subscribeWithTimeout(this.pool, filters, parseProfile, 5000) 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> { async createZapRequest(targetPubkey: string, targetEventId: string, amount: number): Promise<Event> {
if (!this.privateKey) { if (!this.privateKey) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { nostrService } from './nostr' import { nostrService } from './nostr'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
import type { Event } from 'nostr-tools'
export function parseZapAmount(event: import('nostr-tools').Event): number { export function parseZapAmount(event: import('nostr-tools').Event): number {
const amountTag = event.tags.find((tag) => tag[0] === 'amount')?.[1] const amountTag = event.tags.find((tag) => tag[0] === 'amount')?.[1]
@ -48,7 +49,7 @@ export function createZapReceiptPromise(
resolve(value) resolve(value)
} }
sub.on('event', (event) => { sub.on('event', (event: Event) => {
handleZapReceiptEvent(event, amount, recipientPubkey, finalize) 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' import type { ContentDeliveryTracking } from './platformTrackingTypes'
const TRACKING_KIND = 30078 // Custom kind for platform tracking const TRACKING_KIND = 30078 // Custom kind for platform tracking
@ -21,7 +22,7 @@ export function buildTrackingTags(tracking: ContentDeliveryTracking, platformPub
export function buildTrackingEvent( export function buildTrackingEvent(
tracking: ContentDeliveryTracking, tracking: ContentDeliveryTracking,
authorPubkey: string, _authorPubkey: string,
authorPrivateKey: string, authorPrivateKey: string,
platformPubkey: string platformPubkey: string
): Event { ): Event {
@ -43,16 +44,8 @@ export function buildTrackingEvent(
}), }),
} }
const unsignedEvent = { const secretKey = hexToBytes(authorPrivateKey)
pubkey: authorPubkey, return finalizeEvent(eventTemplate, secretKey)
...eventTemplate,
}
return {
...unsignedEvent,
id: getEventHash(unsignedEvent),
sig: signEvent(unsignedEvent, authorPrivateKey),
} as Event
} }
export function getTrackingKind(): number { 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 { nostrService } from './nostr'
import { PLATFORM_NPUB } from './platformConfig' import { PLATFORM_NPUB } from './platformConfig'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
@ -45,7 +46,7 @@ export class SponsoringTrackingService {
private buildSponsoringTrackingEvent( private buildSponsoringTrackingEvent(
tracking: SponsoringTracking, tracking: SponsoringTracking,
authorPubkey: string, _authorPubkey: string,
authorPrivateKey: string authorPrivateKey: string
): Event { ): Event {
const eventTemplate: EventTemplate = { const eventTemplate: EventTemplate = {
@ -65,16 +66,8 @@ export class SponsoringTrackingService {
}), }),
} }
const unsignedEvent = { const secretKey = hexToBytes(authorPrivateKey)
pubkey: authorPubkey, return finalizeEvent(eventTemplate, secretKey)
...eventTemplate,
}
return {
...unsignedEvent,
id: getEventHash(unsignedEvent),
sig: signEvent(unsignedEvent, authorPrivateKey),
} as Event
} }
private async publishSponsoringTrackingEvent(event: Event): Promise<void> { 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 * Service for verifying zap receipts and their signatures
@ -9,7 +9,7 @@ export class ZapVerificationService {
*/ */
verifyZapReceiptSignature(event: Event): boolean { verifyZapReceiptSignature(event: Event): boolean {
try { try {
return validateEvent(event) && verifySignature(event) return validateEvent(event) && verifyEvent(event)
} catch (error) { } catch (error) {
console.error('Error verifying zap receipt signature:', error) console.error('Error verifying zap receipt signature:', error)
return false return false

View File

@ -68,6 +68,9 @@ presentation.field.picture.uploading=Uploading...
presentation.field.picture.remove=Remove presentation.field.picture.remove=Remove
presentation.field.picture.error.imagesOnly=Only images are allowed presentation.field.picture.error.imagesOnly=Only images are allowed
presentation.field.picture.error.uploadFailed=Upload error 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=Personal presentation
presentation.field.presentation.placeholder=Introduce yourself: who you are, your background, your interests... 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 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" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"next": "^14.0.4", "next": "^16.1.1",
"nostr-tools": "1.17.0", "nostr-tools": "2.19.4",
"react": "^18.2.0", "react": "^19.2.3",
"react-dom": "^18.2.0", "react-dom": "^19.2.3",
"react-qr-code": "^2.0.18" "react-qr-code": "^2.0.18"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.5", "@types/node": "^25.0.3",
"@types/react": "^18.3.27", "@types/react": "^19.2.7",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.23",
"eslint": "^8.56.0", "eslint": "^9.39.2",
"eslint-config-next": "^14.0.4", "eslint-config-next": "^16.1.1",
"postcss": "^8.4.32", "postcss": "^8.5.6",
"tailwindcss": "^3.3.6", "tailwindcss": "^4.1.18",
"typescript": "^5.3.3" "typescript": "^5.9.3"
} }
} }

View File

@ -68,6 +68,9 @@ presentation.field.picture.uploading=Uploading...
presentation.field.picture.remove=Remove presentation.field.picture.remove=Remove
presentation.field.picture.error.imagesOnly=Only images are allowed presentation.field.picture.error.imagesOnly=Only images are allowed
presentation.field.picture.error.uploadFailed=Upload error 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=Personal presentation
presentation.field.presentation.placeholder=Introduce yourself: who you are, your background, your interests... 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 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.remove=Supprimer
presentation.field.picture.error.imagesOnly=Seules les images sont autorisées presentation.field.picture.error.imagesOnly=Seules les images sont autorisées
presentation.field.picture.error.uploadFailed=Erreur lors de l'upload 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=Présentation personnelle
presentation.field.presentation.placeholder=Présentez-vous : qui êtes-vous, votre parcours, vos intérêts... 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 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 { 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. * 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 { export interface SimplePoolWithSub extends SimplePool {
sub: SimplePool['sub'] sub(relays: string[], filters: Filter[]): Subscription
} }
export function hasSubMethod(pool: SimplePool): pool is SimplePoolWithSub { export function hasSubMethod(pool: SimplePool): pool is SimplePoolWithSub {
return typeof (pool as SimplePoolWithSub).sub === 'function' return typeof (pool as any).sub === 'function'
} }