import * as nostrTools from '@nostr/tools'; import * as nip06 from '@nostr/tools/nip06'; import type { Database } from 'jsr:@db/sqlite'; import { decodeBase64, encodeBase64 } from 'jsr:@std/encoding@0.224/base64'; import { exists } from 'jsr:@std/fs'; import { decryptUint8Array, encryptUint8Array, encryptionKey, } from './utils/encryption.ts'; import { getEveFilePath } from './utils/files.ts'; import { sql } from './utils/queries.ts'; export function isLocalhost(req: Request): boolean { const url = new URL(req.url); const hostname = url.hostname; return ( hostname === '127.0.0.1' || hostname === '::1' || hostname === 'localhost' ); } export function isValidJSON(str: string) { try { JSON.parse(str); } catch { return false; } return true; } export function isArray(obj: unknown): obj is T[] { return Array.isArray(obj); } export function randomTimeUpTo2DaysInThePast() { const now = Date.now(); const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000 - 3600 * 1000; // 1 hour buffer in case of clock skew return Math.floor( (Math.floor(Math.random() * (now - twoDaysAgo)) + twoDaysAgo) / 1000, ); } /** * Get all CCNs from the database */ export function getAllCCNs(db: Database): { pubkey: string; name: string }[] { return sql`SELECT pubkey, name FROM ccns`(db) as { pubkey: string; name: string; }[]; } /** * Create a new CCN and store it in the database * * @param db - The database instance * @param name - The name of the CCN * @param seed - The seed words for the CCN * @returns The public key and private key of the CCN */ export async function createNewCCN( db: Database, name: string, creator: string, seed?: string, ): Promise<{ pubkey: string; privkey: Uint8Array }> { const ccnSeed = seed || nip06.generateSeedWords(); const ccnPrivateKey = nip06.privateKeyFromSeedWords(ccnSeed); const ccnPublicKey = nostrTools.getPublicKey(ccnPrivateKey); const ccnSeedPath = await getEveFilePath(`ccn_seeds/${ccnPublicKey}`); const ccnPrivPath = await getEveFilePath(`ccn_keys/${ccnPublicKey}`); await Deno.mkdir(await getEveFilePath('ccn_seeds'), { recursive: true }); await Deno.mkdir(await getEveFilePath('ccn_keys'), { recursive: true }); const encryptedPrivateKey = encryptUint8Array(ccnPrivateKey, encryptionKey); Deno.writeTextFileSync(ccnSeedPath, ccnSeed); Deno.writeTextFileSync(ccnPrivPath, encodeBase64(encryptedPrivateKey)); db.run('BEGIN TRANSACTION'); sql`INSERT INTO ccns (pubkey, name) VALUES (${ccnPublicKey}, ${name})`(db); sql`INSERT INTO allowed_writes (ccn_pubkey, pubkey) VALUES (${ccnPublicKey}, ${creator})`( db, ); db.run('COMMIT TRANSACTION'); return { pubkey: ccnPublicKey, privkey: ccnPrivateKey, }; } /** * Get the private key for a specific CCN */ export async function getCCNPrivateKeyByPubkey( pubkey: string, ): Promise { const ccnPrivPath = await getEveFilePath(`ccn_keys/${pubkey}`); if (await exists(ccnPrivPath)) { const encryptedPrivateKey = Deno.readTextFileSync(ccnPrivPath); return decryptUint8Array(decodeBase64(encryptedPrivateKey), encryptionKey); } throw new Error(`CCN private key for ${pubkey} not found`); } export function isReplaceableEvent(kind: number): boolean { return (kind >= 10000 && kind < 20000) || kind === 0 || kind === 3; } export function isAddressableEvent(kind: number): boolean { return kind >= 30000 && kind < 40000; } export function isRegularEvent(kind: number): boolean { return ( (kind >= 1000 && kind < 10000) || (kind >= 4 && kind < 45) || kind === 1 || kind === 2 ); } export function isDeleteEvent(kind: number): boolean { return kind === 5; } export function isCCNReplaceableEvent(kind: number): boolean { return kind >= 60000 && kind < 65536; } export function parseATagQuery(aTagValue: string): { kind: number; pubkey: string; dTag?: string; } { const parts = aTagValue.split(':'); if (parts.length < 2) return { kind: 0, pubkey: '' }; return { kind: Number.parseInt(parts[0], 10), pubkey: parts[1], dTag: parts.length > 2 ? parts[2] : undefined, }; } /** * Get the single active CCN from the database * @returns The active CCN or null if none is active */ export function getActiveCCN( db: Database, ): { pubkey: string; name: string } | null { const result = sql`SELECT pubkey, name FROM ccns WHERE is_active = 1 LIMIT 1`( db, ); return result.length > 0 ? (result[0] as { pubkey: string; name: string }) : null; }