This commit is contained in:
ennmichael
2023-03-20 21:23:03 +01:00
parent 1a4fb162ed
commit 09950fd547
20 changed files with 312 additions and 94 deletions

View File

@ -1,6 +1,6 @@
import { EventKind, RawEvent, Unsigned } from "."
import { EventKind, RawEvent, signEvent } from "."
import { NostrError } from "../common"
import { PublicKey } from "../crypto"
import { HexOrBechPrivateKey, PublicKey } from "../crypto"
/**
* Contact list event.
@ -28,18 +28,24 @@ export interface Contact {
/**
* Create a contact list event.
*/
export function createContactList(contacts: Contact[]): Unsigned<ContactList> {
return {
kind: EventKind.ContactList,
tags: contacts.map((contact) => [
"p",
contact.pubkey,
contact.relay?.toString() ?? "",
contact.petname ?? "",
]),
content: "",
getContacts,
}
export function createContactList(
contacts: Contact[],
priv?: HexOrBechPrivateKey
): Promise<ContactList> {
return signEvent(
{
kind: EventKind.ContactList,
tags: contacts.map((contact) => [
"p",
contact.pubkey,
contact.relay?.toString() ?? "",
contact.petname ?? "",
]),
content: "",
getContacts,
},
priv
)
}
export function getContacts(this: ContactList): Contact[] {

View File

@ -1,12 +1,5 @@
import {
EventId,
EventKind,
RawEvent,
signEvent,
Unsigned,
UnsignedWithPubkey,
} from "."
import { defined, NostrError } from "../common"
import { EventId, EventKind, RawEvent, signEvent } from "."
import { NostrError } from "../common"
import {
aesDecryptBase64,
aesEncryptBase64,
@ -77,7 +70,7 @@ export async function createDirectMessage(
}
export async function getMessage(
this: UnsignedWithPubkey<DirectMessage>,
this: DirectMessage,
priv?: HexOrBechPrivateKey
): Promise<string | undefined> {
if (priv !== undefined) {
@ -96,7 +89,7 @@ export async function getMessage(
return undefined
}
export function getRecipient(this: Unsigned<RawEvent>): PublicKey {
export function getRecipient(this: DirectMessage): PublicKey {
const recipientTag = this.tags.find((tag) => tag[0] === "p")
if (typeof recipientTag?.[1] !== "string") {
throw new NostrError(
@ -108,7 +101,7 @@ export function getRecipient(this: Unsigned<RawEvent>): PublicKey {
return recipientTag[1]
}
export function getPrevious(this: Unsigned<RawEvent>): EventId | undefined {
export function getPrevious(this: DirectMessage): EventId | undefined {
const previousTag = this.tags.find((tag) => tag[0] === "e")
if (previousTag === undefined) {
return undefined

View File

@ -9,7 +9,11 @@ import {
} from "../crypto"
import { Timestamp, unixTimestamp, NostrError } from "../common"
import { TextNote } from "./text"
import { getUserMetadata, SetMetadata } from "./set-metadata"
import {
getUserMetadata,
SetMetadata,
verifyInternetIdentifier,
} from "./set-metadata"
import {
DirectMessage,
getMessage,
@ -94,11 +98,10 @@ export type Unsigned<T extends Event | RawEvent> = {
pubkey?: PublicKey
}
// TODO This doesn't need to be exposed by the lib
/**
* Same as @see {@link Unsigned}, but with the pubkey field.
*/
export type UnsignedWithPubkey<T extends Event | RawEvent> = {
type UnsignedWithPubkey<T extends Event | RawEvent> = {
[Property in keyof T as Exclude<
Property,
"id" | "sig" | "created_at"
@ -113,7 +116,7 @@ export type UnsignedWithPubkey<T extends Event | RawEvent> = {
* Add the "id," "sig," and "pubkey" fields to the event. Set "created_at" to the current timestamp
* if missing. Return the event.
*/
export async function signEvent<T extends Event | RawEvent>(
export async function signEvent<T extends RawEvent>(
event: Unsigned<T>,
priv?: HexOrBechPrivateKey
): Promise<T> {
@ -164,6 +167,7 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
...event,
kind: EventKind.SetMetadata,
getUserMetadata,
verifyInternetIdentifier,
}
}
@ -194,14 +198,14 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
async function serializeEventId(
event: UnsignedWithPubkey<RawEvent>
): Promise<EventId> {
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
// Building the JSON string manually as follows ensures that there's no whitespace.
// In hindsight using JSON as a data format for hashing and signing is not the best
// design decision.
const serializedTags = `[${event.tags
.map((tag) => `[${tag.map((v) => `"${v}"`).join(",")}]`)
.join(",")}]`
const serialized = `[0,"${event.pubkey}",${event.created_at},${event.kind},${serializedTags},"${event.content}"]`
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content,
])
return await sha256(Uint8Array.from(charCodes(serialized)))
}

View File

@ -1,5 +1,6 @@
import { EventKind, RawEvent, Unsigned } from "."
import { EventKind, RawEvent, signEvent } from "."
import { NostrError, parseJson } from "../common"
import { HexOrBechPrivateKey } from "../crypto"
/**
* Set metadata event. Used for disseminating use profile information.
@ -13,29 +14,45 @@ export interface SetMetadata extends RawEvent {
* Get the user metadata specified in this event.
*/
getUserMetadata(): UserMetadata
/**
* Verify the NIP-05 DNS-based internet identifier associated with the user metadata.
* Throws if the internet identifier is invalid or fails verification.
* @param pubkey The public key to use if the event does not specify a pubkey. If the event
* does specify a pubkey
* @return The internet identifier. `undefined` if there is no internet identifier.
*/
verifyInternetIdentifier(
opts?: VerificationOptions
): Promise<InternetIdentifier | undefined>
}
export interface UserMetadata {
name: string
about: string
picture: string
nip05?: string
}
/**
* Create a set metadata event.
*/
export function createSetMetadata(
content: UserMetadata
): Unsigned<SetMetadata> {
return {
kind: EventKind.SetMetadata,
tags: [],
content: JSON.stringify(content),
getUserMetadata,
}
content: UserMetadata,
priv?: HexOrBechPrivateKey
): Promise<SetMetadata> {
return signEvent(
{
kind: EventKind.SetMetadata,
tags: [],
content: JSON.stringify(content),
getUserMetadata,
verifyInternetIdentifier,
},
priv
)
}
export function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
export function getUserMetadata(this: SetMetadata): UserMetadata {
const userMetadata = parseJson(this.content)
if (
typeof userMetadata.name !== "string" ||
@ -48,3 +65,59 @@ export function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
}
return userMetadata
}
export async function verifyInternetIdentifier(
this: SetMetadata,
opts?: VerificationOptions
): Promise<InternetIdentifier | undefined> {
const metadata = this.getUserMetadata()
if (metadata.nip05 === undefined) {
return undefined
}
const [name, domain] = metadata.nip05.split("@")
if (
name === undefined ||
domain === undefined ||
!/^[a-zA-Z0-9-_]+$/.test(name)
) {
throw new NostrError(
`invalid NIP-05 internet identifier: ${metadata.nip05}`
)
}
const res = await fetch(
`${
opts?.https === false ? "http" : "https"
}://${domain}/.well-known/nostr.json?name=${name}`,
{ redirect: "error" }
)
const wellKnown = await res.json()
const pubkey = wellKnown.names?.[name]
if (pubkey !== this.pubkey) {
throw new NostrError(
`invalid NIP-05 internet identifier: ${
metadata.nip05
} pubkey does not match, ${JSON.stringify(wellKnown)}`
)
}
const relays = wellKnown.relays?.[pubkey]
if (
relays !== undefined &&
(!(relays instanceof Array) ||
relays.some((relay) => typeof relay !== "string"))
) {
throw new NostrError(`invalid NIP-05 relays: ${wellKnown}`)
}
return {
name,
relays,
}
}
export interface InternetIdentifier {
name: string
relays?: string[]
}
export interface VerificationOptions {
https?: boolean
}

View File

@ -1,4 +1,5 @@
import { EventKind, RawEvent, Unsigned } from "."
import { EventKind, RawEvent, signEvent } from "."
import { HexOrBechPrivateKey } from "../crypto"
/**
* A text note event. Used for transmitting user posts.
@ -9,10 +10,16 @@ export interface TextNote extends RawEvent {
kind: EventKind.TextNote
}
export function createTextNote(content: string): Unsigned<TextNote> {
return {
kind: EventKind.TextNote,
tags: [],
content,
}
export function createTextNote(
content: string,
priv?: HexOrBechPrivateKey
): Promise<TextNote> {
return signEvent(
{
kind: EventKind.TextNote,
tags: [],
content,
},
priv
)
}