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 { KeyStorage, Nip46Signer } from "@snort/system";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
@ -51,11 +51,38 @@ export default function useLoginHandler() {
|
|||||||
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
|
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
|
||||||
} else if (key.match(EmailRegex)) {
|
} else if (key.match(EmailRegex)) {
|
||||||
const [name, domain] = key.split("@");
|
const [name, domain] = key.split("@");
|
||||||
const hexKey = await fetchNip05Pubkey(name, domain);
|
const json = await fetchNostrAddress(name, domain);
|
||||||
if (!hexKey) {
|
if (!json) {
|
||||||
throw new Error("Invalid nostr address");
|
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://")) {
|
} else if (key.startsWith("bunker://")) {
|
||||||
const nip46 = new Nip46Signer(key);
|
const nip46 = new Nip46Signer(key);
|
||||||
await nip46.init();
|
await nip46.init();
|
||||||
|
@ -160,10 +160,13 @@ export function bech32ToText(str: string) {
|
|||||||
return new TextDecoder().decode(Uint8Array.from(buf));
|
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> {
|
export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2_000): Promise<string | undefined> {
|
||||||
interface NostrJson {
|
|
||||||
names: Record<string, string>;
|
|
||||||
}
|
|
||||||
if (!name || !domain) {
|
if (!name || !domain) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -182,6 +185,21 @@ export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2
|
|||||||
return undefined;
|
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>) {
|
export function removeUndefined<T>(v: Array<T | undefined>) {
|
||||||
return v.filter(a => a != undefined).map(a => unwrap(a));
|
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() {
|
export function throwIfOffline() {
|
||||||
if (isOffline()) {
|
if (isOffline()) {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
|
@ -8,6 +8,7 @@ import { EventSigner, PrivateKeySigner } from "../signer";
|
|||||||
import { NostrEvent } from "../nostr";
|
import { NostrEvent } from "../nostr";
|
||||||
import { EventBuilder } from "../event-builder";
|
import { EventBuilder } from "../event-builder";
|
||||||
import EventKind from "../event-kind";
|
import EventKind from "../event-kind";
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
|
||||||
const NIP46_KIND = 24_133;
|
const NIP46_KIND = 24_133;
|
||||||
|
|
||||||
@ -31,11 +32,15 @@ interface Nip46Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface QueueObj {
|
interface QueueObj {
|
||||||
resolve: (o: any) => void;
|
resolve: (o: Nip46Response) => void;
|
||||||
reject: (e: Error) => 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;
|
#conn?: Connection;
|
||||||
#relay: string;
|
#relay: string;
|
||||||
#localPubkey: string;
|
#localPubkey: string;
|
||||||
@ -47,10 +52,16 @@ export class Nip46Signer implements EventSigner {
|
|||||||
#proto: string;
|
#proto: string;
|
||||||
#didInit: boolean = false;
|
#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) {
|
constructor(config: string, insideSigner?: EventSigner) {
|
||||||
|
super();
|
||||||
const u = new URL(config);
|
const u = new URL(config);
|
||||||
this.#proto = u.protocol;
|
this.#proto = u.protocol;
|
||||||
this.#localPubkey = u.pathname.substring(2);
|
this.#localPubkey = u.hostname || u.pathname.substring(2);
|
||||||
|
|
||||||
if (u.hash.length > 1) {
|
if (u.hash.length > 1) {
|
||||||
this.#token = u.hash.substring(1);
|
this.#token = u.hash.substring(1);
|
||||||
@ -98,16 +109,35 @@ export class Nip46Signer implements EventSigner {
|
|||||||
"#p": [this.#localPubkey],
|
"#p": [this.#localPubkey],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => {},
|
() => { },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isBunker) {
|
if (isBunker) {
|
||||||
await this.#connect(unwrap(this.#remotePubkey));
|
const rsp = await this.#connect(unwrap(this.#remotePubkey));
|
||||||
resolve();
|
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 {
|
} else {
|
||||||
this.#commandQueue.set("connect", {
|
this.#commandQueue.set("connect", {
|
||||||
reject,
|
reject,
|
||||||
resolve,
|
resolve: () => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -127,21 +157,24 @@ export class Nip46Signer implements EventSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async describe() {
|
async describe() {
|
||||||
return await this.#rpc<Array<string>>("describe", []);
|
const rsp = await this.#rpc("describe", []);
|
||||||
|
return rsp.result as Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPubKey() {
|
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) {
|
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) {
|
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 {
|
try {
|
||||||
return JSON.parse(payload)[0];
|
return JSON.parse(rsp.result)[0];
|
||||||
} catch {
|
} catch {
|
||||||
return "<error>";
|
return "<error>";
|
||||||
}
|
}
|
||||||
@ -156,8 +189,8 @@ export class Nip46Signer implements EventSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sign(ev: NostrEvent) {
|
async sign(ev: NostrEvent) {
|
||||||
const evStr = await this.#rpc<string>("sign_event", [JSON.stringify(ev)]);
|
const rsp = await this.#rpc("sign_event", [JSON.stringify(ev)]);
|
||||||
return JSON.parse(evStr);
|
return JSON.parse(rsp.result as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #disconnect() {
|
async #disconnect() {
|
||||||
@ -169,7 +202,7 @@ export class Nip46Signer implements EventSigner {
|
|||||||
if (this.#token) {
|
if (this.#token) {
|
||||||
connectParams.push(this.#token);
|
connectParams.push(this.#token);
|
||||||
}
|
}
|
||||||
return await this.#rpc<string>("connect", connectParams);
|
return await this.#rpc("connect", connectParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #onReply(e: NostrEvent) {
|
async #onReply(e: NostrEvent) {
|
||||||
@ -199,11 +232,11 @@ export class Nip46Signer implements EventSigner {
|
|||||||
throw new Error("No pending command found");
|
throw new Error("No pending command found");
|
||||||
}
|
}
|
||||||
|
|
||||||
pending.resolve(reply);
|
pending.resolve(reply as Nip46Response);
|
||||||
this.#commandQueue.delete(reply.id);
|
this.#commandQueue.delete(reply.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #rpc<T>(method: string, params: Array<any>) {
|
async #rpc(method: string, params: Array<any>) {
|
||||||
if (!this.#didInit) {
|
if (!this.#didInit) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
@ -216,10 +249,10 @@ export class Nip46Signer implements EventSigner {
|
|||||||
} as Nip46Request;
|
} as Nip46Request;
|
||||||
|
|
||||||
this.#sendCommand(payload, unwrap(this.#remotePubkey));
|
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, {
|
this.#commandQueue.set(payload.id, {
|
||||||
resolve: async (o: Nip46Response) => {
|
resolve: async (o: Nip46Response) => {
|
||||||
resolve(o.result as T);
|
resolve(o);
|
||||||
},
|
},
|
||||||
reject,
|
reject,
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
|
Loading…
Reference in New Issue
Block a user