nip-05
This commit is contained in:
@ -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[] {
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user