1import * as secp from '@noble/secp256k1'
3// The peer's identity is a secp256k1 / BIP340 (schnorr) keypair.
4// - The x-only public key (hex) IS the peer ID.
5// - The key is generated fresh per page load (NOT persisted), so every tab —
6// even in the same browser profile — is its own peer. Nothing else is
7// persisted either; a reload is simply a new peer joining.
8// - The same key signs both nostr events (for relay discovery/signaling) and
9// every application-level message sent over WebRTC.
11const toHex = (bytes: Uint8Array): string =>
12 bytes.reduce((s, b) => s + b.toString(16).padStart(2, '0'), '')
14const fromHex = (hex: string): Uint8Array => {
15 const out = new Uint8Array(hex.length / 2)
16 for (let i = 0; i < out.length; i++) {
17 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
18 }
19 return out
20}
22const {secretKey} = secp.schnorr.keygen()
23const publicKey = secp.schnorr.getPublicKey(secretKey)
25/** This peer's ID = its x-only public key, as hex. */
26export const selfId: string = toHex(publicKey)
28const sha256 = async (str: string): Promise<Uint8Array> =>
29 new Uint8Array(
30 await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
31 )
33/** SHA-256 of raw bytes, as hex (used to authenticate binary blobs). */
34export const sha256HexBytes = async (bytes: ArrayBuffer): Promise<string> =>
35 toHex(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)))
37/** Sign an arbitrary string payload; returns hex signature. */
38export const sign = async (payload: string): Promise<string> =>
39 toHex(await secp.schnorr.signAsync(await sha256(payload), secretKey))
41/** Verify a hex signature over a string payload against a peer's ID (pubkey hex). */
42export const verify = async (
43 payload: string,
44 sigHex: string,
45 pubkeyHex: string
46): Promise<boolean> => {
47 try {
48 return await secp.schnorr.verifyAsync(
49 fromHex(sigHex),
50 await sha256(payload),
51 fromHex(pubkeyHex)
52 )
53 } catch {
54 return false
55 }
56}
58// ---- nostr event signing (schnorr over the nostr event id) ----
60export interface NostrEvent {
61 id: string
62 pubkey: string
63 created_at: number
64 kind: number
65 tags: string[][]
66 content: string
67 sig: string
68}
70/** Build and sign a nostr event with this peer's key. */
71export const makeNostrEvent = async (
72 kind: number,
73 tags: string[][],
74 content: string
75): Promise<NostrEvent> => {
76 const created_at = Math.floor(Date.now() / 1000)
77 const serialized = JSON.stringify([
78 0,
79 selfId,
80 created_at,
81 kind,
82 tags,
83 content
84 ])
85 const id = toHex(await sha256(serialized))
86 const sig = toHex(await secp.schnorr.signAsync(fromHex(id), secretKey))
87 return {id, pubkey: selfId, created_at, kind, tags, content, sig}
88}
90export {toHex, fromHex}