import { bytesToHex } from '@noble/ciphers/utils'; import { sha512 } from '@noble/hashes/sha2'; import * as nostrTools from '@nostr/tools'; import { base64 } from '@scure/base'; import type { Database } from 'jsr:@db/sqlite'; import { POW_TO_MINE } from '../consts.ts'; import { handleDeletionEvent } from '../dbEvents/deletionEvent.ts'; import { EventAlreadyExistsException, createEncryptedEventForPubkey, } from '../eventEncryptionDecryption.ts'; import { publishToRelays } from '../relays.ts'; import { isAddressableEvent, isCCNReplaceableEvent, isDeleteEvent, isReplaceableEvent, } from '../utils/eventTypes.ts'; import { getCCNPrivateKeyByPubkey } from '../utils/getCCNPrivateKeyByPubkey.ts'; import { log } from '../utils/logs.ts'; import { sql } from '../utils/queries.ts'; import { SecurityEventType, SecuritySeverity, logCCNViolation, logSecurityEvent, } from '../utils/securityLogs.ts'; export function addEventToDb( db: Database, decryptedEvent: nostrTools.VerifiedEvent, encryptedEvent: nostrTools.VerifiedEvent, ccnPubkey: string, ) { log.debug('start', { tag: 'addEventToDb', decryptedId: decryptedEvent.id, encryptedId: encryptedEvent.id, kind: decryptedEvent.kind, ccnPubkey, }); const existingEvent = sql` SELECT * FROM events WHERE id = ${decryptedEvent.id} `(db)[0]; if (existingEvent) throw new EventAlreadyExistsException(); if (isDeleteEvent(decryptedEvent.kind)) { log.debug('isDeleteEvent, delegating to handleDeletionEvent', { tag: 'addEventToDb', decryptId: decryptedEvent.id, }); handleDeletionEvent(db, decryptedEvent, encryptedEvent, ccnPubkey); return; } const isInvite = decryptedEvent.tags.findIndex( (tag: string[]) => tag[0] === 'type' && tag[1] === 'invite', ) !== -1; if (isInvite) { log.debug('isInvite event', { tag: 'addEventToDb' }); const shadContent = bytesToHex( sha512.create().update(decryptedEvent.content).digest(), ); const inviteUsed = sql` SELECT COUNT(*) as count FROM inviter_invitee WHERE invite_hash = ${shadContent} `(db)[0].count; if (inviteUsed > 0) { log.debug('invite already used', { tag: 'addEventToDb' }); logSecurityEvent({ eventType: SecurityEventType.INVITE_ALREADY_USED, severity: SecuritySeverity.HIGH, source: 'invite_processing', details: { invite_hash: shadContent, event_id: decryptedEvent.id, ccn_pubkey: ccnPubkey, invitee_pubkey: decryptedEvent.pubkey, }, }); throw new Error('Invite already used'); } const inviteEvent = sql` SELECT * FROM events WHERE kind = 9999 AND id IN ( SELECT event_id FROM event_tags WHERE tag_name = 'i' AND tag_id IN ( SELECT tag_id FROM event_tags_values WHERE value_position = 1 AND value = ${shadContent} ) ) `(db)[0]; if (!inviteEvent) { log.debug('invite event not found', { tag: 'addEventToDb' }); logSecurityEvent({ eventType: SecurityEventType.INVITE_VALIDATION_FAILURE, severity: SecuritySeverity.HIGH, source: 'invite_processing', details: { error: 'invite_event_not_found', invite_hash: shadContent, event_id: decryptedEvent.id, ccn_pubkey: ccnPubkey, }, }); throw new Error('Invite event not found'); } const inviterPubkey = inviteEvent.pubkey; const inviteePubkey = decryptedEvent.pubkey; db.run('BEGIN TRANSACTION'); log.debug('inserting inviter_invitee and allowed_writes', { tag: 'addEventToDb', }); sql` INSERT INTO inviter_invitee (ccn_pubkey, inviter_pubkey, invitee_pubkey, invite_hash) VALUES (${ccnPubkey}, ${inviterPubkey}, ${inviteePubkey}, ${shadContent}) `(db); sql` INSERT INTO allowed_writes (ccn_pubkey, pubkey) VALUES (${ccnPubkey}, ${inviteePubkey}) `(db); db.run('COMMIT TRANSACTION'); log.debug('committed invite transaction', { tag: 'addEventToDb' }); const allowedPubkeys = sql` SELECT pubkey FROM allowed_writes WHERE ccn_pubkey = ${ccnPubkey} `(db).flatMap((row) => row.pubkey); const ccnName = sql` SELECT name FROM ccns WHERE pubkey = ${ccnPubkey} `(db)[0].name; getCCNPrivateKeyByPubkey(ccnPubkey).then((ccnPrivateKey) => { if (!ccnPrivateKey) { log.error('CCN private key not found', { tag: 'addEventToDb' }); throw new Error('CCN private key not found'); } const tags = allowedPubkeys.map((pubkey) => ['p', pubkey]); tags.push(['t', 'invite']); tags.push(['name', ccnName]); const privateKeyEvent = nostrTools.finalizeEvent( nostrTools.nip13.minePow( { kind: 9998, created_at: Date.now(), content: base64.encode(ccnPrivateKey), tags, pubkey: ccnPubkey, }, POW_TO_MINE, ), ccnPrivateKey, ); const encryptedKeyEvent = createEncryptedEventForPubkey( inviteePubkey, privateKeyEvent, ); publishToRelays(encryptedKeyEvent); log.debug('published encryptedKeyEvent to relays', { tag: 'addEventToDb', }); }); return; } const isAllowedWrite = sql` SELECT COUNT(*) as count FROM allowed_writes WHERE ccn_pubkey = ${ccnPubkey} AND pubkey = ${decryptedEvent.pubkey} `(db)[0].count; if (isAllowedWrite === 0) { log.debug('not allowed to write to this CCN', { tag: 'addEventToDb', pubkey: decryptedEvent.pubkey, }); logCCNViolation( SecurityEventType.UNAUTHORIZED_WRITE_ATTEMPT, ccnPubkey, 'write_event', { attempted_pubkey: decryptedEvent.pubkey, event_id: decryptedEvent.id, event_kind: decryptedEvent.kind, ccn_pubkey: ccnPubkey, }, ); throw new Error('Not allowed to write to this CCN'); } try { db.run('BEGIN TRANSACTION'); log.debug('begin transaction', { tag: 'addEventToDb' }); if (isReplaceableEvent(decryptedEvent.kind)) { log.debug('isReplaceableEvent, updating replaced events', { tag: 'addEventToDb', }); sql` UPDATE events SET replaced = 1 WHERE kind = ${decryptedEvent.kind} AND pubkey = ${decryptedEvent.pubkey} AND ccn_pubkey = ${ccnPubkey} AND (created_at < ${decryptedEvent.created_at} OR (created_at = ${decryptedEvent.created_at} AND id > ${decryptedEvent.id})) `(db); } if (isAddressableEvent(decryptedEvent.kind)) { log.debug('isAddressableEvent, updating replaced events', { tag: 'addEventToDb', }); const dTag = decryptedEvent.tags.find((tag) => tag[0] === 'd')?.[1]; if (dTag) { sql` UPDATE events SET replaced = 1 WHERE kind = ${decryptedEvent.kind} AND pubkey = ${decryptedEvent.pubkey} AND ccn_pubkey = ${ccnPubkey} AND (created_at < ${decryptedEvent.created_at} OR (created_at = ${decryptedEvent.created_at} AND id > ${decryptedEvent.id})) AND id IN ( SELECT event_id FROM event_tags WHERE tag_name = 'd' AND tag_id IN ( SELECT tag_id FROM event_tags_values WHERE value_position = 1 AND value = ${dTag} ) ) `(db); } } if (isCCNReplaceableEvent(decryptedEvent.kind)) { log.debug('isCCNReplaceableEvent, updating replaced events', { tag: 'addEventToDb', }); const dTag = decryptedEvent.tags.find((tag) => tag[0] === 'd')?.[1]; log.debug('dTag', { tag: 'addEventToDb', dTag }); if (dTag) { sql` UPDATE events SET replaced = 1 WHERE kind = ${decryptedEvent.kind} AND ccn_pubkey = ${ccnPubkey} AND (created_at < ${decryptedEvent.created_at} OR (created_at = ${decryptedEvent.created_at} AND id > ${decryptedEvent.id})) AND id IN ( SELECT event_id FROM event_tags WHERE tag_name = 'd' AND tag_id IN ( SELECT tag_id FROM event_tags_values WHERE value_position = 1 AND value = ${dTag} ) ) `(db); } else { sql` UPDATE events SET replaced = 1 WHERE kind = ${decryptedEvent.kind} AND ccn_pubkey = ${ccnPubkey} AND (created_at < ${decryptedEvent.created_at} OR (created_at = ${decryptedEvent.created_at} AND id > ${decryptedEvent.id})) `(db); } } sql` INSERT INTO events (id, original_id, pubkey, created_at, kind, content, sig, first_seen, ccn_pubkey) VALUES ( ${decryptedEvent.id}, ${encryptedEvent.id}, ${decryptedEvent.pubkey}, ${decryptedEvent.created_at}, ${decryptedEvent.kind}, ${decryptedEvent.content}, ${decryptedEvent.sig}, unixepoch(), ${ccnPubkey} ) `(db); log.debug('inserted event', { tag: 'addEventToDb', id: decryptedEvent.id }); if (decryptedEvent.tags) { for (let i = 0; i < decryptedEvent.tags.length; i++) { const tag = sql` INSERT INTO event_tags(event_id, tag_name, tag_index) VALUES ( ${decryptedEvent.id}, ${decryptedEvent.tags[i][0]}, ${i} ) RETURNING tag_id `(db)[0]; for (let j = 1; j < decryptedEvent.tags[i].length; j++) { sql` INSERT INTO event_tags_values(tag_id, value_position, value) VALUES ( ${tag.tag_id}, ${j}, ${decryptedEvent.tags[i][j]} ) `(db); } } log.debug('inserted tags for event', { tag: 'addEventToDb', id: decryptedEvent.id, }); } db.run('COMMIT TRANSACTION'); log.debug('committed transaction', { tag: 'addEventToDb' }); } catch (e) { db.run('ROLLBACK TRANSACTION'); log.error('transaction rolled back', { tag: 'addEventToDb', error: e }); throw e; } log.debug('end', { tag: 'addEventToDb', id: decryptedEvent.id }); }