import { exists } from "jsr:@std/fs"; import * as nostrTools from "@nostr/tools"; import * as nip06 from "@nostr/tools/nip06"; import { decodeBase64, encodeBase64 } from "jsr:@std/encoding@0.224/base64"; import { getEveFilePath } from "./utils/files.ts"; import { decryptUint8Array, encryptionKey, encryptUint8Array, } from "./utils/encryption.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) || kind === 0; } 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, }; }