forked from Kieran/snort
Merge pull request #474 from v0l/nostr-package-nip05-dns-identifiers
`nostr` package: implement NIP-05
This commit is contained in:
commit
25c1f48a95
2
packages/nostr/.prettierignore
Normal file
2
packages/nostr/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
src/legacy
|
@ -9,33 +9,33 @@ A strongly-typed nostr client for Node and the browser.
|
|||||||
The goal of the project is to have all of the following implemented
|
The goal of the project is to have all of the following implemented
|
||||||
and tested against a real-world relay implementation.
|
and tested against a real-world relay implementation.
|
||||||
|
|
||||||
_Progress: 7/34 (20%)._
|
_Progress: 8/34 (23%)._
|
||||||
|
|
||||||
- [X] NIP-01: Basic protocol flow description
|
- [x] NIP-01: Basic protocol flow description
|
||||||
- [X] NIP-02: Contact List and Petnames
|
- [x] NIP-02: Contact List and Petnames
|
||||||
- [ ] NIP-03: OpenTimestamps Attestations for Events
|
- [ ] NIP-03: OpenTimestamps Attestations for Events
|
||||||
- [X] NIP-04: Encrypted Direct Message
|
- [x] NIP-04: Encrypted Direct Message
|
||||||
- [ ] NIP-05: Mapping Nostr keys to DNS-based internet identifiers
|
- [x] NIP-05: Mapping Nostr keys to DNS-based internet identifiers
|
||||||
- [ ] NIP-06: Basic key derivation from mnemonic seed phrase
|
- [ ] NIP-06: Basic key derivation from mnemonic seed phrase
|
||||||
- [ ] NIP-07: window.nostr capability for web browsers
|
- [ ] NIP-07: window.nostr capability for web browsers
|
||||||
- [ ] NIP-08: Handling Mentions
|
|
||||||
- [ ] NIP-09: Event Deletion
|
- [ ] NIP-09: Event Deletion
|
||||||
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
||||||
- TODO Check if this applies
|
- TODO Check if this applies
|
||||||
- [X] NIP-11: Relay Information Document
|
- [x] NIP-11: Relay Information Document
|
||||||
- [X] NIP-12: Generic Tag Queries
|
- [x] NIP-12: Generic Tag Queries
|
||||||
- [ ] NIP-13: Proof of Work
|
- [ ] NIP-13: Proof of Work
|
||||||
- [ ] NIP-14: Subject tag in text events
|
- [ ] NIP-14: Subject tag in text events
|
||||||
- [X] NIP-15: End of Stored Events Notice
|
- [x] NIP-15: End of Stored Events Notice
|
||||||
- [ ] NIP-19: bech32-encoded entities
|
- [ ] NIP-19: bech32-encoded entities
|
||||||
- [X] `npub`
|
- [x] `npub`
|
||||||
- [X] `nsec`
|
- [x] `nsec`
|
||||||
- [ ] `note`, `nprofile`, `nevent`, `nrelay`, `naddr`
|
- [ ] `note`, `nprofile`, `nevent`, `nrelay`, `naddr`
|
||||||
- [X] NIP-20: Command Results
|
- [x] NIP-20: Command Results
|
||||||
- [ ] NIP-21: `nostr:` URL scheme
|
- [ ] NIP-21: `nostr:` URL scheme
|
||||||
- [ ] NIP-23: Long-form Content
|
- [ ] NIP-23: Long-form Content
|
||||||
- [ ] NIP-25: Reactions
|
- [ ] NIP-25: Reactions
|
||||||
- [ ] NIP-26: Delegated Event Signing
|
- [ ] NIP-26: Delegated Event Signing
|
||||||
|
- [ ] NIP-27: Text Note References
|
||||||
- [ ] NIP-28: Public Chat
|
- [ ] NIP-28: Public Chat
|
||||||
- [ ] NIP-36: Sensitive Content
|
- [ ] NIP-36: Sensitive Content
|
||||||
- [ ] NIP-39: External Identities in Profiles
|
- [ ] NIP-39: External Identities in Profiles
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
version: "3.1"
|
version: "3.1"
|
||||||
services:
|
services:
|
||||||
|
well-known:
|
||||||
|
build: ./docker/well-known
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- 12647:80
|
||||||
relay:
|
relay:
|
||||||
build: ./relay
|
build: ./docker/relay
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
ports:
|
||||||
- 12648:8080
|
- 12648:8080
|
||||||
|
@ -9,4 +9,4 @@ EXPOSE 8000
|
|||||||
COPY . .
|
COPY . .
|
||||||
USER $APP_USER
|
USER $APP_USER
|
||||||
RUN yarn
|
RUN yarn
|
||||||
CMD yarn app /bin/bash -c "rm -rf /usr/src/app/db/* && ./nostr-rs-relay --db /usr/src/app/db --config ./config.toml"
|
CMD ["/bin/bash", "-c", "while :; do yarn app /bin/bash -c 'rm -rf /usr/src/app/db/* && ./nostr-rs-relay --db /usr/src/app/db --config ./config.toml'; done;"]
|
4
packages/nostr/docker/well-known/Dockerfile
Normal file
4
packages/nostr/docker/well-known/Dockerfile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# An nginx server to return a .well-known/nostr.json file for testing.
|
||||||
|
FROM nginx
|
||||||
|
RUN mkdir /usr/share/nginx/html/.well-known
|
||||||
|
COPY nostr.json /usr/share/nginx/html/.well-known/nostr.json
|
10
packages/nostr/docker/well-known/nostr.json
Normal file
10
packages/nostr/docker/well-known/nostr.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"names": {
|
||||||
|
"bob": "be4be3c0c4f2d18f11215087d7d18fad2c9e0269fc50e19d526bdfb11a522a64"
|
||||||
|
},
|
||||||
|
"relays": {
|
||||||
|
"be4be3c0c4f2d18f11215087d7d18fad2c9e0269fc50e19d526bdfb11a522a64": [
|
||||||
|
"ws://example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { EventKind, RawEvent, Unsigned } from "."
|
import { EventKind, RawEvent, signEvent } from "."
|
||||||
import { NostrError } from "../common"
|
import { NostrError } from "../common"
|
||||||
import { PublicKey } from "../crypto"
|
import { HexOrBechPrivateKey, PublicKey } from "../crypto"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact list event.
|
* Contact list event.
|
||||||
@ -28,8 +28,12 @@ export interface Contact {
|
|||||||
/**
|
/**
|
||||||
* Create a contact list event.
|
* Create a contact list event.
|
||||||
*/
|
*/
|
||||||
export function createContactList(contacts: Contact[]): Unsigned<ContactList> {
|
export function createContactList(
|
||||||
return {
|
contacts: Contact[],
|
||||||
|
priv?: HexOrBechPrivateKey
|
||||||
|
): Promise<ContactList> {
|
||||||
|
return signEvent(
|
||||||
|
{
|
||||||
kind: EventKind.ContactList,
|
kind: EventKind.ContactList,
|
||||||
tags: contacts.map((contact) => [
|
tags: contacts.map((contact) => [
|
||||||
"p",
|
"p",
|
||||||
@ -39,7 +43,9 @@ export function createContactList(contacts: Contact[]): Unsigned<ContactList> {
|
|||||||
]),
|
]),
|
||||||
content: "",
|
content: "",
|
||||||
getContacts,
|
getContacts,
|
||||||
}
|
},
|
||||||
|
priv
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContacts(this: ContactList): Contact[] {
|
export function getContacts(this: ContactList): Contact[] {
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import {
|
import { EventId, EventKind, RawEvent, signEvent } from "."
|
||||||
EventId,
|
import { NostrError } from "../common"
|
||||||
EventKind,
|
|
||||||
RawEvent,
|
|
||||||
signEvent,
|
|
||||||
Unsigned,
|
|
||||||
UnsignedWithPubkey,
|
|
||||||
} from "."
|
|
||||||
import { defined, NostrError } from "../common"
|
|
||||||
import {
|
import {
|
||||||
aesDecryptBase64,
|
aesDecryptBase64,
|
||||||
aesEncryptBase64,
|
aesEncryptBase64,
|
||||||
@ -77,7 +70,7 @@ export async function createDirectMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMessage(
|
export async function getMessage(
|
||||||
this: UnsignedWithPubkey<DirectMessage>,
|
this: DirectMessage,
|
||||||
priv?: HexOrBechPrivateKey
|
priv?: HexOrBechPrivateKey
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
if (priv !== undefined) {
|
if (priv !== undefined) {
|
||||||
@ -96,7 +89,7 @@ export async function getMessage(
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRecipient(this: Unsigned<RawEvent>): PublicKey {
|
export function getRecipient(this: DirectMessage): PublicKey {
|
||||||
const recipientTag = this.tags.find((tag) => tag[0] === "p")
|
const recipientTag = this.tags.find((tag) => tag[0] === "p")
|
||||||
if (typeof recipientTag?.[1] !== "string") {
|
if (typeof recipientTag?.[1] !== "string") {
|
||||||
throw new NostrError(
|
throw new NostrError(
|
||||||
@ -108,7 +101,7 @@ export function getRecipient(this: Unsigned<RawEvent>): PublicKey {
|
|||||||
return recipientTag[1]
|
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")
|
const previousTag = this.tags.find((tag) => tag[0] === "e")
|
||||||
if (previousTag === undefined) {
|
if (previousTag === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -9,7 +9,11 @@ import {
|
|||||||
} from "../crypto"
|
} from "../crypto"
|
||||||
import { Timestamp, unixTimestamp, NostrError } from "../common"
|
import { Timestamp, unixTimestamp, NostrError } from "../common"
|
||||||
import { TextNote } from "./text"
|
import { TextNote } from "./text"
|
||||||
import { getUserMetadata, SetMetadata } from "./set-metadata"
|
import {
|
||||||
|
getUserMetadata,
|
||||||
|
SetMetadata,
|
||||||
|
verifyInternetIdentifier,
|
||||||
|
} from "./set-metadata"
|
||||||
import {
|
import {
|
||||||
DirectMessage,
|
DirectMessage,
|
||||||
getMessage,
|
getMessage,
|
||||||
@ -94,11 +98,10 @@ export type Unsigned<T extends Event | RawEvent> = {
|
|||||||
pubkey?: PublicKey
|
pubkey?: PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO This doesn't need to be exposed by the lib
|
|
||||||
/**
|
/**
|
||||||
* Same as @see {@link Unsigned}, but with the pubkey field.
|
* 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 in keyof T as Exclude<
|
||||||
Property,
|
Property,
|
||||||
"id" | "sig" | "created_at"
|
"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
|
* Add the "id," "sig," and "pubkey" fields to the event. Set "created_at" to the current timestamp
|
||||||
* if missing. Return the event.
|
* if missing. Return the event.
|
||||||
*/
|
*/
|
||||||
export async function signEvent<T extends Event | RawEvent>(
|
export async function signEvent<T extends RawEvent>(
|
||||||
event: Unsigned<T>,
|
event: Unsigned<T>,
|
||||||
priv?: HexOrBechPrivateKey
|
priv?: HexOrBechPrivateKey
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@ -164,6 +167,7 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
|
|||||||
...event,
|
...event,
|
||||||
kind: EventKind.SetMetadata,
|
kind: EventKind.SetMetadata,
|
||||||
getUserMetadata,
|
getUserMetadata,
|
||||||
|
verifyInternetIdentifier,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,14 +198,14 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
|
|||||||
async function serializeEventId(
|
async function serializeEventId(
|
||||||
event: UnsignedWithPubkey<RawEvent>
|
event: UnsignedWithPubkey<RawEvent>
|
||||||
): Promise<EventId> {
|
): Promise<EventId> {
|
||||||
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
|
const serialized = JSON.stringify([
|
||||||
// Building the JSON string manually as follows ensures that there's no whitespace.
|
0,
|
||||||
// In hindsight using JSON as a data format for hashing and signing is not the best
|
event.pubkey,
|
||||||
// design decision.
|
event.created_at,
|
||||||
const serializedTags = `[${event.tags
|
event.kind,
|
||||||
.map((tag) => `[${tag.map((v) => `"${v}"`).join(",")}]`)
|
event.tags,
|
||||||
.join(",")}]`
|
event.content,
|
||||||
const serialized = `[0,"${event.pubkey}",${event.created_at},${event.kind},${serializedTags},"${event.content}"]`
|
])
|
||||||
return await sha256(Uint8Array.from(charCodes(serialized)))
|
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 { NostrError, parseJson } from "../common"
|
||||||
|
import { HexOrBechPrivateKey } from "../crypto"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set metadata event. Used for disseminating use profile information.
|
* 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.
|
* Get the user metadata specified in this event.
|
||||||
*/
|
*/
|
||||||
getUserMetadata(): UserMetadata
|
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 {
|
export interface UserMetadata {
|
||||||
name: string
|
name: string
|
||||||
about: string
|
about: string
|
||||||
picture: string
|
picture: string
|
||||||
|
nip05?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a set metadata event.
|
* Create a set metadata event.
|
||||||
*/
|
*/
|
||||||
export function createSetMetadata(
|
export function createSetMetadata(
|
||||||
content: UserMetadata
|
content: UserMetadata,
|
||||||
): Unsigned<SetMetadata> {
|
priv?: HexOrBechPrivateKey
|
||||||
return {
|
): Promise<SetMetadata> {
|
||||||
|
return signEvent(
|
||||||
|
{
|
||||||
kind: EventKind.SetMetadata,
|
kind: EventKind.SetMetadata,
|
||||||
tags: [],
|
tags: [],
|
||||||
content: JSON.stringify(content),
|
content: JSON.stringify(content),
|
||||||
getUserMetadata,
|
getUserMetadata,
|
||||||
}
|
verifyInternetIdentifier,
|
||||||
|
},
|
||||||
|
priv
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
|
export function getUserMetadata(this: SetMetadata): UserMetadata {
|
||||||
const userMetadata = parseJson(this.content)
|
const userMetadata = parseJson(this.content)
|
||||||
if (
|
if (
|
||||||
typeof userMetadata.name !== "string" ||
|
typeof userMetadata.name !== "string" ||
|
||||||
@ -48,3 +65,59 @@ export function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
|
|||||||
}
|
}
|
||||||
return 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.
|
* A text note event. Used for transmitting user posts.
|
||||||
@ -9,10 +10,16 @@ export interface TextNote extends RawEvent {
|
|||||||
kind: EventKind.TextNote
|
kind: EventKind.TextNote
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTextNote(content: string): Unsigned<TextNote> {
|
export function createTextNote(
|
||||||
return {
|
content: string,
|
||||||
|
priv?: HexOrBechPrivateKey
|
||||||
|
): Promise<TextNote> {
|
||||||
|
return signEvent(
|
||||||
|
{
|
||||||
kind: EventKind.TextNote,
|
kind: EventKind.TextNote,
|
||||||
tags: [],
|
tags: [],
|
||||||
content,
|
content,
|
||||||
}
|
},
|
||||||
|
priv
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
import { EventKind, signEvent } from "../src/event"
|
import { EventKind } from "../src/event"
|
||||||
import { createContactList } from "../src/event/contact-list"
|
import { createContactList } from "../src/event/contact-list"
|
||||||
import { setup } from "./setup"
|
import { setup } from "./setup"
|
||||||
|
|
||||||
@ -47,9 +47,7 @@ describe("contact-list", () => {
|
|||||||
// After the subscription event sync is done, publish the test event.
|
// After the subscription event sync is done, publish the test event.
|
||||||
subscriber.on("eose", async () => {
|
subscriber.on("eose", async () => {
|
||||||
// TODO No signEvent, have a convenient way to do this
|
// TODO No signEvent, have a convenient way to do this
|
||||||
publisher.publish(
|
publisher.publish(await createContactList(contacts, subscriberSecret))
|
||||||
await signEvent(createContactList(contacts), subscriberSecret)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { EventKind, signEvent } from "../src/event"
|
import { EventKind } from "../src/event"
|
||||||
import { parsePublicKey } from "../src/crypto"
|
import { parsePublicKey } from "../src/crypto"
|
||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
import { setup } from "./setup"
|
import { setup } from "./setup"
|
||||||
@ -49,13 +49,11 @@ describe("dm", () => {
|
|||||||
const subscriptionId = subscriber.subscribe([])
|
const subscriptionId = subscriber.subscribe([])
|
||||||
|
|
||||||
subscriber.on("eose", async () => {
|
subscriber.on("eose", async () => {
|
||||||
// TODO No signEvent, do something more convenient
|
const event = await createDirectMessage(
|
||||||
const event = await signEvent(
|
{
|
||||||
await createDirectMessage({
|
|
||||||
message,
|
message,
|
||||||
recipient: subscriberPubkey,
|
recipient: subscriberPubkey,
|
||||||
priv: publisherSecret,
|
},
|
||||||
}),
|
|
||||||
publisherSecret
|
publisherSecret
|
||||||
)
|
)
|
||||||
publisher.publish(event)
|
publisher.publish(event)
|
||||||
|
75
packages/nostr/test/internet-identifier.ts
Normal file
75
packages/nostr/test/internet-identifier.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import assert from "assert"
|
||||||
|
import { defined } from "../src/common"
|
||||||
|
import { EventKind } from "../src/event"
|
||||||
|
import { createSetMetadata } from "../src/event/set-metadata"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
|
||||||
|
describe("internet-identifier", () => {
|
||||||
|
it("present", (done) => {
|
||||||
|
setup(done, ({ publisher, publisherSecret, subscriber, done }) => {
|
||||||
|
subscriber.on("event", async ({ event }) => {
|
||||||
|
// Assert that the internet identifier can be verified.
|
||||||
|
assert.strictEqual(event.kind, EventKind.SetMetadata)
|
||||||
|
if (event.kind === EventKind.SetMetadata) {
|
||||||
|
const identifier = await event.verifyInternetIdentifier({
|
||||||
|
https: false,
|
||||||
|
})
|
||||||
|
assert.ok(identifier)
|
||||||
|
const { name, relays } = defined(identifier)
|
||||||
|
assert.strictEqual(name, "bob")
|
||||||
|
assert.deepStrictEqual(relays, ["ws://example.com"])
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
subscriber.subscribe([])
|
||||||
|
|
||||||
|
// After the subscription event sync is done, publish the test event.
|
||||||
|
subscriber.on("eose", async () => {
|
||||||
|
publisher.publish({
|
||||||
|
...(await createSetMetadata(
|
||||||
|
{
|
||||||
|
about: "",
|
||||||
|
name: "",
|
||||||
|
picture: "",
|
||||||
|
nip05: "bob@localhost:12647",
|
||||||
|
},
|
||||||
|
publisherSecret
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("missing", (done) => {
|
||||||
|
setup(done, ({ publisher, publisherSecret, subscriber, done }) => {
|
||||||
|
subscriber.on("event", async ({ event }) => {
|
||||||
|
// Assert that undefined is returned if the internet identifier is missing.
|
||||||
|
assert.strictEqual(event.kind, EventKind.SetMetadata)
|
||||||
|
if (event.kind === EventKind.SetMetadata) {
|
||||||
|
const identifier = await event.verifyInternetIdentifier({
|
||||||
|
https: false,
|
||||||
|
})
|
||||||
|
assert.strictEqual(identifier, undefined)
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
subscriber.subscribe([])
|
||||||
|
|
||||||
|
// After the subscription event sync is done, publish the test event.
|
||||||
|
subscriber.on("eose", async () => {
|
||||||
|
publisher.publish({
|
||||||
|
...(await createSetMetadata(
|
||||||
|
{
|
||||||
|
about: "",
|
||||||
|
name: "",
|
||||||
|
picture: "",
|
||||||
|
},
|
||||||
|
publisherSecret
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
54
packages/nostr/test/set-metadata.ts
Normal file
54
packages/nostr/test/set-metadata.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { EventKind } from "../src/event"
|
||||||
|
import { parsePublicKey } from "../src/crypto"
|
||||||
|
import assert from "assert"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
import { createSetMetadata } from "../src/event/set-metadata"
|
||||||
|
|
||||||
|
describe("set metadata", () => {
|
||||||
|
const name = "bob"
|
||||||
|
const about = "this is bob"
|
||||||
|
const picture = "https://example.com/bob.jpg"
|
||||||
|
|
||||||
|
// Test that a set metadata event can be published by one client and received by the other.
|
||||||
|
it("publish and receive", (done) => {
|
||||||
|
setup(
|
||||||
|
done,
|
||||||
|
({
|
||||||
|
publisher,
|
||||||
|
publisherSecret,
|
||||||
|
publisherPubkey,
|
||||||
|
subscriber,
|
||||||
|
timestamp,
|
||||||
|
done,
|
||||||
|
}) => {
|
||||||
|
// Expect the test event.
|
||||||
|
subscriber.on("event", ({ event }) => {
|
||||||
|
assert.strictEqual(event.kind, EventKind.SetMetadata)
|
||||||
|
if (event.kind === EventKind.SetMetadata) {
|
||||||
|
const user = event.getUserMetadata()
|
||||||
|
assert.strictEqual(event.pubkey, parsePublicKey(publisherPubkey))
|
||||||
|
assert.strictEqual(event.created_at, timestamp)
|
||||||
|
assert.strictEqual(event.tags.length, 0)
|
||||||
|
assert.strictEqual(user.name, name)
|
||||||
|
assert.strictEqual(user.about, about)
|
||||||
|
assert.strictEqual(user.picture, picture)
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
subscriber.subscribe([])
|
||||||
|
|
||||||
|
// After the subscription event sync is done, publish the test event.
|
||||||
|
subscriber.on("eose", async () => {
|
||||||
|
publisher.publish({
|
||||||
|
...(await createSetMetadata(
|
||||||
|
{ name, about, picture },
|
||||||
|
publisherSecret
|
||||||
|
)),
|
||||||
|
created_at: timestamp,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
@ -1,8 +1,8 @@
|
|||||||
import { EventKind, signEvent, Unsigned } from "../src/event"
|
import { EventKind } from "../src/event"
|
||||||
import { parsePublicKey } from "../src/crypto"
|
import { parsePublicKey } from "../src/crypto"
|
||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
import { setup } from "./setup"
|
import { setup } from "./setup"
|
||||||
import { createTextNote, TextNote } from "../src/event/text"
|
import { createTextNote } from "../src/event/text"
|
||||||
|
|
||||||
describe("text note", () => {
|
describe("text note", () => {
|
||||||
const note = "hello world"
|
const note = "hello world"
|
||||||
@ -40,16 +40,10 @@ describe("text note", () => {
|
|||||||
assert.strictEqual(nostr, subscriber)
|
assert.strictEqual(nostr, subscriber)
|
||||||
assert.strictEqual(id, subscriptionId)
|
assert.strictEqual(id, subscriptionId)
|
||||||
|
|
||||||
// TODO No signEvent, have a convenient way to do this
|
publisher.publish({
|
||||||
publisher.publish(
|
...(await createTextNote(note, publisherSecret)),
|
||||||
await signEvent(
|
|
||||||
{
|
|
||||||
...createTextNote(note),
|
|
||||||
created_at: timestamp,
|
created_at: timestamp,
|
||||||
} as Unsigned<TextNote>,
|
})
|
||||||
publisherSecret
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -57,9 +51,8 @@ describe("text note", () => {
|
|||||||
|
|
||||||
// Test that a client interprets an "OK" message after publishing a text note.
|
// Test that a client interprets an "OK" message after publishing a text note.
|
||||||
it("publish and ok", function (done) {
|
it("publish and ok", function (done) {
|
||||||
setup(done, ({ publisher, publisherSecret, url, done }) => {
|
setup(done, async ({ publisher, publisherSecret, url, done }) => {
|
||||||
// TODO No signEvent, have a convenient way to do this
|
const event = await createTextNote(note, publisherSecret)
|
||||||
signEvent(createTextNote(note), publisherSecret).then((event) => {
|
|
||||||
publisher.on("ok", (params, nostr) => {
|
publisher.on("ok", (params, nostr) => {
|
||||||
assert.strictEqual(nostr, publisher)
|
assert.strictEqual(nostr, publisher)
|
||||||
assert.strictEqual(params.eventId, event.id)
|
assert.strictEqual(params.eventId, event.id)
|
||||||
@ -67,9 +60,7 @@ describe("text note", () => {
|
|||||||
assert.strictEqual(params.ok, true)
|
assert.strictEqual(params.ok, true)
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
||||||
publisher.publish(event)
|
publisher.publish(event)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user