You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

213 lines
6.2 KiB

import { finalizeEvent, getEventHash, getPublicKey, nip19 } from "nostr-tools";
import { EventKind, EventMetadata, SignedEvent } from "../types";
/**
* Normalize secret key from bech32 nsec or hex format to hex
*/
export function normalizeSecretKey(key: string): Uint8Array {
if (key.startsWith("nsec")) {
try {
const decoded = nip19.decode(key);
if (decoded.type === "nsec") {
return decoded.data;
}
} catch (e) {
throw new Error(`Invalid nsec format: ${e}`);
}
}
// Assume hex format (64 chars)
if (key.length === 64) {
const hex = key.toLowerCase();
const bytes = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
throw new Error("Invalid key format. Expected nsec bech32 or 64-char hex string.");
}
/**
* Get public key from private key
*/
export function getPubkeyFromPrivkey(privkey: string): string {
const normalized = normalizeSecretKey(privkey);
return getPublicKey(normalized);
}
/**
* Get public key from private key (Uint8Array version)
*/
export function getPubkeyFromPrivkeyBytes(privkey: Uint8Array): string {
return getPublicKey(privkey);
}
/**
* Build tags array from metadata
*/
export function buildTagsFromMetadata(
metadata: EventMetadata,
pubkey: string,
childEvents?: Array<{ kind: number; dTag: string; eventId?: string }>
): string[][] {
const tags: string[][] = [];
switch (metadata.kind) {
case 1:
// No special tags required
break;
case 11:
// No special tags required
break;
case 30023:
// Long-form article
if (!metadata.title) {
throw new Error("Title is mandatory for kind 30023");
}
tags.push(["d", normalizeDTag(metadata.title)]);
if (metadata.title) tags.push(["title", metadata.title]);
if (metadata.image) tags.push(["image", metadata.image]);
if (metadata.summary) tags.push(["summary", metadata.summary]);
if (metadata.published_at) tags.push(["published_at", metadata.published_at]);
if (metadata.topics) {
metadata.topics.forEach((topic) => tags.push(["t", topic]));
}
break;
case 30040:
// Publication index
if (!metadata.title) {
throw new Error("Title is mandatory for kind 30040");
}
tags.push(["d", normalizeDTag(metadata.title)]);
if (metadata.title) tags.push(["title", metadata.title]);
if (metadata.author) tags.push(["author", metadata.author]);
if (metadata.type) tags.push(["type", metadata.type]);
if (metadata.version) tags.push(["version", metadata.version]);
if (metadata.published_on) tags.push(["published_on", metadata.published_on]);
if (metadata.published_by) tags.push(["published_by", metadata.published_by]);
if (metadata.summary) tags.push(["summary", metadata.summary]);
if (metadata.source) tags.push(["source", metadata.source]);
if (metadata.image) tags.push(["image", metadata.image]);
if (metadata.auto_update) {
tags.push(["auto-update", metadata.auto_update]);
}
if (metadata.derivative_author) {
tags.push(["p", metadata.derivative_author]);
}
if (metadata.derivative_event) {
const eTag = ["E", metadata.derivative_event];
if (metadata.derivative_relay) eTag.push(metadata.derivative_relay);
if (metadata.derivative_pubkey) eTag.push(metadata.derivative_pubkey);
tags.push(eTag);
}
// NKBIP-08 tags
if (metadata.collection_id) tags.push(["C", metadata.collection_id]);
if (metadata.version_tag) tags.push(["v", metadata.version_tag]);
// Additional tags
if (metadata.additional_tags) {
metadata.additional_tags.forEach((tag) => tags.push(tag));
}
// a tags for child events
if (childEvents) {
childEvents.forEach((child) => {
const aTag = ["a", `${child.kind}:${pubkey}:${child.dTag}`];
if (child.eventId) aTag.push("", child.eventId);
tags.push(aTag);
});
}
break;
case 30041:
// Publication content
if (!metadata.title) {
throw new Error("Title is mandatory for kind 30041");
}
tags.push(["d", normalizeDTag(metadata.title)]);
if (metadata.title) tags.push(["title", metadata.title]);
// NKBIP-08 tags
if (metadata.collection_id) tags.push(["C", metadata.collection_id]);
if (metadata.title_id) tags.push(["T", metadata.title_id]);
if (metadata.chapter_id) tags.push(["c", metadata.chapter_id]);
if (metadata.section_id) tags.push(["s", metadata.section_id]);
if (metadata.version_tag) tags.push(["v", metadata.version_tag]);
break;
case 30817:
// Wiki page (Markdown)
if (!metadata.title) {
throw new Error("Title is mandatory for kind 30817");
}
tags.push(["d", normalizeDTag(metadata.title)]);
if (metadata.title) tags.push(["title", metadata.title]);
if (metadata.summary) tags.push(["summary", metadata.summary]);
break;
case 30818:
// Wiki page (AsciiDoc)
if (!metadata.title) {
throw new Error("Title is mandatory for kind 30818");
}
tags.push(["d", normalizeDTag(metadata.title)]);
if (metadata.title) tags.push(["title", metadata.title]);
if (metadata.summary) tags.push(["summary", metadata.summary]);
break;
}
return tags;
}
/**
* Normalize d-tag per NIP-54 rules
*/
export function normalizeDTag(title: string): string {
// All letters with uppercase/lowercase variants → lowercase
let normalized = title.toLowerCase();
// Whitespace → `-`
normalized = normalized.replace(/\s+/g, "-");
// Punctuation and symbols → removed (except hyphens)
normalized = normalized.replace(/[^\p{L}\p{N}-]/gu, "");
// Multiple consecutive `-` → single `-`
normalized = normalized.replace(/-+/g, "-");
// Leading and trailing `-` → removed
normalized = normalized.replace(/^-+|-+$/g, "");
// Non-ASCII letters and numbers are preserved (already handled by regex above)
return normalized;
}
/**
* Create and sign a Nostr event
*/
export function createSignedEvent(
kind: EventKind,
content: string,
tags: string[][],
privkey: string,
createdAt?: number
): SignedEvent {
const normalizedKey = normalizeSecretKey(privkey);
const pubkey = getPublicKey(normalizedKey);
const created_at = createdAt || Math.floor(Date.now() / 1000);
const eventTemplate = {
kind,
created_at,
tags,
content,
};
const signedEvent = finalizeEvent(eventTemplate, normalizedKey);
return {
...signedEvent,
kind: kind as EventKind,
};
}