331 lines
10 KiB
TypeScript
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 });
|
|
}
|