162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
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<T>(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<Uint8Array> {
|
|
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 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;
|
|
}
|