Eve-Relay/src/dbEvents/addEventToDb.ts
2025-06-04 12:43:23 +02:00

331 lines
10 KiB
TypeScript

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 });
}