Files
grimoire/src/lib/open-parser.ts
2025-12-14 12:00:55 +01:00

143 lines
3.6 KiB
TypeScript

import { nip19 } from "nostr-tools";
import {
isValidHexEventId,
isValidHexPubkey,
normalizeHex,
} from "./nostr-validation";
import { normalizeRelayURL } from "./relay-url";
// Define pointer types locally since they're not exported from nostr-tools
export interface EventPointer {
id: string;
relays?: string[];
author?: string;
}
export interface AddressPointer {
kind: number;
pubkey: string;
identifier: string;
relays?: string[];
}
export interface ParsedOpenCommand {
pointer: EventPointer | AddressPointer;
}
/**
* Parse OPEN command arguments into an event pointer
* Supports:
* - note1... (bech32 note)
* - nevent1... (bech32 nevent with relay hints)
* - naddr1... (bech32 naddr for addressable events)
* - abc123... (64-char hex event ID)
* - kind:pubkey:d-tag (address pointer format)
*/
export function parseOpenCommand(args: string[]): ParsedOpenCommand {
const identifier = args[0];
if (!identifier) {
throw new Error("Event identifier required");
}
// Try bech32 decode first (note, nevent, naddr)
if (
identifier.startsWith("note") ||
identifier.startsWith("nevent") ||
identifier.startsWith("naddr")
) {
try {
const decoded = nip19.decode(identifier);
if (decoded.type === "note") {
// note1... -> EventPointer with just ID
return {
pointer: {
id: decoded.data,
},
};
}
if (decoded.type === "nevent") {
// nevent1... -> EventPointer (already has id and optional relays)
return {
pointer: {
...decoded.data,
relays: decoded.data.relays
?.map((url) => {
try {
return normalizeRelayURL(url);
} catch (error) {
console.warn(`Skipping invalid relay hint in nevent: ${url}`, error);
return null;
}
})
.filter((url): url is string => url !== null),
},
};
}
if (decoded.type === "naddr") {
// naddr1... -> AddressPointer (already has kind, pubkey, identifier)
return {
pointer: {
...decoded.data,
relays: decoded.data.relays
?.map((url) => {
try {
return normalizeRelayURL(url);
} catch (error) {
console.warn(`Skipping invalid relay hint in naddr: ${url}`, error);
return null;
}
})
.filter((url): url is string => url !== null),
},
};
}
} catch (error) {
throw new Error(`Invalid bech32 identifier: ${error}`);
}
}
// Check if it's a hex event ID
if (isValidHexEventId(identifier)) {
return {
pointer: {
id: normalizeHex(identifier),
},
};
}
// Check if it's an address format (kind:pubkey:d-tag)
if (identifier.includes(":")) {
const parts = identifier.split(":");
if (parts.length >= 2) {
const kind = parseInt(parts[0], 10);
const pubkey = parts[1];
const dTag = parts[2] || "";
if (isNaN(kind)) {
throw new Error("Invalid address format: kind must be a number");
}
if (!isValidHexPubkey(pubkey)) {
throw new Error("Invalid address format: pubkey must be 64 hex chars");
}
return {
pointer: {
kind,
pubkey: normalizeHex(pubkey),
identifier: dTag,
},
};
}
}
throw new Error(
"Invalid event identifier. Supported formats: note1..., nevent1..., naddr1..., hex ID, or kind:pubkey:d-tag",
);
}