├── .gitignore ├── .prettierignore ├── .prettierrc ├── src ├── messages │ ├── IWireMessage.ts │ ├── BigIntUtils.ts │ ├── MessageFactory.ts │ ├── CommandoMessage.ts │ ├── read-tlvs.ts │ ├── PongMessage.ts │ ├── InitFeatureFlags.ts │ ├── PingMessage.ts │ ├── BitField.ts │ ├── InitMessage.ts │ └── buf.ts ├── socket-wrapper.ts ├── validation.ts ├── chacha │ ├── chacha20.ts │ ├── index.ts │ └── poly1305.ts ├── crypto.ts ├── types.ts ├── noise-state.ts └── index.ts ├── .eslintignore ├── .eslintrc.cjs ├── tsconfig.json ├── package.json ├── LICENCE ├── README.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /package 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /src/messages/IWireMessage.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from '../types.js' 2 | 3 | export interface IWireMessage { 4 | type: MessageType 5 | serialize(): Buffer 6 | } 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | /static 7 | .env 8 | .env.* 9 | !.env.example 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | parserOptions: { 7 | sourceType: 'module', 8 | ecmaVersion: 2020 9 | }, 10 | env: { 11 | browser: true, 12 | es2017: true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/messages/BigIntUtils.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | 3 | export function calcBytes(num: bigint) { 4 | let b = 0 5 | while (num > BigInt(0)) { 6 | b += 1 7 | num /= BigInt(2 ** 8) 8 | } 9 | return b 10 | } 11 | 12 | export function bigintToBuffer(num: bigint): Buffer { 13 | const bytes = calcBytes(num) 14 | return Buffer.from(num.toString(16).padStart(bytes * 2, '0'), 'hex') 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "sourceMap": true, 6 | "outDir": "dist", 7 | "declaration": true, 8 | "declarationDir": "dist", 9 | "noImplicitAny": true, 10 | "strict": true, 11 | "moduleResolution": "node", 12 | "allowSyntheticDefaultImports": true, 13 | "strictPropertyInitialization": false 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /src/messages/MessageFactory.ts: -------------------------------------------------------------------------------- 1 | import { IWireMessage } from './IWireMessage.js' 2 | import { InitMessage } from './InitMessage.js' 3 | import { PingMessage } from './PingMessage.js' 4 | import { PongMessage } from './PongMessage.js' 5 | import { CommandoMessage } from './CommandoMessage.js' 6 | import { MessageType } from '../types.js' 7 | 8 | export function deserialize(buffer: Buffer): IWireMessage | { type: number } { 9 | const type = buffer.readUInt16BE(0) 10 | 11 | switch (type) { 12 | case MessageType.Init: 13 | return InitMessage.deserialize(buffer) 14 | case MessageType.Ping: 15 | return PingMessage.deserialize(buffer) 16 | case MessageType.Pong: 17 | return PongMessage.deserialize(buffer) 18 | case MessageType.CommandoResponse: 19 | case MessageType.CommandoResponseContinues: 20 | return CommandoMessage.deserialize(buffer) 21 | } 22 | 23 | return { type } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lnmessage", 3 | "version": "0.2.7", 4 | "description": "Talk to Lightning nodes from your browser", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "author": "Aaron Barnard", 12 | "license": "MIT", 13 | "repository": "github:aaronbarnardsound/lnmessage", 14 | "scripts": { 15 | "build": "tsc" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^18.14.0", 19 | "@types/ws": "^8.5.4", 20 | "@typescript-eslint/eslint-plugin": "^5.27.0", 21 | "@typescript-eslint/parser": "^5.27.0", 22 | "eslint": "^8.16.0", 23 | "eslint-config-prettier": "^8.3.0", 24 | "prettier": "^2.6.2", 25 | "typescript": "^5.1.6" 26 | }, 27 | "dependencies": { 28 | "@noble/hashes": "^1.3.1", 29 | "@noble/secp256k1": "^2.0.0", 30 | "buffer": "^6.0.3", 31 | "rxjs": "^7.5.7", 32 | "ws": "^8.13.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Aaron Barnard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/messages/CommandoMessage.ts: -------------------------------------------------------------------------------- 1 | import { BufferReader } from './buf.js' 2 | import { CommandoResponse, JsonRpcErrorResponse, MessageType } from '../types.js' 3 | 4 | export class CommandoMessage { 5 | /** 6 | * Processes a buffer containing the message information. This method 7 | * will capture the id of the commando response as well as the payload 8 | */ 9 | public static deserialize(buffer: Buffer): CommandoMessage { 10 | const instance = new CommandoMessage() 11 | const reader = new BufferReader(buffer) 12 | 13 | // read the type bytes 14 | reader.readUInt16BE() 15 | 16 | instance.id = reader.readBytes(8).toString('hex') 17 | const json = reader.readBytes(buffer.byteLength - 26).toString() 18 | 19 | try { 20 | instance.response = JSON.parse(json) 21 | } catch (error) { 22 | instance.response = { 23 | jsonrpc: '2.0', 24 | id: null, 25 | error: { code: 1, message: 'Could not parse json response' } 26 | } as JsonRpcErrorResponse 27 | } 28 | 29 | return instance 30 | } 31 | 32 | public type: MessageType = MessageType.CommandoResponse 33 | public id: string 34 | public response: CommandoResponse 35 | } 36 | -------------------------------------------------------------------------------- /src/messages/read-tlvs.ts: -------------------------------------------------------------------------------- 1 | import { BufferReader } from './buf.js' 2 | 3 | /** 4 | * Reads TLVs from a reader until the entire stream is processed. The handler is 5 | * responsible for doing something with the data bytes. 6 | * @param reader 7 | * @param handler 8 | */ 9 | export function readTlvs( 10 | reader: BufferReader, 11 | handler: (type: bigint, value: BufferReader) => boolean 12 | ) { 13 | let lastType: bigint = BigInt(-1) 14 | 15 | while (!reader.eof) { 16 | try { 17 | const type = reader.readBigSize() 18 | const len = reader.readBigSize() 19 | const value = reader.readBytes(Number(len)) 20 | const valueReader = new BufferReader(value) 21 | 22 | if (type <= lastType) { 23 | throw new Error('Invalid TLV stream') 24 | } 25 | 26 | const isEven = type % BigInt(2) === BigInt(0) 27 | const wasHandled = handler(type, valueReader) 28 | 29 | if (!wasHandled && isEven) { 30 | throw new Error('Unknown even type') 31 | } 32 | 33 | if (wasHandled && !valueReader.eof) { 34 | throw new Error('Non-canonical length') 35 | } 36 | 37 | lastType = type 38 | } catch (error) { 39 | // tried to read an index out of range 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/socket-wrapper.ts: -------------------------------------------------------------------------------- 1 | import type { Socket } from 'net' 2 | import type { Buffer } from 'buffer' 3 | 4 | //**Wraps a TCP socket with the WebSocket API */ 5 | class SocketWrapper { 6 | public onopen?: () => void 7 | public onclose?: () => void 8 | public onerror?: (error: { message: string }) => void 9 | public onmessage?: (event: { data: ArrayBuffer }) => void 10 | public send: (message: Buffer) => void 11 | public close: () => void 12 | 13 | constructor(connection: string, socket: Socket) { 14 | socket.on('connect', () => { 15 | this.onopen && this.onopen() 16 | }) 17 | 18 | socket.on('close', () => { 19 | this.onclose && this.onclose() 20 | socket.removeAllListeners() 21 | }) 22 | 23 | socket.on('error', (error) => { 24 | this.onerror && this.onerror(error) 25 | }) 26 | 27 | socket.on('data', (data) => { 28 | this.onmessage && this.onmessage({ data }) 29 | }) 30 | 31 | this.send = (message: Buffer) => { 32 | socket.write(message) 33 | } 34 | 35 | this.close = () => { 36 | socket.end() 37 | } 38 | 39 | const url = new URL(connection) 40 | const { host } = url 41 | const [nodeIP, port] = host.split(':') 42 | 43 | socket.connect(parseInt(port), nodeIP) 44 | } 45 | } 46 | 47 | export default SocketWrapper 48 | -------------------------------------------------------------------------------- /src/validation.ts: -------------------------------------------------------------------------------- 1 | import { validPrivateKey, validPublicKey } from './crypto.js' 2 | import { LnWebSocketOptions } from './types' 3 | 4 | export function validateInit(options: LnWebSocketOptions): void { 5 | const { remoteNodePublicKey, wsProxy, privateKey, ip, port, logger } = options 6 | 7 | if (!remoteNodePublicKey || !validPublicKey(remoteNodePublicKey)) { 8 | throw new Error(`${remoteNodePublicKey} is not a valid public key`) 9 | } 10 | 11 | const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/ 12 | 13 | const dnsRegex = 14 | /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ 15 | 16 | if (!ip || (!ip.match(ipRegex) && !ip.match(dnsRegex) && ip !== 'localhost')) { 17 | throw new Error(`${ip} is not a valid IP or DNS address`) 18 | } 19 | 20 | if (!port || port < 1 || port > 65535) { 21 | throw new Error(`${port} is not a valid port number`) 22 | } 23 | 24 | if (wsProxy) { 25 | const errMsg = `${wsProxy} is not a valid url` 26 | 27 | try { 28 | const url = new URL(wsProxy) 29 | if (url.protocol !== 'wss:' && url.protocol !== 'ws:') { 30 | throw new Error(errMsg) 31 | } 32 | } catch (err) { 33 | throw new Error(errMsg) 34 | } 35 | } 36 | 37 | if (privateKey && !validPrivateKey(privateKey)) { 38 | throw new Error(`${privateKey} is not a valid private key`) 39 | } 40 | 41 | if (logger) { 42 | if (typeof logger !== 'object') { 43 | throw new Error('Logger must be of type object') 44 | } 45 | 46 | const validLevels = ['info', 'warn', 'error'] 47 | 48 | Object.entries(logger).forEach(([level, handler]) => { 49 | if (!validLevels.includes(level)) { 50 | throw new Error(`Invalid logger level: ${level}`) 51 | } 52 | 53 | if (typeof handler !== 'function') { 54 | throw new Error(`Logger for level: ${level} is not a function`) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/messages/PongMessage.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import { BufferReader, BufferWriter } from './buf.js' 3 | import { MessageType } from '../types.js' 4 | import { IWireMessage } from './IWireMessage.js' 5 | 6 | export class PongMessage implements IWireMessage { 7 | /** 8 | * Deserializes a pong message from a Buffer into a PongMessage 9 | * instance. 10 | */ 11 | public static deserialize(payload: Buffer): PongMessage { 12 | const instance = new PongMessage() 13 | 14 | const reader = new BufferReader(payload) 15 | reader.readUInt16BE() // read off type 16 | 17 | const byteslen = reader.readUInt16BE() 18 | instance.ignored = reader.readBytes(byteslen) 19 | return instance 20 | } 21 | 22 | /** 23 | * Message type = 19 24 | */ 25 | public type: MessageType = MessageType.Pong 26 | 27 | /** 28 | * Should be set to zeros of length specified in a ping message's 29 | * num_pong_bytes. Must not set ignored to sensitive data such as 30 | * secrets or portions of initialized memory. 31 | */ 32 | public ignored: Buffer 33 | 34 | /** 35 | * In order to allow for the existence of long-lived TCP 36 | * connections, at times it may be required that both ends keep 37 | * alive the TCP connection at the application level. 38 | * 39 | * The pong message is a reply to a ping message and must 40 | * reply with the specify number of bytes when the num_pong_bytes 41 | * value is less than 65532. 42 | * for the number of pong bytes it expects to receive as 43 | * a reply. The ignored bits should be set to 0. 44 | */ 45 | constructor(numPongBytes = 0) { 46 | this.ignored = Buffer.alloc(numPongBytes) 47 | } 48 | 49 | /** 50 | * Serializes a PongMessage into a Buffer that can be 51 | * streamed on the wire. 52 | */ 53 | public serialize(): Buffer { 54 | const len = 55 | 2 + // type 56 | 2 + // byteslen 57 | +this.ignored.length 58 | 59 | const writer = new BufferWriter(Buffer.alloc(len)) 60 | writer.writeUInt16BE(this.type) 61 | writer.writeUInt16BE(this.ignored.length) 62 | writer.writeBytes(this.ignored) 63 | return writer.toBuffer() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/messages/InitFeatureFlags.ts: -------------------------------------------------------------------------------- 1 | export enum InitFeatureFlags { 2 | /** 3 | * option_data_loss_protect local flag is set. This flag enables / requires 4 | * the support of the extra channel_reestablish fields defined in BOLT #2. 5 | */ 6 | optionDataLossProtectRequired = 0, 7 | 8 | /** 9 | * option_data_loss_protect local flag is set. This flag enables / requires 10 | * the support of the extra channel_reestablish fields defined in BOLT #2. 11 | */ 12 | optionDataLossProtectOptional = 1, 13 | 14 | /** 15 | * initial_routing_sync asks the remote node to send a complete routing 16 | * information dump. The initial_routing_sync feature is overridden (and 17 | * should be considered equal to 0) by the gossip_queries feature if the 18 | * latter is negotiated via init. 19 | */ 20 | initialRoutingSyncOptional = 3, 21 | 22 | /** 23 | * option_upfront_shutdown_script flag asks to commit to a shutdown 24 | * scriptpubkey when opening a channel as defined in BOLT #2. 25 | */ 26 | optionUpfrontShutdownScriptRequired = 4, 27 | 28 | /** 29 | * option_upfront_shutdown_script flag asks to commit to a shutdown 30 | * scriptpubkey when opening a channel as defined in BOLT #2. 31 | */ 32 | optionUpfrontShutdownScriptOptional = 5, 33 | 34 | /** 35 | * gossip_queries flag signals that the node wishes to use more advanced 36 | * gossip control. When negotiated, this flag will override the 37 | * initial_routing_sync flag. Advanced querying is defined in BOLT #7. 38 | */ 39 | gossipQueriesRequired = 6, 40 | 41 | /** 42 | * gossip_queries flag signals that the node wishes to use more advanced 43 | * gossip control. When negotiated, this flag will override the 44 | * initial_routing_sync flag. Advanced querying is defined in BOLT #7. 45 | */ 46 | gossipQueriesOptional = 7, 47 | 48 | optionVarOptionOptinRequired = 8, 49 | optionVarOptionOptinOptional = 9, 50 | 51 | gossipQueriesExRequired = 10, 52 | gossipQueriesExOptional = 11, 53 | 54 | optionStaticRemoteKeyRequired = 12, 55 | optionStaticRemoteKeyOptional = 13, 56 | 57 | paymentSecretRequired = 14, 58 | paymentSecretOptional = 15, 59 | 60 | basicMppRequired = 16, 61 | basicMppOptional = 17, 62 | 63 | optionSupportLargeChannelRequired = 18, 64 | optionSupportLargeChannelOptional = 19, 65 | } 66 | -------------------------------------------------------------------------------- /src/messages/PingMessage.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import { BufferReader, BufferWriter } from './buf.js' 3 | import { MessageType } from '../types.js' 4 | import { IWireMessage } from './IWireMessage.js' 5 | 6 | export const PONG_BYTE_THRESHOLD = 65532 7 | 8 | /** 9 | * In order to allow for the existence of long-lived TCP 10 | * connections, at times it may be required that both ends keep 11 | * alive the TCP connection at the application level. 12 | * 13 | * The ping message is sent by an initiator and includes a value 14 | * for the number of pong bytes it expects to receive as 15 | * a reply. The ignored bits should be set to 0. 16 | */ 17 | export class PingMessage implements IWireMessage { 18 | /** 19 | * Deserialize a message and return a new instance of the 20 | * PingMessage type. 21 | */ 22 | public static deserialize(payload: Buffer): PingMessage { 23 | const cursor = new BufferReader(payload) 24 | cursor.readUInt16BE() 25 | 26 | const instance = new PingMessage() 27 | instance.numPongBytes = cursor.readUInt16BE() 28 | 29 | const bytesLength = cursor.readUInt16BE() 30 | 31 | instance.ignored = cursor.readBytes(bytesLength) 32 | return instance 33 | } 34 | 35 | /** 36 | * Ping message type is 18 37 | */ 38 | public type: MessageType = MessageType.Ping 39 | 40 | /** 41 | * The number of bytes that should be returned in the pong message. 42 | * Can be set to 65532 to indicate that no pong message should be 43 | * sent. Setting to any number below 65532 will require a pong 44 | * matching the corresponding number of bytes. If the reply 45 | * byteslen does not match this, you may terminate the channels 46 | * with the client. 47 | */ 48 | public numPongBytes: number = 1 49 | 50 | /** 51 | * Should set ignored to 0s. Must not set ignored to 52 | * sensitive data such as secrets or portions of initialized 53 | * memory. 54 | */ 55 | public ignored: Buffer = Buffer.alloc(0) 56 | 57 | /** 58 | * Serialize the PingMessage and return a Buffer 59 | */ 60 | public serialize(): Buffer { 61 | const len = 62 | 2 + // type 63 | 2 + // num_pong_bytes 64 | 2 + // byteslen 65 | this.ignored.length 66 | 67 | const br = new BufferWriter(Buffer.alloc(len)) 68 | br.writeUInt16BE(this.type) 69 | br.writeUInt16BE(this.numPongBytes) 70 | br.writeUInt16BE(this.ignored.length) 71 | br.writeBytes(this.ignored) 72 | return br.toBuffer() 73 | } 74 | 75 | /** 76 | * triggersReply indicates if a pong message must send a reply. 77 | * Ping messages than are smaller than 65532 must send a reply 78 | * with the corresponding number of bytes. Above this value 79 | * no reply is necessary. Refer to BOLT #1. 80 | */ 81 | public get triggersReply(): boolean { 82 | return this.numPongBytes < PONG_BYTE_THRESHOLD 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/messages/BitField.ts: -------------------------------------------------------------------------------- 1 | import * as bigintutil from './BigIntUtils.js' 2 | import { Buffer } from 'buffer' 3 | 4 | type BigIntInput = string | number | bigint | boolean 5 | 6 | /** 7 | * BitField assists with using bit flags to set or unset values in the bit 8 | * field. Preferrably a flag type is provided, otherwise it defaults to allow 9 | * arbitrary setting of integers corresponding to a particular bit index. 10 | * 11 | * Internally, values are stored as bigint so that more than 32 values 12 | * can be used since there is a limit of 31 digits that can be manipulated 13 | * using bitwise operations in JavaScript. 14 | */ 15 | export class BitField { 16 | /** 17 | * Constructs a bitmask from a number 18 | */ 19 | public static fromNumber(value: number) { 20 | return new BitField(BigInt(value)) 21 | } 22 | 23 | /** 24 | * Constructs a bitmask from a buffer 25 | */ 26 | public static fromBuffer(value: Buffer) { 27 | if (value.length === 0) return new BitField() 28 | return new BitField(BigInt('0x' + value.toString('hex'))) 29 | } 30 | 31 | public value: bigint 32 | 33 | constructor(value?: bigint) { 34 | this.value = value || BigInt(0) 35 | } 36 | 37 | public isSet(bit: BigIntInput): boolean { 38 | return (this.value & (BigInt(1) << BigInt(bit))) > BigInt(0) 39 | } 40 | 41 | public set(bit: BigIntInput) { 42 | this.value |= BigInt(1) << BigInt(bit) 43 | } 44 | 45 | public unset(bit: BigIntInput) { 46 | this.value &= ~(this.value & (BigInt(1) << BigInt(bit))) 47 | } 48 | 49 | public toggle(bit: BigIntInput) { 50 | this.value ^= BigInt(1) << BigInt(bit) 51 | } 52 | 53 | /** 54 | * Returns the full list of set flags for the bit field 55 | */ 56 | public flags(): T[] { 57 | const bits: T[] = [] 58 | let bit = 0 59 | let val = 1n 60 | while (val < this.value) { 61 | if (this.value & val) bits.push(bit as any) 62 | bit += 1 63 | val <<= 1n 64 | } 65 | return bits 66 | } 67 | 68 | /** 69 | * Returns the index of the most-significant bit that is set 70 | */ 71 | public msb(): number { 72 | let num = this.value 73 | let bit = 0 74 | while (num > 1) { 75 | num = num >> 1n 76 | bit += 1 77 | } 78 | return bit 79 | } 80 | 81 | /** 82 | * Returns a new BitField with the bitwise AND of the two BitFields 83 | * @param bitfield 84 | */ 85 | public and(bitfield: BitField): BitField { 86 | return new BitField(this.value & bitfield.value) 87 | } 88 | 89 | /** 90 | * Returns a new BitField with the bitwise OR of the two BitFields 91 | * @param bitfield 92 | */ 93 | public or(bitfield: BitField): BitField { 94 | return new BitField(this.value | bitfield.value) 95 | } 96 | 97 | /** 98 | * Returns a new BitField with the bitwise XOR of the two BitFields 99 | * @param bitfield 100 | */ 101 | public xor(bitfield: BitField): BitField { 102 | return new BitField(this.value ^ bitfield.value) 103 | } 104 | 105 | public toBigInt() { 106 | return this.value 107 | } 108 | 109 | public toNumber() { 110 | return Number(this.value) 111 | } 112 | 113 | public toBuffer(): Buffer { 114 | if (this.value === BigInt(0)) return Buffer.alloc(0) 115 | return bigintutil.bigintToBuffer(this.value) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/chacha/chacha20.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | 3 | function ROTATE(v: number, c: number) { 4 | return (v << c) | (v >>> (32 - c)) 5 | } 6 | 7 | const constants = Buffer.from('expand 32-byte k') 8 | 9 | class Chacha20 { 10 | public input: Uint32Array 11 | public cachePos: number 12 | public buffer: Uint32Array 13 | public output: Buffer 14 | 15 | constructor(key: Buffer, nonce: Buffer) { 16 | this.input = new Uint32Array(16) 17 | 18 | // https://tools.ietf.org/html/draft-irtf-cfrg-chacha20-poly1305-01#section-2.3 19 | this.input[0] = constants.readUInt32LE(0) 20 | this.input[1] = constants.readUInt32LE(4) 21 | this.input[2] = constants.readUInt32LE(8) 22 | this.input[3] = constants.readUInt32LE(12) 23 | this.input[4] = key.readUInt32LE(0) 24 | this.input[5] = key.readUInt32LE(4) 25 | this.input[6] = key.readUInt32LE(8) 26 | this.input[7] = key.readUInt32LE(12) 27 | this.input[8] = key.readUInt32LE(16) 28 | this.input[9] = key.readUInt32LE(20) 29 | this.input[10] = key.readUInt32LE(24) 30 | this.input[11] = key.readUInt32LE(28) 31 | 32 | this.input[12] = 0 33 | 34 | this.input[13] = nonce.readUInt32LE(0) 35 | this.input[14] = nonce.readUInt32LE(4) 36 | this.input[15] = nonce.readUInt32LE(8) 37 | 38 | this.cachePos = 64 39 | this.buffer = new Uint32Array(16) 40 | this.output = Buffer.alloc(64) 41 | } 42 | 43 | quarterRound(a: number, b: number, c: number, d: number) { 44 | const x = this.buffer 45 | x[a] += x[b] 46 | x[d] = ROTATE(x[d] ^ x[a], 16) 47 | x[c] += x[d] 48 | x[b] = ROTATE(x[b] ^ x[c], 12) 49 | x[a] += x[b] 50 | x[d] = ROTATE(x[d] ^ x[a], 8) 51 | x[c] += x[d] 52 | x[b] = ROTATE(x[b] ^ x[c], 7) 53 | } 54 | 55 | makeBlock(output: Buffer, start: number) { 56 | let i = -1 57 | // copy input into working buffer 58 | while (++i < 16) { 59 | this.buffer[i] = this.input[i] 60 | } 61 | i = -1 62 | while (++i < 10) { 63 | // straight round 64 | this.quarterRound(0, 4, 8, 12) 65 | this.quarterRound(1, 5, 9, 13) 66 | this.quarterRound(2, 6, 10, 14) 67 | this.quarterRound(3, 7, 11, 15) 68 | 69 | //diaganle round 70 | this.quarterRound(0, 5, 10, 15) 71 | this.quarterRound(1, 6, 11, 12) 72 | this.quarterRound(2, 7, 8, 13) 73 | this.quarterRound(3, 4, 9, 14) 74 | } 75 | 76 | i = -1 77 | 78 | // copy working buffer into output 79 | while (++i < 16) { 80 | this.buffer[i] += this.input[i] 81 | output.writeUInt32LE(this.buffer[i], start) 82 | start += 4 83 | } 84 | 85 | this.input[12]++ 86 | 87 | if (!this.input[12]) { 88 | throw new Error('counter is exausted') 89 | } 90 | } 91 | 92 | getBytes(len: number) { 93 | let dpos = 0 94 | const dst = Buffer.alloc(len) 95 | const cacheLen = 64 - this.cachePos 96 | 97 | if (cacheLen) { 98 | if (cacheLen >= len) { 99 | this.output.copy(dst, 0, this.cachePos, 64) 100 | this.cachePos += len 101 | return dst 102 | } else { 103 | this.output.copy(dst, 0, this.cachePos, 64) 104 | len -= cacheLen 105 | dpos += cacheLen 106 | this.cachePos = 64 107 | } 108 | } 109 | 110 | while (len > 0) { 111 | if (len <= 64) { 112 | this.makeBlock(this.output, 0) 113 | this.output.copy(dst, dpos, 0, len) 114 | if (len < 64) { 115 | this.cachePos = len 116 | } 117 | return dst 118 | } else { 119 | this.makeBlock(dst, dpos) 120 | } 121 | len -= 64 122 | dpos += 64 123 | } 124 | 125 | throw new Error('something bad happended') 126 | } 127 | } 128 | 129 | export default Chacha20 130 | -------------------------------------------------------------------------------- /src/messages/InitMessage.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import { BufferReader, BufferWriter } from './buf.js' 3 | import { BitField } from './BitField.js' 4 | import { InitFeatureFlags } from './InitFeatureFlags.js' 5 | import { MessageType } from '../types.js' 6 | import { readTlvs } from './read-tlvs.js' 7 | import { IWireMessage } from './IWireMessage.js' 8 | 9 | /** 10 | * InitMessage is defined in BOLT #1. Once authentication is complete, the first 11 | * message reveals the features supported or required by the node sending the 12 | * message. This message is sent even on a reconnection. 13 | * 14 | * This message contains two fields; global features and local features, that 15 | * are used to signal how the message should operate. The values of are defined 16 | * in the BOLT #9. 17 | */ 18 | export class InitMessage implements IWireMessage { 19 | /** 20 | * Processes a buffer containing the message information. This method 21 | * will capture the arbitrary length global and local 22 | * features into two internal properties of the newly constructed 23 | * init message object. 24 | */ 25 | public static deserialize(buffer: Buffer): InitMessage { 26 | const instance = new InitMessage() 27 | const reader = new BufferReader(buffer) 28 | 29 | // read the type bytes 30 | reader.readUInt16BE() 31 | 32 | // read the global features and per the specification, the global 33 | // features should not exceed features greater than 13. 34 | const gflen = reader.readUInt16BE() 35 | const gf = BitField.fromBuffer(reader.readBytes(gflen)) 36 | 37 | // Read the local length and parse into a BN value. 38 | const lflen = reader.readUInt16BE() 39 | const lf = BitField.fromBuffer(reader.readBytes(lflen)) 40 | 41 | // construct a single features object by bitwise or of the global and 42 | // local features. 43 | instance.features = new BitField().or(gf).or(lf) 44 | 45 | // process TLVs 46 | readTlvs(reader, (type: bigint, valueReader: BufferReader) => { 47 | switch (type) { 48 | // Process networks TLVs which is a series of chain_hash 32 49 | // byte values. This method will simply read from the stream 50 | // until every thing has been read 51 | case BigInt(1): { 52 | while (!valueReader.eof) { 53 | const chainHash = valueReader.readBytes(32) 54 | instance.chainHashes.push(chainHash) 55 | } 56 | return true 57 | } 58 | default: 59 | return false 60 | } 61 | }) 62 | 63 | return instance 64 | } 65 | 66 | /** 67 | * Message type 16 68 | */ 69 | public type: MessageType = MessageType.Init 70 | 71 | /** 72 | * BitField containing the features provided in by the local or remote node 73 | */ 74 | public features: BitField = new BitField() 75 | 76 | /** 77 | * Supported chain_hashes for the remote peer 78 | */ 79 | public chainHashes: Buffer[] = [] 80 | 81 | /** 82 | * Serialize will construct a properly formatted message based on the 83 | * properties of the configured message. 84 | */ 85 | public serialize() { 86 | const writer = new BufferWriter() 87 | 88 | // write the type 89 | writer.writeUInt16BE(this.type) 90 | 91 | // write gflen 92 | const gflen = 0 93 | writer.writeUInt16BE(gflen) 94 | 95 | // write features 96 | const features = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]) 97 | const featuresLen = features.length 98 | writer.writeUInt16BE(featuresLen) 99 | writer.writeBytes(features) 100 | 101 | // write chainhash tlv 102 | if (this.chainHashes.length) { 103 | writer.writeBigSize(1) // type 104 | writer.writeBigSize(this.chainHashes.length * 32) // length 105 | writer.writeBytes(Buffer.concat(this.chainHashes)) // value 106 | } 107 | 108 | return writer.toBuffer() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/crypto.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import * as secp256k1 from '@noble/secp256k1' 3 | import { createCipher, createDecipher } from './chacha/index.js' 4 | import { hmac } from '@noble/hashes/hmac' 5 | import { sha256 as sha256Array } from '@noble/hashes/sha256' 6 | import { bytesToHex, randomBytes } from '@noble/hashes/utils' 7 | 8 | export function sha256(input: Uint8Array): Buffer { 9 | return Buffer.from(sha256Array(input)) 10 | } 11 | 12 | export function ecdh(pubkey: Uint8Array, privkey: Uint8Array) { 13 | const point = secp256k1.ProjectivePoint.fromHex(secp256k1.getSharedSecret(privkey, pubkey)) 14 | return Buffer.from(sha256(point.toRawBytes(true))) 15 | } 16 | export function hmacHash(key: Buffer, input: Buffer) { 17 | return Buffer.from(hmac(sha256Array, key, input)) 18 | } 19 | 20 | export function hkdf(ikm: Buffer, len: number, salt = Buffer.alloc(0), info = Buffer.alloc(0)) { 21 | // extract step 22 | const prk = hmacHash(salt, ikm) 23 | 24 | // expand 25 | const n = Math.ceil(len / prk.byteLength) 26 | if (n > 255) throw new Error('Output length exceeds maximum') 27 | 28 | const t = [Buffer.alloc(0)] 29 | 30 | for (let i = 1; i <= n; i++) { 31 | const tp = t[t.length - 1] 32 | const bi = Buffer.from([i]) 33 | t.push(hmacHash(prk, Buffer.concat([tp, info, bi]))) 34 | } 35 | 36 | return Buffer.concat(t.slice(1)).subarray(0, len) 37 | } 38 | 39 | export function getPublicKey(privKey: Buffer, compressed = true) { 40 | return Buffer.from(secp256k1.getPublicKey(privKey, compressed)) 41 | } 42 | 43 | /** 44 | * Encrypt data using authenticated encryption with associated data (AEAD) 45 | * ChaCha20-Poly1305. 46 | * 47 | * @param k private key, 64-bytes 48 | * @param n nonce, 12-bytes 49 | * @param ad associated data 50 | * @param plaintext raw data to encrypt 51 | * @returns encrypted data + tag as a variable length buffer 52 | */ 53 | export function ccpEncrypt(k: Buffer, n: Buffer, ad: Buffer, plaintext: Buffer): Buffer { 54 | const cipher = createCipher(k, n) 55 | cipher.setAAD(ad) 56 | 57 | const pad = cipher.update(plaintext) as Buffer 58 | 59 | cipher.final && cipher.final() 60 | const tag = cipher.getAuthTag() 61 | return Buffer.concat([pad, tag]) 62 | } 63 | 64 | /** 65 | * Decrypt data uusing authenticated encryption with associated data (AEAD) 66 | * ChaCha20-Poly1305 67 | * 68 | * @param k private key, 64-bytes 69 | * @param n nonce, 12-bytes 70 | * @param ad associated data, variable length 71 | * @param ciphertext encrypted data to decrypt 72 | * @returns decrypteed data as a variable length Buffer 73 | */ 74 | export function ccpDecrypt(k: Buffer, n: Buffer, ad: Buffer, ciphertext: Buffer) { 75 | const decipher = createDecipher(k, n) 76 | 77 | decipher.setAAD(ad) 78 | 79 | if (ciphertext.length === 16) { 80 | decipher.setAuthTag(ciphertext) 81 | return decipher.final() 82 | } 83 | 84 | if (ciphertext.length > 16) { 85 | const tag = ciphertext.subarray(ciphertext.length - 16) 86 | const pad = ciphertext.subarray(0, ciphertext.length - 16) 87 | decipher.setAuthTag(tag) 88 | let m = decipher.update(pad) 89 | const f = decipher.final() 90 | m = Buffer.concat([m as Buffer, f as Buffer]) 91 | return m 92 | } 93 | } 94 | 95 | export function createRandomPrivateKey(): string { 96 | let privKey 97 | do { 98 | privKey = randomBytes(32) 99 | } while (!validPrivateKey(Buffer.from(privKey))) 100 | 101 | return bytesToHex(privKey) 102 | } 103 | 104 | export function validPublicKey(publicKey: string): boolean { 105 | try { 106 | secp256k1.ProjectivePoint.fromHex(publicKey) 107 | return true 108 | } catch (e) { 109 | return false 110 | } 111 | } 112 | 113 | export function validPrivateKey(privateKey: string | Buffer): boolean { 114 | return secp256k1.utils.isValidPrivateKey(privateKey) 115 | } 116 | 117 | export function createRandomBytes(length: number) { 118 | return randomBytes(length) 119 | } 120 | -------------------------------------------------------------------------------- /src/chacha/index.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import Chacha20 from './chacha20.js' 3 | import Poly1305 from './poly1305.js' 4 | 5 | class Cipher { 6 | private alen: number 7 | private clen: number 8 | private chacha: Chacha20 9 | private poly: Poly1305 10 | private tag: null | Buffer 11 | private _decrypt: boolean 12 | private _hasData: boolean 13 | 14 | constructor(key: Buffer, iv: Buffer, decrypt: boolean = false) { 15 | this.alen = 0 16 | this.clen = 0 17 | this.chacha = new Chacha20(key, iv) 18 | this.poly = new Poly1305(this.chacha.getBytes(64)) 19 | this.tag = null 20 | this._decrypt = decrypt 21 | this._hasData = false 22 | } 23 | 24 | setAAD(aad: Buffer) { 25 | if (this._hasData) { 26 | throw new Error('Attempting to set AAD in unsupported state') 27 | } 28 | this.alen = aad.length 29 | this.poly.update(aad) 30 | const padding = Buffer.alloc(padAmount(this.alen)) 31 | if (padding.length) { 32 | padding.fill(0) 33 | this.poly.update(padding) 34 | } 35 | } 36 | 37 | update(data: string | Buffer, inputEnc?: BufferEncoding, outputEnc?: BufferEncoding) { 38 | if (typeof data === 'string') { 39 | data = Buffer.from(data, inputEnc) 40 | } 41 | 42 | let outData = this._update(data) || Buffer.from('') 43 | 44 | return outputEnc ? outData.toString(outputEnc) : outData 45 | } 46 | 47 | final(outputEnc?: BufferEncoding) { 48 | let outData = this._final() || Buffer.from('') 49 | 50 | return outputEnc ? outData.toString(outputEnc) : outData 51 | } 52 | 53 | _update(chunk: Buffer) { 54 | if (!this._hasData) { 55 | this._hasData = true 56 | } 57 | 58 | const len = chunk.length 59 | 60 | if (!len) { 61 | return 62 | } 63 | 64 | this.clen += len 65 | const pad = this.chacha.getBytes(len) 66 | 67 | let i = -1 68 | while (++i < len) { 69 | pad[i] ^= chunk[i] 70 | } 71 | 72 | if (this._decrypt) { 73 | this.poly.update(chunk) 74 | } else { 75 | this.poly.update(pad) 76 | } 77 | 78 | return pad 79 | } 80 | 81 | _final() { 82 | if (this._decrypt && !this.tag) { 83 | throw new Error('Unsupported state or unable to authenticate data') 84 | } 85 | 86 | const padding = Buffer.alloc(padAmount(this.clen)) 87 | 88 | if (padding.length) { 89 | padding.fill(0) 90 | this.poly.update(padding) 91 | } 92 | 93 | const lens = Buffer.alloc(16) 94 | lens.fill(0) 95 | lens.writeUInt32LE(this.alen, 0) 96 | lens.writeUInt32LE(this.clen, 8) 97 | 98 | const tag = this.poly.update(lens).finish() 99 | 100 | if (this._decrypt) { 101 | if (xorTest(tag, this.tag as Buffer)) { 102 | throw new Error('Unsupported state or unable to authenticate data') 103 | } 104 | } else { 105 | this.tag = tag 106 | } 107 | 108 | return tag 109 | } 110 | 111 | getAuthTag() { 112 | if (this._decrypt || this.tag === null) { 113 | return Buffer.from('') 114 | } 115 | return this.tag 116 | } 117 | 118 | setAuthTag(tag: Buffer) { 119 | if (this._decrypt) { 120 | this.tag = tag 121 | } else { 122 | throw new Error('Attempting to set auth tag in unsupported state') 123 | } 124 | } 125 | } 126 | 127 | function padAmount(len: number) { 128 | const rem = len % 16 129 | return rem ? 16 - rem : 0 130 | } 131 | 132 | function xorTest(a: Buffer, b: Buffer) { 133 | let out = 0 134 | 135 | if (a.length !== b.length) { 136 | out++ 137 | } 138 | 139 | const len = Math.min(a.length, b.length) 140 | 141 | let i = -1 142 | while (++i < len) { 143 | out += a[i] ^ b[i] 144 | } 145 | 146 | return out 147 | } 148 | 149 | export function createDecipher(key: Buffer, iv: Buffer) { 150 | return new Cipher(key, iv, true) 151 | } 152 | 153 | export function createCipher(key: Buffer, iv: Buffer) { 154 | return new Cipher(key, iv) 155 | } 156 | -------------------------------------------------------------------------------- /src/chacha/poly1305.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | 3 | class Poly1305 { 4 | public buffer: Buffer 5 | public leftover: number 6 | public r: Uint16Array 7 | public h: Uint16Array 8 | public pad: Uint16Array 9 | public finished: number 10 | 11 | constructor(key: Buffer) { 12 | this.buffer = Buffer.alloc(16) 13 | this.leftover = 0 14 | this.r = new Uint16Array(10) 15 | this.h = new Uint16Array(10) 16 | this.pad = new Uint16Array(8) 17 | this.finished = 0 18 | 19 | let t = new Uint16Array(8), 20 | i 21 | 22 | for (i = 8; i--; ) t[i] = key.readUInt16LE(i * 2) 23 | 24 | this.r[0] = t[0] & 0x1fff 25 | this.r[1] = ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff 26 | this.r[2] = ((t[1] >>> 10) | (t[2] << 6)) & 0x1f03 27 | this.r[3] = ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff 28 | this.r[4] = ((t[3] >>> 4) | (t[4] << 12)) & 0x00ff 29 | this.r[5] = (t[4] >>> 1) & 0x1ffe 30 | this.r[6] = ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff 31 | this.r[7] = ((t[5] >>> 11) | (t[6] << 5)) & 0x1f81 32 | this.r[8] = ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff 33 | this.r[9] = (t[7] >>> 5) & 0x007f 34 | 35 | for (i = 8; i--; ) { 36 | this.h[i] = 0 37 | this.pad[i] = key.readUInt16LE(16 + 2 * i) 38 | } 39 | this.h[8] = 0 40 | this.h[9] = 0 41 | this.leftover = 0 42 | this.finished = 0 43 | } 44 | 45 | blocks(m: Buffer, mpos: number, bytes: number) { 46 | const hibit = this.finished ? 0 : 1 << 11 47 | let t = new Uint16Array(8), 48 | d = new Uint32Array(10), 49 | c = 0, 50 | i = 0, 51 | j = 0 52 | 53 | while (bytes >= 16) { 54 | for (i = 8; i--; ) t[i] = m.readUInt16LE(i * 2 + mpos) 55 | 56 | this.h[0] += t[0] & 0x1fff 57 | this.h[1] += ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff 58 | this.h[2] += ((t[1] >>> 10) | (t[2] << 6)) & 0x1fff 59 | this.h[3] += ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff 60 | this.h[4] += ((t[3] >>> 4) | (t[4] << 12)) & 0x1fff 61 | this.h[5] += (t[4] >>> 1) & 0x1fff 62 | this.h[6] += ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff 63 | this.h[7] += ((t[5] >>> 11) | (t[6] << 5)) & 0x1fff 64 | this.h[8] += ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff 65 | this.h[9] += (t[7] >>> 5) | hibit 66 | 67 | for (i = 0, c = 0; i < 10; i++) { 68 | d[i] = c 69 | for (j = 0; j < 10; j++) { 70 | d[i] += (this.h[j] & 0xffffffff) * (j <= i ? this.r[i - j] : 5 * this.r[i + 10 - j]) 71 | if (j === 4) { 72 | c = d[i] >>> 13 73 | d[i] &= 0x1fff 74 | } 75 | } 76 | c += d[i] >>> 13 77 | d[i] &= 0x1fff 78 | } 79 | c = (c << 2) + c 80 | c += d[0] 81 | d[0] = c & 0xffff & 0x1fff 82 | c = c >>> 13 83 | d[1] += c 84 | 85 | for (i = 10; i--; ) this.h[i] = d[i] 86 | 87 | mpos += 16 88 | bytes -= 16 89 | } 90 | } 91 | 92 | update(m: Buffer) { 93 | let bytes = m.length 94 | let want = 0, 95 | i = 0, 96 | mpos = 0 97 | 98 | if (this.leftover) { 99 | want = 16 - this.leftover 100 | if (want > bytes) want = bytes 101 | for (i = want; i--; ) { 102 | this.buffer[this.leftover + i] = m[i + mpos] 103 | } 104 | bytes -= want 105 | mpos += want 106 | this.leftover += want 107 | if (this.leftover < 16) return this 108 | this.blocks(this.buffer, 0, 16) 109 | this.leftover = 0 110 | } 111 | 112 | if (bytes >= 16) { 113 | want = bytes & ~(16 - 1) 114 | this.blocks(m, mpos, want) 115 | mpos += want 116 | bytes -= want 117 | } 118 | 119 | if (bytes) { 120 | for (i = bytes; i--; ) { 121 | this.buffer[this.leftover + i] = m[i + mpos] 122 | } 123 | this.leftover += bytes 124 | } 125 | return this 126 | } 127 | 128 | finish() { 129 | let mac = Buffer.alloc(16), 130 | g = new Uint16Array(10), 131 | c = 0, 132 | mask = 0, 133 | f = 0, 134 | i = 0 135 | if (this.leftover) { 136 | i = this.leftover 137 | this.buffer[i++] = 1 138 | for (; i < 16; i++) { 139 | this.buffer[i] = 0 140 | } 141 | this.finished = 1 142 | this.blocks(this.buffer, 0, 16) 143 | } 144 | 145 | c = this.h[1] >>> 13 146 | this.h[1] &= 0x1fff 147 | for (i = 2; i < 10; i++) { 148 | this.h[i] += c 149 | c = this.h[i] >>> 13 150 | this.h[i] &= 0x1fff 151 | } 152 | this.h[0] += c * 5 153 | c = this.h[0] >>> 13 154 | this.h[0] &= 0x1fff 155 | this.h[1] += c 156 | c = this.h[1] >>> 13 157 | this.h[1] &= 0x1fff 158 | this.h[2] += c 159 | 160 | g[0] = this.h[0] + 5 161 | c = g[0] >>> 13 162 | g[0] &= 0x1fff 163 | for (i = 1; i < 10; i++) { 164 | g[i] = this.h[i] + c 165 | c = g[i] >>> 13 166 | g[i] &= 0x1fff 167 | } 168 | g[9] -= 1 << 13 169 | 170 | mask = (g[9] >>> 15) - 1 171 | for (i = 10; i--; ) g[i] &= mask 172 | mask = ~mask 173 | for (i = 10; i--; ) { 174 | this.h[i] = (this.h[i] & mask) | g[i] 175 | } 176 | 177 | this.h[0] = this.h[0] | (this.h[1] << 13) 178 | this.h[1] = (this.h[1] >> 3) | (this.h[2] << 10) 179 | this.h[2] = (this.h[2] >> 6) | (this.h[3] << 7) 180 | this.h[3] = (this.h[3] >> 9) | (this.h[4] << 4) 181 | this.h[4] = (this.h[4] >> 12) | (this.h[5] << 1) | (this.h[6] << 14) 182 | this.h[5] = (this.h[6] >> 2) | (this.h[7] << 11) 183 | this.h[6] = (this.h[7] >> 5) | (this.h[8] << 8) 184 | this.h[7] = (this.h[8] >> 8) | (this.h[9] << 5) 185 | 186 | f = (this.h[0] & 0xffffffff) + this.pad[0] 187 | this.h[0] = f 188 | for (i = 1; i < 8; i++) { 189 | f = (this.h[i] & 0xffffffff) + this.pad[i] + (f >>> 16) 190 | this.h[i] = f 191 | } 192 | 193 | for (i = 8; i--; ) { 194 | mac.writeUInt16LE(this.h[i], i * 2) 195 | this.pad[i] = 0 196 | } 197 | for (i = 10; i--; ) { 198 | this.h[i] = 0 199 | this.r[i] = 0 200 | } 201 | 202 | return mac 203 | } 204 | } 205 | 206 | export default Poly1305 207 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Buffer } from 'buffer' 2 | import type { Socket as TCPSocket } from 'net' 3 | 4 | export type LnWebSocketOptions = { 5 | /** 6 | * 33-byte hex remote compressed public key. 7 | * The identity of the node you would like to initiate a connection with 8 | */ 9 | remoteNodePublicKey: string 10 | /** 11 | * The IP address of the remote node 12 | */ 13 | ip: string 14 | /** 15 | * The port of the remote node. Defaults to 9735 16 | */ 17 | port?: number 18 | /** 19 | * A WebSocket proxy endpoint for the browser to connect to, 20 | * so that a server can create a direct connection to the node without the need for a tls certificate runnning on the remote node 21 | * or if the Lightning node implementation does not support WebSocket connections directly 22 | * Checkout https://github.com/clams-tech/lnsocket-proxy and https://github.com/jb55/ln-ws-proxy 23 | */ 24 | wsProxy?: string 25 | /** 26 | * When connecting directly to a node, the protocol to use. Defaults to 'wss://' 27 | */ 28 | wsProtocol?: 'ws:' | 'wss:' 29 | /**In nodejs or react native you can connect directly via a TCP socket */ 30 | tcpSocket?: TCPSocket 31 | /** 32 | * 32 byte hex encoded private key to be used as the local node secret. 33 | * Use this to ensure a consistent local node identity across connection sessions 34 | */ 35 | privateKey?: string 36 | /** 37 | Logger object to log info, warn, and error logs 38 | */ 39 | logger?: Logger 40 | } 41 | 42 | export type NoiseStateOptions = { 43 | /** 44 | * Local private key as a 32-byte buffer 45 | */ 46 | ls: Buffer 47 | 48 | /** 49 | * Ephemeral private key as a 32-byte 50 | */ 51 | es: Buffer 52 | } 53 | 54 | /** 55 | * Defined in BOLT01 56 | */ 57 | export enum MessageType { 58 | // Setup and Control (0 - 31) 59 | Init = 16, 60 | Error = 17, 61 | Ping = 18, 62 | Pong = 19, 63 | 64 | // Channel (32-127) 65 | OpenChannel = 32, 66 | AcceptChannel = 33, 67 | FundingCreated = 34, 68 | FundingSigned = 35, 69 | FundingLocked = 36, 70 | Shutdown = 38, 71 | ClosingSigned = 39, 72 | 73 | // Commitment (128-255) 74 | // 75 | 76 | // Routing (256-511) 77 | ChannelAnnouncement = 256, 78 | NodeAnnouncement = 257, 79 | ChannelUpdate = 258, 80 | AnnouncementSignatures = 259, 81 | QueryShortChannelIds = 261, 82 | ReplyShortChannelIdsEnd = 262, 83 | QueryChannelRange = 263, 84 | ReplyChannelRange = 264, 85 | GossipTimestampFilter = 265, 86 | 87 | CommandoRequest = 19535, 88 | CommandoResponseContinues = 22859, 89 | CommandoResponse = 22861 90 | } 91 | 92 | /** 93 | * States that the handshake process can be in. States depend on 94 | * whether the socket is the connection Initiator or Responder. 95 | * 96 | * Initiator: 97 | * 1. create and send Iniatitor act1 and transition to 98 | * AWAITING_RESPONDER_REPLY 99 | * 2. process the Responder's reply as act2 100 | * 3. create Initiator act3 reply to complete the handshake 101 | * and transitions to READY 102 | * 103 | * Responder: 104 | * 1. begins in AWAITING_INITIATOR waiting to receive act1 105 | * 2. processes act1 and creates a reply as act2 and transitions 106 | * to AWAITING_INITIATOR_REPLY 107 | * 3. processes the Initiator's reply to complete the handshake 108 | * and transition to READY 109 | */ 110 | export enum HANDSHAKE_STATE { 111 | /** 112 | * Initial state for the Initiator. Initiator will transition to 113 | * AWAITING_RESPONDER_REPLY once act1 is completed and sent. 114 | */ 115 | INITIATOR_INITIATING = 0, 116 | 117 | /** 118 | * Responders begin in this state and wait for the Intiator to begin 119 | * the handshake. Sockets originating from the NoiseServer will 120 | * begin in this state. 121 | */ 122 | AWAITING_INITIATOR = 1, 123 | 124 | /** 125 | * Initiator has sent act1 and is awaiting the reply from the responder. 126 | * Once received, the intiator will create the reply 127 | */ 128 | AWAITING_RESPONDER_REPLY = 2, 129 | 130 | /** 131 | * Responder has sent a reply to the inititator, the Responder will be 132 | * waiting for the final stage of the handshake sent by the Initiator. 133 | */ 134 | AWAITING_INITIATOR_REPLY = 3, 135 | 136 | /** 137 | * Responder/Initiator have completed the handshake and we're ready to 138 | * start sending and receiving over the socket. 139 | */ 140 | READY = 100 141 | } 142 | 143 | export enum READ_STATE { 144 | READY_FOR_LEN = 2, 145 | READY_FOR_BODY = 3, 146 | BLOCKED = 4 147 | } 148 | 149 | export type JsonRpcRequest = { 150 | /**The RPC method you would like to call*/ 151 | method: string 152 | /**The params to for the above method. 153 | * Can be an object with named parameters (like the -k options for the CLI) 154 | * or an array of ordered params. If no value is passed in it defaults to an 155 | * empty array 156 | */ 157 | params?: unknown | unknown[] 158 | } 159 | 160 | type JsonRpcBaseResponse = { 161 | jsonrpc: string 162 | id: string | number | null 163 | } 164 | 165 | export type JsonRpcSuccessResponse = JsonRpcBaseResponse & { result: unknown } 166 | 167 | export type JsonRpcErrorResponse = JsonRpcBaseResponse & { 168 | error: { code: number; message: string; data?: unknown } 169 | } 170 | 171 | export type CommandoRequest = JsonRpcRequest & { 172 | /**Base64 encoded rune token as outputted by the commando-rune cli command 173 | * If the rune does not have adequate permissions for this request an error will 174 | * be returned 175 | */ 176 | rune: string 177 | /**Optional 8 byte hex encoded random string for matching the request to a response 178 | * Lnmessage will handle this automatically, but in some instances it is handy to know the 179 | * request id ahead of time 180 | */ 181 | reqId?: string 182 | } 183 | 184 | export type CommandoResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse 185 | 186 | export type Logger = { 187 | info: (msg: string) => void 188 | warn: (msg: string) => void 189 | error: (msg: string) => void 190 | } 191 | 192 | export type ConnectionStatus = 'connected' | 'connecting' | 'disconnected' | 'waiting_reconnect' 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lnmessage 2 | 3 | Talk to Lightning nodes from the Browser and NodeJS apps. 4 | 5 | ## Features 6 | 7 | - Connect to a lightning node via a WebSocket or TCP Socket connection. 8 | - Works in the Browser and Node without any polyfilling. 9 | - Initialise with a session secret to have a persistent node public key for the browser. 10 | - Control a Core Lightning node via [Commando](https://lightning.readthedocs.io/lightning-commando.7.html) RPC calls. 11 | - Automatic handling of ping messages to ensure constant connection to the node. 12 | - Automatic decryption of all incoming messages. You can subscribe to a stream of decrypted messages and do whatever you like with them. The idea is that this library will eventually send and handle more than just init, ping and commando messages. In the mean time it can be extended pretty easily to handle any kind of Lightning messages. 13 | - Automatic WebSocket re-connection handling. 14 | 15 | ## Installation 16 | 17 | ### Yarn 18 | 19 | `yarn add lnmessage` 20 | 21 | ### NPM 22 | 23 | `npm i lnmessage` 24 | 25 | ## Quickstart 26 | 27 | ```javascript 28 | import Lnmessage from 'lnmessage' 29 | 30 | async function connect() { 31 | // initialise the library 32 | const ln = new Lnmessage({ 33 | // The public key of the node you would like to connect to 34 | remoteNodePublicKey: '02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f', 35 | // Optional WebSocket proxy endpoint to connect through (see WebSocket Proxy section) 36 | wsProxy: 'wss://', 37 | // Optional TCP Socket to connect through (either use wsProxy OR tcpSocket) 38 | tcpSocket: new net.Socket(), 39 | // The IP address of the node 40 | ip: '35.232.170.67', 41 | // The port of the node, defaults to 9735 42 | port: 9735, 43 | // Hex encoded private key string to use for the node local secret. Use this to persist the node public key across connections 44 | privateKey: 'd6a2eba36168cc31e97396a781a4dd46dd3648c001d3f4fde221d256e41715ea' 45 | }) 46 | 47 | // initiate the connection to the remote node 48 | await ln.connect() 49 | 50 | // if you have connected to a Core Lightning node that you have a rune for.... 51 | ln.commando({ 52 | method: 'getinfo', 53 | params: [], 54 | rune: '' 55 | }) 56 | } 57 | 58 | connect() 59 | ``` 60 | 61 | ## Initialisation 62 | 63 | ```typescript 64 | type LnWebSocketOptions = { 65 | /** 66 | * 33-byte hex remote compressed public key. 67 | * The identity of the node you would like to initiate a connection with 68 | */ 69 | remoteNodePublicKey: string 70 | /** 71 | * The IP address of the remote node 72 | */ 73 | ip: string 74 | /** 75 | * The port of the remote node. Defaults to 9735 76 | */ 77 | port?: number 78 | /** 79 | * A WebSocket proxy endpoint for the browser to connect to, 80 | * so that a server can create a direct connection to the node without the need for a tls certificate runnning on the remote node 81 | * or if the Lightning node implementation does not support WebSocket connections directly 82 | * Checkout https://github.com/clams-tech/lnsocket-proxy and https://github.com/jb55/ln-ws-proxy 83 | */ 84 | wsProxy?: string 85 | /** 86 | * When connecting directly to a node and not using a proxy, the protocol to use. Defaults to 'wss://' 87 | */ 88 | wsProtocol?: 'ws:' | 'wss:' 89 | /**In Nodejs or React Native you can connect directly via a TCP socket */ 90 | tcpSocket?: TCPSocket 91 | /** 92 | * 32 byte hex encoded private key to be used as the local node secret. 93 | * Use this to ensure a consistent local node identity across connection sessions 94 | */ 95 | privateKey?: string 96 | /** 97 | Logger object to log info, warn, and error logs 98 | */ 99 | logger?: Logger 100 | } 101 | 102 | const options: LnWebSocketOptions = { 103 | remoteNodePublicKey: '02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f', 104 | wsProxy: 'wss://', 105 | ip: '35.232.170.67', 106 | port: 9735, 107 | privateKey: 'd6a2eba36168cc31e97396a781a4dd46dd3648c001d3f4fde221d256e41715ea', 108 | logger: { 109 | info: console.log, 110 | warn: console.warn, 111 | error: console.error 112 | } 113 | } 114 | 115 | const ln = new Lnmessage(options) 116 | ``` 117 | 118 | ## Connecting 119 | 120 | ```javascript 121 | const connected = await ln.connect() 122 | 123 | if (connected) { 124 | console.log('Connected and ready to send/receive messages') 125 | } 126 | ``` 127 | 128 | ## Commando RPC Requests 129 | 130 | If you are connecting to a Core Ln node and you have a valid [rune](https://lightning.readthedocs.io/lightning-commando-rune.7.html) authentication token, you can use Lnmessage to securely call the node RPC server using the `commando` method. 131 | 132 | ```typescript 133 | type CommandoRequest = { 134 | /**The RPC method you would like to call*/ 135 | method: string 136 | /**The params to for the above method. 137 | * Can be an object with named parameters (like the -k options for the CLI) 138 | * or an array of ordered params. If no value is passed in it defaults to an 139 | * empty array 140 | */ 141 | params?: unknown | unknown[] 142 | /**Base64 encoded rune token as outputted by the commando-rune cli command 143 | * If the rune does not have adequate permissions for this request an error will 144 | * be returned 145 | */ 146 | rune: string 147 | /**Optional 8 byte hex encoded random string for matching the request to a response 148 | * Lnmessage will handle this automatically, but in some instances it is handy to know the 149 | * request id ahead of time 150 | */ 151 | reqId?: string 152 | } 153 | 154 | // Basic Get Info request 155 | const getInfoRequest: CommandoRequest = { 156 | method: 'getinfo', 157 | rune: '7jN2zKjkWlvncm_La3uZc9vLVGLu7xl9oBoun6pth7E9MA==' 158 | } 159 | 160 | const getInfoReponse = await ln.commando(getInfoRequest) 161 | 162 | // Some helpers for creating a request id 163 | function toHexString(byteArray: Uint8Array) { 164 | return byteArray.reduce((output, elem) => output + ('0' + elem.toString(16)).slice(-2), '') 165 | } 166 | 167 | function createRandomHex(length = 32) { 168 | const bytes = new Uint8Array(length) 169 | return toHexString(crypto.getRandomValues(bytes)) 170 | } 171 | 172 | // 8 byte random hex string request id 173 | const reqId = await createRandomHex(8) 174 | 175 | // a request utilising the reqId param 176 | const waitInvoiceRequest: CommandoRequest = { 177 | method: 'waitanyinvoice', 178 | params: { lastpay_index: lastPayIndex }, 179 | rune: '7jN2zKjkWlvncm_La3uZc9vLVGLu7xl9oBoun6pth7E9MA==', 180 | reqId 181 | } 182 | 183 | const invoiceUpdate = await ln.commando(waitInvoiceRequest) 184 | ``` 185 | 186 | ## API 187 | 188 | RxJs Observables are used throughout the API and are indicated by a `$` at the end of the variable name. You do not need to use or understand RxJs to make use of these variables. Simply call the `subscribe` method on these variable and pass in a call back for all updates and then call the `unsubscribe` method on the returned object when you no longer want to receive updates. 189 | 190 | ```typescript 191 | class Lnmessage { 192 | /**The underlying Noise protocol. Can be used if you want to play around with the low level Lightning transport protocol*/ 193 | public noise: NoiseState 194 | /**The public key of the node that Lnmessage is connected to*/ 195 | public remoteNodePublicKey: string 196 | /**The public key Lnmessage uses when connecting to a remote node 197 | * If you passed in a private key when initialising, 198 | * this public key will be derived from it and can be used for persistent identity 199 | * across session connections 200 | */ 201 | public publicKey: string 202 | /**The private key that was either passed in on init or generated automatically 203 | * Reuse this when reconnecting for persistent id 204 | */ 205 | public privateKey: string 206 | /**The url that the WebSocket will connect to. It uses the wsProxy option if provided 207 | * or otherwise will initiate a WebSocket connection directly to the node 208 | */ 209 | public wsUrl: string 210 | /**The WebSocket instance*/ 211 | public socket: WebSocket | null 212 | /** 213 | * Observable that indicates the current socket connection status 214 | * Can be either 'connected', 'connecting', 'waiting_reconnect' or 'disconnected'. 215 | */ 216 | public connectionStatus$: BehaviorSubject 217 | /**Observable stream of decypted messages. This can be used to extend Lnmessage 218 | * functionality so that it can handle other Lightning message types 219 | */ 220 | public decryptedMsgs$: Observable 221 | /**Obserable stream of all commando response messages*/ 222 | public commandoMsgs$: Observable< 223 | (JsonRpcSuccessResponse | JsonRpcErrorResponse) & { reqId: string } 224 | > 225 | /**Node JS Buffer instance, useful if handling decrypted messages manually*/ 226 | public Buffer: BufferConstructor 227 | /**Connect to the remote node*/ 228 | public connect(attemptReconnect = true): Promise 229 | /**Disconnect from the remote node*/ 230 | public disconnect(): void 231 | /**Commando requests*/ 232 | public commando(request: CommandoRequest): Promise 233 | } 234 | ``` 235 | 236 | ## WebSocket Proxy 237 | 238 | There are some limitations to connecting to Lightning nodes within a browser. Core Lightning nodes can be directly connected to if the [`experimental-websocket-port`](https://lightning.readthedocs.io/lightningd-config.5.html#experimental-options) option is set in the config. This will allow a direct connection to the node, but if you are running a browser app on https, then it will not allow a connection to a non SSL WebSocket endpoint, so you would need to setup SSL for your node. As far as I know LND nodes do not accept connections via WebSocket at this time. 239 | 240 | So to simplify connecting to any Lightning node, you can go through a WebSocket proxy (see [Clams](https://github.com/clams-tech/lnsocket-proxy) and [jb55](https://github.com/jb55/ln-ws-proxy)'s WebSocket proxy server repos). Going through a proxy like this requires no trust in the server. The WebSocket connection is initated with the proxy, which then creates a regular TCP socket connection to the node. Then all messages are fully encrypted via the noise protocol, so the server only sees encrypted binary traffic that is simply proxied between the browser and the node. Currently only clearnet is supported, but I believe that the WebSocket proxy code could be modified to create a socket connection to a TOR only node to make this work. 241 | 242 | ## TOR 243 | 244 | Connecting to a node over TOR requires the proxy server to support it. 245 | 246 | ## Running Locally 247 | 248 | ### Install Deps 249 | 250 | **Yarn** - `yarn` 251 | 252 | **NPM** - `npm i` 253 | 254 | ### Build 255 | 256 | **Yarn** - `yarn build` 257 | 258 | **NPM** - `npm run build` 259 | 260 | ## Acknowledgments 261 | 262 | - This library was inspired by [jb55](https://github.com/jb55)'s [lnsocket](https://github.com/jb55/lnsocket) library. Lnsocket is awesome, but I wanted something that was a bit more browser friendly and with a few more features. 263 | - Most of the code comes from the [Node Lightning](https://github.com/altangent/node-lightning) project and has been modified to be browser friendly. 264 | - The chacha encryption algorithm is a modified version of the [chacha](https://github.com/calvinmetcalf/chacha20poly1305) library. 265 | -------------------------------------------------------------------------------- /src/noise-state.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import { ccpDecrypt, ccpEncrypt, ecdh, getPublicKey, hkdf, sha256 } from './crypto.js' 3 | import type { NoiseStateOptions } from './types.js' 4 | 5 | export class NoiseState { 6 | /** 7 | * The official protocol name for the Lightning variant of Noise. This 8 | * value is mixed into the iniitialiization function to start the 9 | * handshake. 10 | */ 11 | public protocolName = Buffer.from('Noise_XK_secp256k1_ChaChaPoly_SHA256') 12 | /** 13 | * Appended to the hash of the protocolName during initialization. 14 | */ 15 | public prologue = Buffer.from('lightning') 16 | /** 17 | * Local secret is a 32-bit private key valid in elliptic curve 18 | * secp256k1. This value is unique to the node and should be 19 | * chosen with strong cryptographic randomness. 20 | */ 21 | public ls: Buffer 22 | /** 23 | * Local compressed public key derviced from the local secret `ls`. 24 | * This value is stored as a 33-byte buffer. 25 | */ 26 | public lpk: Buffer 27 | /** 28 | * Ephemeral secret is a 32-bit private key valid in elliptic curve 29 | * secp256k1. This value is generated by each node for each connection. 30 | * This value must be generated with strong cryptographic randomness. 31 | */ 32 | public es: Buffer 33 | /** 34 | * Ephemeral compressed public key derived from the ephemeral secret 35 | * `es`. This value is stored as a 33-byte buffer. 36 | */ 37 | public epk: Buffer 38 | /** 39 | * Remote compressed public key stored as a 33-byte buffer. 40 | */ 41 | public rpk: Buffer 42 | /** 43 | * Remote party's ephemeral public key as a 33-byte buffer storing 44 | * the compressed public key. This value is extracted in act 2 where 45 | * it is sent during act 1 to the opposing side. 46 | */ 47 | public repk: Buffer 48 | /** 49 | * Hanshake hash. This value is the accumulated hash of all handshake data that 50 | * has been sent and received during the handshake process. 51 | */ 52 | public h: Buffer 53 | /** 54 | * Chaining key. This value is the accumulated hash of all previous ECDH outputs. 55 | * At the end of the handshake, `ck` is used to dervice the encryption keys 56 | * for messages. 57 | */ 58 | public ck: Buffer 59 | /** 60 | * The key used is the receiving key used to decrypt messages sent by the 61 | * other side. It is generated in Act3. 62 | */ 63 | public rk: Buffer 64 | /** 65 | * The key used by the sender to encrypt messages to the receiver. This value 66 | * is generated in Act3. 67 | */ 68 | public sk: Buffer 69 | /** 70 | * Nonce incremented when sending messages. Initialized to zero in Act3. 71 | */ 72 | public sn: Buffer 73 | /** 74 | * Nonce incremented when receiving messages. Initialized to zero in Act3. 75 | */ 76 | public rn: Buffer 77 | /** 78 | * Chaining key for sending. Initialized to ck in Act 3. 79 | */ 80 | public sck: Buffer; 81 | /** 82 | * Chaining key for receiving. Initialized to ck in Act 3. 83 | */ 84 | public rck: Buffer; 85 | /** 86 | * Intermediate key 1. Used to encrypt or decrypt the zero-length AEAD 87 | * payload in the corresponding initiator or receiver act. 88 | */ 89 | public tempK1: Buffer 90 | /** 91 | * Intermediate key 2. Used to encrypt or decrypt the zero-length AEAD 92 | * payload in the corresponding initiator or receiver act. 93 | */ 94 | public tempK2: Buffer 95 | /** 96 | * Intermediate key 3. Used to encrypt or decrypt the zero-length AEAD 97 | * payload in the corresponding initiator or receiver act. 98 | */ 99 | public tempK3: Buffer 100 | /** 101 | * State machine for perforing noise-protocol handshake, message 102 | * encryption and decryption, and key rotation. 103 | */ 104 | constructor({ ls, es }: NoiseStateOptions) { 105 | this.ls = ls 106 | this.lpk = getPublicKey(ls) 107 | this.es = es 108 | this.epk = getPublicKey(es) 109 | } 110 | /** 111 | * Initiator Act1 is the starting point for the authenticated key exchange 112 | * handshake. The initiator attempts to satisfy an implicit challenge by the 113 | * responder: knowledge of the static public key of the responder. It also 114 | * transmits the initiators ephemeral key. 115 | * @param rpk remote public key 116 | * @return Buffer that is 50 bytes 117 | */ 118 | public initiatorAct1(rpk: Buffer): Buffer { 119 | this.rpk = rpk 120 | this._initialize(this.rpk) 121 | 122 | // 2. h = SHA-256(h || epk) 123 | this.h = sha256(Buffer.concat([this.h, this.epk])) 124 | 125 | // 3. es = ECDH(e.priv, rs) 126 | const ss = ecdh(this.rpk, this.es) 127 | 128 | // 4. ck, temp_k1 = HKDF(ck, es) 129 | const tempK1 = hkdf(ss, 64, this.ck) 130 | this.ck = tempK1.subarray(0, 32) 131 | this.tempK1 = Buffer.from(tempK1.subarray(32)) 132 | 133 | // 5. c = encryptWithAD(temp_k1, 0, h, zero) 134 | const c = ccpEncrypt(this.tempK1, Buffer.alloc(12), this.h, Buffer.alloc(0)) 135 | 136 | // 6. h = SHA-256(h || c) 137 | this.h = sha256(Buffer.concat([this.h, c])) 138 | // 7. m = 0 || epk || c 139 | const m = Buffer.concat([Buffer.alloc(1), this.epk, c]) 140 | return m 141 | } 142 | /** 143 | * Initiator Act2 handles the response generated by the receiver's 144 | * Act1, a 50-byte message. The responder's ephemeral key is extacted 145 | * from the message during this phase. 146 | * 147 | * @param m 50-byte message from responder's act1 148 | */ 149 | public initiatorAct2(m: Buffer) { 150 | // 1. read exactly 50 bytes off the stream 151 | if (m.length !== 50) throw new Error('ACT2_READ_FAILED') 152 | // 2. parse th read message m into v, re, and c 153 | const v = m.subarray(0, 1)[0] 154 | const re = m.subarray(1, 34) 155 | const c = m.subarray(34) 156 | // 2a. convert re to public key 157 | this.repk = re 158 | // 3. assert version is known version 159 | if (v !== 0) throw new Error('ACT2_BAD_VERSION') 160 | // 4. sha256(h || re.serializedCompressed'); 161 | this.h = sha256(Buffer.concat([this.h, this.repk])) 162 | // 5. ss = ECDH(re, e.priv); 163 | const ss = ecdh(this.repk, this.es) 164 | // 6. ck, temp_k2 = HKDF(cd, ss) 165 | const tempK2 = hkdf(ss, 64, this.ck) 166 | this.ck = tempK2.subarray(0, 32) 167 | this.tempK2 = tempK2.subarray(32) 168 | // 7. p = decryptWithAD() 169 | ccpDecrypt(this.tempK2, Buffer.alloc(12), this.h, c) 170 | // 8. h = sha256(h || c) 171 | this.h = sha256(Buffer.concat([this.h, c])) 172 | } 173 | /** 174 | * Initiator Act3 is the final phase in the authenticated 175 | * key agreement. This act is executed only if act 2 176 | * was successful. The initiator transports its static public key 177 | * to the responder. 178 | */ 179 | public initiatorAct3() { 180 | // 1. c = encryptWithAD(temp_k2, 1, h, lpk) 181 | const c = ccpEncrypt( 182 | this.tempK2, 183 | Buffer.from([0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]), 184 | this.h, 185 | this.lpk 186 | ) 187 | // 2. h = sha256(h || c) 188 | this.h = sha256(Buffer.concat([this.h, c])) 189 | // 3. ss = ECDH(re, s.priv) 190 | const ss = ecdh(this.repk, this.ls) 191 | // 4. ck, temp_k3 = HKDF(ck, ss) 192 | const tempK3 = hkdf(ss, 64, this.ck) 193 | this.ck = tempK3.subarray(0, 32) 194 | this.tempK3 = Buffer.from(tempK3.subarray(32)) 195 | this.rck = this.sck = this.ck; 196 | // 5. t = encryptWithAD(temp_k3, 0, h, zero) 197 | const t = ccpEncrypt(this.tempK3, Buffer.alloc(12), this.h, Buffer.alloc(0)) 198 | // 6. sk, rk = hkdf(ck, zero) 199 | const sk = hkdf(Buffer.alloc(0), 64, this.ck) 200 | this.rk = sk.subarray(32) 201 | this.sk = sk.subarray(0, 32) 202 | // 7. rn = 0, sn = 0 203 | this.sn = Buffer.alloc(12) 204 | this.rn = Buffer.alloc(12) 205 | // 8. send m = 0 || c || t 206 | const m = Buffer.concat([Buffer.alloc(1), c, t]) 207 | return m 208 | } 209 | /** 210 | * Receiver Act1 extracts the initiators ephemeral key. It also 211 | * validates that the initiator knows the receivers public key. 212 | * @param m 50-byte message sent by the initiator 213 | */ 214 | public receiveAct1(m: Buffer) { 215 | this._initialize(this.lpk) 216 | // 1. read exactly 50 bytes off the stream 217 | if (m.length !== 50) throw new Error('ACT1_READ_FAILED') 218 | // 2. parse th read message m into v,re, and c 219 | const v = m.subarray(0, 1)[0] 220 | const re = m.subarray(1, 34) 221 | const c = m.subarray(34) 222 | this.repk = re 223 | // 3. assert version is known version 224 | if (v !== 0) throw new Error('ACT1_BAD_VERSION') 225 | // 4. sha256(h || re.serializedCompressed'); 226 | this.h = sha256(Buffer.concat([this.h, re])) 227 | // 5. ss = ECDH(re, ls.priv); 228 | const ss = ecdh(re, this.ls) 229 | // 6. ck, temp_k1 = HKDF(cd, ss) 230 | const tempK1 = hkdf(ss, 64, this.ck) 231 | this.ck = tempK1.subarray(0, 32) 232 | this.tempK1 = tempK1.subarray(32) 233 | // 7. p = decryptWithAD(temp_k1, 0, h, c) 234 | ccpDecrypt(this.tempK1, Buffer.alloc(12), this.h, c) 235 | // 8. h = sha256(h || c) 236 | this.h = sha256(Buffer.concat([this.h, c])) 237 | } 238 | /** 239 | * Receiver Act2 takes place only if Act1 was successful. 240 | * This act sends responder's ephermeral key to the initiator. 241 | */ 242 | public recieveAct2(): Buffer { 243 | // 1. e = generateKey() => done in initialization 244 | // 2. h = sha256(h || e.pub.compressed()) 245 | this.h = sha256(Buffer.concat([this.h, this.epk])) 246 | // 3. ss = ecdh(re, e.priv) 247 | const ss = ecdh(this.repk, this.es) 248 | // 4. ck, temp_k2 = hkdf(ck, ss) 249 | const tempK2 = hkdf(ss, 64, this.ck) 250 | this.ck = tempK2.subarray(0, 32) 251 | this.tempK2 = Buffer.from(tempK2.subarray(32)) 252 | // 5. c = encryptWithAd(temp_k2, 0, h, zero) 253 | const c = ccpEncrypt(this.tempK2, Buffer.alloc(12), this.h, Buffer.alloc(0)) 254 | // 6. h = sha256(h || c) 255 | this.h = sha256(Buffer.concat([this.h, c])) 256 | // 7. m = 0 || e.pub.compressed() Z|| c 257 | const m = Buffer.concat([Buffer.alloc(1), this.epk, c]) 258 | return m 259 | } 260 | /** 261 | * Receiver Act3 is the final phase in the authenticated key 262 | * agreement. This act is executed only if act 2 was successful. 263 | * The receiver extracts the public key of the initiator. 264 | * @param m 66-byte message 265 | */ 266 | public receiveAct3(m: Buffer) { 267 | // 1. read exactly 66 bytes from the network buffer 268 | if (m.length !== 66) throw new Error('ACT3_READ_FAILED') 269 | // 2. parse m into v, c, t 270 | const v = m.subarray(0, 1)[0] 271 | const c = m.subarray(1, 50) 272 | const t = m.subarray(50) 273 | // 3. validate v is recognized 274 | if (v !== 0) throw new Error('ACT3_BAD_VERSION') 275 | // 4. rs = decryptWithAD(temp_k2, 1, h, c) 276 | const rs = ccpDecrypt(this.tempK2, Buffer.from('000000000100000000000000', 'hex'), this.h, c) 277 | this.rpk = rs as Buffer 278 | // 5. h = sha256(h || c) 279 | this.h = sha256(Buffer.concat([this.h, c])) 280 | // 6. ss = ECDH(rs, e.priv) 281 | const ss = ecdh(this.rpk, this.es) 282 | // 7. ck, temp_k3 = hkdf(cs, ss) 283 | const tempK3 = hkdf(ss, 64, this.ck) 284 | this.ck = tempK3.subarray(0, 32) 285 | this.tempK3 = tempK3.subarray(32) 286 | // 8. p = decryptWithAD(temp_k3, 0, h, t) 287 | ccpDecrypt(this.tempK3, Buffer.alloc(12), this.h, t) 288 | // 9. rk, sk = hkdf(ck, zero) 289 | const sk = hkdf(Buffer.alloc(0), 64, this.ck) 290 | this.rk = sk.subarray(0, 32) 291 | this.sk = sk.subarray(32) 292 | // 10. rn = 0, sn = 0 293 | this.rn = Buffer.alloc(12) 294 | this.sn = Buffer.alloc(12) 295 | } 296 | /** 297 | * Sends an encrypted message using the shared sending key and nonce. 298 | * The nonce is rotated once the message is sent. The sending key is 299 | * rotated every 1000 messages. 300 | * @param m 301 | */ 302 | public encryptMessage(m: Buffer): Buffer { 303 | // step 1/2. serialize m length into int16 304 | const l = Buffer.alloc(2) 305 | l.writeUInt16BE(m.length, 0) 306 | // step 3. encrypt l, using chachapoly1305, sn, sk) 307 | const lc = ccpEncrypt(this.sk, this.sn, Buffer.alloc(0), l) 308 | // step 3a: increment sn 309 | if (this._incrementSendingNonce() >= 1000) this._rotateSendingKeys() 310 | // step 4 encrypt m using chachapoly1305, sn, sk 311 | const c = ccpEncrypt(this.sk, this.sn, Buffer.alloc(0), m) 312 | // step 4a: increment sn 313 | if (this._incrementSendingNonce() >= 1000) this._rotateSendingKeys() 314 | // step 5 return m to be sent 315 | return Buffer.concat([lc, c]) 316 | } 317 | /** 318 | * Decrypts the length of the message using the receiving key and nonce. 319 | * The receiving key is rotated every 1000 messages. 320 | */ 321 | public decryptLength(lc: Buffer): number { 322 | const l = ccpDecrypt(this.rk, this.rn, Buffer.alloc(0), lc) as Buffer 323 | if (this._incrementRecievingNonce() >= 1000) this._rotateRecievingKeys() 324 | return l.readUInt16BE(0) 325 | } 326 | /** 327 | * Decrypts the message using the receiving key and nonce. The receiving 328 | * key is rotated every 1000 messages. 329 | */ 330 | public decryptMessage(c: Buffer) { 331 | const m = ccpDecrypt(this.rk, this.rn, Buffer.alloc(0), c) 332 | if (this._incrementRecievingNonce() >= 1000) this._rotateRecievingKeys() 333 | return m as Buffer 334 | } 335 | ///////////////////////////////////// 336 | /** 337 | * Initializes the noise state prior to Act1. 338 | */ 339 | private _initialize(pubkey: Buffer) { 340 | // 1. h = SHA-256(protocolName) 341 | this.h = sha256(Buffer.from(this.protocolName)) 342 | 343 | // 2. ck = h 344 | this.ck = this.h 345 | 346 | // 3. h = SHA-256(h || prologue) 347 | this.h = sha256(Buffer.concat([this.h, this.prologue])) 348 | 349 | // 4. h = SHA-256(h || pubkey) 350 | this.h = sha256(Buffer.concat([this.h, pubkey])) 351 | } 352 | 353 | private _incrementSendingNonce() { 354 | const newValue = this.sn.readUInt16LE(4) + 1 355 | this.sn.writeUInt16LE(newValue, 4) 356 | return newValue 357 | } 358 | 359 | private _incrementRecievingNonce() { 360 | const newValue = this.rn.readUInt16LE(4) + 1 361 | this.rn.writeUInt16LE(newValue, 4) 362 | return newValue 363 | } 364 | 365 | private _rotateSendingKeys() { 366 | const result = hkdf(this.sk, 64, this.sck) 367 | this.sk = result.subarray(32) 368 | this.sck = result.subarray(0, 32) 369 | this.sn = Buffer.alloc(12) 370 | } 371 | 372 | private _rotateRecievingKeys() { 373 | const result = hkdf(this.rk, 64, this.rck) 374 | this.rk = result.subarray(32) 375 | this.rck = result.subarray(0, 32) 376 | this.rn = Buffer.alloc(12) 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/messages/buf.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | 3 | /** 4 | * BufferReader class is used to simplify reading information from a Buffer 5 | */ 6 | export class BufferReader { 7 | /** 8 | * Returns the number of bytes that will be used to encode 9 | * a BigSize number. BigSize is defined in Lightning Network BOLT 07 10 | */ 11 | public static bigSizeBytes(num: bigint): number { 12 | if (num < BigInt(0xfd)) return 1 13 | if (num < BigInt(0x10000)) return 3 14 | if (num < BigInt(0x100000000)) return 5 15 | else return 9 16 | } 17 | 18 | private _buffer: Buffer 19 | private _position: number 20 | private _lastReadBytes: number 21 | 22 | /** 23 | * Constructs a reader from the supplied Buffer 24 | */ 25 | constructor(buffer: Buffer) { 26 | this._buffer = buffer 27 | this._position = 0 28 | this._lastReadBytes = 0 29 | } 30 | 31 | /** 32 | * Gets or sets the current position of the cursor in the buffer 33 | */ 34 | public get position(): number { 35 | return this._position 36 | } 37 | 38 | public set position(val: number) { 39 | this._position = val 40 | } 41 | 42 | /** 43 | * Gets if the cursor is at the end of file. 44 | */ 45 | public get eof(): boolean { 46 | return this._position === this._buffer.length 47 | } 48 | 49 | /** 50 | * Gets the underlying buffer that the cursor 51 | * is reading from. 52 | */ 53 | public get buffer(): Buffer { 54 | return this._buffer 55 | } 56 | 57 | /** 58 | * Number of bytes read in last operation executed on the cursor. 59 | * Especially useful for operations that return variable length of 60 | * results such as readBytes or readVarUint. 61 | */ 62 | public get lastReadBytes(): number { 63 | return this._lastReadBytes 64 | } 65 | 66 | /** 67 | * Read a UInt8 number 68 | */ 69 | public readUInt8(): number { 70 | return this._readStandard(this.readUInt8.name, 1) 71 | } 72 | 73 | /** 74 | * Read a UInt16 number as little-endian 75 | */ 76 | public readUInt16LE(): number { 77 | return this._readStandard(this.readUInt16LE.name, 2) 78 | } 79 | 80 | /** 81 | * Read a UInt16 number as big-endian 82 | */ 83 | public readUInt16BE(): number { 84 | return this._readStandard(this.readUInt16BE.name, 2) 85 | } 86 | 87 | /** 88 | * Read a UInt32 number as little-endian 89 | */ 90 | public readUInt32LE(): number { 91 | return this._readStandard(this.readUInt32LE.name, 4) 92 | } 93 | 94 | /** 95 | * Read a UInt32 number as big-endian 96 | */ 97 | public readUInt32BE(): number { 98 | return this._readStandard(this.readUInt32BE.name, 4) 99 | } 100 | 101 | /** 102 | * Read a UInt64 number as big-endian 103 | */ 104 | public readUInt64BE(): bigint { 105 | return BigInt('0x' + this.readBytes(8).toString('hex')) 106 | } 107 | 108 | /** 109 | * Read a UInt64 number as little-endian 110 | */ 111 | public readUInt64LE(): bigint { 112 | return BigInt('0x' + this.readBytes(8).reverse().toString('hex')) 113 | } 114 | 115 | /** 116 | * Reads a variable length unsigned integer as specified in the protocol 117 | * documentation and aways returns a BN to maintain a consistant call 118 | * signature. 119 | * 120 | * @remarks 121 | * Specified in: 122 | * https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer 123 | * 124 | * Reads the first byte and determines the length of the remaining integer. 125 | * < 0xfd = 1 byte number 126 | * 0xfd = 2 byte number (3 bytes total) 127 | * 0xfe = 4 byte number (5 bytes total) 128 | * 0xff = 8 byte number (9 bytes total) 129 | */ 130 | public readVarUint(): bigint | void { 131 | const size = this.readUInt8() 132 | if (size < 0xfd) { 133 | this._lastReadBytes = 1 134 | return BigInt(size) 135 | } 136 | switch (size) { 137 | case 0xfd: 138 | this._lastReadBytes = 3 139 | return BigInt(this.readUInt16LE()) 140 | case 0xfe: 141 | this._lastReadBytes = 5 142 | return BigInt(this.readUInt32LE()) 143 | case 0xff: 144 | this._lastReadBytes = 9 145 | return this.readUInt64LE() 146 | } 147 | } 148 | 149 | /** 150 | * Reads a variable length unsigned integer as specified in the Lightning Network 151 | * protocol documentation and always returns a BigInt to maintain a consistent 152 | * call signature. 153 | * 154 | * @remarks 155 | * Specified in: 156 | * https://github.com/lightningnetwork/lightning-rfc/blob/master/01-messaging.md#appendix-a-bigsize-test-vectors 157 | * 158 | * < 0xfd = 1 byte number 159 | * 0xfd = 2 byte number (3 bytes total) 160 | * 0xfe = 4 byte number (5 bytes total) 161 | * 0xff = 8 byte number (9 bytes total) 162 | */ 163 | public readBigSize(): bigint { 164 | const size = this.readUInt8() 165 | 166 | if (size < 0xfd) { 167 | this._lastReadBytes = 1 168 | return BigInt(size) 169 | } 170 | switch (size) { 171 | case 0xfd: { 172 | this._lastReadBytes = 3 173 | const val = this.readUInt16BE() 174 | if (val < 0xfd) throw new Error('decoded varint is not canonical') 175 | return BigInt(val) 176 | } 177 | case 0xfe: { 178 | this._lastReadBytes = 5 179 | const val = this.readUInt32BE() 180 | if (val < 0x10000) throw new Error('decoded varint is not canonical') 181 | return BigInt(val) 182 | } 183 | case 0xff: { 184 | this._lastReadBytes = 9 185 | const val = this.readUInt64BE() 186 | if (val < BigInt(0x100000000)) throw new Error('decoded varint is not canonical') 187 | return val 188 | } 189 | default: 190 | throw new Error(`Unrecognised size: ${size} when trying to read BigSize`) 191 | } 192 | } 193 | 194 | /** 195 | * Read bytes from the buffer into a new Buffer. Unlike the default 196 | * slice method, the values do not point to the same memory location 197 | * as the source buffer. The values are copied to a new buffer. 198 | * 199 | * @param len optional number of bytes to read, returns 200 | * all remaining bytes when omitted 201 | */ 202 | public readBytes(len?: number): Buffer { 203 | if (len === 0) { 204 | this._lastReadBytes = 0 205 | return Buffer.alloc(0) 206 | } else if (typeof len === 'number' && len > 0) { 207 | if (this._position + len > this._buffer.length) { 208 | throw new RangeError('Index out of range') 209 | } 210 | const slice = this._buffer.subarray(this._position, this._position + len) 211 | const result = Buffer.alloc(slice.length, slice) 212 | this._position += len 213 | this._lastReadBytes = len 214 | return result 215 | } else { 216 | if (this._position === this._buffer.length) { 217 | this._lastReadBytes = 0 218 | return Buffer.alloc(0) 219 | } 220 | const slice = this._buffer.subarray(this._position) 221 | const result = Buffer.alloc(slice.length, slice) 222 | this._position = this._buffer.length 223 | this._lastReadBytes = result.length 224 | return result 225 | } 226 | } 227 | 228 | /** 229 | * Reads bytes from the buffer at the current position without 230 | * moving the cursor. 231 | * @param len optional number of bytes to read 232 | */ 233 | public peakBytes(len?: number): Buffer { 234 | if (len === 0) { 235 | return Buffer.alloc(0) 236 | } else if (typeof len === 'number' && len > 0) { 237 | if (this._position + len > this._buffer.length) { 238 | throw new RangeError('Index out of range') 239 | } 240 | const slice = this._buffer.subarray(this._position, this._position + len) 241 | const result = Buffer.alloc(slice.length, slice) 242 | return result 243 | } else { 244 | if (this._position === this._buffer.length) throw new RangeError('Index out of range') 245 | const slice = this._buffer.subarray(this._position) 246 | const result = Buffer.alloc(slice.length, slice) 247 | return result 248 | } 249 | } 250 | 251 | /** 252 | * TLV 0 to 2 byte unsigned integer encoded in big-endian. 253 | */ 254 | public readTUInt16(): number { 255 | const size = Math.min(2, this._buffer.length - this._position) 256 | if (size === 0) return 0 257 | const val = this._buffer.readUIntBE(this._position, size) 258 | this._assertMinimalTUInt(BigInt(val), size) 259 | this._position += size 260 | return val 261 | } 262 | 263 | /** 264 | * TLV 0 to 4 byte unsigned integer encoded in big-endian. 265 | */ 266 | public readTUInt32(): number { 267 | const size = Math.min(4, this._buffer.length - this._position) 268 | if (size === 0) return 0 269 | const val = this._buffer.readUIntBE(this._position, size) 270 | this._assertMinimalTUInt(BigInt(val), size) 271 | this._position += size 272 | return val 273 | } 274 | 275 | /** 276 | * TLV 0 to 8 byte unsigned integer encoded in big-endian. 277 | */ 278 | public readTUInt64(): bigint { 279 | const size = Math.min(8, this._buffer.length - this._position) 280 | if (size === 0) return BigInt(0) 281 | const hex = this._buffer.subarray(this._position, this._position + size).toString('hex') || '0' 282 | const val = BigInt('0x' + hex) 283 | this._assertMinimalTUInt(val, size) 284 | this._position += size 285 | return val 286 | } 287 | 288 | /** 289 | * Helper for reading off buffer using built-in read functions 290 | * @param fn name of function 291 | * @param len length to read 292 | */ 293 | private _readStandard(fn: string, len: number): number { 294 | if (this._position + len > this._buffer.length) { 295 | throw new RangeError('Index out of range') 296 | } 297 | 298 | // @ts-ignore 299 | const result: number = this._buffer[fn](this._position) 300 | this._position += len 301 | this._lastReadBytes = len 302 | return result 303 | } 304 | 305 | /** 306 | * Ensures the TUInt value is minimally encoded 307 | * @param num 308 | * @param bytes 309 | */ 310 | private _assertMinimalTUInt(num: bigint, bytes: number) { 311 | const msg = 'TUInt not minimal' 312 | for (let i = 0; i < 9; i++) { 313 | if (num < BigInt('0x1' + ''.padStart(i * 2, '0'))) { 314 | if (bytes !== i) { 315 | throw new Error(msg) 316 | } 317 | } 318 | } 319 | } 320 | } 321 | 322 | /** 323 | * Utility class for writing arbitrary data into a Buffer. This class will 324 | * automatically expand the underlying Buffer and return a trimmed view 325 | * when complete. 326 | */ 327 | export class BufferWriter { 328 | private _position: number 329 | private _fixed: boolean 330 | private _buffer: Buffer 331 | 332 | /** 333 | * Constructs a BufferWriter that can optionally wrap an existing Buffer. 334 | * If no buffer is provided, the BufferWriter will internally manage an 335 | * exponentially growing Buffer to allow writing of data of an unknown size. 336 | * 337 | * If a Buffer is provided, writing that would overflow will throw an 338 | * exception. 339 | * @param buffer 340 | */ 341 | constructor(buffer?: Buffer) { 342 | this._position = 0 343 | this._fixed = !!buffer 344 | this._buffer = buffer || Buffer.alloc(0) 345 | } 346 | 347 | /** 348 | * Gets the current size of the output Buffer 349 | */ 350 | public get size(): number { 351 | return this._position 352 | } 353 | 354 | /** 355 | * Returns the Buffer which will be either the full Buffer if this was a 356 | * fixed Buffer or will be the expandable Buffer sliced to the current 357 | * position 358 | */ 359 | public toBuffer(): Buffer { 360 | if (this._fixed) return this._buffer 361 | else return this._buffer.subarray(0, this._position) 362 | } 363 | 364 | /** 365 | * Write at the current positiion 366 | * @param val 367 | */ 368 | public writeUInt8(val: number) { 369 | this._writeStandard(this.writeUInt8.name, val, 1) 370 | } 371 | 372 | /** 373 | * Write at the current positiion 374 | * @param val 375 | */ 376 | public writeUInt16LE(val: number) { 377 | this._writeStandard(this.writeUInt16LE.name, val, 2) 378 | } 379 | 380 | /** 381 | * Write at the current positiion 382 | * @param val 383 | */ 384 | public writeUInt16BE(val: number) { 385 | this._writeStandard(this.writeUInt16BE.name, val, 2) 386 | } 387 | 388 | /** 389 | * Write at the current positiion 390 | * @param val 391 | */ 392 | public writeUInt32LE(val: number) { 393 | this._writeStandard(this.writeUInt32LE.name, val, 4) 394 | } 395 | 396 | /** 397 | * Write at the current positiion 398 | * @param val 399 | */ 400 | public writeUInt32BE(val: number) { 401 | this._writeStandard(this.writeUInt32BE.name, val, 4) 402 | } 403 | 404 | /** 405 | * Write at the current positiion 406 | * @param value 407 | */ 408 | public writeUInt64LE(value: number | bigint) { 409 | const val = BigInt(value) 410 | if (val < 0 || val >= BigInt(2) ** BigInt(64)) { 411 | throw new RangeError( 412 | `The value of "value" is out of range. It must be >= 0 and <= 18446744073709551615. Received ${value.toString()}` 413 | ) 414 | } 415 | const buf = Buffer.from(val.toString(16).padStart(16, '0'), 'hex') 416 | this.writeBytes(buf.reverse()) 417 | } 418 | 419 | /** 420 | * Write at the current positiion 421 | * @param value 422 | */ 423 | public writeUInt64BE(value: number | bigint) { 424 | const val = BigInt(value) 425 | if (val < 0 || val >= BigInt(2) ** BigInt(64)) { 426 | throw new RangeError( 427 | `The value of "value" is out of range. It must be >= 0 and <= 18446744073709551615. Received ${value.toString()}` 428 | ) 429 | } 430 | const buf = Buffer.from(val.toString(16).padStart(16, '0'), 'hex') 431 | this.writeBytes(buf) 432 | } 433 | 434 | /** 435 | * Write bytes at the current positiion 436 | * @param buffer 437 | */ 438 | public writeBytes(buffer: Buffer) { 439 | if (!buffer || !buffer.length) return 440 | this._expand(buffer.length) 441 | buffer.copy(this._buffer, this._position) 442 | this._position += buffer.length 443 | } 444 | 445 | /** 446 | * Reads a variable length unsigned integer in little-endian as specified in 447 | * the Bitcoin protocol documentation. 448 | * 449 | * < 0xfd = 1 byte number 450 | * 0xfd = 2 byte number (3 bytes total) 451 | * 0xfe = 4 byte number (5 bytes total) 452 | * 0xff = 8 byte number (9 bytes total) 453 | */ 454 | public writeVarInt(val: bigint | number) { 455 | const num = BigInt(val) 456 | if (num < BigInt(0xfd)) { 457 | this.writeUInt8(Number(num)) 458 | } else if (num < BigInt(0x10000)) { 459 | this.writeUInt8(0xfd) 460 | this.writeUInt16LE(Number(num)) 461 | } else if (num < BigInt(0x100000000)) { 462 | this.writeUInt8(0xfe) 463 | this.writeUInt32LE(Number(num)) 464 | } else { 465 | this.writeUInt8(0xff) 466 | this.writeUInt64LE(num) 467 | } 468 | } 469 | 470 | /** 471 | * Reads a variable length unsigned integer as specified in the Lightning Network 472 | * protocol documentation and always returns a BigInt to maintain a consistent 473 | * call signature. 474 | * 475 | * @remarks 476 | * Specified in: 477 | * https://github.com/lightningnetwork/lightning-rfc/blob/master/01-messaging.md#appendix-a-bigsize-test-vectors 478 | * 479 | * < 0xfd = 1 byte number 480 | * 0xfd = 2 byte number (3 bytes total) 481 | * 0xfe = 4 byte number (5 bytes total) 482 | * 0xff = 8 byte number (9 bytes total) 483 | */ 484 | public writeBigSize(val: bigint | number) { 485 | const num = BigInt(val) 486 | if (num < BigInt(0xfd)) { 487 | this.writeUInt8(Number(num)) 488 | } else if (num < BigInt(0x10000)) { 489 | this.writeUInt8(0xfd) 490 | this.writeUInt16BE(Number(num)) 491 | } else if (num < BigInt(0x100000000)) { 492 | this.writeUInt8(0xfe) 493 | this.writeUInt32BE(Number(num)) 494 | } else { 495 | this.writeUInt8(0xff) 496 | this.writeUInt64BE(num) 497 | } 498 | } 499 | 500 | /** 501 | * TLV 0 to 2 byte unsigned integer encoded in big-endian. 502 | * @param val 503 | */ 504 | public writeTUInt16(val: number) { 505 | if (val === 0) return 506 | const size = val > 0xff ? 2 : 1 507 | this._expand(size) 508 | this._buffer.writeUIntBE(val, this._position, size) 509 | this._position += size 510 | } 511 | 512 | /** 513 | * TLV 0 to 4 byte unsigned integer encoded in big-endian. 514 | */ 515 | public writeTUInt32(val: number) { 516 | if (val === 0) return 517 | const size = val > 0xffffff ? 4 : val > 0xffff ? 3 : val > 0xff ? 2 : 1 518 | this._expand(size) 519 | this._buffer.writeUIntBE(val, this._position, size) 520 | this._position += size 521 | } 522 | 523 | /** 524 | * TLV 0 to 8 byte unsigned integer encoded in big-endian. 525 | */ 526 | public writeTUInt64(val: bigint) { 527 | if (val === BigInt(0)) return 528 | let valString = val.toString(16) 529 | if (valString.length % 2 === 1) valString = '0' + valString 530 | const buf = Buffer.from(valString, 'hex') 531 | this.writeBytes(buf) 532 | } 533 | 534 | /** 535 | * Expands the underlying buffer as needed by doubling the size of the 536 | * Buffer when it needs to grow. 537 | * @param needed 538 | */ 539 | private _expand(needed: number) { 540 | const required = this._position + needed 541 | 542 | // Ensure that a fixed Buffer length is not violated 543 | if (this._fixed && required > this._buffer.length) { 544 | throw new RangeError('Out of range') 545 | } 546 | 547 | // expand the buffer if the current buffer is insufficiently lengthed 548 | if (this._buffer.length < required) { 549 | // calculate the new length based on the required length and some 550 | // maths where we determine the number of bytes required and at the 551 | // next power of 2. 552 | const newLen = 1 << Math.ceil(Math.log2(required)) 553 | const newBuf = Buffer.alloc(newLen) 554 | 555 | // copy the old data to the new buffer and then dispose of the old 556 | // buffer 557 | this._buffer.copy(newBuf) 558 | this._buffer = newBuf 559 | } 560 | } 561 | 562 | /** 563 | * Helper for writing to the buffer using built-in write 564 | * functions 565 | * @param fn name of function 566 | * @param val number to write 567 | * @param len length of number in bytes 568 | */ 569 | private _writeStandard(fn: string, val: number, len: number) { 570 | this._expand(len) 571 | // @ts-ignore 572 | this._buffer[fn](val, this._position) 573 | this._position += len 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, firstValueFrom, Observable, Subject } from 'rxjs' 2 | import { filter, map } from 'rxjs/operators' 3 | import { Buffer } from 'buffer' 4 | import { bytesToHex } from '@noble/hashes/utils' 5 | import { createRandomBytes, createRandomPrivateKey } from './crypto.js' 6 | import { NoiseState } from './noise-state.js' 7 | import { validateInit } from './validation.js' 8 | import { deserialize } from './messages/MessageFactory.js' 9 | import { IWireMessage } from './messages/IWireMessage.js' 10 | import { BufferReader, BufferWriter } from './messages/buf.js' 11 | import { CommandoMessage } from './messages/CommandoMessage.js' 12 | import { PongMessage } from './messages/PongMessage.js' 13 | import { PingMessage } from './messages/PingMessage.js' 14 | import type { WebSocket as NodeWebSocket } from 'ws' 15 | import type { Socket as TCPSocket } from 'net' 16 | import type SocketWrapper from './socket-wrapper.js' 17 | 18 | import { 19 | LnWebSocketOptions, 20 | HANDSHAKE_STATE, 21 | READ_STATE, 22 | MessageType, 23 | JsonRpcSuccessResponse, 24 | JsonRpcErrorResponse, 25 | Logger, 26 | CommandoRequest, 27 | ConnectionStatus 28 | } from './types.js' 29 | 30 | const DEFAULT_RECONNECT_ATTEMPTS = 5 31 | 32 | class LnMessage { 33 | /**The underlying Noise protocol. Can be used if you want to play around with the low level Lightning transport protocol*/ 34 | public noise: NoiseState 35 | /**The public key of the node that Lnmessage is connected to*/ 36 | public remoteNodePublicKey: string 37 | /**The public key Lnmessage uses when connecting to a remote node 38 | * If you passed in a private key when initialising, 39 | * this public key will be derived from it and can be used for persistent identity 40 | * across session connections 41 | */ 42 | public publicKey: string 43 | /**The private key that was either passed in on init or generated automatically 44 | * Reuse this when reconnecting for persistent id 45 | */ 46 | public privateKey: string 47 | /**The url that the WebSocket will connect to. It uses the wsProxy option if provided 48 | * or otherwise will initiate a WebSocket connection directly to the node 49 | */ 50 | public wsUrl: string 51 | /**The WebSocket instance*/ 52 | public socket: WebSocket | NodeWebSocket | null | SocketWrapper 53 | /**TCP socket instance*/ 54 | public tcpSocket?: TCPSocket 55 | /** 56 | * @deprecated Use connectionStatus$ instead 57 | */ 58 | public connected$: BehaviorSubject 59 | /** 60 | * Observable that indicates the current socket connection status 61 | * Can be either 'connected', 'connecting', 'waiting_reconnect' or 'disconnected'. 62 | */ 63 | public connectionStatus$: BehaviorSubject 64 | /** 65 | * @deprecated Use connectionStatus$ instead 66 | */ 67 | public connecting: boolean 68 | /**Observable stream of decypted messages. This can be used to extend Lnmessage 69 | * functionality so that it can handle other Lightning message types 70 | */ 71 | public decryptedMsgs$: Observable 72 | /**Obserable stream of all commando response messages*/ 73 | public commandoMsgs$: Observable< 74 | (JsonRpcSuccessResponse | JsonRpcErrorResponse) & { reqId: string } 75 | > 76 | /**Node JS Buffer instance, useful if handling decrypted messages manually*/ 77 | public Buffer: Buffer['constructor'] 78 | 79 | private _ls: Buffer 80 | private _es: Buffer 81 | private _handshakeState: HANDSHAKE_STATE 82 | private _readState: READ_STATE 83 | private _decryptedMsgs$: Subject 84 | private _commandoMsgs$: Subject 85 | private _partialCommandoMsgs: Record 86 | private _attemptedReconnects: number 87 | private _logger: Logger | void 88 | private _attemptReconnect: boolean 89 | private _messageBuffer: BufferReader 90 | private _processingBuffer: boolean 91 | private _l: number | null 92 | private _pingTimeout: NodeJS.Timeout | null 93 | private _pongTimeout: NodeJS.Timeout | null 94 | 95 | constructor(options: LnWebSocketOptions) { 96 | validateInit(options) 97 | 98 | const { 99 | remoteNodePublicKey, 100 | wsProxy, 101 | wsProtocol = 'wss:', 102 | privateKey, 103 | ip, 104 | port = 9735, 105 | logger, 106 | tcpSocket 107 | } = options 108 | 109 | this._ls = Buffer.from(privateKey || createRandomPrivateKey(), 'hex') 110 | this._es = Buffer.from(createRandomPrivateKey(), 'hex') 111 | 112 | this.noise = new NoiseState({ 113 | ls: this._ls, 114 | es: this._es 115 | }) 116 | 117 | this.remoteNodePublicKey = remoteNodePublicKey 118 | this.publicKey = this.noise.lpk.toString('hex') 119 | this.privateKey = this._ls.toString('hex') 120 | this.wsUrl = wsProxy ? `${wsProxy}/${ip}:${port}` : `${wsProtocol}//${ip}:${port}` 121 | this.connectionStatus$ = new BehaviorSubject('disconnected') 122 | this.connected$ = new BehaviorSubject(false) 123 | this.connecting = false 124 | this.Buffer = Buffer 125 | this.tcpSocket = tcpSocket 126 | 127 | this._handshakeState = HANDSHAKE_STATE.INITIATOR_INITIATING 128 | this._decryptedMsgs$ = new Subject() 129 | this.decryptedMsgs$ = this._decryptedMsgs$.asObservable() 130 | this._commandoMsgs$ = new Subject() 131 | this.commandoMsgs$ = this._commandoMsgs$ 132 | .asObservable() 133 | .pipe(map(({ response, id }) => ({ ...response, reqId: id }))) 134 | 135 | this._partialCommandoMsgs = {} 136 | this._attemptedReconnects = 0 137 | this._logger = logger 138 | this._readState = READ_STATE.READY_FOR_LEN 139 | this._processingBuffer = false 140 | this._l = null 141 | this._pingTimeout = null 142 | this._pongTimeout = null 143 | 144 | this.decryptedMsgs$.subscribe((msg) => { 145 | this.handleDecryptedMessage(msg) 146 | }) 147 | } 148 | 149 | async connect(attemptReconnect = true): Promise { 150 | const currentStatus = this.connectionStatus$.value 151 | 152 | // already connected 153 | if (currentStatus === 'connected') { 154 | return true 155 | } 156 | 157 | // connecting so return connected status 158 | if (currentStatus === 'connecting') { 159 | return firstValueFrom( 160 | this.connectionStatus$.pipe( 161 | filter((status) => status === 'connected' || status === 'disconnected'), 162 | map((status) => status === 'connected') 163 | ) 164 | ) 165 | } 166 | 167 | this._log('info', `Initiating connection to node ${this.remoteNodePublicKey}`) 168 | this._messageBuffer = new BufferReader(Buffer.from('')) 169 | this.connecting = true 170 | this.connectionStatus$.next('connecting') 171 | this._attemptReconnect = attemptReconnect 172 | 173 | this.socket = this.tcpSocket 174 | ? new (await import('./socket-wrapper.js')).default(this.wsUrl, this.tcpSocket) 175 | : typeof globalThis.WebSocket === 'undefined' 176 | ? new (await import('ws')).default(this.wsUrl) 177 | : new globalThis.WebSocket(this.wsUrl) 178 | 179 | if ((this.socket as WebSocket | NodeWebSocket).binaryType) { 180 | ;(this.socket as WebSocket | NodeWebSocket).binaryType = 'arraybuffer' 181 | } 182 | 183 | this.socket.onopen = async () => { 184 | this._log('info', 'WebSocket is connected at ' + new Date().toISOString()) 185 | this._log('info', 'Creating Act1 message') 186 | 187 | const msg = this.noise.initiatorAct1(Buffer.from(this.remoteNodePublicKey, 'hex')) 188 | 189 | if (this.socket) { 190 | this._log('info', 'Sending Act1 message') 191 | this.socket.send(msg) 192 | this._handshakeState = HANDSHAKE_STATE.AWAITING_RESPONDER_REPLY 193 | } 194 | 195 | this._pingTimeout = setTimeout(this._sendPingMessage.bind(this), 45 * 1000) 196 | } 197 | 198 | this.socket.onclose = async () => { 199 | this._log('error', 'WebSocket is closed at ' + new Date().toISOString()) 200 | this._pingTimeout && clearTimeout(this._pingTimeout) 201 | this._pongTimeout && clearTimeout(this._pongTimeout) 202 | 203 | this.connectionStatus$.next('disconnected') 204 | this.connected$.next(false) 205 | 206 | if (this._attemptReconnect && this._attemptedReconnects < DEFAULT_RECONNECT_ATTEMPTS) { 207 | this._log('info', 'Waiting to reconnect') 208 | this._log('info', `Attempted reconnects: ${this._attemptedReconnects}`) 209 | 210 | this.connectionStatus$.next('waiting_reconnect') 211 | this.connecting = true 212 | 213 | await new Promise((resolve) => setTimeout(resolve, (this._attemptedReconnects || 1) * 1000)) 214 | 215 | this.connect() 216 | this._attemptedReconnects += 1 217 | } 218 | } 219 | 220 | this.socket.onerror = (err: { message: string }) => { 221 | this._log('error', `WebSocket error: ${JSON.stringify(err)}`) 222 | } 223 | 224 | this.socket.onmessage = this.queueMessage.bind(this) 225 | 226 | return firstValueFrom( 227 | this.connectionStatus$.pipe( 228 | filter((status) => status === 'connected' || status === 'disconnected'), 229 | map((status) => status === 'connected') 230 | ) 231 | ) 232 | } 233 | 234 | private _sendPingMessage() { 235 | const pongMessage = new PingMessage().serialize() 236 | const ping = this.noise.encryptMessage(pongMessage) 237 | 238 | if (this.socket) { 239 | this._log('info', 'Sending a Ping message') 240 | this.socket.send(ping) 241 | 242 | // wait 5 seconds for a reply and close if no reply 243 | this._pongTimeout = setTimeout(() => this._close(), 5 * 1000) 244 | } 245 | } 246 | 247 | private _close() { 248 | this._log('error', 'Closing connection') 249 | 250 | this.socket?.close() 251 | } 252 | 253 | private queueMessage(event: { data: ArrayBuffer }) { 254 | const { data } = event 255 | const message = Buffer.from(data) 256 | 257 | const currentData = 258 | this._messageBuffer && !this._messageBuffer.eof && this._messageBuffer.readBytes() 259 | 260 | this._messageBuffer = new BufferReader( 261 | currentData ? Buffer.concat([currentData, message]) : message 262 | ) 263 | 264 | if (!this._processingBuffer) { 265 | this._processingBuffer = true 266 | this._processBuffer() 267 | } 268 | } 269 | 270 | disconnect() { 271 | this._log('info', 'Manually disconnecting from WebSocket') 272 | 273 | // reset noise state 274 | this.noise = new NoiseState({ 275 | ls: this._ls, 276 | es: this._es 277 | }) 278 | 279 | this._attemptReconnect = false 280 | this._pingTimeout && clearTimeout(this._pingTimeout) 281 | this._pongTimeout && clearTimeout(this._pongTimeout) 282 | this._close() 283 | } 284 | 285 | private async _processBuffer() { 286 | try { 287 | // Loop while there was still data to process on the process 288 | // buffer. 289 | let readMore = true 290 | do { 291 | if (this._handshakeState !== HANDSHAKE_STATE.READY) { 292 | switch (this._handshakeState) { 293 | // Initiator received data before initialized 294 | case HANDSHAKE_STATE.INITIATOR_INITIATING: 295 | throw new Error('Received data before intialised') 296 | 297 | // Initiator Act2 298 | case HANDSHAKE_STATE.AWAITING_RESPONDER_REPLY: 299 | readMore = this._processResponderReply() 300 | break 301 | } 302 | } else { 303 | switch (this._readState) { 304 | case READ_STATE.READY_FOR_LEN: 305 | readMore = this._processPacketLength() 306 | break 307 | case READ_STATE.READY_FOR_BODY: 308 | readMore = this._processPacketBody() 309 | break 310 | case READ_STATE.BLOCKED: 311 | readMore = false 312 | break 313 | default: 314 | throw new Error('Unknown read state') 315 | } 316 | } 317 | } while (readMore) 318 | } catch (err) { 319 | // Terminate on failures as we won't be able to recover 320 | // since the noise state has rotated nonce and we won't 321 | // be able to any more data without additional errors. 322 | this._log('error', `Noise state has rotated nonce: ${err}`) 323 | this.disconnect() 324 | } 325 | 326 | this._processingBuffer = false 327 | } 328 | 329 | private _processResponderReply() { 330 | // read 50 bytes 331 | const data = this._messageBuffer.readBytes(50) 332 | 333 | if (data.byteLength !== 50) { 334 | throw new Error('Invalid message received from remote node') 335 | } 336 | 337 | // process reply 338 | this._log('info', 'Validating message as part of Act2') 339 | this.noise.initiatorAct2(data) 340 | 341 | // create final act of the handshake 342 | this._log('info', 'Creating reply for Act3') 343 | const reply = this.noise.initiatorAct3() 344 | 345 | if (this.socket) { 346 | this._log('info', 'Sending reply for act3') 347 | // send final handshake 348 | this.socket.send(reply) 349 | 350 | // transition 351 | this._handshakeState = HANDSHAKE_STATE.READY 352 | } 353 | 354 | // return true to continue processing 355 | return true 356 | } 357 | 358 | private _processPacketLength() { 359 | const LEN_CIPHER_BYTES = 2 360 | const LEN_MAC_BYTES = 16 361 | 362 | try { 363 | // Try to read the length cipher bytes and the length MAC bytes 364 | // If we cannot read the 18 bytes, the attempt to process the 365 | // message will abort. 366 | const lc = this._messageBuffer.readBytes(LEN_CIPHER_BYTES + LEN_MAC_BYTES) 367 | if (!lc) return false 368 | 369 | // Decrypt the length including the MAC 370 | const l = this.noise.decryptLength(lc) 371 | 372 | // We need to store the value in a local variable in case 373 | // we are unable to read the message body in its entirety. 374 | // This allows us to skip the length read and prevents 375 | // nonce issues since we've already decrypted the length. 376 | this._l = l 377 | 378 | // Transition state 379 | this._readState = READ_STATE.READY_FOR_BODY 380 | 381 | // return true to continue reading 382 | return true 383 | } catch (err) { 384 | return false 385 | } 386 | } 387 | 388 | private _processPacketBody() { 389 | const MESSAGE_MAC_BYTES = 16 390 | 391 | if (!this._l) return false 392 | 393 | try { 394 | // With the length, we can attempt to read the message plus 395 | // the MAC for the message. If we are unable to read because 396 | // there is not enough data in the read buffer, we need to 397 | // store l. We are not able to simply unshift it becuase we 398 | // have already rotated the keys. 399 | const c = this._messageBuffer.readBytes(this._l + MESSAGE_MAC_BYTES) 400 | if (!c) return false 401 | 402 | // Decrypt the full message cipher + MAC 403 | const m = this.noise.decryptMessage(c) 404 | 405 | // Now that we've read the message, we can remove the 406 | // cached length before we transition states 407 | this._l = null 408 | 409 | // Push the message onto the read buffer for the consumer to 410 | // read. We are mindful of slow reads by the consumer and 411 | // will respect backpressure signals. 412 | this._decryptedMsgs$.next(m) 413 | this._readState = READ_STATE.READY_FOR_LEN 414 | 415 | return true 416 | } catch (err) { 417 | return false 418 | } 419 | } 420 | 421 | async handleDecryptedMessage(decrypted: Buffer) { 422 | // reset ping and pong timeout 423 | this._pongTimeout && clearTimeout(this._pongTimeout) 424 | this._pingTimeout && clearTimeout(this._pingTimeout) 425 | this._pingTimeout = setTimeout(this._sendPingMessage.bind(this), 40 * 1000) 426 | 427 | try { 428 | const reader = new BufferReader(decrypted) 429 | const type = reader.readUInt16BE() 430 | const [typeName] = Object.entries(MessageType).find(([name, val]) => val === type) || [] 431 | const requestId = reader.readBytes(8).toString('hex') 432 | const message = reader.readBytes() 433 | 434 | this._log('info', `Received message type is: ${typeName || 'unknown'}`) 435 | 436 | if (type === MessageType.CommandoResponseContinues) { 437 | this._log( 438 | 'info', 439 | 'Received a partial commando message, caching it to join with other parts' 440 | ) 441 | 442 | this._partialCommandoMsgs[requestId] = this._partialCommandoMsgs[requestId] 443 | ? Buffer.concat([ 444 | this._partialCommandoMsgs[requestId], 445 | message.subarray(0, message.byteLength - 16) 446 | ]) 447 | : decrypted.subarray(0, decrypted.length - 16) 448 | 449 | return 450 | } 451 | 452 | if (type === MessageType.CommandoResponse && this._partialCommandoMsgs[requestId]) { 453 | this._log( 454 | 'info', 455 | 'Received a final commando msg and we have a partial message to join it to. Joining now' 456 | ) 457 | 458 | // join commando msg chunks 459 | decrypted = Buffer.concat([this._partialCommandoMsgs[requestId], message]) 460 | delete this._partialCommandoMsgs[requestId] 461 | } 462 | 463 | // deserialise 464 | this._log('info', 'Deserialising payload') 465 | const payload = deserialize(decrypted) 466 | 467 | switch (payload.type) { 468 | case MessageType.Init: { 469 | this._log('info', 'Constructing Init message reply') 470 | const reply = this.noise.encryptMessage((payload as IWireMessage).serialize()) 471 | 472 | if (this.socket) { 473 | this._log('info', 'Sending Init message reply') 474 | this.socket.send(reply) 475 | 476 | this._log('info', 'Connected and ready to send messages!') 477 | 478 | this.connectionStatus$.next('connected') 479 | this.connected$.next(true) 480 | this.connecting = false 481 | this._attemptedReconnects = 0 482 | } 483 | 484 | break 485 | } 486 | 487 | case MessageType.Ping: { 488 | this._log('info', 'Received a Ping message') 489 | this._log('info', 'Creating a Pong message') 490 | 491 | const pongMessage = new PongMessage((payload as PingMessage).numPongBytes).serialize() 492 | const pong = this.noise.encryptMessage(pongMessage) 493 | 494 | if (this.socket) { 495 | this._log('info', 'Sending a Pong message') 496 | this.socket.send(pong) 497 | } 498 | 499 | break 500 | } 501 | 502 | case MessageType.CommandoResponse: { 503 | this._commandoMsgs$.next(payload as CommandoMessage) 504 | } 505 | 506 | // ignore all other messages 507 | } 508 | } catch (error) { 509 | this._log('error', `Error handling incoming message: ${(error as Error).message}`) 510 | } 511 | } 512 | 513 | async commando({ 514 | method, 515 | params = [], 516 | rune, 517 | reqId 518 | }: CommandoRequest): Promise { 519 | this._log('info', `Commando request method: ${method} params: ${JSON.stringify(params)}`) 520 | 521 | // not connected, so initiate a connection 522 | if (this.connectionStatus$.value === 'disconnected') { 523 | this._log('info', 'No socket connection, so creating one now') 524 | 525 | const connected = await this.connect() 526 | 527 | if (!connected) { 528 | throw { 529 | code: 2, 530 | message: 'Could not establish a connection to node' 531 | } 532 | } 533 | } else { 534 | this._log('info', 'Ensuring we have a connection before making request') 535 | 536 | // ensure that we are connected before making any requests 537 | await firstValueFrom(this.connectionStatus$.pipe(filter((status) => status === 'connected'))) 538 | } 539 | 540 | const writer = new BufferWriter() 541 | 542 | if (!reqId) { 543 | // create random id to match request with response 544 | const id = createRandomBytes(8) 545 | reqId = bytesToHex(id) 546 | } 547 | 548 | // write the type 549 | writer.writeUInt16BE(MessageType.CommandoRequest) 550 | 551 | // write the id 552 | writer.writeBytes(Buffer.from(reqId, 'hex')) 553 | 554 | // Unique request id with prefix, method and id 555 | const detailedReqId = `lnmessage:${method}#${reqId}` 556 | 557 | // write the request 558 | writer.writeBytes( 559 | Buffer.from( 560 | JSON.stringify({ 561 | id: detailedReqId, // Adding id for easier debugging with commando 562 | rune, 563 | method, 564 | params 565 | }) 566 | ) 567 | ) 568 | 569 | this._log('info', 'Creating message to send') 570 | const message = this.noise.encryptMessage(writer.toBuffer()) 571 | 572 | if (this.socket) { 573 | this._log('info', 'Sending commando message') 574 | this.socket.send(message) 575 | 576 | this._log('info', `Message sent with id ${detailedReqId} and awaiting response`) 577 | 578 | const { response } = await firstValueFrom( 579 | this._commandoMsgs$.pipe(filter((commandoMsg) => commandoMsg.id === reqId)) 580 | ) 581 | 582 | const { result } = response as JsonRpcSuccessResponse 583 | const { error } = response as JsonRpcErrorResponse 584 | 585 | this._log( 586 | 'info', 587 | result 588 | ? `Successful response received for ID: ${response.id}` 589 | : `Error response received: ${error.message}` 590 | ) 591 | 592 | if (error) throw error 593 | 594 | return result 595 | } else { 596 | throw new Error('No socket initialised and connected') 597 | } 598 | } 599 | 600 | _log(level: keyof Logger, msg: string) { 601 | if (this._logger && this._logger[level]) { 602 | this._logger[level](`[${level.toUpperCase()} - ${new Date().toISOString()}]: ${msg}`) 603 | } 604 | } 605 | } 606 | 607 | export default LnMessage 608 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@eslint/eslintrc@^1.3.2": 6 | version "1.3.2" 7 | resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356" 8 | integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ== 9 | dependencies: 10 | ajv "^6.12.4" 11 | debug "^4.3.2" 12 | espree "^9.4.0" 13 | globals "^13.15.0" 14 | ignore "^5.2.0" 15 | import-fresh "^3.2.1" 16 | js-yaml "^4.1.0" 17 | minimatch "^3.1.2" 18 | strip-json-comments "^3.1.1" 19 | 20 | "@humanwhocodes/config-array@^0.10.5": 21 | version "0.10.5" 22 | resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.5.tgz#bb679745224745fff1e9a41961c1d45a49f81c04" 23 | integrity sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug== 24 | dependencies: 25 | "@humanwhocodes/object-schema" "^1.2.1" 26 | debug "^4.1.1" 27 | minimatch "^3.0.4" 28 | 29 | "@humanwhocodes/gitignore-to-minimatch@^1.0.2": 30 | version "1.0.2" 31 | resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d" 32 | integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== 33 | 34 | "@humanwhocodes/module-importer@^1.0.1": 35 | version "1.0.1" 36 | resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" 37 | integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== 38 | 39 | "@humanwhocodes/object-schema@^1.2.1": 40 | version "1.2.1" 41 | resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" 42 | integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== 43 | 44 | "@noble/hashes@^1.3.1": 45 | version "1.3.1" 46 | resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" 47 | integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== 48 | 49 | "@noble/secp256k1@^2.0.0": 50 | version "2.0.0" 51 | resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-2.0.0.tgz#c214269d45e0233ad6a8ae5104655453636e253d" 52 | integrity sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw== 53 | 54 | "@nodelib/fs.scandir@2.1.5": 55 | version "2.1.5" 56 | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 57 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 58 | dependencies: 59 | "@nodelib/fs.stat" "2.0.5" 60 | run-parallel "^1.1.9" 61 | 62 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 63 | version "2.0.5" 64 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 65 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 66 | 67 | "@nodelib/fs.walk@^1.2.3": 68 | version "1.2.8" 69 | resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 70 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 71 | dependencies: 72 | "@nodelib/fs.scandir" "2.1.5" 73 | fastq "^1.6.0" 74 | 75 | "@types/json-schema@^7.0.9": 76 | version "7.0.11" 77 | resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" 78 | integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== 79 | 80 | "@types/node@*", "@types/node@^18.14.0": 81 | version "18.14.0" 82 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0" 83 | integrity sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A== 84 | 85 | "@types/ws@^8.5.4": 86 | version "8.5.4" 87 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" 88 | integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== 89 | dependencies: 90 | "@types/node" "*" 91 | 92 | "@typescript-eslint/eslint-plugin@^5.27.0": 93 | version "5.38.0" 94 | resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz#ac919a199548861012e8c1fb2ec4899ac2bc22ae" 95 | integrity sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ== 96 | dependencies: 97 | "@typescript-eslint/scope-manager" "5.38.0" 98 | "@typescript-eslint/type-utils" "5.38.0" 99 | "@typescript-eslint/utils" "5.38.0" 100 | debug "^4.3.4" 101 | ignore "^5.2.0" 102 | regexpp "^3.2.0" 103 | semver "^7.3.7" 104 | tsutils "^3.21.0" 105 | 106 | "@typescript-eslint/parser@^5.27.0": 107 | version "5.38.0" 108 | resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.0.tgz#5a59a1ff41a7b43aacd1bb2db54f6bf1c02b2ff8" 109 | integrity sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA== 110 | dependencies: 111 | "@typescript-eslint/scope-manager" "5.38.0" 112 | "@typescript-eslint/types" "5.38.0" 113 | "@typescript-eslint/typescript-estree" "5.38.0" 114 | debug "^4.3.4" 115 | 116 | "@typescript-eslint/scope-manager@5.38.0": 117 | version "5.38.0" 118 | resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz#8f0927024b6b24e28671352c93b393a810ab4553" 119 | integrity sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA== 120 | dependencies: 121 | "@typescript-eslint/types" "5.38.0" 122 | "@typescript-eslint/visitor-keys" "5.38.0" 123 | 124 | "@typescript-eslint/type-utils@5.38.0": 125 | version "5.38.0" 126 | resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz#c8b7f681da825fcfc66ff2b63d70693880496876" 127 | integrity sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA== 128 | dependencies: 129 | "@typescript-eslint/typescript-estree" "5.38.0" 130 | "@typescript-eslint/utils" "5.38.0" 131 | debug "^4.3.4" 132 | tsutils "^3.21.0" 133 | 134 | "@typescript-eslint/types@5.38.0": 135 | version "5.38.0" 136 | resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.38.0.tgz#8cd15825e4874354e31800dcac321d07548b8a5f" 137 | integrity sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA== 138 | 139 | "@typescript-eslint/typescript-estree@5.38.0": 140 | version "5.38.0" 141 | resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz#89f86b2279815c6fb7f57d68cf9b813f0dc25d98" 142 | integrity sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg== 143 | dependencies: 144 | "@typescript-eslint/types" "5.38.0" 145 | "@typescript-eslint/visitor-keys" "5.38.0" 146 | debug "^4.3.4" 147 | globby "^11.1.0" 148 | is-glob "^4.0.3" 149 | semver "^7.3.7" 150 | tsutils "^3.21.0" 151 | 152 | "@typescript-eslint/utils@5.38.0": 153 | version "5.38.0" 154 | resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.38.0.tgz#5b31f4896471818153790700eb02ac869a1543f4" 155 | integrity sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA== 156 | dependencies: 157 | "@types/json-schema" "^7.0.9" 158 | "@typescript-eslint/scope-manager" "5.38.0" 159 | "@typescript-eslint/types" "5.38.0" 160 | "@typescript-eslint/typescript-estree" "5.38.0" 161 | eslint-scope "^5.1.1" 162 | eslint-utils "^3.0.0" 163 | 164 | "@typescript-eslint/visitor-keys@5.38.0": 165 | version "5.38.0" 166 | resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz#60591ca3bf78aa12b25002c0993d067c00887e34" 167 | integrity sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w== 168 | dependencies: 169 | "@typescript-eslint/types" "5.38.0" 170 | eslint-visitor-keys "^3.3.0" 171 | 172 | acorn-jsx@^5.3.2: 173 | version "5.3.2" 174 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" 175 | integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== 176 | 177 | acorn@^8.8.0: 178 | version "8.8.0" 179 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" 180 | integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== 181 | 182 | ajv@^6.10.0, ajv@^6.12.4: 183 | version "6.12.6" 184 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 185 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 186 | dependencies: 187 | fast-deep-equal "^3.1.1" 188 | fast-json-stable-stringify "^2.0.0" 189 | json-schema-traverse "^0.4.1" 190 | uri-js "^4.2.2" 191 | 192 | ansi-regex@^5.0.1: 193 | version "5.0.1" 194 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 195 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 196 | 197 | ansi-styles@^4.1.0: 198 | version "4.3.0" 199 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 200 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 201 | dependencies: 202 | color-convert "^2.0.1" 203 | 204 | argparse@^2.0.1: 205 | version "2.0.1" 206 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 207 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 208 | 209 | array-union@^2.1.0: 210 | version "2.1.0" 211 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" 212 | integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== 213 | 214 | balanced-match@^1.0.0: 215 | version "1.0.2" 216 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 217 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 218 | 219 | base64-js@^1.3.1: 220 | version "1.5.1" 221 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 222 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 223 | 224 | brace-expansion@^1.1.7: 225 | version "1.1.11" 226 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 227 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 228 | dependencies: 229 | balanced-match "^1.0.0" 230 | concat-map "0.0.1" 231 | 232 | braces@^3.0.2: 233 | version "3.0.2" 234 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 235 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 236 | dependencies: 237 | fill-range "^7.0.1" 238 | 239 | buffer@^6.0.3: 240 | version "6.0.3" 241 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" 242 | integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== 243 | dependencies: 244 | base64-js "^1.3.1" 245 | ieee754 "^1.2.1" 246 | 247 | callsites@^3.0.0: 248 | version "3.1.0" 249 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 250 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 251 | 252 | chalk@^4.0.0: 253 | version "4.1.2" 254 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 255 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 256 | dependencies: 257 | ansi-styles "^4.1.0" 258 | supports-color "^7.1.0" 259 | 260 | color-convert@^2.0.1: 261 | version "2.0.1" 262 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 263 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 264 | dependencies: 265 | color-name "~1.1.4" 266 | 267 | color-name@~1.1.4: 268 | version "1.1.4" 269 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 270 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 271 | 272 | concat-map@0.0.1: 273 | version "0.0.1" 274 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 275 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 276 | 277 | cross-spawn@^7.0.2: 278 | version "7.0.3" 279 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" 280 | integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 281 | dependencies: 282 | path-key "^3.1.0" 283 | shebang-command "^2.0.0" 284 | which "^2.0.1" 285 | 286 | debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: 287 | version "4.3.4" 288 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 289 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 290 | dependencies: 291 | ms "2.1.2" 292 | 293 | deep-is@^0.1.3: 294 | version "0.1.4" 295 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" 296 | integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== 297 | 298 | dir-glob@^3.0.1: 299 | version "3.0.1" 300 | resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" 301 | integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== 302 | dependencies: 303 | path-type "^4.0.0" 304 | 305 | doctrine@^3.0.0: 306 | version "3.0.0" 307 | resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" 308 | integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== 309 | dependencies: 310 | esutils "^2.0.2" 311 | 312 | escape-string-regexp@^4.0.0: 313 | version "4.0.0" 314 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 315 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 316 | 317 | eslint-config-prettier@^8.3.0: 318 | version "8.5.0" 319 | resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" 320 | integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== 321 | 322 | eslint-scope@^5.1.1: 323 | version "5.1.1" 324 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" 325 | integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== 326 | dependencies: 327 | esrecurse "^4.3.0" 328 | estraverse "^4.1.1" 329 | 330 | eslint-scope@^7.1.1: 331 | version "7.1.1" 332 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" 333 | integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== 334 | dependencies: 335 | esrecurse "^4.3.0" 336 | estraverse "^5.2.0" 337 | 338 | eslint-utils@^3.0.0: 339 | version "3.0.0" 340 | resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" 341 | integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== 342 | dependencies: 343 | eslint-visitor-keys "^2.0.0" 344 | 345 | eslint-visitor-keys@^2.0.0: 346 | version "2.1.0" 347 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" 348 | integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== 349 | 350 | eslint-visitor-keys@^3.3.0: 351 | version "3.3.0" 352 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" 353 | integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== 354 | 355 | eslint@^8.16.0: 356 | version "8.24.0" 357 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8" 358 | integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ== 359 | dependencies: 360 | "@eslint/eslintrc" "^1.3.2" 361 | "@humanwhocodes/config-array" "^0.10.5" 362 | "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" 363 | "@humanwhocodes/module-importer" "^1.0.1" 364 | ajv "^6.10.0" 365 | chalk "^4.0.0" 366 | cross-spawn "^7.0.2" 367 | debug "^4.3.2" 368 | doctrine "^3.0.0" 369 | escape-string-regexp "^4.0.0" 370 | eslint-scope "^7.1.1" 371 | eslint-utils "^3.0.0" 372 | eslint-visitor-keys "^3.3.0" 373 | espree "^9.4.0" 374 | esquery "^1.4.0" 375 | esutils "^2.0.2" 376 | fast-deep-equal "^3.1.3" 377 | file-entry-cache "^6.0.1" 378 | find-up "^5.0.0" 379 | glob-parent "^6.0.1" 380 | globals "^13.15.0" 381 | globby "^11.1.0" 382 | grapheme-splitter "^1.0.4" 383 | ignore "^5.2.0" 384 | import-fresh "^3.0.0" 385 | imurmurhash "^0.1.4" 386 | is-glob "^4.0.0" 387 | js-sdsl "^4.1.4" 388 | js-yaml "^4.1.0" 389 | json-stable-stringify-without-jsonify "^1.0.1" 390 | levn "^0.4.1" 391 | lodash.merge "^4.6.2" 392 | minimatch "^3.1.2" 393 | natural-compare "^1.4.0" 394 | optionator "^0.9.1" 395 | regexpp "^3.2.0" 396 | strip-ansi "^6.0.1" 397 | strip-json-comments "^3.1.0" 398 | text-table "^0.2.0" 399 | 400 | espree@^9.4.0: 401 | version "9.4.0" 402 | resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a" 403 | integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== 404 | dependencies: 405 | acorn "^8.8.0" 406 | acorn-jsx "^5.3.2" 407 | eslint-visitor-keys "^3.3.0" 408 | 409 | esquery@^1.4.0: 410 | version "1.4.0" 411 | resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" 412 | integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== 413 | dependencies: 414 | estraverse "^5.1.0" 415 | 416 | esrecurse@^4.3.0: 417 | version "4.3.0" 418 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" 419 | integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 420 | dependencies: 421 | estraverse "^5.2.0" 422 | 423 | estraverse@^4.1.1: 424 | version "4.3.0" 425 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" 426 | integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 427 | 428 | estraverse@^5.1.0, estraverse@^5.2.0: 429 | version "5.3.0" 430 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" 431 | integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== 432 | 433 | esutils@^2.0.2: 434 | version "2.0.3" 435 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 436 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 437 | 438 | fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: 439 | version "3.1.3" 440 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 441 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 442 | 443 | fast-glob@^3.2.9: 444 | version "3.2.12" 445 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" 446 | integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== 447 | dependencies: 448 | "@nodelib/fs.stat" "^2.0.2" 449 | "@nodelib/fs.walk" "^1.2.3" 450 | glob-parent "^5.1.2" 451 | merge2 "^1.3.0" 452 | micromatch "^4.0.4" 453 | 454 | fast-json-stable-stringify@^2.0.0: 455 | version "2.1.0" 456 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 457 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 458 | 459 | fast-levenshtein@^2.0.6: 460 | version "2.0.6" 461 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 462 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== 463 | 464 | fastq@^1.6.0: 465 | version "1.13.0" 466 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" 467 | integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== 468 | dependencies: 469 | reusify "^1.0.4" 470 | 471 | file-entry-cache@^6.0.1: 472 | version "6.0.1" 473 | resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" 474 | integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== 475 | dependencies: 476 | flat-cache "^3.0.4" 477 | 478 | fill-range@^7.0.1: 479 | version "7.0.1" 480 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 481 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 482 | dependencies: 483 | to-regex-range "^5.0.1" 484 | 485 | find-up@^5.0.0: 486 | version "5.0.0" 487 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 488 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 489 | dependencies: 490 | locate-path "^6.0.0" 491 | path-exists "^4.0.0" 492 | 493 | flat-cache@^3.0.4: 494 | version "3.0.4" 495 | resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" 496 | integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== 497 | dependencies: 498 | flatted "^3.1.0" 499 | rimraf "^3.0.2" 500 | 501 | flatted@^3.1.0: 502 | version "3.2.7" 503 | resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" 504 | integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== 505 | 506 | fs.realpath@^1.0.0: 507 | version "1.0.0" 508 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 509 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 510 | 511 | glob-parent@^5.1.2: 512 | version "5.1.2" 513 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 514 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 515 | dependencies: 516 | is-glob "^4.0.1" 517 | 518 | glob-parent@^6.0.1: 519 | version "6.0.2" 520 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" 521 | integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== 522 | dependencies: 523 | is-glob "^4.0.3" 524 | 525 | glob@^7.1.3: 526 | version "7.2.3" 527 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 528 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 529 | dependencies: 530 | fs.realpath "^1.0.0" 531 | inflight "^1.0.4" 532 | inherits "2" 533 | minimatch "^3.1.1" 534 | once "^1.3.0" 535 | path-is-absolute "^1.0.0" 536 | 537 | globals@^13.15.0: 538 | version "13.17.0" 539 | resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4" 540 | integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== 541 | dependencies: 542 | type-fest "^0.20.2" 543 | 544 | globby@^11.1.0: 545 | version "11.1.0" 546 | resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" 547 | integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== 548 | dependencies: 549 | array-union "^2.1.0" 550 | dir-glob "^3.0.1" 551 | fast-glob "^3.2.9" 552 | ignore "^5.2.0" 553 | merge2 "^1.4.1" 554 | slash "^3.0.0" 555 | 556 | grapheme-splitter@^1.0.4: 557 | version "1.0.4" 558 | resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" 559 | integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== 560 | 561 | has-flag@^4.0.0: 562 | version "4.0.0" 563 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 564 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 565 | 566 | ieee754@^1.2.1: 567 | version "1.2.1" 568 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 569 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 570 | 571 | ignore@^5.2.0: 572 | version "5.2.0" 573 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" 574 | integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== 575 | 576 | import-fresh@^3.0.0, import-fresh@^3.2.1: 577 | version "3.3.0" 578 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 579 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 580 | dependencies: 581 | parent-module "^1.0.0" 582 | resolve-from "^4.0.0" 583 | 584 | imurmurhash@^0.1.4: 585 | version "0.1.4" 586 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 587 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 588 | 589 | inflight@^1.0.4: 590 | version "1.0.6" 591 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 592 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 593 | dependencies: 594 | once "^1.3.0" 595 | wrappy "1" 596 | 597 | inherits@2: 598 | version "2.0.4" 599 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 600 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 601 | 602 | is-extglob@^2.1.1: 603 | version "2.1.1" 604 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 605 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 606 | 607 | is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: 608 | version "4.0.3" 609 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 610 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 611 | dependencies: 612 | is-extglob "^2.1.1" 613 | 614 | is-number@^7.0.0: 615 | version "7.0.0" 616 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 617 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 618 | 619 | isexe@^2.0.0: 620 | version "2.0.0" 621 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 622 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 623 | 624 | js-sdsl@^4.1.4: 625 | version "4.1.4" 626 | resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" 627 | integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== 628 | 629 | js-yaml@^4.1.0: 630 | version "4.1.0" 631 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 632 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 633 | dependencies: 634 | argparse "^2.0.1" 635 | 636 | json-schema-traverse@^0.4.1: 637 | version "0.4.1" 638 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 639 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 640 | 641 | json-stable-stringify-without-jsonify@^1.0.1: 642 | version "1.0.1" 643 | resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 644 | integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== 645 | 646 | levn@^0.4.1: 647 | version "0.4.1" 648 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" 649 | integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== 650 | dependencies: 651 | prelude-ls "^1.2.1" 652 | type-check "~0.4.0" 653 | 654 | locate-path@^6.0.0: 655 | version "6.0.0" 656 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 657 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 658 | dependencies: 659 | p-locate "^5.0.0" 660 | 661 | lodash.merge@^4.6.2: 662 | version "4.6.2" 663 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" 664 | integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== 665 | 666 | lru-cache@^6.0.0: 667 | version "6.0.0" 668 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 669 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 670 | dependencies: 671 | yallist "^4.0.0" 672 | 673 | merge2@^1.3.0, merge2@^1.4.1: 674 | version "1.4.1" 675 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 676 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 677 | 678 | micromatch@^4.0.4: 679 | version "4.0.5" 680 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 681 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 682 | dependencies: 683 | braces "^3.0.2" 684 | picomatch "^2.3.1" 685 | 686 | minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: 687 | version "3.1.2" 688 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 689 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 690 | dependencies: 691 | brace-expansion "^1.1.7" 692 | 693 | ms@2.1.2: 694 | version "2.1.2" 695 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 696 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 697 | 698 | natural-compare@^1.4.0: 699 | version "1.4.0" 700 | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 701 | integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== 702 | 703 | once@^1.3.0: 704 | version "1.4.0" 705 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 706 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 707 | dependencies: 708 | wrappy "1" 709 | 710 | optionator@^0.9.1: 711 | version "0.9.1" 712 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" 713 | integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== 714 | dependencies: 715 | deep-is "^0.1.3" 716 | fast-levenshtein "^2.0.6" 717 | levn "^0.4.1" 718 | prelude-ls "^1.2.1" 719 | type-check "^0.4.0" 720 | word-wrap "^1.2.3" 721 | 722 | p-limit@^3.0.2: 723 | version "3.1.0" 724 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 725 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 726 | dependencies: 727 | yocto-queue "^0.1.0" 728 | 729 | p-locate@^5.0.0: 730 | version "5.0.0" 731 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 732 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 733 | dependencies: 734 | p-limit "^3.0.2" 735 | 736 | parent-module@^1.0.0: 737 | version "1.0.1" 738 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 739 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 740 | dependencies: 741 | callsites "^3.0.0" 742 | 743 | path-exists@^4.0.0: 744 | version "4.0.0" 745 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 746 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 747 | 748 | path-is-absolute@^1.0.0: 749 | version "1.0.1" 750 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 751 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 752 | 753 | path-key@^3.1.0: 754 | version "3.1.1" 755 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 756 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 757 | 758 | path-type@^4.0.0: 759 | version "4.0.0" 760 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 761 | integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== 762 | 763 | picomatch@^2.3.1: 764 | version "2.3.1" 765 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 766 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 767 | 768 | prelude-ls@^1.2.1: 769 | version "1.2.1" 770 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" 771 | integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 772 | 773 | prettier@^2.6.2: 774 | version "2.7.1" 775 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" 776 | integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== 777 | 778 | punycode@^2.1.0: 779 | version "2.1.1" 780 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 781 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 782 | 783 | queue-microtask@^1.2.2: 784 | version "1.2.3" 785 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 786 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 787 | 788 | regexpp@^3.2.0: 789 | version "3.2.0" 790 | resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" 791 | integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== 792 | 793 | resolve-from@^4.0.0: 794 | version "4.0.0" 795 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 796 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 797 | 798 | reusify@^1.0.4: 799 | version "1.0.4" 800 | resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" 801 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 802 | 803 | rimraf@^3.0.2: 804 | version "3.0.2" 805 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" 806 | integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== 807 | dependencies: 808 | glob "^7.1.3" 809 | 810 | run-parallel@^1.1.9: 811 | version "1.2.0" 812 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 813 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 814 | dependencies: 815 | queue-microtask "^1.2.2" 816 | 817 | rxjs@^7.5.7: 818 | version "7.5.7" 819 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" 820 | integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== 821 | dependencies: 822 | tslib "^2.1.0" 823 | 824 | semver@^7.3.7: 825 | version "7.5.4" 826 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" 827 | integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== 828 | dependencies: 829 | lru-cache "^6.0.0" 830 | 831 | shebang-command@^2.0.0: 832 | version "2.0.0" 833 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 834 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 835 | dependencies: 836 | shebang-regex "^3.0.0" 837 | 838 | shebang-regex@^3.0.0: 839 | version "3.0.0" 840 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 841 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 842 | 843 | slash@^3.0.0: 844 | version "3.0.0" 845 | resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" 846 | integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== 847 | 848 | strip-ansi@^6.0.1: 849 | version "6.0.1" 850 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 851 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 852 | dependencies: 853 | ansi-regex "^5.0.1" 854 | 855 | strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: 856 | version "3.1.1" 857 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 858 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 859 | 860 | supports-color@^7.1.0: 861 | version "7.2.0" 862 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 863 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 864 | dependencies: 865 | has-flag "^4.0.0" 866 | 867 | text-table@^0.2.0: 868 | version "0.2.0" 869 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 870 | integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== 871 | 872 | to-regex-range@^5.0.1: 873 | version "5.0.1" 874 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 875 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 876 | dependencies: 877 | is-number "^7.0.0" 878 | 879 | tslib@^1.8.1: 880 | version "1.14.1" 881 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" 882 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 883 | 884 | tslib@^2.1.0: 885 | version "2.4.0" 886 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" 887 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== 888 | 889 | tsutils@^3.21.0: 890 | version "3.21.0" 891 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" 892 | integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== 893 | dependencies: 894 | tslib "^1.8.1" 895 | 896 | type-check@^0.4.0, type-check@~0.4.0: 897 | version "0.4.0" 898 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" 899 | integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== 900 | dependencies: 901 | prelude-ls "^1.2.1" 902 | 903 | type-fest@^0.20.2: 904 | version "0.20.2" 905 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" 906 | integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== 907 | 908 | typescript@^5.1.6: 909 | version "5.1.6" 910 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" 911 | integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== 912 | 913 | uri-js@^4.2.2: 914 | version "4.4.1" 915 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 916 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 917 | dependencies: 918 | punycode "^2.1.0" 919 | 920 | which@^2.0.1: 921 | version "2.0.2" 922 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 923 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 924 | dependencies: 925 | isexe "^2.0.0" 926 | 927 | word-wrap@^1.2.3: 928 | version "1.2.5" 929 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" 930 | integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== 931 | 932 | wrappy@1: 933 | version "1.0.2" 934 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 935 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 936 | 937 | ws@^8.13.0: 938 | version "8.13.0" 939 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" 940 | integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== 941 | 942 | yallist@^4.0.0: 943 | version "4.0.0" 944 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 945 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 946 | 947 | yocto-queue@^0.1.0: 948 | version "0.1.0" 949 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 950 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 951 | --------------------------------------------------------------------------------