mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-26 11:37:40 +02:00
add support for nostr signing device
This commit is contained in:
5
.changeset/quick-radios-collect.md
Normal file
5
.changeset/quick-radios-collect.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add support for Nostr Signing Device
|
@@ -24,6 +24,7 @@
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@getalby/bitcoin-connect-react": "^2.4.2",
|
||||
"@noble/hashes": "^1.3.2",
|
||||
"@noble/secp256k1": "^1.7.0",
|
||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||
"bech32": "^2.0.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
@@ -44,7 +45,6 @@
|
||||
"match-sorter": "^6.3.1",
|
||||
"nanoid": "^5.0.2",
|
||||
"ngeohash": "^0.6.3",
|
||||
"noble-secp256k1": "^1.2.14",
|
||||
"nostr-tools": "^1.17.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -69,6 +69,7 @@
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"@types/chroma-js": "^2.4.1",
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/dom-serial": "^1.0.6",
|
||||
"@types/identicon.js": "^2.3.1",
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@types/leaflet.locatecontrol": "^0.74.1",
|
||||
|
@@ -2,13 +2,20 @@ import { Badge, BadgeProps } from "@chakra-ui/react";
|
||||
import { Account } from "../services/account";
|
||||
|
||||
export default function AccountInfoBadge({ account, ...props }: BadgeProps & { account: Account }) {
|
||||
if (account.useExtension) {
|
||||
if (account.connectionType === "extension") {
|
||||
return (
|
||||
<Badge {...props} variant="solid" colorScheme="green">
|
||||
extension
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (account.connectionType === "serial") {
|
||||
return (
|
||||
<Badge {...props} variant="solid" colorScheme="teal">
|
||||
serial
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (account.secKey) {
|
||||
return (
|
||||
<Badge {...props} variant="solid" colorScheme="red">
|
||||
|
@@ -3,6 +3,8 @@ import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
import { GlobalProviders } from "./providers";
|
||||
|
||||
import "./services/serial-port";
|
||||
|
||||
// setup dayjs
|
||||
import dayjs from "dayjs";
|
||||
import relativeTimePlugin from "dayjs/plugin/relativeTime";
|
||||
|
@@ -8,7 +8,7 @@ export type Account = {
|
||||
relays?: string[];
|
||||
secKey?: ArrayBuffer;
|
||||
iv?: Uint8Array;
|
||||
useExtension?: boolean;
|
||||
connectionType?: 'extension'|'serial';
|
||||
localSettings?: AppSettings;
|
||||
};
|
||||
|
||||
|
@@ -17,7 +17,6 @@ const DEFAULT_RELAYS = [
|
||||
{ url: "wss://relay.damus.io", mode: RelayMode.READ },
|
||||
{ url: "wss://nostr.wine", mode: RelayMode.READ },
|
||||
{ url: "wss://relay.snort.social", mode: RelayMode.READ },
|
||||
{ url: "wss://eden.nostr.land", mode: RelayMode.READ },
|
||||
{ url: "wss://nos.lol", mode: RelayMode.READ },
|
||||
{ url: "wss://purplerelay.com", mode: RelayMode.READ },
|
||||
];
|
||||
|
255
src/services/serial-port.ts
Normal file
255
src/services/serial-port.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { getEventHash, validateEvent } from "nostr-tools";
|
||||
import { base64 } from "@scure/base";
|
||||
import { randomBytes, hexToBytes } from "@noble/hashes/utils";
|
||||
import { Point } from "@noble/secp256k1";
|
||||
|
||||
import { logger } from "../helpers/debug";
|
||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import createDefer, { Deferred } from "../classes/deferred";
|
||||
|
||||
const METHOD_PING = "/ping";
|
||||
// const METHOD_LOG = '/log'
|
||||
|
||||
export const METHOD_SIGN_MESSAGE = "/sign-message";
|
||||
export const METHOD_SHARED_SECRET = "/shared-secret";
|
||||
export const METHOD_PUBLIC_KEY = "/public-key";
|
||||
|
||||
export const PUBLIC_METHODS = [METHOD_PUBLIC_KEY, METHOD_SIGN_MESSAGE, METHOD_SHARED_SECRET];
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const log = logger.extend("SerialPortService");
|
||||
const deviceLog = log.extend("DeviceLog");
|
||||
let writer: WritableStreamDefaultWriter<string> | null;
|
||||
|
||||
export function isConnected() {
|
||||
return !!writer;
|
||||
}
|
||||
|
||||
type Callback = () => void;
|
||||
type DeviceOpts = {
|
||||
onConnect?: Callback;
|
||||
onDisconnect?: Callback;
|
||||
onError?: (err: Error) => void;
|
||||
onDone?: Callback;
|
||||
};
|
||||
|
||||
let lastCommand: Deferred<string> | null = null;
|
||||
export async function callMethodOnDevice(method: string, params: string[], opts: DeviceOpts = {}) {
|
||||
if (!writer) await connectToDevice(opts);
|
||||
|
||||
// only one command can be pending at any time
|
||||
// but each will only wait 6 seconds
|
||||
if (lastCommand) throw new Error("Previous command to device still pending!");
|
||||
const command = createDefer<string>();
|
||||
lastCommand = command;
|
||||
|
||||
// send actual command
|
||||
sendCommand(method, params);
|
||||
setTimeout(() => {
|
||||
command.reject(new Error("Device timeout"));
|
||||
if (lastCommand === command) lastCommand = null;
|
||||
}, 6000);
|
||||
|
||||
return lastCommand;
|
||||
}
|
||||
|
||||
export async function connectToDevice({ onConnect, onDisconnect, onError, onDone }: DeviceOpts): Promise<void> {
|
||||
let port: SerialPort = await navigator.serial.requestPort();
|
||||
let reader;
|
||||
|
||||
const startSerialPortReading = async () => {
|
||||
// reading responses
|
||||
while (port && port.readable) {
|
||||
const textDecoder = new window.TextDecoderStream();
|
||||
port.readable.pipeTo(textDecoder.writable);
|
||||
reader = textDecoder.readable.getReader();
|
||||
const readStringUntil = readFromSerialPort(reader);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await readStringUntil("\n");
|
||||
if (value) {
|
||||
const { method, data } = parseResponse(value);
|
||||
|
||||
if (method === "/log") deviceLog(data);
|
||||
if (method === "/ping") log("Pong");
|
||||
|
||||
if (PUBLIC_METHODS.indexOf(method) === -1) {
|
||||
// ignore /ping, /log responses
|
||||
continue;
|
||||
}
|
||||
|
||||
log("Received: ", method, data);
|
||||
|
||||
if (lastCommand) {
|
||||
lastCommand.resolve(data);
|
||||
lastCommand = null;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
lastCommand = null;
|
||||
writer = null;
|
||||
if (onDone) onDone();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
writer = null;
|
||||
if (onError) onError(error);
|
||||
if (lastCommand) {
|
||||
lastCommand.reject(error);
|
||||
lastCommand = null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await port.open({ baudRate: 9600 });
|
||||
|
||||
// this `sleep()` is a hack, I know!
|
||||
// but `port.onconnect` is never called. I don't know why!
|
||||
await sleep(1000);
|
||||
startSerialPortReading();
|
||||
|
||||
const textEncoder = new window.TextEncoderStream();
|
||||
textEncoder.readable.pipeTo(port.writable);
|
||||
writer = textEncoder.writable.getWriter();
|
||||
|
||||
// send ping first
|
||||
await sendCommand(METHOD_PING);
|
||||
await sendCommand(METHOD_PING, [window.location.host]);
|
||||
|
||||
if (onConnect) onConnect();
|
||||
|
||||
port.addEventListener("disconnect", () => {
|
||||
log("Disconnected");
|
||||
lastCommand = null;
|
||||
writer = null;
|
||||
if (onDisconnect) onDisconnect();
|
||||
});
|
||||
}
|
||||
|
||||
async function sendCommand(method: string, params: string[] = []) {
|
||||
if (!writer) return;
|
||||
log("Send command", method, params);
|
||||
const message = [method].concat(params).join(" ");
|
||||
await writer.write(message + "\n");
|
||||
}
|
||||
|
||||
function readFromSerialPort(reader: ReadableStreamDefaultReader<string>) {
|
||||
let partialChunk: string | undefined;
|
||||
let fulliness: string[] = [];
|
||||
|
||||
const readStringUntil = async (separator = "\n") => {
|
||||
if (fulliness.length) return { value: fulliness.shift()!.trim(), done: false };
|
||||
|
||||
const chunks = [];
|
||||
if (partialChunk) {
|
||||
// leftovers from previous read
|
||||
chunks.push(partialChunk);
|
||||
partialChunk = undefined;
|
||||
}
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (value) {
|
||||
const values = value.split(separator);
|
||||
// found one or more separators
|
||||
if (values.length > 1) {
|
||||
chunks.push(values.shift()); // first element
|
||||
partialChunk = values.pop(); // last element
|
||||
fulliness = values; // full lines
|
||||
return { value: chunks.join("").trim(), done: false };
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
if (done) return { value: chunks.join("").trim(), done: true };
|
||||
}
|
||||
};
|
||||
return readStringUntil;
|
||||
}
|
||||
|
||||
function parseResponse(value: string) {
|
||||
const method = value.split(" ")[0];
|
||||
const data = value.substring(method.length).trim();
|
||||
|
||||
return { method, data };
|
||||
}
|
||||
|
||||
export const utf8Decoder = new TextDecoder("utf-8");
|
||||
export const utf8Encoder = new TextEncoder();
|
||||
|
||||
export async function encrypt(pubkey: string, text: string) {
|
||||
const sharedSecretStr = await callMethodOnDevice(METHOD_SHARED_SECRET, [xOnlyToXY(pubkey)]);
|
||||
const sharedSecret = hexToBytes(sharedSecretStr);
|
||||
|
||||
let iv = Uint8Array.from(randomBytes(16));
|
||||
let plaintext = utf8Encoder.encode(text);
|
||||
let cryptoKey = await crypto.subtle.importKey("raw", sharedSecret, { name: "AES-CBC" }, false, ["encrypt"]);
|
||||
let ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, cryptoKey, plaintext);
|
||||
let ctb64 = base64.encode(new Uint8Array(ciphertext));
|
||||
let ivb64 = base64.encode(new Uint8Array(iv.buffer));
|
||||
|
||||
return `${ctb64}?iv=${ivb64}`;
|
||||
}
|
||||
|
||||
export async function decrypt(pubkey: string, data: string) {
|
||||
let [ctb64, ivb64] = data.split("?iv=");
|
||||
|
||||
const sharedSecretStr = await callMethodOnDevice(METHOD_SHARED_SECRET, [xOnlyToXY(pubkey)]);
|
||||
const sharedSecret = hexToBytes(sharedSecretStr);
|
||||
|
||||
let cryptoKey = await crypto.subtle.importKey("raw", sharedSecret, { name: "AES-CBC" }, false, ["decrypt"]);
|
||||
let ciphertext = base64.decode(ctb64);
|
||||
let iv = base64.decode(ivb64);
|
||||
|
||||
let plaintext = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, cryptoKey, ciphertext);
|
||||
|
||||
let text = utf8Decoder.decode(plaintext);
|
||||
return text;
|
||||
}
|
||||
|
||||
export function xOnlyToXY(p: string) {
|
||||
return Point.fromHex(p).toHex().substring(2);
|
||||
}
|
||||
|
||||
async function getPublicKey() {
|
||||
return await callMethodOnDevice(METHOD_PUBLIC_KEY, []);
|
||||
}
|
||||
async function signEvent(draft: DraftNostrEvent) {
|
||||
let signed = { ...draft } as NostrEvent;
|
||||
|
||||
if (!signed.pubkey) signed.pubkey = await callMethodOnDevice(METHOD_PUBLIC_KEY, []);
|
||||
if (!signed.created_at) signed.created_at = Math.round(Date.now() / 1000);
|
||||
if (!signed.id) signed.id = getEventHash(signed);
|
||||
if (!validateEvent(signed)) throw new Error("Tnvalid event");
|
||||
|
||||
signed.sig = await callMethodOnDevice(METHOD_SIGN_MESSAGE, [signed.id]);
|
||||
return signed;
|
||||
}
|
||||
|
||||
const serialPortService = {
|
||||
supported: !!navigator.serial,
|
||||
signEvent,
|
||||
getPublicKey,
|
||||
encrypt,
|
||||
decrypt,
|
||||
callMethodOnDevice,
|
||||
connectToDevice,
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
if (writer) {
|
||||
sendCommand(METHOD_PING, [window.location.host]);
|
||||
}
|
||||
}, 1000 * 10);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
//@ts-ignore
|
||||
window.serialPortService = serialPortService;
|
||||
}
|
||||
|
||||
export default serialPortService;
|
@@ -2,6 +2,7 @@ import { nip04, getPublicKey, finishEvent } from "nostr-tools";
|
||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import { Account } from "./account";
|
||||
import db from "./db";
|
||||
import serialPortService from "./serial-port";
|
||||
|
||||
const decryptedKeys = new Map<string, string>();
|
||||
|
||||
@@ -78,13 +79,21 @@ class SigningService {
|
||||
}
|
||||
|
||||
async requestSignature(draft: DraftNostrEvent, account: Account) {
|
||||
if (account?.readonly) throw new Error("Cant sign in readonly mode");
|
||||
if (account?.useExtension) {
|
||||
if (window.nostr) {
|
||||
const signed = await window.nostr.signEvent(draft);
|
||||
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey!");
|
||||
return signed;
|
||||
} else throw new Error("Missing nostr extension");
|
||||
if (account.readonly) throw new Error("Cant sign in readonly mode");
|
||||
if (account.connectionType) {
|
||||
if (account.connectionType === "extension") {
|
||||
if (window.nostr) {
|
||||
const signed = await window.nostr.signEvent(draft);
|
||||
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey!");
|
||||
return signed;
|
||||
} else throw new Error("Missing nostr extension");
|
||||
} else if (account.connectionType === "serial") {
|
||||
if (serialPortService.supported) {
|
||||
const signed = await serialPortService.signEvent(draft);
|
||||
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey!");
|
||||
return signed;
|
||||
} else throw new Error("Serial devices are not supported");
|
||||
} else throw new Error("Unknown connection type " + account.connectionType);
|
||||
} else if (account?.secKey) {
|
||||
const secKey = await this.decryptSecKey(account);
|
||||
const tmpDraft = { ...draft, pubkey: getPublicKey(secKey) };
|
||||
@@ -95,13 +104,17 @@ class SigningService {
|
||||
}
|
||||
|
||||
async requestDecrypt(data: string, pubkey: string, account: Account) {
|
||||
if (account?.readonly) throw new Error("Cant decrypt in readonly mode");
|
||||
if (account?.useExtension) {
|
||||
if (window.nostr) {
|
||||
if (window.nostr.nip04) {
|
||||
return await window.nostr.nip04.decrypt(pubkey, data);
|
||||
} else throw new Error("Extension dose not support decryption");
|
||||
} else throw new Error("Missing nostr extension");
|
||||
if (account.readonly) throw new Error("Cant decrypt in readonly mode");
|
||||
if (account.connectionType) {
|
||||
if (account.connectionType === "extension") {
|
||||
if (window.nostr) {
|
||||
if (window.nostr.nip04) {
|
||||
return await window.nostr.nip04.decrypt(pubkey, data);
|
||||
} else throw new Error("Extension dose not support decryption");
|
||||
} else throw new Error("Missing nostr extension");
|
||||
} else if (account.connectionType === "serial") {
|
||||
return await serialPortService.decrypt(pubkey, data);
|
||||
} else throw new Error("Unknown connection type " + account.connectionType);
|
||||
} else if (account?.secKey) {
|
||||
const secKey = await this.decryptSecKey(account);
|
||||
return await nip04.decrypt(secKey, pubkey, data);
|
||||
@@ -109,13 +122,17 @@ class SigningService {
|
||||
}
|
||||
|
||||
async requestEncrypt(text: string, pubkey: string, account: Account) {
|
||||
if (account?.readonly) throw new Error("Cant encrypt in readonly mode");
|
||||
if (account?.useExtension) {
|
||||
if (window.nostr) {
|
||||
if (window.nostr.nip04) {
|
||||
return await window.nostr.nip04.encrypt(pubkey, text);
|
||||
} else throw new Error("Extension dose not support encryption");
|
||||
} else throw new Error("Missing nostr extension");
|
||||
if (account.readonly) throw new Error("Cant encrypt in readonly mode");
|
||||
if (account.connectionType) {
|
||||
if (account.connectionType === "extension") {
|
||||
if (window.nostr) {
|
||||
if (window.nostr.nip04) {
|
||||
return await window.nostr.nip04.encrypt(pubkey, text);
|
||||
} else throw new Error("Extension dose not support encryption");
|
||||
} else throw new Error("Missing nostr extension");
|
||||
} else if (account.connectionType === "serial") {
|
||||
return await serialPortService.encrypt(pubkey, text);
|
||||
} else throw new Error("Unknown connection type " + account.connectionType);
|
||||
} else if (account?.secKey) {
|
||||
const secKey = await this.decryptSecKey(account);
|
||||
return await nip04.encrypt(secKey, pubkey, text);
|
||||
|
1
src/types/serial.d.ts
vendored
Normal file
1
src/types/serial.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import "@types/dom-serial";
|
@@ -1,11 +1,25 @@
|
||||
import { useState } from "react";
|
||||
import { Badge, Button, Flex, Spinner, Text, useDisclosure, useToast } from "@chakra-ui/react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
Spinner,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||
|
||||
import accountService from "../../services/account";
|
||||
import Key01 from "../../components/icons/key-01";
|
||||
import ChevronDown from "../../components/icons/chevron-down";
|
||||
import ChevronUp from "../../components/icons/chevron-up";
|
||||
import serialPortService from "../../services/serial-port";
|
||||
import UsbFlashDrive from "../../components/icons/usb-flash-drive";
|
||||
import HelpCircle from "../../components/icons/help-circle";
|
||||
|
||||
export default function LoginStartView() {
|
||||
const location = useLocation();
|
||||
@@ -33,17 +47,44 @@ export default function LoginStartView() {
|
||||
relays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"];
|
||||
}
|
||||
|
||||
accountService.addAccount({ pubkey, relays, useExtension: true, readonly: false });
|
||||
accountService.addAccount({ pubkey, relays, connectionType: "extension", readonly: false });
|
||||
}
|
||||
|
||||
accountService.switchAccount(pubkey);
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setLoading(false);
|
||||
} else {
|
||||
toast({
|
||||
status: "warning",
|
||||
title: "Cant find extension",
|
||||
});
|
||||
toast({ status: "warning", title: "Cant find extension" });
|
||||
}
|
||||
};
|
||||
const loginWithSerial = async () => {
|
||||
if (serialPortService.supported) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const pubkey = await serialPortService.getPublicKey();
|
||||
|
||||
if (!accountService.hasAccount(pubkey)) {
|
||||
let relays: string[] = [];
|
||||
|
||||
// TODO: maybe get relays from device
|
||||
|
||||
if (relays.length === 0) {
|
||||
relays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"];
|
||||
}
|
||||
|
||||
accountService.addAccount({ pubkey, relays, connectionType: "serial", readonly: false });
|
||||
}
|
||||
|
||||
accountService.switchAccount(pubkey);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setLoading(false);
|
||||
} else {
|
||||
toast({ status: "warning", title: "Serial is not supported" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +95,21 @@ export default function LoginStartView() {
|
||||
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="sm" colorScheme="primary">
|
||||
Sign in with extension
|
||||
</Button>
|
||||
{serialPortService.supported && (
|
||||
<ButtonGroup colorScheme="purple">
|
||||
<Button onClick={loginWithSerial} leftIcon={<UsbFlashDrive boxSize={6} />} w="xs">
|
||||
Use Signing Device
|
||||
</Button>
|
||||
<IconButton
|
||||
as={Link}
|
||||
aria-label="What is NSD?"
|
||||
title="What is NSD?"
|
||||
isExternal
|
||||
href="https://lnbits.github.io/nostr-signing-device/installer/"
|
||||
icon={<HelpCircle boxSize={5} />}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={advanced.onToggle}
|
||||
|
15
yarn.lock
15
yarn.lock
@@ -2382,6 +2382,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
||||
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
||||
|
||||
"@noble/secp256k1@^1.7.0":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
|
||||
integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@@ -2685,6 +2690,11 @@
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/dom-serial@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/dom-serial/-/dom-serial-1.0.6.tgz#48462122c0e943195d0611027ff9de979f04f43f"
|
||||
integrity sha512-eUHKbc6mdMgMm75/oBLocs3wkOQkPQ/oNCT+b5OgUT6mLgIvDTp3wCCE9tYZNvDPPh6Cj9lVg2IguWfS/mDrrQ==
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
@@ -5556,11 +5566,6 @@ ngraph.random@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ngraph.random/-/ngraph.random-1.1.0.tgz#5345c4bb63865c85d98ee6f13eab1395d8545a90"
|
||||
integrity sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==
|
||||
|
||||
noble-secp256k1@^1.2.14:
|
||||
version "1.2.14"
|
||||
resolved "https://registry.yarnpkg.com/noble-secp256k1/-/noble-secp256k1-1.2.14.tgz#39429c941d51211ca40161569cee03e61d72599e"
|
||||
integrity sha512-GSCXyoZBUaaPwVWdYncMEmzlSUjF9J/YeEHpklYJCyg8wPuJP3NzDx0BkiwArzINkdX2HJHvUJhL6vVWPOQQcg==
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
|
Reference in New Issue
Block a user