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:
parent
b3c25bf16f
commit
42e3e7e692
@ -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[]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 } : {})}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
17
deploy.sh
17
deploy.sh
@ -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
|
||||
|
||||
@ -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}`
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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('**')) {
|
||||
|
||||
60
lib/nip95.ts
60
lib/nip95.ts
@ -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, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.text().catch(() => 'Upload failed')
|
||||
throw new Error(message || 'Upload failed')
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
} 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')
|
||||
}
|
||||
|
||||
|
||||
53
lib/nostr.ts
53
lib/nostr.ts
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
2716
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user