Fully rewrite relay

This commit is contained in:
Danny Morabito 2025-06-04 12:43:23 +02:00
parent 190e38dfc1
commit 20ffbd4c6d
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: 7CC8056A5A04557E
47 changed files with 3489 additions and 128 deletions

314
src/commands/ccn.ts Normal file
View file

@ -0,0 +1,314 @@
import type { Database } from 'jsr:@db/sqlite';
import { encodeBase64 } from 'jsr:@std/encoding@0.224/base64';
import { hexToBytes } from '@noble/ciphers/utils';
import * as nostrTools from '@nostr/tools';
import type { UserConnection } from '../UserConnection.ts';
import { handleSocketError } from '../index.ts';
import { createNewCCN } from '../utils/createNewCCN.ts';
import { encryptUint8Array, encryptionKey } from '../utils/encryption.ts';
import { getEveFilePath } from '../utils/files.ts';
import { getAllCCNs } from '../utils/getAllCCNs.ts';
import { log } from '../utils/logs.ts';
import { sql } from '../utils/queries.ts';
import {
SecurityEventType,
SecuritySeverity,
logAuthEvent,
logSecurityEvent,
} from '../utils/securityLogs.ts';
function activateCCN(database: Database, pubkey: string): void {
sql`UPDATE ccns SET is_active = 0`(database);
sql`UPDATE ccns SET is_active = 1 WHERE pubkey = ${pubkey}`(database);
logAuthEvent(SecurityEventType.CCN_ACTIVATION_ATTEMPT, true, {
ccn_pubkey: pubkey,
});
}
async function handleCreateCCN(
connection: UserConnection,
data: { name: string; seed?: string; creator: string },
): Promise<void> {
log.debug('start', { tag: 'handleCreateCCN', data });
logSecurityEvent({
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
severity: SecuritySeverity.MEDIUM,
source: 'ccn_management',
details: {
ccn_name: data.name,
creator: data.creator,
has_seed: !!data.seed,
},
});
try {
if (!data.name || typeof data.name !== 'string') {
logSecurityEvent({
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
severity: SecuritySeverity.MEDIUM,
source: 'ccn_management',
details: {
error: 'invalid_name',
name_provided: !!data.name,
name_type: typeof data.name,
},
});
connection.sendNotice('Name is required');
return;
}
if (!data.creator || typeof data.creator !== 'string') {
logSecurityEvent({
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
severity: SecuritySeverity.MEDIUM,
source: 'ccn_management',
details: {
error: 'invalid_creator',
creator_provided: !!data.creator,
creator_type: typeof data.creator,
},
});
connection.sendNotice('Creator is required');
return;
}
const newCcn = await createNewCCN(
connection.db,
data.name,
data.creator,
data.seed,
);
log.debug('created new CCN', {
tag: 'handleCreateCCN',
pubkey: newCcn.pubkey,
});
activateCCN(connection.db, newCcn.pubkey);
log.debug('activated new CCN', {
tag: 'handleCreateCCN',
pubkey: newCcn.pubkey,
});
logSecurityEvent({
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
severity: SecuritySeverity.LOW,
source: 'ccn_management',
details: {
success: true,
ccn_pubkey: newCcn.pubkey,
ccn_name: data.name,
creator: data.creator,
},
});
connection.sendResponse([
'OK',
'CCN CREATED',
true,
JSON.stringify({
pubkey: newCcn.pubkey,
name: data.name,
}),
]);
log.info('CCN created', data);
} catch (error: unknown) {
log.error('error', { tag: 'handleCreateCCN', error });
logSecurityEvent({
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
severity: SecuritySeverity.HIGH,
source: 'ccn_management',
details: {
success: false,
error_message: error instanceof Error ? error.message : 'Unknown error',
ccn_name: data.name,
creator: data.creator,
},
});
handleSocketError(connection, 'create CCN', error);
}
log.debug('end', { tag: 'handleCreateCCN' });
}
function handleGetCCNs(connection: UserConnection): void {
try {
const ccns = getAllCCNs(connection.db);
connection.sendResponse(['OK', 'CCN LIST', true, JSON.stringify(ccns)]);
} catch (error: unknown) {
handleSocketError(connection, 'get CCNs', error);
}
}
function handleActivateCCN(
connection: UserConnection,
data: { pubkey: string },
): void {
log.debug('start', { tag: 'handleActivateCCN', data });
try {
if (!data.pubkey || typeof data.pubkey !== 'string') {
connection.sendNotice('CCN pubkey is required');
return;
}
const ccnExists = sql`
SELECT COUNT(*) as count FROM ccns WHERE pubkey = ${data.pubkey}
`(connection.db)[0].count;
if (ccnExists === 0) {
connection.sendNotice('CCN not found');
log.debug('CCN not found', {
tag: 'handleActivateCCN',
pubkey: data.pubkey,
});
return;
}
for (const subscriptionId of connection.subscriptions.keys()) {
connection.sendResponse([
'CLOSED',
subscriptionId,
'Subscription closed due to CCN activation',
]);
log.debug('closed subscription', {
tag: 'handleActivateCCN',
subscriptionId,
});
}
connection.subscriptions.clear();
log.info('All subscriptions cleared due to CCN activation', {});
activateCCN(connection.db, data.pubkey);
log.debug('activated CCN', {
tag: 'handleActivateCCN',
pubkey: data.pubkey,
});
const activatedCCN = sql`
SELECT pubkey, name FROM ccns WHERE pubkey = ${data.pubkey}
`(connection.db)[0];
connection.sendResponse([
'OK',
'CCN ACTIVATED',
true,
JSON.stringify(activatedCCN),
]);
log.info(`CCN activated: ${activatedCCN.name}`, {});
} catch (error: unknown) {
log.error('error', { tag: 'handleActivateCCN', error });
handleSocketError(connection, 'activate CCN', error);
}
log.debug('end', { tag: 'handleActivateCCN' });
}
async function handleAddCCN(
connection: UserConnection,
data: { name: string; allowedPubkeys: string[]; privateKey: string },
): Promise<void> {
log.debug('start', { tag: 'handleAddCCN', data });
try {
if (!data.privateKey || typeof data.privateKey !== 'string') {
connection.sendNotice('CCN private key is required');
return;
}
const privateKeyBytes = hexToBytes(data.privateKey);
const pubkey = nostrTools.getPublicKey(privateKeyBytes);
log.debug('derived pubkey', { tag: 'handleAddCCN', pubkey });
const ccnExists = sql`
SELECT COUNT(*) as count FROM ccns WHERE pubkey = ${pubkey}
`(connection.db)[0].count;
if (ccnExists > 0) {
connection.sendNotice('CCN already exists');
log.debug('CCN already exists', {
tag: 'handleAddCCN',
pubkey,
});
return;
}
const ccnPublicKey = nostrTools.getPublicKey(privateKeyBytes);
const ccnPrivPath = await getEveFilePath(`ccn_keys/${ccnPublicKey}`);
const encryptedPrivateKey = encryptUint8Array(
privateKeyBytes,
encryptionKey,
);
Deno.writeTextFileSync(ccnPrivPath, encodeBase64(encryptedPrivateKey));
connection.db.run('BEGIN TRANSACTION');
log.debug('begin transaction', { tag: 'handleAddCCN' });
sql`INSERT INTO ccns (pubkey, name) VALUES (${ccnPublicKey}, ${data.name})`(
connection.db,
);
for (const allowedPubkey of data.allowedPubkeys)
sql`INSERT INTO allowed_writes (ccn_pubkey, pubkey) VALUES (${ccnPublicKey}, ${allowedPubkey})`(
connection.db,
);
connection.db.run('COMMIT TRANSACTION');
log.debug('committed transaction', { tag: 'handleAddCCN' });
activateCCN(connection.db, ccnPublicKey);
log.debug('activated CCN', {
tag: 'handleAddCCN',
pubkey: ccnPublicKey,
});
connection.sendResponse([
'OK',
'CCN ADDED',
true,
JSON.stringify({
pubkey: ccnPublicKey,
name: 'New CCN',
}),
]);
} catch (error: unknown) {
log.error('error', { tag: 'handleAddCCN', error });
handleSocketError(connection, 'ADD CCN', error);
}
log.debug('end', { tag: 'handleAddCCN' });
}
function handleCCNCommands(
connection: UserConnection,
command: string,
data: unknown,
) {
switch (command) {
case 'CREATE':
return handleCreateCCN(
connection,
data as { name: string; seed?: string; creator: string },
);
case 'ADD':
return handleAddCCN(
connection,
data as { name: string; allowedPubkeys: string[]; privateKey: string },
);
case 'LIST':
return handleGetCCNs(connection);
case 'ACTIVATE':
return handleActivateCCN(connection, data as { pubkey: string });
default:
return log.warn('Invalid CCN command', {});
}
}
export {
activateCCN,
handleActivateCCN,
handleAddCCN,
handleCCNCommands,
handleCreateCCN,
handleGetCCNs,
};

