From 470ab58af751b0ffad7ffa6fcb23d205abcfbc95 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 16 Oct 2022 00:32:16 +0200 Subject: [PATCH] JS SDK first try --- .gitignore | 2 + .node-version | 1 + examples/oauth2-public-callback_pkce_s256.mjs | 84 +++ package.json | 37 ++ src/OAuth2Bearer.ts | 15 + src/OAuth2User.ts | 163 ++++++ src/WeblnProvider.ts | 115 +++++ src/auth.ts | 2 + src/client.ts | 128 +++++ src/index.ts | 5 + src/request.ts | 109 ++++ src/types.ts | 98 ++++ src/utils.ts | 14 + tsconfig.json | 12 + yarn.lock | 479 ++++++++++++++++++ 15 files changed, 1264 insertions(+) create mode 100644 .gitignore create mode 100644 .node-version create mode 100644 examples/oauth2-public-callback_pkce_s256.mjs create mode 100644 package.json create mode 100644 src/OAuth2Bearer.ts create mode 100644 src/OAuth2User.ts create mode 100644 src/WeblnProvider.ts create mode 100644 src/auth.ts create mode 100644 src/client.ts create mode 100644 src/index.ts create mode 100644 src/request.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db4c6d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..d4df104 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.1.0 diff --git a/examples/oauth2-public-callback_pkce_s256.mjs b/examples/oauth2-public-callback_pkce_s256.mjs new file mode 100644 index 0000000..d42546a --- /dev/null +++ b/examples/oauth2-public-callback_pkce_s256.mjs @@ -0,0 +1,84 @@ +import { auth, Client } from "../dist/index.js"; +import express from "express"; + +const app = express(); + +const authClient = new auth.OAuth2User({ + client_id: process.env.CLIENT_ID, + client_secret: process.env.CLIENT_SECRET, + callback: "http://localhost:8080/callback", + scopes: ["invoices:read", "account:read", "balance:read", "invoices:create", "invoices:read", "payments:send"], + token: {access_token: undefined, refresh_token: undefined, expires_at: undefined} // initialize with existing token +}); + +const client = new Client(authClient); + +const STATE = "my-state"; + +app.get("/callback", async function (req, res) { + try { + const { code, state } = req.query; + if (state !== STATE) return res.status(500).send("State isn't matching"); + await authClient.requestAccessToken(code); + console.log(authClient); + const invoices = await client.accountBalance(); + res.send(invoices); + } catch (error) { + console.log(error); + } +}); + +app.get("/login", async function (req, res) { + const authUrl = authClient.generateAuthURL({ + state: STATE, + code_challenge_method: "S256", + }); + res.redirect(authUrl); +}); + +app.get("/balance", async function (req, res) { + const result = await client.accountBalance(); + res.send(result); +}); + +app.get("/summary", async function (req, res) { + const result = await client.accountSummary(); + res.send(result); +}); + +app.get("/value4value", async function (req, res) { + const result = await client.accountValue4Value(); + res.send(result); +}); + +app.get("/make-invoice", async function (req, res) { + const result = await client.createInvoice({amount: 1000}); + res.send(result); +}); + +app.get("/bolt11/:invoice", async function(req, res) { + const result = await client.sendPayment({invoice: req.params.invoice}); + res.send(result); +}); + +app.get('/keysend/:destination', async function(req, res) { + const result = await client.keysend({ + destination: req.params.destination, + amount: 10, + memo: req.query.memo + }); + res.send(result); +}); + +app.get("/refresh", async function (req, res) { + try { + await authClient.refreshAccessToken(); + res.send("Refreshed Access Token"); + } catch (error) { + console.log(error); + } +}); + +app.listen(8080, () => { + console.log(`Go here to login: http://localhost:8080/login`); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa80919 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "alby-js-sdk", + "version": "1.0.0", + "description": "Alby OAuth2 Client", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "prebuild": "yarn clean", + "generate": "ts-node scripts/generate.ts", + "prepublishOnly": "yarn test", + "build:watch": "tsc --watch", + "test": "jest", + "clean": "rm -rf dist" + }, + "dependencies": { + "crypto-js": "^4.1.1", + "node-fetch": "^2.6.1" + }, + "devDependencies": { + "@types/crypto-js": "^4.1.1", + "@types/node-fetch": "^2.6.1", + "express": "^4.18.2" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node" + }, + "engines": { + "node": ">=14" + }, + "author": "", + "license": "MIT" +} diff --git a/src/OAuth2Bearer.ts b/src/OAuth2Bearer.ts new file mode 100644 index 0000000..0217780 --- /dev/null +++ b/src/OAuth2Bearer.ts @@ -0,0 +1,15 @@ +import { AuthClient, AuthHeader } from "./types"; + +export class OAuth2Bearer implements AuthClient { + private bearer_token: string; + + constructor(bearer_token: string) { + this.bearer_token = bearer_token; + } + + getAuthHeader(): AuthHeader { + return { + Authorization: `Bearer ${this.bearer_token}`, + }; + } +} diff --git a/src/OAuth2User.ts b/src/OAuth2User.ts new file mode 100644 index 0000000..b9f5f5e --- /dev/null +++ b/src/OAuth2User.ts @@ -0,0 +1,163 @@ +import sha256 from 'crypto-js/sha256'; +import CryptoJS from 'crypto-js'; +import Base64 from 'crypto-js/enc-base64'; +import { buildQueryString, basicAuthHeader } from "./utils"; +import { OAuthClient, AuthHeader, GetTokenResponse, Token, GenerateAuthUrlOptions } from "./types"; +import { RequestOptions, rest } from "./request"; + +const AUTHORIZE_URL = "https://getalby.com/oauth"; + +export type OAuth2Scopes = + | "account:read" + | "invoices:create" + | "invoices:read" + | "transactions:read" + | "balance:read" + | "payments:send"; + +export interface OAuth2UserOptions { + client_id: string; + client_secret?: string; + callback: string; + scopes: OAuth2Scopes[]; + request_options?: Partial; + token?: Token; +} + +function processTokenResponse(token: GetTokenResponse): Token { + const { expires_in, ...rest } = token; + return { + ...rest, + ...(!!expires_in && { + expires_at: Date.now() + expires_in * 1000, + }), + }; +} + +export class OAuth2User implements OAuthClient { + token?: Token; + #options: OAuth2UserOptions; + #code_verifier?: string; + #code_challenge?: string; + constructor(options: OAuth2UserOptions) { + const { token, ...defaultOptions } = options; + this.#options = {client_secret: '', ...defaultOptions}; + this.token = token; + } + + /** + * Refresh the access token + */ + async refreshAccessToken(): Promise<{ token: Token }> { + const refresh_token = this.token?.refresh_token; + const { client_id, client_secret, request_options } = this.#options; + if (!client_id) { + throw new Error("client_id is required"); + } + if (!refresh_token) { + throw new Error("refresh_token is required"); + } + const data = await rest({ + ...request_options, + endpoint: `/oauth/token`, + params: { + client_id, + grant_type: "refresh_token", + refresh_token, + }, + method: "POST", + headers: { + ...request_options?.headers, + "Content-type": "application/x-www-form-urlencoded", + ...{ + Authorization: basicAuthHeader(client_id, client_secret), + }, + }, + }); + const token = processTokenResponse(data); + this.token = token; + return { token }; + } + + /** + * Check if an access token is expired + */ + isAccessTokenExpired(): boolean { + const refresh_token = this.token?.refresh_token; + const expires_at = this.token?.expires_at; + if (!expires_at) return true; + return !!refresh_token && expires_at <= Date.now() + 1000; + } + + /** + * Request an access token + */ + async requestAccessToken(code?: string): Promise<{ token: Token }> { + const { client_id, client_secret, callback, request_options } = + this.#options; + const code_verifier = this.#code_verifier; + if (!client_id) { + throw new Error("client_id is required"); + } + if (!callback) { + throw new Error("callback is required"); + } + const params = { + code, + grant_type: "authorization_code", + code_verifier, + client_id, + redirect_uri: callback, + }; + const data = await rest({ + ...request_options, + endpoint: `/oauth/token`, + params, + method: "POST", + headers: { + ...request_options?.headers, + "Content-Type": "application/x-www-form-urlencoded", + ...{ + Authorization: basicAuthHeader(client_id, client_secret), + }, + }, + }); + const token = processTokenResponse(data); + this.token = token; + return { token }; + } + + generateAuthURL(options: GenerateAuthUrlOptions): string { + const { client_id, callback, scopes } = this.#options; + if (!callback) throw new Error("callback required"); + if (!scopes) throw new Error("scopes required"); + if (options.code_challenge_method === "S256") { + const code_verifier = CryptoJS.lib.WordArray.random(64); + this.#code_verifier = code_verifier.toString(); + this.#code_challenge = sha256(this.#code_verifier).toString(Base64).replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '') + } else { + this.#code_challenge = options.code_challenge; + this.#code_verifier = options.code_challenge; + } + const code_challenge = this.#code_challenge; + const url = new URL(AUTHORIZE_URL); + url.search = buildQueryString({ + ...options, + client_id, + scope: scopes.join(" "), + response_type: "code", + redirect_uri: callback, + code_challenge_method: options.code_challenge_method || "plain", + code_challenge, + }); + return url.toString(); + } + + async getAuthHeader(): Promise { + if (!this.token?.access_token) throw new Error("access_token is required"); + if (this.isAccessTokenExpired()) await this.refreshAccessToken(); + return { + Authorization: `Bearer ${this.token.access_token}`, + }; + } +} diff --git a/src/WeblnProvider.ts b/src/WeblnProvider.ts new file mode 100644 index 0000000..043b56e --- /dev/null +++ b/src/WeblnProvider.ts @@ -0,0 +1,115 @@ +import { Client } from './client'; +import { + OAuthClient, + KeysendRequestParams, +} from "./types"; + +interface RequestInvoiceArgs { + amount: string | number; + defaultMemo?: string; +} + +const isBrowser = () => typeof window !== "undefined" && typeof window.document !== "undefined"; + +export class WebLNProvider { + client: Client; + auth: OAuthClient; + + constructor(auth: OAuthClient) { + this.auth = auth; + this.client = new Client(auth); + } + + openAuthorization() { + const height = 700; + const width = 600; + const top = window.outerHeight / 2 + window.screenY - height / 2; + const left = window.outerWidth / 2 + window.screenX - width / 2; + const url = this.auth.generateAuthURL({ code_challenge_method: "S256" }); + + return new Promise((resolve, reject) => { + const popup = window.open( + url, + `${document.title} - WebLN enable`, + `height=${height},width=${width},top=${top},left=${left}` + ); + window.addEventListener('message', async (message) => { + const data = message.data; + if (data && data.type === 'alby:oauth:success' && message.origin === `${document.location.protocol}//${document.location.host}`) { + const code = data.payload.code; + try { + this.auth.requestAccessToken(code); + this.client = new Client(this.auth); + if (popup) { + popup.close(); + } + resolve({ enabled: true }); + } catch(e) { + console.error(e); + reject({ enabled: false }); + } + } + }); + }); + } + + + async enable() { + if (this.auth.token?.access_token) { + return { enabled: true }; + } + if (isBrowser()) { + return this.openAuthorization(); + } else { + throw new Error("Missing access token"); + } + } + + async sendPayment(invoice: string) { + try { + const result = await this.client.sendPayment({ invoice }); + if (result.error) { + throw new Error(result.message); + } + return { + preimage: result.payment_preimage + } + } catch(error) { + let message = 'Unknown Error' + if (error instanceof Error) message = error.message + throw new Error(message); + } + } + + async keysend(params: KeysendRequestParams) { + try { + const result = await this.client.keysend(params); + if (result.error) { + throw new Error(result.message); + } + return { + preimage: result.payment_preimage + } + } catch(error) { + let message = 'Unknown Error' + if (error instanceof Error) message = error.message + throw new Error(message); + } + } + + async getInfo() { + return { + alias: "Alby" + }; + } + + async makeInvoice(params: RequestInvoiceArgs) { + const result = await this.client.createInvoice({ + amount: parseInt(params.amount.toString()), + description: params.defaultMemo + }); + return { + paymentRequest: result.payment_request + } + } +} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..eada8e2 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,2 @@ +export * from './OAuth2User' +export * from "./OAuth2Bearer"; diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..af2a21e --- /dev/null +++ b/src/client.ts @@ -0,0 +1,128 @@ +import { rest, RequestOptions } from "./request"; +import { + AuthClient, + InvoiceRequestParams, + KeysendRequestParams, + SendPaymentRequestParams +} from "./types"; +import { OAuth2Bearer } from "./auth"; + + +export class Client { + #auth: AuthClient; + #defaultRequestOptions?: Partial; + + constructor( + auth: string | AuthClient, + requestOptions?: Partial + ) { + this.#auth = typeof auth === "string" ? new OAuth2Bearer(auth) : auth; + this.#defaultRequestOptions = { + ...requestOptions, + headers: { + "User-Agent": "alby-api", + ...requestOptions?.headers, + }, + }; + } + + accountBalance(params: {}, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/balance`, + params, + method: "GET", + }); + } + + accountSummary(params: {}, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/user/summary`, + params, + method: "GET", + }); + } + + + accountValue4Value(params: {}, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/user/value4value`, + params, + method: "GET", + }); + } + + incomingInvoices(params: {}, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/invoices/incoming`, + params, + method: "GET", + }); + } + + outgoingInvoices(params: {}, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/invoices/outgoing`, + params, + method: "GET", + }); + } + + getInvoice(paymentHash: string, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/invoices/${paymentHash}`, + method: "GET", + }); + } + + createInvoice(invoice: InvoiceRequestParams, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/invoices`, + request_body: invoice, + method: "POST", + }); + } + + keysend(keysend: KeysendRequestParams, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/payments/keysend`, + request_body: keysend, + method: "POST", + }); + } + + sendPayment(params: SendPaymentRequestParams, request_options?: Partial) { + return rest({ + auth: this.#auth, + ...this.#defaultRequestOptions, + ...request_options, + endpoint: `/payments/bolt11`, + request_body: params, + method: "POST", + }); + } + +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..518285c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +export * as auth from "./auth"; +export * as types from './types' +export { Client } from "./client"; +export { WebLNProvider } from "./WeblnProvider"; +export { Client as default } from "./client"; diff --git a/src/request.ts b/src/request.ts new file mode 100644 index 0000000..cb2c954 --- /dev/null +++ b/src/request.ts @@ -0,0 +1,109 @@ +import fetch from "node-fetch"; +import type { RequestInfo, RequestInit, Response, Headers } from "node-fetch"; +import { buildQueryString } from "./utils"; +import { + AuthClient, +} from "./types"; + +const BASE_URL = "https://api.getalby.com"; + +export interface RequestOptions extends Omit { + auth?: AuthClient; + endpoint: string; + params?: Record; + request_body?: Record; + method?: string; + max_retries?: number; + base_url?: string; +} + +async function fetchWithRetries( + url: RequestInfo, + init: RequestInit, + max_retries = 0 +): Promise { + const res = await fetch(url, init); + if (res.status === 429 && max_retries > 0) { + const rateLimitReset = Number(res.headers.get("x-rate-limit-reset")); + const rateLimitRemaining = Number(res.headers.get("x-rate-limit-remaining")); + const timeTillReset = rateLimitReset * 1000 - Date.now(); + let timeToWait = 1000; + if (rateLimitRemaining === 0) + timeToWait = timeTillReset; + await new Promise((resolve) => setTimeout(resolve, timeToWait)); + return fetchWithRetries(url, init, max_retries - 1); + } + return res; +} + +class AlbyResponseError extends Error { + status: number; + statusText: string; + headers: Record; + error: Record; + constructor( + status: number, + statusText: string, + headers: Headers, + error: Record + ) { + super(); + this.status = status; + this.statusText = statusText; + this.headers = Object.fromEntries(headers); + this.error = error; + } +} + +export async function request({ + auth, + endpoint, + params: query = {}, + request_body, + method, + max_retries, + base_url = BASE_URL, + headers, + ...options +}: RequestOptions): Promise { + const url = new URL(base_url + endpoint); + url.search = buildQueryString(query); + const isPost = method === "POST" && !!request_body; + const authHeader = auth + ? await auth.getAuthHeader(url.href, method) + : undefined; + const response = await fetchWithRetries( + url.toString(), + { + headers: { + ...(isPost + ? { "Content-Type": "application/json; charset=utf-8" } + : undefined), + ...authHeader, + ...headers, + }, + method, + body: isPost ? JSON.stringify(request_body) : undefined, + timeout: 120000, + ...options, + }, + max_retries + ); + if (!response.ok) { + const error = await response.json(); + throw new AlbyResponseError( + response.status, + response.statusText, + response.headers, + error + ); + } + return response; +} + +export async function rest>( + args: RequestOptions +): Promise { + const response = await request(args); + return response.json(); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..65e0239 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,98 @@ + +export type SuccessStatus = 200 | 201; +export type ResponseType = "application/json"; + +export interface AuthHeader { + Authorization: string; +} + +export interface GetTokenResponse { + /** Allows an application to obtain a new access token without prompting the user via the refresh token flow. */ + refresh_token?: string; + /** Access tokens are the token that applications use to make API requests on behalf of a user. */ + access_token?: string; + token_type?: string; + expires_in?: number; + /** Comma-separated list of scopes for the token */ + scope?: string; +} + +export interface Token extends Omit { + /** Date that the access_token will expire at. */ + expires_at?: number; +} + + +export type GenerateAuthUrlOptions = + | { + /** A random string you provide to verify against CSRF attacks. The length of this string can be up to 500 characters. */ + state?: string; + /** Specifies the method you are using to make a request (S256 OR plain). */ + code_challenge_method: "S256"; + } + | { + /** A random string you provide to verify against CSRF attacks. The length of this string can be up to 500 characters. */ + state: string; + /** A PKCE parameter, a random secret for each request you make. */ + code_challenge: string; + /** Specifies the method you are using to make a request (S256 OR plain). */ + code_challenge_method?: "plain"; + }; + +export abstract class OAuthClient implements AuthClient { + abstract token?: Token; + abstract generateAuthURL(options: GenerateAuthUrlOptions): string; + abstract requestAccessToken(code?: string): Promise<{ token: Token }> + abstract getAuthHeader( + url?: string, + method?: string + ): Promise | AuthHeader; +} + +export abstract class AuthClient { + abstract getAuthHeader( + url?: string, + method?: string + ): Promise | AuthHeader; +} + +// https://stackoverflow.com/a/50375286 +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never; + +export type GetSuccess = { + [K in SuccessStatus & keyof T]: GetContent; +}[SuccessStatus & keyof T]; + +export type AlbyResponse = UnionToIntersection>; + +export type GetContent = "content" extends keyof T + ? ResponseType extends keyof T["content"] + ? T["content"][ResponseType] + : never + : never; + +export type ExtractAlbyResponse = "responses" extends keyof T + ? GetSuccess + : never; + +export type InvoiceRequestParams = { + description?: string, + description_hash?: string, + amount: number, +} + +export type KeysendRequestParams = { + amount: number, + destination: string, + memo?: string, + customRecords?: Record +} + +export type SendPaymentRequestParams = { + invoice: string, + amount?: number, +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..b9a1075 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,14 @@ +// https://stackoverflow.com/a/62969380 +export function buildQueryString(query: Record): string { + return Object.entries(query) + .map(([key, value]) => + key && value + ? `${key}=${value}` + : "" + ) + .join("&"); +} + +export function basicAuthHeader(client_id: string, client_secret: string | undefined) { + return `Basic ${btoa(`${client_id}:${client_secret}`)}`; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4246044 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "strict": true, + "target": "ES2020", + "declaration": true, + "esModuleInterop": true, + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["examples/*", "scripts/*"] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..0e52f08 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,479 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/crypto-js@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" + integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== + +"@types/node-fetch@^2.6.1": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" + integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + +"@types/node@*": + version "18.8.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.5.tgz#6a31f820c1077c3f8ce44f9e203e68a176e8f59e" + integrity sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-fetch@^2.6.1: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0"