Merge pull request 'nostr
package: NIP-07' (#503) from nostr-package-window-nostr-object into main
Reviewed-on: #503
This commit is contained in:
commit
551169c2c7
@ -109,8 +109,8 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doNip07Login() {
|
async function doNip07Login() {
|
||||||
const relays = "getRelays" in window.nostr ? await window.nostr.getRelays() : undefined;
|
const relays = "getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays)() : undefined;
|
||||||
const pubKey = await window.nostr.getPublicKey();
|
const pubKey = await unwrap(window.nostr).getPublicKey();
|
||||||
LoginStore.loginWithPubkey(pubKey, relays);
|
LoginStore.loginWithPubkey(pubKey, relays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,20 +17,6 @@ import { unwrap } from "Util";
|
|||||||
import { EventBuilder } from "./EventBuilder";
|
import { EventBuilder } from "./EventBuilder";
|
||||||
import { EventExt } from "./EventExt";
|
import { EventExt } from "./EventExt";
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
nostr: {
|
|
||||||
getPublicKey: () => Promise<HexKey>;
|
|
||||||
signEvent: (event: RawEvent) => Promise<RawEvent>;
|
|
||||||
getRelays: () => Promise<Record<string, { read: boolean; write: boolean }>>;
|
|
||||||
nip04: {
|
|
||||||
encrypt: (pubkey: HexKey, content: string) => Promise<string>;
|
|
||||||
decrypt: (pubkey: HexKey, content: string) => Promise<string>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Nip7QueueItem {
|
interface Nip7QueueItem {
|
||||||
next: () => Promise<unknown>;
|
next: () => Promise<unknown>;
|
||||||
resolve(v: unknown): void;
|
resolve(v: unknown): void;
|
||||||
@ -91,12 +77,12 @@ export class EventPublisher {
|
|||||||
|
|
||||||
async #sign(eb: EventBuilder) {
|
async #sign(eb: EventBuilder) {
|
||||||
if (this.#hasNip07 && !this.#privateKey) {
|
if (this.#hasNip07 && !this.#privateKey) {
|
||||||
const nip7PubKey = await barrierNip07(() => window.nostr.getPublicKey());
|
const nip7PubKey = await barrierNip07(() => unwrap(window.nostr).getPublicKey());
|
||||||
if (nip7PubKey !== this.#pubKey) {
|
if (nip7PubKey !== this.#pubKey) {
|
||||||
throw new Error("Can't sign event, NIP-07 pubkey does not match");
|
throw new Error("Can't sign event, NIP-07 pubkey does not match");
|
||||||
}
|
}
|
||||||
const ev = eb.build();
|
const ev = eb.build();
|
||||||
return await barrierNip07(() => window.nostr.signEvent(ev));
|
return await barrierNip07(() => unwrap(window.nostr).signEvent(ev));
|
||||||
} else if (this.#privateKey) {
|
} else if (this.#privateKey) {
|
||||||
return await eb.buildAndSign(this.#privateKey);
|
return await eb.buildAndSign(this.#privateKey);
|
||||||
} else {
|
} else {
|
||||||
@ -106,11 +92,11 @@ export class EventPublisher {
|
|||||||
|
|
||||||
async nip4Encrypt(content: string, key: HexKey) {
|
async nip4Encrypt(content: string, key: HexKey) {
|
||||||
if (this.#hasNip07 && !this.#privateKey) {
|
if (this.#hasNip07 && !this.#privateKey) {
|
||||||
const nip7PubKey = await barrierNip07(() => window.nostr.getPublicKey());
|
const nip7PubKey = await barrierNip07(() => unwrap(window.nostr).getPublicKey());
|
||||||
if (nip7PubKey !== this.#pubKey) {
|
if (nip7PubKey !== this.#pubKey) {
|
||||||
throw new Error("Can't encrypt content, NIP-07 pubkey does not match");
|
throw new Error("Can't encrypt content, NIP-07 pubkey does not match");
|
||||||
}
|
}
|
||||||
return await barrierNip07(() => window.nostr.nip04.encrypt(key, content));
|
return await barrierNip07(() => unwrap(window.nostr?.nip04?.encrypt)(key, content));
|
||||||
} else if (this.#privateKey) {
|
} else if (this.#privateKey) {
|
||||||
return await EventExt.encryptData(content, key, this.#privateKey);
|
return await EventExt.encryptData(content, key, this.#privateKey);
|
||||||
} else {
|
} else {
|
||||||
@ -120,7 +106,7 @@ export class EventPublisher {
|
|||||||
|
|
||||||
async nip4Decrypt(content: string, otherKey: HexKey) {
|
async nip4Decrypt(content: string, otherKey: HexKey) {
|
||||||
if (this.#hasNip07 && !this.#privateKey) {
|
if (this.#hasNip07 && !this.#privateKey) {
|
||||||
return await barrierNip07(() => window.nostr.nip04.decrypt(otherKey, content));
|
return await barrierNip07(() => unwrap(window.nostr?.nip04?.decrypt)(otherKey, content));
|
||||||
} else if (this.#privateKey) {
|
} else if (this.#privateKey) {
|
||||||
return await EventExt.decryptDm(content, this.#privateKey, otherKey);
|
return await EventExt.decryptDm(content, this.#privateKey, otherKey);
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
"main": "dist/lib.js",
|
"main": "dist/lib.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "rm -rf dist && webpack",
|
||||||
"watch": "webpack -w",
|
"watch": "rm -rf dist && webpack -w",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
"test": "ts-mocha --type-check -j 1 --timeout 5s test/test.*.ts",
|
"test": "ts-mocha --type-check -j 1 --timeout 5s test/test.*.ts",
|
||||||
"test-browser": "ts-node test/browser/server.ts",
|
"test-browser": "ts-node test/browser/server.ts",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import * as secp from "@noble/secp256k1"
|
import * as secp from "@noble/secp256k1"
|
||||||
import base64 from "base64-js"
|
import base64 from "base64-js"
|
||||||
import { bech32 } from "bech32"
|
import { bech32 } from "bech32"
|
||||||
import { NostrError } from "./common"
|
|
||||||
|
|
||||||
// TODO Use toHex as well as toString? Might be more explicit
|
// TODO Use toHex as well as toString? Might be more explicit
|
||||||
// Or maybe replace toString with toHex
|
// Or maybe replace toString with toHex
|
||||||
|
@ -50,8 +50,24 @@ export async function createDirectMessage(
|
|||||||
): Promise<DirectMessage> {
|
): Promise<DirectMessage> {
|
||||||
recipient = parsePublicKey(recipient)
|
recipient = parsePublicKey(recipient)
|
||||||
if (priv === undefined) {
|
if (priv === undefined) {
|
||||||
// TODO Use NIP-07
|
if (
|
||||||
throw new NostrError("todo")
|
typeof window === "undefined" ||
|
||||||
|
window.nostr?.nip04?.encrypt === undefined
|
||||||
|
) {
|
||||||
|
throw new NostrError("private key not specified")
|
||||||
|
}
|
||||||
|
const content = await window.nostr.nip04.encrypt(recipient, message)
|
||||||
|
return await signEvent(
|
||||||
|
{
|
||||||
|
kind: EventKind.DirectMessage,
|
||||||
|
tags: [["p", recipient]],
|
||||||
|
content,
|
||||||
|
getMessage,
|
||||||
|
getRecipient,
|
||||||
|
getPrevious,
|
||||||
|
},
|
||||||
|
priv
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
priv = parsePrivateKey(priv)
|
priv = parsePrivateKey(priv)
|
||||||
const { data, iv } = await aesEncryptBase64(priv, recipient, message)
|
const { data, iv } = await aesEncryptBase64(priv, recipient, message)
|
||||||
@ -81,8 +97,13 @@ export async function getMessage(
|
|||||||
throw new NostrError(`invalid direct message content ${this.content}`)
|
throw new NostrError(`invalid direct message content ${this.content}`)
|
||||||
}
|
}
|
||||||
if (priv === undefined) {
|
if (priv === undefined) {
|
||||||
// TODO Try to use NIP-07
|
if (
|
||||||
throw new NostrError("todo")
|
typeof window === "undefined" ||
|
||||||
|
window.nostr?.nip04?.decrypt === undefined
|
||||||
|
) {
|
||||||
|
throw new NostrError("private key not specified")
|
||||||
|
}
|
||||||
|
return await window.nostr.nip04.decrypt(this.pubkey, this.content)
|
||||||
} else if (getPublicKey(priv) === this.getRecipient()) {
|
} else if (getPublicKey(priv) === this.getRecipient()) {
|
||||||
return await aesDecryptBase64(this.pubkey, priv, { data, iv })
|
return await aesDecryptBase64(this.pubkey, priv, { data, iv })
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from "./direct-message"
|
} from "./direct-message"
|
||||||
import { ContactList, getContacts } from "./contact-list"
|
import { ContactList, getContacts } from "./contact-list"
|
||||||
import { Deletion, getEvents } from "./deletion"
|
import { Deletion, getEvents } from "./deletion"
|
||||||
|
import "../nostr-object"
|
||||||
|
|
||||||
// TODO Add remaining event types
|
// TODO Add remaining event types
|
||||||
|
|
||||||
@ -135,8 +136,22 @@ export async function signEvent<T extends RawEvent>(
|
|||||||
event.sig = await schnorrSign(id, priv)
|
event.sig = await schnorrSign(id, priv)
|
||||||
return event as T
|
return event as T
|
||||||
} else {
|
} else {
|
||||||
// TODO Try to use NIP-07, otherwise throw
|
if (typeof window === "undefined" || window.nostr === undefined) {
|
||||||
throw new NostrError("todo")
|
throw new NostrError("no private key provided")
|
||||||
|
}
|
||||||
|
// Extensions like nos2x expect to receive only the event data, without any of the methods.
|
||||||
|
const methods: { [key: string]: unknown } = {}
|
||||||
|
for (const [key, value] of Object.entries(event)) {
|
||||||
|
if (typeof value === "function") {
|
||||||
|
methods[key] = value
|
||||||
|
delete event[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const signed = await window.nostr.signEvent(event)
|
||||||
|
return {
|
||||||
|
...signed,
|
||||||
|
...methods,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./legacy"
|
export * from "./legacy"
|
||||||
|
import "./nostr-object"
|
||||||
|
|
||||||
// TODO This file should only contain re-exports and only re-export what is needed
|
// TODO This file should only contain re-exports and only re-export what is needed
|
||||||
|
20
packages/nostr/src/nostr-object.ts
Normal file
20
packages/nostr/src/nostr-object.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { PublicKey } from "./crypto"
|
||||||
|
import { RawEvent, Unsigned } from "./event"
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
nostr?: {
|
||||||
|
getPublicKey: () => Promise<PublicKey>
|
||||||
|
signEvent: <T extends RawEvent>(event: Unsigned<T>) => Promise<T>
|
||||||
|
|
||||||
|
getRelays?: () => Promise<
|
||||||
|
Record<string, { read: boolean; write: boolean }>
|
||||||
|
>
|
||||||
|
|
||||||
|
nip04?: {
|
||||||
|
encrypt?: (pubkey: PublicKey, plaintext: string) => Promise<string>
|
||||||
|
decrypt?: (pubkey: PublicKey, ciphertext: string) => Promise<string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,21 @@
|
|||||||
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div style="margin-left: 5px; margin-top: 15px">
|
||||||
|
<a style="color: var(--mocha-color)" id="window.nostr"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const a = document.getElementById("window.nostr")
|
||||||
|
if (window.location.pathname === "/nostr-object") {
|
||||||
|
a.href = "/"
|
||||||
|
a.innerHTML = "disable window.nostr"
|
||||||
|
} else {
|
||||||
|
a.href = "/nostr-object"
|
||||||
|
a.innerHTML = "enable window.nostr"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div id="mocha"></div>
|
<div id="mocha"></div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/mocha/mocha.js"></script>
|
<script src="https://unpkg.com/mocha/mocha.js"></script>
|
||||||
@ -15,6 +30,7 @@
|
|||||||
mocha.setup({
|
mocha.setup({
|
||||||
ui: "bdd",
|
ui: "bdd",
|
||||||
timeout: "5s",
|
timeout: "5s",
|
||||||
|
global: ["nostr"],
|
||||||
})
|
})
|
||||||
mocha.checkLeaks()
|
mocha.checkLeaks()
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,7 +10,7 @@ const port = 33543
|
|||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use("/", (req: express.Request, res: express.Response) => {
|
app.use("/", (req: express.Request, res: express.Response) => {
|
||||||
if (req.path === "/") {
|
if (req.path === "/" || req.path === "/nostr-object") {
|
||||||
const index = fs.readFileSync(path.join(__dirname, "index.html"), {
|
const index = fs.readFileSync(path.join(__dirname, "index.html"), {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
|
import "../src/nostr-object"
|
||||||
import { Nostr } from "../src/client"
|
import { Nostr } from "../src/client"
|
||||||
import { Timestamp, unixTimestamp } from "../src/common"
|
import { Timestamp, unixTimestamp } from "../src/common"
|
||||||
|
import {
|
||||||
|
aesDecryptBase64,
|
||||||
|
aesEncryptBase64,
|
||||||
|
parsePrivateKey,
|
||||||
|
parsePublicKey,
|
||||||
|
PublicKey,
|
||||||
|
} from "../src/crypto"
|
||||||
|
import { RawEvent } from "../src"
|
||||||
|
import { signEvent, Unsigned } from "../src/event"
|
||||||
|
|
||||||
export const relayUrl = new URL("ws://localhost:12648")
|
export const relayUrl = new URL("ws://localhost:12648")
|
||||||
|
|
||||||
export interface Setup {
|
export interface Setup {
|
||||||
publisher: Nostr
|
publisher: Nostr
|
||||||
publisherSecret: string
|
publisherSecret?: string
|
||||||
publisherPubkey: string
|
publisherPubkey: string
|
||||||
subscriber: Nostr
|
subscriber: Nostr
|
||||||
subscriberSecret: string
|
subscriberSecret: string
|
||||||
@ -25,6 +35,50 @@ export async function setup(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await restartRelay()
|
await restartRelay()
|
||||||
|
|
||||||
|
const publisherPubkey =
|
||||||
|
"npub1he978sxy7tgc7yfp2zra05v045kfuqnfl3gwr82jd00mzxjj9fjqzw2dg7"
|
||||||
|
const publisherSecret =
|
||||||
|
"nsec15fnff4uxlgyu79ua3l7327w0wstrd6x565cx6zze78zgkktmr8vs90j363"
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
if (window.location.pathname === "/nostr-object") {
|
||||||
|
// Mock the global window.nostr object for the publisher.
|
||||||
|
window.nostr = {
|
||||||
|
getPublicKey: () => Promise.resolve(parsePublicKey(publisherPubkey)),
|
||||||
|
signEvent: <T extends RawEvent>(event: Unsigned<T>) =>
|
||||||
|
signEvent(event, publisherSecret),
|
||||||
|
|
||||||
|
getRelays: () => Promise.resolve({}),
|
||||||
|
|
||||||
|
nip04: {
|
||||||
|
encrypt: async (pubkey: PublicKey, plaintext: string) => {
|
||||||
|
const { data, iv } = await aesEncryptBase64(
|
||||||
|
parsePrivateKey(publisherSecret),
|
||||||
|
pubkey,
|
||||||
|
plaintext
|
||||||
|
)
|
||||||
|
return `${data}?iv=${iv}`
|
||||||
|
},
|
||||||
|
decrypt: async (pubkey: PublicKey, ciphertext: string) => {
|
||||||
|
const [data, iv] = ciphertext.split("?iv=")
|
||||||
|
return await aesDecryptBase64(
|
||||||
|
pubkey,
|
||||||
|
parsePrivateKey(publisherSecret),
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
iv,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Disable the user's nostr extension if they have one.
|
||||||
|
window.nostr = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const publisher = new Nostr()
|
const publisher = new Nostr()
|
||||||
const subscriber = new Nostr()
|
const subscriber = new Nostr()
|
||||||
|
|
||||||
@ -44,9 +98,10 @@ export async function setup(
|
|||||||
const result = test({
|
const result = test({
|
||||||
publisher,
|
publisher,
|
||||||
publisherSecret:
|
publisherSecret:
|
||||||
"nsec15fnff4uxlgyu79ua3l7327w0wstrd6x565cx6zze78zgkktmr8vs90j363",
|
typeof window === "undefined" || window.nostr === undefined
|
||||||
publisherPubkey:
|
? publisherSecret
|
||||||
"npub1he978sxy7tgc7yfp2zra05v045kfuqnfl3gwr82jd00mzxjj9fjqzw2dg7",
|
: undefined,
|
||||||
|
publisherPubkey,
|
||||||
subscriber,
|
subscriber,
|
||||||
subscriberSecret:
|
subscriberSecret:
|
||||||
"nsec1fxvlyqn3rugvxwaz6dr5h8jcfn0fe0lxyp7pl4mgntxfzqr7dmgst7z9ps",
|
"nsec1fxvlyqn3rugvxwaz6dr5h8jcfn0fe0lxyp7pl4mgntxfzqr7dmgst7z9ps",
|
||||||
|
@ -4,7 +4,7 @@ import { parsePublicKey } from "../src/crypto"
|
|||||||
import { setup } from "./setup"
|
import { setup } from "./setup"
|
||||||
import { createDirectMessage } from "../src/event/direct-message"
|
import { createDirectMessage } from "../src/event/direct-message"
|
||||||
|
|
||||||
describe("dm", () => {
|
describe("direct-message", () => {
|
||||||
const message = "for your eyes only"
|
const message = "for your eyes only"
|
||||||
|
|
||||||
// Test that the intended recipient can receive and decrypt the direct message.
|
// Test that the intended recipient can receive and decrypt the direct message.
|
Loading…
x
Reference in New Issue
Block a user