15
src/commands/close.ts Normal file
View file

@ -0,0 +1,15 @@
import type { UserConnection } from '../UserConnection.ts';
import { log } from '../utils/logs.ts';
export function handleClose(
connection: UserConnection,
subscriptionId: string,
) {
if (!connection.subscriptions.has(subscriptionId)) {
return log.warn(
`Closing unknown subscription? That's weird. Subscription ID: ${subscriptionId}`,
);
}
connection.subscriptions.delete(subscriptionId);
}

85
src/commands/event.ts Normal file
View file

@ -0,0 +1,85 @@
import * as nostrTools from '@nostr/tools';
import type { UserConnection } from '../UserConnection.ts';
import { addEventToDb } from '../dbEvents/addEventToDb.ts';
import {
EventAlreadyExistsException,
createEncryptedEvent,
} from '../eventEncryptionDecryption.ts';
import { filtersMatchingEvent } from '../utils/filtersMatchingEvent.ts';
import { getActiveCCN } from '../utils/getActiveCCN.ts';
import { isArray } from '../utils/isArray.ts';
import { log } from '../utils/logs.ts';
import { queueEventForTransmission } from '../utils/outboundQueue.ts';
export async function handleEvent(
connection: UserConnection,
event: nostrTools.Event,
) {
log.debug('start', { tag: 'handleEvent', eventId: event.id });
const valid = nostrTools.verifyEvent(event);
if (!valid) {
connection.sendNotice('Invalid event');
return log.warn('Invalid event', { tag: 'handleEvent' });
}
const activeCCN = getActiveCCN(connection.db);
if (!activeCCN) {
connection.sendNotice('No active CCN found');
return log.warn('No active CCN found', { tag: 'handleEvent' });
}
const encryptedEvent = await createEncryptedEvent(event, connection.db);
try {
if (isArray(encryptedEvent)) {
log.debug('adding chunked event to database', {
tag: 'handleEvent',
});
addEventToDb(connection.db, event, encryptedEvent[0], activeCCN.pubkey);
} else {
addEventToDb(connection.db, event, encryptedEvent, activeCCN.pubkey);
}
queueEventForTransmission(
connection.db,
event.id,
encryptedEvent,
activeCCN.pubkey,
);
log.debug('event queued for transmission', {
tag: 'handleEvent',
eventId: event.id,
});
} catch (e) {
if (e instanceof EventAlreadyExistsException) {
log.warn('Event already exists');
return;
}
if (e instanceof Error)
log.error('error adding event', {
tag: 'handleEvent',
error: e.stack,
});
else
log.error('error adding event', {
tag: 'handleEvent',
error: String(e),
});
}
connection.sendOK(event.id, true, 'Event added');
log.debug('sent OK', { tag: 'handleEvent', eventId: event.id });
const filtersThatMatchEvent = filtersMatchingEvent(event, connection);
for (let i = 0; i < filtersThatMatchEvent.length; i++) {
const filter = filtersThatMatchEvent[i];
connection.sendEvent(filter, event);
log.debug('sent event to filter', {
tag: 'handleEvent',
filter,
eventId: event.id,
});
}
log.debug('end', { tag: 'handleEvent', eventId: event.id });
}

