import { decodeBase64, encodeBase64 } from 'jsr:@std/encoding@0.224/base64'; import { exists } from 'jsr:@std/fs'; import * as nostrTools from '@nostr/tools'; import * as nip06 from '@nostr/tools/nip06'; import { decryptUint8Array, encryptUint8Array, encryptionKey, } from './utils/encryption.ts'; import { getEveFilePath } from './utils/files.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, ); } export async function getCCNPubkey(): Promise { const ccnPubPath = await getEveFilePath('ccn.pub'); const seedPath = await getEveFilePath('ccn.seed'); const doWeHaveKey = await exists(ccnPubPath); if (doWeHaveKey) return Deno.readTextFileSync(ccnPubPath); const ccnSeed = Deno.env.get('CCN_SEED') || ((await exists(seedPath)) ? Deno.readTextFileSync(seedPath) : nip06.generateSeedWords()); const ccnPrivateKey = nip06.privateKeyFromSeedWords(ccnSeed); const ccnPublicKey = nostrTools.getPublicKey(ccnPrivateKey); const encryptedPrivateKey = encryptUint8Array(ccnPrivateKey, encryptionKey); Deno.writeTextFileSync(ccnPubPath, ccnPublicKey); Deno.writeTextFileSync( await getEveFilePath('ccn.priv'), encodeBase64(encryptedPrivateKey), ); Deno.writeTextFileSync(seedPath, ccnSeed); return ccnPublicKey; } export async function getCCNPrivateKey(): Promise { const encryptedPrivateKey = Deno.readTextFileSync( await getEveFilePath('ccn.priv'), ); return decryptUint8Array(decodeBase64(encryptedPrivateKey), encryptionKey); } 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 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, }; }