feat: nip46 oAuth login
This commit is contained in:
parent
512307f42d
commit
0d9d5a0a4c
@ -1,4 +1,4 @@
|
||||
import { fetchNip05Pubkey, unwrap } from "@snort/shared";
|
||||
import { fetchNostrAddress, unwrap } from "@snort/shared";
|
||||
import { KeyStorage, Nip46Signer } from "@snort/system";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
@ -51,11 +51,38 @@ export default function useLoginHandler() {
|
||||
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
|
||||
} else if (key.match(EmailRegex)) {
|
||||
const [name, domain] = key.split("@");
|
||||
const hexKey = await fetchNip05Pubkey(name, domain);
|
||||
if (!hexKey) {
|
||||
const json = await fetchNostrAddress(name, domain);
|
||||
if (!json) {
|
||||
throw new Error("Invalid nostr address");
|
||||
}
|
||||
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
|
||||
const match = Object.keys(json.names).find(n => {
|
||||
return n.toLowerCase() === name.toLowerCase();
|
||||
});
|
||||
if (!match) {
|
||||
throw new Error("Invalid nostr address");
|
||||
}
|
||||
const pubkey = json.names[match];
|
||||
|
||||
if (json.nip46) {
|
||||
const bunkerRelays = json.nip46[json.names["_"]];
|
||||
const nip46 = new Nip46Signer(`bunker://${pubkey}?relay=${encodeURIComponent(bunkerRelays[0])}`);
|
||||
nip46.on("oauth", url => {
|
||||
window.open(url, CONFIG.appNameCapitalized, "width=600,height=800,popup=yes");
|
||||
});
|
||||
await nip46.init();
|
||||
|
||||
const loginPubkey = await nip46.getPubKey();
|
||||
LoginStore.loginWithPubkey(
|
||||
loginPubkey,
|
||||
LoginSessionType.Nip46,
|
||||
undefined,
|
||||
nip46.relays,
|
||||
await pin(unwrap(nip46.privateKey)),
|
||||
);
|
||||
nip46.close();
|
||||
} else {
|
||||
LoginStore.loginWithPubkey(pubkey, LoginSessionType.PublicKey);
|
||||
}
|
||||
} else if (key.startsWith("bunker://")) {
|
||||
const nip46 = new Nip46Signer(key);
|
||||
await nip46.init();
|
||||
|
@ -160,10 +160,13 @@ export function bech32ToText(str: string) {
|
||||
return new TextDecoder().decode(Uint8Array.from(buf));
|
||||
}
|
||||
|
||||
export interface NostrJson {
|
||||
names: Record<string, string>;
|
||||
relays?: Record<string, Array<string>>;
|
||||
nip46?: Record<string, Array<string>>;
|
||||
}
|
||||
|
||||
export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2_000): Promise<string | undefined> {
|
||||
interface NostrJson {
|
||||
names: Record<string, string>;
|
||||
}
|
||||
if (!name || !domain) {
|
||||
return undefined;
|
||||
}
|
||||
@ -182,6 +185,21 @@ export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function fetchNostrAddress(name: string, domain: string, timeout = 2_000): Promise<NostrJson | undefined> {
|
||||
if (!name || !domain) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`, {
|
||||
signal: AbortSignal.timeout(timeout),
|
||||
});
|
||||
return await res.json() as NostrJson;
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function removeUndefined<T>(v: Array<T | undefined>) {
|
||||
return v.filter(a => a != undefined).map(a => unwrap(a));
|
||||
}
|
||||
@ -208,7 +226,7 @@ export function normalizeReaction(content: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export class OfflineError extends Error {}
|
||||
export class OfflineError extends Error { }
|
||||
|
||||
export function throwIfOffline() {
|
||||
if (isOffline()) {
|
||||
|
@ -8,6 +8,7 @@
|
||||
"module": "ESNext",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSourceMap": true,
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
|
@ -8,6 +8,7 @@ import { EventSigner, PrivateKeySigner } from "../signer";
|
||||
import { NostrEvent } from "../nostr";
|
||||
import { EventBuilder } from "../event-builder";
|
||||
import EventKind from "../event-kind";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
const NIP46_KIND = 24_133;
|
||||
|
||||
@ -31,11 +32,15 @@ interface Nip46Response {
|
||||
}
|
||||
|
||||
interface QueueObj {
|
||||
resolve: (o: any) => void;
|
||||
resolve: (o: Nip46Response) => void;
|
||||
reject: (e: Error) => void;
|
||||
}
|
||||
|
||||
export class Nip46Signer implements EventSigner {
|
||||
interface Nip46Events {
|
||||
oauth: (url: string) => void;
|
||||
}
|
||||
|
||||
export class Nip46Signer extends EventEmitter<Nip46Events> implements EventSigner {
|
||||
#conn?: Connection;
|
||||
#relay: string;
|
||||
#localPubkey: string;
|
||||
@ -47,10 +52,16 @@ export class Nip46Signer implements EventSigner {
|
||||
#proto: string;
|
||||
#didInit: boolean = false;
|
||||
|
||||
/**
|
||||
* Start NIP-46 connection
|
||||
* @param config bunker/nostrconnect://{npub/hex-pubkey}?relay={websocket-url}#{token-hex}
|
||||
* @param insideSigner
|
||||
*/
|
||||
constructor(config: string, insideSigner?: EventSigner) {
|
||||
super();
|
||||
const u = new URL(config);
|
||||
this.#proto = u.protocol;
|
||||
this.#localPubkey = u.pathname.substring(2);
|
||||
this.#localPubkey = u.hostname || u.pathname.substring(2);
|
||||
|
||||
if (u.hash.length > 1) {
|
||||
this.#token = u.hash.substring(1);
|
||||
@ -98,16 +109,35 @@ export class Nip46Signer implements EventSigner {
|
||||
"#p": [this.#localPubkey],
|
||||
},
|
||||
],
|
||||
() => {},
|
||||
() => { },
|
||||
);
|
||||
|
||||
if (isBunker) {
|
||||
await this.#connect(unwrap(this.#remotePubkey));
|
||||
resolve();
|
||||
const rsp = await this.#connect(unwrap(this.#remotePubkey));
|
||||
if (rsp.result === "auth_url") {
|
||||
// re-insert the command into queue for result of oAuth flow
|
||||
this.#commandQueue.set(rsp.id, {
|
||||
resolve: async (o: Nip46Response) => {
|
||||
if (o.result === "ack") {
|
||||
resolve();
|
||||
} else {
|
||||
reject(o.error);
|
||||
}
|
||||
},
|
||||
reject,
|
||||
});
|
||||
this.emit("oauth", rsp.error);
|
||||
} else if (rsp.result === "ack") {
|
||||
resolve();
|
||||
} else {
|
||||
reject(rsp.error);
|
||||
}
|
||||
} else {
|
||||
this.#commandQueue.set("connect", {
|
||||
reject,
|
||||
resolve,
|
||||
resolve: () => {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -127,21 +157,24 @@ export class Nip46Signer implements EventSigner {
|
||||
}
|
||||
|
||||
async describe() {
|
||||
return await this.#rpc<Array<string>>("describe", []);
|
||||
const rsp = await this.#rpc("describe", []);
|
||||
return rsp.result as Array<string>;
|
||||
}
|
||||
|
||||
async getPubKey() {
|
||||
return await this.#rpc<string>("get_public_key", []);
|
||||
const rsp = await this.#rpc("get_public_key", []);
|
||||
return rsp.result as string;
|
||||
}
|
||||
|
||||
async nip4Encrypt(content: string, otherKey: string) {
|
||||
return await this.#rpc<string>("nip04_encrypt", [otherKey, content]);
|
||||
const rsp = await this.#rpc("nip04_encrypt", [otherKey, content]);
|
||||
return rsp.result as string;
|
||||
}
|
||||
|
||||
async nip4Decrypt(content: string, otherKey: string) {
|
||||
const payload = await this.#rpc<string>("nip04_decrypt", [otherKey, content]);
|
||||
const rsp = await this.#rpc("nip04_decrypt", [otherKey, content]);
|
||||
try {
|
||||
return JSON.parse(payload)[0];
|
||||
return JSON.parse(rsp.result)[0];
|
||||
} catch {
|
||||
return "<error>";
|
||||
}
|
||||
@ -156,8 +189,8 @@ export class Nip46Signer implements EventSigner {
|
||||
}
|
||||
|
||||
async sign(ev: NostrEvent) {
|
||||
const evStr = await this.#rpc<string>("sign_event", [JSON.stringify(ev)]);
|
||||
return JSON.parse(evStr);
|
||||
const rsp = await this.#rpc("sign_event", [JSON.stringify(ev)]);
|
||||
return JSON.parse(rsp.result as string);
|
||||
}
|
||||
|
||||
async #disconnect() {
|
||||
@ -169,7 +202,7 @@ export class Nip46Signer implements EventSigner {
|
||||
if (this.#token) {
|
||||
connectParams.push(this.#token);
|
||||
}
|
||||
return await this.#rpc<string>("connect", connectParams);
|
||||
return await this.#rpc("connect", connectParams);
|
||||
}
|
||||
|
||||
async #onReply(e: NostrEvent) {
|
||||
@ -199,11 +232,11 @@ export class Nip46Signer implements EventSigner {
|
||||
throw new Error("No pending command found");
|
||||
}
|
||||
|
||||
pending.resolve(reply);
|
||||
pending.resolve(reply as Nip46Response);
|
||||
this.#commandQueue.delete(reply.id);
|
||||
}
|
||||
|
||||
async #rpc<T>(method: string, params: Array<any>) {
|
||||
async #rpc(method: string, params: Array<any>) {
|
||||
if (!this.#didInit) {
|
||||
await this.init();
|
||||
}
|
||||
@ -216,10 +249,10 @@ export class Nip46Signer implements EventSigner {
|
||||
} as Nip46Request;
|
||||
|
||||
this.#sendCommand(payload, unwrap(this.#remotePubkey));
|
||||
return await new Promise<T>((resolve, reject) => {
|
||||
return await new Promise<Nip46Response>((resolve, reject) => {
|
||||
this.#commandQueue.set(payload.id, {
|
||||
resolve: async (o: Nip46Response) => {
|
||||
resolve(o.result as T);
|
||||
resolve(o);
|
||||
},
|
||||
reject,
|
||||
});
|
||||
|
@ -8,6 +8,7 @@
|
||||
"module": "ESNext",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSourceMap": true,
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
|
Loading…
x
Reference in New Issue
Block a user