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
|
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[]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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 } : {})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
17
deploy.sh
17
deploy.sh
@ -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
|
||||||
|
|||||||
@ -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}`
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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('**')) {
|
||||||
|
|||||||
54
lib/nip95.ts
54
lib/nip95.ts
@ -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
|
||||||
|
try {
|
||||||
|
response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
|
} catch (e) {
|
||||||
if (!response.ok) {
|
const errorMessage = e instanceof Error ? e.message : 'Network error'
|
||||||
const message = await response.text().catch(() => 'Upload failed')
|
console.error('NIP-95 upload fetch error:', {
|
||||||
throw new Error(message || 'Upload failed')
|
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 { 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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user