291
src/commands/request.ts Normal file
View file

@ -0,0 +1,291 @@
import type { NostrClientREQ } from 'jsr:@nostrify/types';
import type { UserConnection } from '../UserConnection.ts';
import { isCCNReplaceableEvent } from '../utils/eventTypes.ts';
import { getActiveCCN } from '../utils/getActiveCCN.ts';
import { log } from '../utils/logs.ts';
import { parseATagQuery } from '../utils/parseATagQuery.ts';
import { mixQuery, sql, sqlPartial } from '../utils/queries.ts';
export function handleRequest(
connection: UserConnection,
request: NostrClientREQ,
) {
log.debug('start', { tag: 'handleRequest', request });
const [, subscriptionId, ...filters] = request;
if (connection.subscriptions.has(subscriptionId)) {
return log.warn('Duplicate subscription ID', {
tag: 'handleRequest',
});
}
log.info(
`New subscription: ${subscriptionId} with filters: ${JSON.stringify(
filters,
)}`,
);
const activeCCN = getActiveCCN(connection.db);
if (!activeCCN) {
connection.sendNotice('No active CCN found');
return log.warn('No active CCN found', { tag: 'handleRequest' });
}
let query = sqlPartial`SELECT * FROM events WHERE replaced = 0 AND deleted = 0 AND ccn_pubkey = ${activeCCN.pubkey}`;
let minLimit: number | null = null;
for (const filter of filters) {
if (filter.limit && filter.limit > 0) {
minLimit =
minLimit === null ? filter.limit : Math.min(minLimit, filter.limit);
}
}
const filtersAreNotEmpty = filters.some((filter) => {
return Object.values(filter).some((value) => {
return value.length > 0;
});
});
if (filtersAreNotEmpty) {
query = mixQuery(query, sqlPartial`AND`);
for (let i = 0; i < filters.length; i++) {
// filters act as OR, filter groups act as AND
query = mixQuery(query, sqlPartial`(`);
const filter = Object.entries(filters[i]).filter(([type, value]) => {
if (type === 'ids') return value.length > 0;
if (type === 'authors') return value.length > 0;
if (type === 'kinds') return value.length > 0;
if (type.startsWith('#')) return value.length > 0;
if (type === 'since') return value > 0;
if (type === 'until') return value > 0;
if (type === 'limit') return value > 0;
return false;
});
const filterWithoutLimit = filter.filter(([type]) => type !== 'limit');
for (let j = 0; j < filter.length; j++) {
const [type, value] = filter[j];
if (type === 'ids') {
const uniqueIds = [...new Set(value)];
query = mixQuery(query, sqlPartial`(`);
for (let k = 0; k < uniqueIds.length; k++) {
const id = uniqueIds[k] as string;
query = mixQuery(query, sqlPartial`(id = ${id})`);
if (k < uniqueIds.length - 1) {
query = mixQuery(query, sqlPartial`OR`);
}
}
query = mixQuery(query, sqlPartial`)`);
}
if (type === 'authors') {
const uniqueAuthors = [...new Set(value)];
query = mixQuery(query, sqlPartial`(`);
for (let k = 0; k < uniqueAuthors.length; k++) {
const author = uniqueAuthors[k] as string;
query = mixQuery(query, sqlPartial`(pubkey = ${author})`);
if (k < uniqueAuthors.length - 1) {
query = mixQuery(query, sqlPartial`OR`);
}
}
query = mixQuery(query, sqlPartial`)`);
}
if (type === 'kinds') {
const uniqueKinds = [...new Set(value)];
query = mixQuery(query, sqlPartial`(`);
for (let k = 0; k < uniqueKinds.length; k++) {
const kind = uniqueKinds[k] as number;
query = mixQuery(query, sqlPartial`(kind = ${kind})`);
if (k < uniqueKinds.length - 1) {
query = mixQuery(query, sqlPartial`OR`);
}
}
query = mixQuery(query, sqlPartial`)`);
}
if (type.startsWith('#')) {
const tag = type.slice(1);
const uniqueValues = [...new Set(value)];
query = mixQuery(query, sqlPartial`(`);
for (let k = 0; k < uniqueValues.length; k++) {
const tagValue = uniqueValues[k] as string;
if (tag === 'a') {
const aTagInfo = parseATagQuery(tagValue);
if (aTagInfo.dTag && aTagInfo.dTag !== '') {
if (isCCNReplaceableEvent(aTagInfo.kind)) {
// CCN replaceable event reference
query = mixQuery(
query,
sqlPartial`id IN (
SELECT e.id
FROM events e
JOIN event_tags t ON e.id = t.event_id
JOIN event_tags_values v ON t.tag_id = v.tag_id
WHERE e.kind = ${aTagInfo.kind}
AND t.tag_name = 'd'
AND v.value_position = 1
AND v.value = ${aTagInfo.dTag}
)`,
);
} else {
// Addressable event reference
query = mixQuery(
query,
sqlPartial`id IN (
SELECT e.id
FROM events e
JOIN event_tags t ON e.id = t.event_id
JOIN event_tags_values v ON t.tag_id = v.tag_id
WHERE e.kind = ${aTagInfo.kind}
AND e.pubkey = ${aTagInfo.pubkey}
AND t.tag_name = 'd'
AND v.value_position = 1
AND v.value = ${aTagInfo.dTag}
)`,
);
}
} else {
// Replaceable event reference
query = mixQuery(
query,
sqlPartial`id IN (
SELECT id
FROM events
WHERE kind = ${aTagInfo.kind}
AND pubkey = ${aTagInfo.pubkey}
)`,
);
}
} else {
// Regular tag handling (unchanged)
query = mixQuery(
query,
sqlPartial`id IN (
SELECT t.event_id
FROM event_tags t
WHERE t.tag_name = ${tag}
AND t.tag_id IN (
SELECT v.tag_id
FROM event_tags_values v
WHERE v.value_position = 1
AND v.value = ${tagValue}
)
)`,
);
}
if (k < uniqueValues.length - 1) {
query = mixQuery(query, sqlPartial`OR`);
}
}
query = mixQuery(query, sqlPartial`)`);
}
if (type === 'since') {
query = mixQuery(query, sqlPartial`created_at > ${value}`);
}
if (type === 'until') {
query = mixQuery(query, sqlPartial`created_at <= ${value}`);
}
if (j < filterWithoutLimit.length - 1)
query = mixQuery(query, sqlPartial`AND`);
}
query = mixQuery(query, sqlPartial`)`);
if (i < filters.length - 1) query = mixQuery(query, sqlPartial`OR`);
}
}
query = mixQuery(query, sqlPartial`ORDER BY created_at ASC`);
if (minLimit !== null) {
query = mixQuery(query, sqlPartial`LIMIT ${minLimit}`);
}
log.debug('built query', {
tag: 'handleRequest',
query: query.query,
values: query.values,
});
const events = connection.db.prepare(query.query).all(...query.values);
log.debug('found events', {
tag: 'handleRequest',
count: events.length,
});
for (let i = 0; i < events.length; i++) {
const rawTags = sql`SELECT * FROM event_tags_view WHERE event_id = ${
events[i].id
}`(connection.db);
const tagsByIndex = new Map<
number,
{
name: string;
values: Map<number, string>;
}
>();
for (const tag of rawTags) {
let tagData = tagsByIndex.get(tag.tag_index);
if (!tagData) {
tagData = {
name: tag.tag_name,
values: new Map(),
};
tagsByIndex.set(tag.tag_index, tagData);
}
tagData.values.set(tag.tag_value_position, tag.tag_value);
}
const tagsArray = Array.from(tagsByIndex.entries())
.sort(([indexA], [indexB]) => indexA - indexB)
.map(([_, tagData]) => {
const { name, values } = tagData;
return [
name,
...Array.from(values.entries())
.sort(([posA], [posB]) => posA - posB)
.map(([_, value]) => value),
];
});
const event = {
id: events[i].id,
pubkey: events[i].pubkey,
created_at: events[i].created_at,
kind: events[i].kind,
tags: tagsArray,
content: events[i].content,
sig: events[i].sig,
};
connection.sendEvent(subscriptionId, event);
log.debug('sent event', {
tag: 'handleRequest',
subscriptionId,
eventId: event.id,
});
}
connection.sendEOSE(subscriptionId);
log.debug('sent EOSE', { tag: 'handleRequest', subscriptionId });
connection.subscriptions.set(subscriptionId, filters);
log.debug('end', { tag: 'handleRequest', subscriptionId });
}