diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 000000000..6a35dbe61 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,21 @@ +module.exports = { + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:require-extensions/recommended", + ], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint", "require-extensions"], + parserOptions: { + project: "./tsconfig.json", + }, + root: true, + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-floating-promises": "error", + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..dd6b144d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,171 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +.DS_Store \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..7cafbdb49 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +coverage +node_modules \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..1a7607611 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright 2023, Garden Computing, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..13f65fee6 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# CoJSON + +CoJSON ("Collaborative JSON") will be a minimal protocol and implementation for collaborative values (CRDTs + public-key cryptography). + +CoJSON is developed by [Garden Computing](https://gcmp.io) as the underpinnings of [Jazz](https://jazz.tools), a framework for building apps with telepathic data. + +The protocol and implementation will cover: + +- how to represent collaborative values internally +- the APIs collaborative values expose +- how to sync and query for collaborative values between peers +- how to enforce access rights within collaborative values locally and at sync boundaries + +THIS IS WORK IN PROGRESS + +## Core Value Types + +### `Immutable` Values (JSON) +- null +- boolean +- number +- string + - stringly-encoded CoJSON identifiers & data (`CoID`, `AgentID`, `SessionID`, `SignerID`, `SignerSecret`, `Signature`, `SealerID`, `SealerSecret`, `Sealed`, `Hash`, `ShortHash`, `KeySecret`, `KeyID`, `Encrypted`, `Role`) + +- array +- object + +### `Collaborative` Values +- CoMap (`string` → `Immutable`, last-writer-wins per key) + - Team (`AgentID` → `Role`) +- CoList (`Immutable[]`, addressable positions, insertAfter semantics) + - Agent (`{signerID, sealerID}[]`) +- CoStream (independent per-session streams of `Immutable`s) +- Static (single addressable `Immutable`) + +## Implementation Abstractions +- CoValue + - Session Logs + - Transactions + - Private (encrypted) transactions + - Trusting (unencrypted) transactions + - Rulesets +- CoValue Content Types +- LocalNode + - Peers + - AgentCredentials +- Peer + +## Extensions & higher-level protocols + +### More complex datastructures +- CoText: a clean way to collaboratively mark up rich text with CoJSON +- CoJSON Tree: a clean way to represent collaborative tree structures with CoJSON \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 000000000..65796aee1 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "cojson", + "module": "dist/index.js", + "main": "dist/index.js", + "types": "src/index.ts", + "type": "module", + "license": "MIT", + "version": "0.0.14", + "devDependencies": { + "@types/jest": "^29.5.3", + "@typescript-eslint/eslint-plugin": "^6.2.1", + "@typescript-eslint/parser": "^6.2.1", + "eslint": "^8.46.0", + "eslint-plugin-require-extensions": "^0.1.3", + "jest": "^29.6.2", + "ts-jest": "^29.1.1", + "typescript": "5.0.2" + }, + "dependencies": { + "@noble/ciphers": "^0.1.3", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.1", + "fast-json-stable-stringify": "^2.1.0", + "isomorphic-streams": "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae" + }, + "scripts": { + "test": "jest", + "lint": "eslint src/**/*.ts", + "build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist", + "prepublishOnly": "npm run build" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "transform": { + "\\.[jt]sx?$": [ + "ts-jest", + { + "useESM": true + } + ] + }, + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, + "extensionsToTreatAsEsm": [ + ".ts" + ], + "modulePathIgnorePatterns": [ + "/node_modules/", + "/dist/" + ] + } +} diff --git a/src/account.test.ts b/src/account.test.ts new file mode 100644 index 000000000..6298e7d27 --- /dev/null +++ b/src/account.test.ts @@ -0,0 +1,67 @@ +import { newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { connectedPeers } from "./streamUtils.js"; + +test("Can create a node while creating a new account with profile", async () => { + const { node, accountID, accountSecret, sessionID } = + LocalNode.withNewlyCreatedAccount("Hermes Puggington"); + + expect(node).not.toBeNull(); + expect(accountID).not.toBeNull(); + expect(accountSecret).not.toBeNull(); + expect(sessionID).not.toBeNull(); + + expect(node.expectProfileLoaded(accountID).get("name")).toEqual( + "Hermes Puggington" + ); + expect((await node.loadProfile(accountID)).get("name")).toEqual( + "Hermes Puggington" + ); +}); + +test("A node with an account can create teams and and objects within them", async () => { + const { node, accountID } = + LocalNode.withNewlyCreatedAccount("Hermes Puggington"); + + const team = await node.createTeam(); + expect(team).not.toBeNull(); + + let map = team.createMap(); + map = map.edit((edit) => { + edit.set("foo", "bar", "private"); + expect(edit.get("foo")).toEqual("bar"); + }); + + expect(map.get("foo")).toEqual("bar"); + + expect(map.getLastEditor("foo")).toEqual(accountID); +}); + +test("Can create account with one node, and then load it on another", async () => { + const { node, accountID, accountSecret } = + LocalNode.withNewlyCreatedAccount("Hermes Puggington"); + + const team = await node.createTeam(); + expect(team).not.toBeNull(); + + let map = team.createMap(); + map = map.edit((edit) => { + edit.set("foo", "bar", "private"); + expect(edit.get("foo")).toEqual("bar"); + }); + + const [node1asPeer, node2asPeer] = connectedPeers("node1", "node2", {trace: true, peer1role: "server", peer2role: "client"}); + + node.sync.addPeer(node2asPeer); + + const node2 = await LocalNode.withLoadedAccount( + accountID, + accountSecret, + newRandomSessionID(accountID), + [node1asPeer] + ); + + const map2 = await node2.load(map.id); + + expect(map2.get("foo")).toEqual("bar"); +}); diff --git a/src/account.ts b/src/account.ts new file mode 100644 index 000000000..9eeb144ae --- /dev/null +++ b/src/account.ts @@ -0,0 +1,152 @@ +import { CoValueHeader } from "./coValue.js"; +import { CoID } from "./contentType.js"; +import { + AgentSecret, + SealerID, + SealerSecret, + SignerID, + SignerSecret, + getAgentID, + getAgentSealerID, + getAgentSealerSecret, + getAgentSignerID, + getAgentSignerSecret, +} from "./crypto.js"; +import { AgentID } from "./ids.js"; +import { CoMap, LocalNode } from "./index.js"; +import { Team, TeamContent } from "./permissions.js"; + +export function accountHeaderForInitialAgentSecret( + agentSecret: AgentSecret +): CoValueHeader { + const agent = getAgentID(agentSecret); + return { + type: "comap", + ruleset: { type: "team", initialAdmin: agent }, + meta: { + type: "account", + }, + createdAt: null, + uniqueness: null, + }; +} + +export class Account extends Team { + get id(): AccountID { + return this.teamMap.id as AccountID; + } + + getCurrentAgentID(): AgentID { + const agents = this.teamMap + .keys() + .filter((k): k is AgentID => k.startsWith("sealer_")); + + if (agents.length !== 1) { + throw new Error( + "Expected exactly one agent in account, got " + agents.length + ); + } + + return agents[0]!; + } +} + +export interface GeneralizedControlledAccount { + id: AccountIDOrAgentID; + agentSecret: AgentSecret; + + currentAgentID: () => AgentID; + currentSignerID: () => SignerID; + currentSignerSecret: () => SignerSecret; + currentSealerID: () => SealerID; + currentSealerSecret: () => SealerSecret; +} + +export class ControlledAccount + extends Account + implements GeneralizedControlledAccount +{ + agentSecret: AgentSecret; + + constructor( + agentSecret: AgentSecret, + teamMap: CoMap, + node: LocalNode + ) { + super(teamMap, node); + + this.agentSecret = agentSecret; + } + + currentAgentID(): AgentID { + return getAgentID(this.agentSecret); + } + + currentSignerID(): SignerID { + return getAgentSignerID(this.currentAgentID()); + } + + currentSignerSecret(): SignerSecret { + return getAgentSignerSecret(this.agentSecret); + } + + currentSealerID(): SealerID { + return getAgentSealerID(this.currentAgentID()); + } + + currentSealerSecret(): SealerSecret { + return getAgentSealerSecret(this.agentSecret); + } +} + +export class AnonymousControlledAccount + implements GeneralizedControlledAccount +{ + agentSecret: AgentSecret; + + constructor(agentSecret: AgentSecret) { + this.agentSecret = agentSecret; + } + + get id(): AgentID { + return getAgentID(this.agentSecret); + } + + currentAgentID(): AgentID { + return getAgentID(this.agentSecret); + } + + currentSignerID(): SignerID { + return getAgentSignerID(this.currentAgentID()); + } + + currentSignerSecret(): SignerSecret { + return getAgentSignerSecret(this.agentSecret); + } + + currentSealerID(): SealerID { + return getAgentSealerID(this.currentAgentID()); + } + + currentSealerSecret(): SealerSecret { + return getAgentSealerSecret(this.agentSecret); + } +} + +export type AccountContent = TeamContent & { profile: CoID }; +export type AccountMeta = { type: "account" }; +export type AccountID = CoID>; + +export type AccountIDOrAgentID = AgentID | AccountID; +export type AccountOrAgentID = AgentID | Account; +export type AccountOrAgentSecret = AgentSecret | Account; + +export function isAccountID(id: AccountIDOrAgentID): id is AccountID { + return id.startsWith("co_"); +} + +export type ProfileContent = { + name: string; +}; +export type ProfileMeta = { type: "profile" }; +export type Profile = CoMap; diff --git a/src/coValue.test.ts b/src/coValue.test.ts new file mode 100644 index 000000000..9fd388ef9 --- /dev/null +++ b/src/coValue.test.ts @@ -0,0 +1,123 @@ +import { Transaction } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { createdNowUnique, getAgentSignerSecret, newRandomAgentSecret, sign } from "./crypto.js"; +import { randomAnonymousAccountAndSessionID } from "./testUtils.js"; + +test("Can create coValue with new agent credentials and add transaction to it", () => { + const [account, sessionID] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(account, sessionID); + + const coValue = node.createCoValue({ + type: "costream", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const transaction: Transaction = { + privacy: "trusting", + madeAt: Date.now(), + changes: [ + { + hello: "world", + }, + ], + }; + + const { expectedNewHash } = coValue.expectedNewHashAfter( + node.ownSessionID, + [transaction] + ); + + expect( + coValue.tryAddTransactions( + node.ownSessionID, + [transaction], + expectedNewHash, + sign(account.currentSignerSecret(), expectedNewHash) + ) + ).toBe(true); +}); + +test("transactions with wrong signature are rejected", () => { + const wrongAgent = newRandomAgentSecret(); + const [agentSecret, sessionID] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(agentSecret, sessionID); + + const coValue = node.createCoValue({ + type: "costream", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const transaction: Transaction = { + privacy: "trusting", + madeAt: Date.now(), + changes: [ + { + hello: "world", + }, + ], + }; + + const { expectedNewHash } = coValue.expectedNewHashAfter( + node.ownSessionID, + [transaction] + ); + + expect( + coValue.tryAddTransactions( + node.ownSessionID, + [transaction], + expectedNewHash, + sign(getAgentSignerSecret(wrongAgent), expectedNewHash) + ) + ).toBe(false); +}); + +test("transactions with correctly signed, but wrong hash are rejected", () => { + const [account, sessionID] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(account, sessionID); + + const coValue = node.createCoValue({ + type: "costream", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const transaction: Transaction = { + privacy: "trusting", + madeAt: Date.now(), + changes: [ + { + hello: "world", + }, + ], + }; + + const { expectedNewHash } = coValue.expectedNewHashAfter( + node.ownSessionID, + [ + { + privacy: "trusting", + madeAt: Date.now(), + changes: [ + { + hello: "wrong", + }, + ], + }, + ] + ); + + expect( + coValue.tryAddTransactions( + node.ownSessionID, + [transaction], + expectedNewHash, + sign(account.currentSignerSecret(), expectedNewHash) + ) + ).toBe(false); +}); diff --git a/src/coValue.ts b/src/coValue.ts new file mode 100644 index 000000000..408b0c632 --- /dev/null +++ b/src/coValue.ts @@ -0,0 +1,547 @@ +import { randomBytes } from "@noble/hashes/utils"; +import { ContentType } from "./contentType.js"; +import { Static } from "./contentTypes/static.js"; +import { CoStream } from "./contentTypes/coStream.js"; +import { CoMap } from "./contentTypes/coMap.js"; +import { + Encrypted, + Hash, + KeySecret, + Signature, + StreamingHash, + unseal, + shortHash, + sign, + verify, + encryptForTransaction, + decryptForTransaction, + KeyID, + decryptKeySecret, + getAgentSignerID, + getAgentSealerID, +} from "./crypto.js"; +import { JsonObject, JsonValue } from "./jsonValue.js"; +import { base58 } from "@scure/base"; +import { + PermissionsDef as RulesetDef, + Team, + determineValidTransactions, + expectTeamContent, + isKeyForKeyField, +} from "./permissions.js"; +import { LocalNode } from "./node.js"; +import { CoValueKnownState, NewContentMessage } from "./sync.js"; +import { RawCoID, SessionID, TransactionID } from "./ids.js"; +import { CoList } from "./contentTypes/coList.js"; +import { + AccountID, + AccountIDOrAgentID, + GeneralizedControlledAccount, +} from "./account.js"; + +export type CoValueHeader = { + type: ContentType["type"]; + ruleset: RulesetDef; + meta: JsonObject | null; + createdAt: `2${string}` | null; + uniqueness: `z${string}` | null; +}; + +export function idforHeader(header: CoValueHeader): RawCoID { + const hash = shortHash(header); + return `co_z${hash.slice("shortHash_z".length)}`; +} + +export function accountOrAgentIDfromSessionID( + sessionID: SessionID +): AccountIDOrAgentID { + return sessionID.split("_session")[0] as AccountIDOrAgentID; +} + +export function newRandomSessionID(accountID: AccountIDOrAgentID): SessionID { + return `${accountID}_session_z${base58.encode(randomBytes(8))}`; +} + +type SessionLog = { + transactions: Transaction[]; + lastHash?: Hash; + streamingHash: StreamingHash; + lastSignature: Signature; +}; + +export type PrivateTransaction = { + privacy: "private"; + madeAt: number; + keyUsed: KeyID; + encryptedChanges: Encrypted< + JsonValue[], + { in: RawCoID; tx: TransactionID } + >; +}; + +export type TrustingTransaction = { + privacy: "trusting"; + madeAt: number; + changes: JsonValue[]; +}; + +export type Transaction = PrivateTransaction | TrustingTransaction; + +export type DecryptedTransaction = { + txID: TransactionID; + changes: JsonValue[]; + madeAt: number; +}; + +export class CoValue { + id: RawCoID; + node: LocalNode; + header: CoValueHeader; + sessions: { [key: SessionID]: SessionLog }; + content?: ContentType; + listeners: Set<(content?: ContentType) => void> = new Set(); + + constructor(header: CoValueHeader, node: LocalNode) { + this.id = idforHeader(header); + this.header = header; + this.sessions = {}; + this.node = node; + } + + testWithDifferentAccount( + account: GeneralizedControlledAccount, + ownSessionID: SessionID + ): CoValue { + const newNode = this.node.testWithDifferentAccount( + account, + ownSessionID + ); + + return newNode.expectCoValueLoaded(this.id); + } + + knownState(): CoValueKnownState { + return { + id: this.id, + header: true, + sessions: Object.fromEntries( + Object.entries(this.sessions).map(([k, v]) => [ + k, + v.transactions.length, + ]) + ), + }; + } + + get meta(): JsonValue { + return this.header?.meta ?? null; + } + + nextTransactionID(): TransactionID { + const sessionID = this.node.ownSessionID; + return { + sessionID, + txIndex: this.sessions[sessionID]?.transactions.length || 0, + }; + } + + tryAddTransactions( + sessionID: SessionID, + newTransactions: Transaction[], + givenExpectedNewHash: Hash | undefined, + newSignature: Signature + ): boolean { + const signerID = getAgentSignerID( + this.node.resolveAccountAgent( + accountOrAgentIDfromSessionID(sessionID), + "Expected to know signer of transaction" + ) + ); + + if (!signerID) { + console.warn( + "Unknown agent", + accountOrAgentIDfromSessionID(sessionID) + ); + return false; + } + + const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter( + sessionID, + newTransactions + ); + + if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) { + console.warn("Invalid hash", { expectedNewHash, givenExpectedNewHash }); + return false; + } + + if (!verify(newSignature, expectedNewHash, signerID)) { + console.warn( + "Invalid signature", + newSignature, + expectedNewHash, + signerID + ); + return false; + } + + const transactions = this.sessions[sessionID]?.transactions ?? []; + + transactions.push(...newTransactions); + + this.sessions[sessionID] = { + transactions, + lastHash: expectedNewHash, + streamingHash: newStreamingHash, + lastSignature: newSignature, + }; + + this.content = undefined; + + const content = this.getCurrentContent(); + + for (const listener of this.listeners) { + listener(content); + } + + return true; + } + + subscribe(listener: (content?: ContentType) => void): () => void { + this.listeners.add(listener); + listener(this.getCurrentContent()); + + return () => { + this.listeners.delete(listener); + }; + } + + expectedNewHashAfter( + sessionID: SessionID, + newTransactions: Transaction[] + ): { expectedNewHash: Hash; newStreamingHash: StreamingHash } { + const streamingHash = + this.sessions[sessionID]?.streamingHash.clone() ?? + new StreamingHash(); + for (const transaction of newTransactions) { + streamingHash.update(transaction); + } + + const newStreamingHash = streamingHash.clone(); + + return { + expectedNewHash: streamingHash.digest(), + newStreamingHash, + }; + } + + makeTransaction( + changes: JsonValue[], + privacy: "private" | "trusting" + ): boolean { + const madeAt = Date.now(); + + let transaction: Transaction; + + if (privacy === "private") { + const { secret: keySecret, id: keyID } = this.getCurrentReadKey(); + + if (!keySecret) { + throw new Error( + "Can't make transaction without read key secret" + ); + } + + transaction = { + privacy: "private", + madeAt, + keyUsed: keyID, + encryptedChanges: encryptForTransaction(changes, keySecret, { + in: this.id, + tx: this.nextTransactionID(), + }), + }; + } else { + transaction = { + privacy: "trusting", + madeAt, + changes, + }; + } + + const sessionID = this.node.ownSessionID; + + const { expectedNewHash } = this.expectedNewHashAfter(sessionID, [ + transaction, + ]); + + const signature = sign( + this.node.account.currentSignerSecret(), + expectedNewHash + ); + + const success = this.tryAddTransactions( + sessionID, + [transaction], + expectedNewHash, + signature + ); + + if (success) { + void this.node.sync.syncCoValue(this); + } + + return success; + } + + getCurrentContent(): ContentType { + if (this.content) { + return this.content; + } + + if (this.header.type === "comap") { + this.content = new CoMap(this); + } else if (this.header.type === "colist") { + this.content = new CoList(this); + } else if (this.header.type === "costream") { + this.content = new CoStream(this); + } else if (this.header.type === "static") { + this.content = new Static(this); + } else { + throw new Error(`Unknown coValue type ${this.header.type}`); + } + + return this.content; + } + + getValidSortedTransactions(): DecryptedTransaction[] { + const validTransactions = determineValidTransactions(this); + + const allTransactions: DecryptedTransaction[] = validTransactions + .map(({ txID, tx }) => { + if (tx.privacy === "trusting") { + return { + txID, + madeAt: tx.madeAt, + changes: tx.changes, + }; + } else { + const readKey = this.getReadKey(tx.keyUsed); + + if (!readKey) { + return undefined; + } else { + const decrytedChanges = decryptForTransaction( + tx.encryptedChanges, + readKey, + { + in: this.id, + tx: txID, + } + ); + + if (!decrytedChanges) { + console.error( + "Failed to decrypt transaction despite having key" + ); + return undefined; + } + return { + txID, + madeAt: tx.madeAt, + changes: decrytedChanges, + }; + } + } + }) + .filter((x): x is Exclude => !!x); + allTransactions.sort( + (a, b) => + a.madeAt - b.madeAt || + (a.txID.sessionID < b.txID.sessionID ? -1 : 1) || + a.txID.txIndex - b.txID.txIndex + ); + + return allTransactions; + } + + getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } { + if (this.header.ruleset.type === "team") { + const content = expectTeamContent(this.getCurrentContent()); + + const currentKeyId = content.get("readKey"); + + if (!currentKeyId) { + throw new Error("No readKey set"); + } + + const secret = this.getReadKey(currentKeyId); + + return { + secret: secret, + id: currentKeyId, + }; + } else if (this.header.ruleset.type === "ownedByTeam") { + return this.node + .expectCoValueLoaded(this.header.ruleset.team) + .getCurrentReadKey(); + } else { + throw new Error( + "Only teams or values owned by teams have read secrets" + ); + } + } + + getReadKey(keyID: KeyID): KeySecret | undefined { + if (this.header.ruleset.type === "team") { + const content = expectTeamContent(this.getCurrentContent()); + + // Try to find key revelation for us + + const readKeyEntry = content.getLastEntry(`${keyID}_for_${this.node.account.id}`); + + if (readKeyEntry) { + const revealer = accountOrAgentIDfromSessionID( + readKeyEntry.txID.sessionID + ); + const revealerAgent = this.node.resolveAccountAgent( + revealer, + "Expected to know revealer" + ); + + const secret = unseal( + readKeyEntry.value, + this.node.account.currentSealerSecret(), + getAgentSealerID(revealerAgent), + { + in: this.id, + tx: readKeyEntry.txID, + } + ); + + if (secret) return secret as KeySecret; + } + + // Try to find indirect revelation through previousKeys + + for (const field of content.keys()) { + if (isKeyForKeyField(field) && field.startsWith(keyID)) { + const encryptingKeyID = field.split("_for_")[1] as KeyID; + const encryptingKeySecret = this.getReadKey(encryptingKeyID); + + if (!encryptingKeySecret) { + continue; + } + + const encryptedPreviousKey = content.get(field)!; + + const secret = decryptKeySecret( + { + encryptedID: keyID, + encryptingID: encryptingKeyID, + encrypted: encryptedPreviousKey, + }, + encryptingKeySecret + ); + + if (secret) { + return secret; + } else { + console.error( + `Encrypting ${encryptingKeyID} key didn't decrypt ${keyID}` + ); + } + } + + } + + return undefined; + } else if (this.header.ruleset.type === "ownedByTeam") { + return this.node + .expectCoValueLoaded(this.header.ruleset.team) + .getReadKey(keyID); + } else { + throw new Error( + "Only teams or values owned by teams have read secrets" + ); + } + } + + getTeam(): Team { + if (this.header.ruleset.type !== "ownedByTeam") { + throw new Error("Only values owned by teams have teams"); + } + + return new Team( + expectTeamContent( + this.node + .expectCoValueLoaded(this.header.ruleset.team) + .getCurrentContent() + ), + this.node + ); + } + + getTx(txID: TransactionID): Transaction | undefined { + return this.sessions[txID.sessionID]?.transactions[txID.txIndex]; + } + + newContentSince( + knownState: CoValueKnownState | undefined + ): NewContentMessage | undefined { + const newContent: NewContentMessage = { + action: "content", + id: this.id, + header: knownState?.header ? undefined : this.header, + new: Object.fromEntries( + Object.entries(this.sessions) + .map(([sessionID, log]) => { + const newTransactions = log.transactions.slice( + knownState?.sessions[sessionID as SessionID] || 0 + ); + + if ( + newTransactions.length === 0 || + !log.lastHash || + !log.lastSignature + ) { + return undefined; + } + + return [ + sessionID, + { + after: + knownState?.sessions[ + sessionID as SessionID + ] || 0, + newTransactions, + lastSignature: log.lastSignature, + }, + ]; + }) + .filter((x): x is Exclude => !!x) + ), + }; + + if ( + !newContent.header && + Object.keys(newContent.new).length === 0 + ) { + return undefined; + } + + return newContent; + } + + getDependedOnCoValues(): RawCoID[] { + return this.header.ruleset.type === "team" + ? expectTeamContent(this.getCurrentContent()) + .keys() + .filter((k): k is AccountID => k.startsWith("co_")) + : this.header.ruleset.type === "ownedByTeam" + ? [this.header.ruleset.team] + : []; + } +} \ No newline at end of file diff --git a/src/contentType.test.ts b/src/contentType.test.ts new file mode 100644 index 000000000..5cdb4988e --- /dev/null +++ b/src/contentType.test.ts @@ -0,0 +1,175 @@ +import { accountOrAgentIDfromSessionID } from "./coValue.js"; +import { createdNowUnique } from "./crypto.js"; +import { LocalNode } from "./node.js"; +import { randomAnonymousAccountAndSessionID } from "./testUtils.js"; + +test("Empty COJSON Map works", () => { + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); + + const coValue = node.createCoValue({ + type: "comap", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const content = coValue.getCurrentContent(); + + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + expect(content.type).toEqual("comap"); + expect([...content.keys()]).toEqual([]); + expect(content.toJSON()).toEqual({}); +}); + +test("Can insert and delete Map entries in edit()", () => { + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); + + const coValue = node.createCoValue({ + type: "comap", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const content = coValue.getCurrentContent(); + + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + expect(content.type).toEqual("comap"); + + content.edit((editable) => { + editable.set("hello", "world", "trusting"); + expect(editable.get("hello")).toEqual("world"); + editable.set("foo", "bar", "trusting"); + expect(editable.get("foo")).toEqual("bar"); + expect([...editable.keys()]).toEqual(["hello", "foo"]); + editable.delete("foo", "trusting"); + expect(editable.get("foo")).toEqual(undefined); + }); +}); + +test("Can get map entry values at different points in time", () => { + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); + + const coValue = node.createCoValue({ + type: "comap", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const content = coValue.getCurrentContent(); + + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + expect(content.type).toEqual("comap"); + + content.edit((editable) => { + const beforeA = Date.now(); + while (Date.now() < beforeA + 10) {} + editable.set("hello", "A", "trusting"); + const beforeB = Date.now(); + while (Date.now() < beforeB + 10) {} + editable.set("hello", "B", "trusting"); + const beforeC = Date.now(); + while (Date.now() < beforeC + 10) {} + editable.set("hello", "C", "trusting"); + expect(editable.get("hello")).toEqual("C"); + expect(editable.getAtTime("hello", Date.now())).toEqual("C"); + expect(editable.getAtTime("hello", beforeA)).toEqual(undefined); + expect(editable.getAtTime("hello", beforeB)).toEqual("A"); + expect(editable.getAtTime("hello", beforeC)).toEqual("B"); + }); +}); + +test("Can get all historic values of key", () => { + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); + + const coValue = node.createCoValue({ + type: "comap", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const content = coValue.getCurrentContent(); + + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + expect(content.type).toEqual("comap"); + + content.edit((editable) => { + editable.set("hello", "A", "trusting"); + const txA = editable.getLastTxID("hello"); + editable.set("hello", "B", "trusting"); + const txB = editable.getLastTxID("hello"); + editable.delete("hello", "trusting"); + const txDel = editable.getLastTxID("hello"); + editable.set("hello", "C", "trusting"); + const txC = editable.getLastTxID("hello"); + expect(editable.getHistory("hello")).toEqual([ + { + txID: txA, + value: "A", + at: txA && coValue.getTx(txA)?.madeAt, + }, + { + txID: txB, + value: "B", + at: txB && coValue.getTx(txB)?.madeAt, + }, + { + txID: txDel, + value: undefined, + at: txDel && coValue.getTx(txDel)?.madeAt, + }, + { + txID: txC, + value: "C", + at: txC && coValue.getTx(txC)?.madeAt, + }, + ]); + }); +}); + +test("Can get last tx ID for a key", () => { + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); + + const coValue = node.createCoValue({ + type: "comap", + ruleset: { type: "unsafeAllowAll" }, + meta: null, + ...createdNowUnique(), + }); + + const content = coValue.getCurrentContent(); + + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + expect(content.type).toEqual("comap"); + + content.edit((editable) => { + expect(editable.getLastTxID("hello")).toEqual(undefined); + editable.set("hello", "A", "trusting"); + const sessionID = editable.getLastTxID("hello")?.sessionID; + expect(sessionID && accountOrAgentIDfromSessionID(sessionID)).toEqual( + node.account.id + ); + expect(editable.getLastTxID("hello")?.txIndex).toEqual(0); + editable.set("hello", "B", "trusting"); + expect(editable.getLastTxID("hello")?.txIndex).toEqual(1); + editable.set("hello", "C", "trusting"); + expect(editable.getLastTxID("hello")?.txIndex).toEqual(2); + }); +}); diff --git a/src/contentType.ts b/src/contentType.ts new file mode 100644 index 000000000..6b4eb268c --- /dev/null +++ b/src/contentType.ts @@ -0,0 +1,26 @@ +import { JsonObject, JsonValue } from "./jsonValue.js"; +import { RawCoID } from "./ids.js"; +import { CoMap } from "./contentTypes/coMap.js"; +import { CoStream } from "./contentTypes/coStream.js"; +import { Static } from "./contentTypes/static.js"; +import { CoList } from "./contentTypes/coList.js"; + +export type CoID = RawCoID & { + readonly __type: T; +}; + +export type ContentType = + | CoMap<{ [key: string]: JsonValue }, JsonObject | null> + | CoList + | CoStream + | Static; + +export function expectMap( + content: ContentType +): CoMap<{ [key: string]: string }, JsonObject | null> { + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + return content as CoMap<{ [key: string]: string }, JsonObject | null>; +} diff --git a/src/contentTypes/coList.ts b/src/contentTypes/coList.ts new file mode 100644 index 000000000..730ed3937 --- /dev/null +++ b/src/contentTypes/coList.ts @@ -0,0 +1,24 @@ +import { JsonObject, JsonValue } from '../jsonValue.js'; +import { CoID } from '../contentType.js'; +import { CoValue } from '../coValue.js'; + +export class CoList { + id: CoID>; + type = "colist" as const; + coValue: CoValue; + + constructor(coValue: CoValue) { + this.id = coValue.id as CoID>; + this.coValue = coValue; + } + + toJSON(): JsonObject { + throw new Error("Method not implemented."); + } + + subscribe(listener: (coMap: CoList) => void): () => void { + return this.coValue.subscribe((content) => { + listener(content as CoList); + }); + } +} diff --git a/src/contentTypes/coMap.ts b/src/contentTypes/coMap.ts new file mode 100644 index 000000000..8965c5f40 --- /dev/null +++ b/src/contentTypes/coMap.ts @@ -0,0 +1,221 @@ +import { JsonObject, JsonValue } from '../jsonValue.js'; +import { TransactionID } from '../ids.js'; +import { CoID } from '../contentType.js'; +import { CoValue, accountOrAgentIDfromSessionID } from '../coValue.js'; +import { AccountID, isAccountID } from '../account.js'; + +type MapOp = { + txID: TransactionID; + madeAt: number; + changeIdx: number; +} & MapOpPayload; +// TODO: add after TransactionID[] for conflicts/ordering + +export type MapOpPayload = { + op: "insert"; + key: K; + value: V; +} | +{ + op: "delete"; + key: K; +}; + +export type MapK = keyof M & string; +export type MapV = M[MapK]; +export type MapM = { + [KK in MapK]: M[KK]; +} + +export class CoMap< + M extends { [key: string]: JsonValue; }, + Meta extends JsonObject | null = null, +> { + id: CoID, Meta>>; + coValue: CoValue; + type = "comap" as const; + ops: { + [KK in MapK]?: MapOp[]; + }; + + constructor(coValue: CoValue) { + this.id = coValue.id as CoID, Meta>>; + this.coValue = coValue; + this.ops = {}; + + this.fillOpsFromCoValue(); + } + + protected fillOpsFromCoValue() { + this.ops = {}; + + for (const { txID, changes, madeAt } of this.coValue.getValidSortedTransactions()) { + for (const [changeIdx, changeUntyped] of ( + changes + ).entries()) { + const change = changeUntyped as MapOpPayload, MapV>; + let entries = this.ops[change.key]; + if (!entries) { + entries = []; + this.ops[change.key] = entries; + } + entries.push({ + txID, + madeAt, + changeIdx, + ...(change as MapOpPayload, MapV>), + }); + } + } + } + + keys(): MapK[] { + return Object.keys(this.ops) as MapK[]; + } + + get>(key: K): M[K] | undefined { + const ops = this.ops[key]; + if (!ops) { + return undefined; + } + + const lastEntry = ops[ops.length - 1]!; + + if (lastEntry.op === "delete") { + return undefined; + } else { + return lastEntry.value; + } + } + + getAtTime>(key: K, time: number): M[K] | undefined { + const ops = this.ops[key]; + if (!ops) { + return undefined; + } + + const lastOpBeforeOrAtTime = ops.findLast((op) => op.madeAt <= time); + + if (!lastOpBeforeOrAtTime) { + return undefined; + } + + if (lastOpBeforeOrAtTime.op === "delete") { + return undefined; + } else { + return lastOpBeforeOrAtTime.value; + } + } + + getLastEditor>(key: K): AccountID | undefined { + const tx = this.getLastTxID(key); + if (!tx) { + return undefined; + } + const accountID = accountOrAgentIDfromSessionID(tx.sessionID); + if (isAccountID(accountID)) { + return accountID; + } else { + return undefined; + } + } + + getLastTxID>(key: K): TransactionID | undefined { + const ops = this.ops[key]; + if (!ops) { + return undefined; + } + + const lastEntry = ops[ops.length - 1]!; + + return lastEntry.txID; + } + + getLastEntry>(key: K): { at: number; txID: TransactionID; value: M[K]; } | undefined { + const ops = this.ops[key]; + if (!ops) { + return undefined; + } + + const lastEntry = ops[ops.length - 1]!; + + if (lastEntry.op === "delete") { + return undefined; + } else { + return { at: lastEntry.madeAt, txID: lastEntry.txID, value: lastEntry.value }; + } + } + + getHistory>(key: K): { at: number; txID: TransactionID; value: M[K] | undefined; }[] { + const ops = this.ops[key]; + if (!ops) { + return []; + } + + const history: { at: number; txID: TransactionID; value: M[K] | undefined; }[] = []; + + for (const op of ops) { + if (op.op === "delete") { + history.push({ at: op.madeAt, txID: op.txID, value: undefined }); + } else { + history.push({ at: op.madeAt, txID: op.txID, value: op.value }); + } + } + + return history; + } + + toJSON(): JsonObject { + const json: JsonObject = {}; + + for (const key of this.keys()) { + const value = this.get(key); + if (value !== undefined) { + json[key] = value; + } + } + + return json; + } + + edit(changer: (editable: WriteableCoMap) => void): CoMap { + const editable = new WriteableCoMap(this.coValue); + changer(editable); + return new CoMap(this.coValue); + } + + subscribe(listener: (coMap: CoMap) => void): () => void { + return this.coValue.subscribe((content) => { + listener(content as CoMap); + }); + } +} + +export class WriteableCoMap< + M extends { [key: string]: JsonValue; }, + Meta extends JsonObject | null = null, + +> extends CoMap { + set>(key: K, value: M[K], privacy: "private" | "trusting" = "private"): void { + this.coValue.makeTransaction([ + { + op: "insert", + key, + value, + }, + ], privacy); + + this.fillOpsFromCoValue(); + } + + delete(key: MapK, privacy: "private" | "trusting" = "private"): void { + this.coValue.makeTransaction([ + { + op: "delete", + key, + }, + ], privacy); + + this.fillOpsFromCoValue(); + } +} diff --git a/src/contentTypes/coStream.ts b/src/contentTypes/coStream.ts new file mode 100644 index 000000000..be479f398 --- /dev/null +++ b/src/contentTypes/coStream.ts @@ -0,0 +1,24 @@ +import { JsonObject, JsonValue } from '../jsonValue.js'; +import { CoID } from '../contentType.js'; +import { CoValue } from '../coValue.js'; + +export class CoStream { + id: CoID>; + type = "costream" as const; + coValue: CoValue; + + constructor(coValue: CoValue) { + this.id = coValue.id as CoID>; + this.coValue = coValue; + } + + toJSON(): JsonObject { + throw new Error("Method not implemented."); + } + + subscribe(listener: (coMap: CoStream) => void): () => void { + return this.coValue.subscribe((content) => { + listener(content as CoStream); + }); + } +} diff --git a/src/contentTypes/static.ts b/src/contentTypes/static.ts new file mode 100644 index 000000000..47c5fdc85 --- /dev/null +++ b/src/contentTypes/static.ts @@ -0,0 +1,22 @@ +import { JsonObject } from '../jsonValue.js'; +import { CoID } from '../contentType.js'; +import { CoValue } from '../coValue.js'; + +export class Static { + id: CoID>; + type = "static" as const; + coValue: CoValue; + + constructor(coValue: CoValue) { + this.id = coValue.id as CoID>; + this.coValue = coValue; + } + + toJSON(): JsonObject { + throw new Error("Method not implemented."); + } + + subscribe(_listener: (coMap: Static) => void): () => void { + throw new Error("Method not implemented."); + } +} diff --git a/src/crypto.test.ts b/src/crypto.test.ts new file mode 100644 index 000000000..2248d6b58 --- /dev/null +++ b/src/crypto.test.ts @@ -0,0 +1,183 @@ +import { + getSealerID, + getSignerID, + secureHash, + newRandomSealer, + newRandomSigner, + seal, + sign, + unseal, + verify, + shortHash, + newRandomKeySecret, + encryptForTransaction, + decryptForTransaction, + encryptKeySecret, + decryptKeySecret, +} from './crypto.js'; +import { base58, base64url } from "@scure/base"; +import { x25519 } from "@noble/curves/ed25519"; +import { xsalsa20_poly1305 } from "@noble/ciphers/salsa"; +import { blake3 } from "@noble/hashes/blake3"; +import stableStringify from "fast-json-stable-stringify"; + +test("Signatures round-trip and use stable stringify", () => { + const data = { b: "world", a: "hello" }; + const signer = newRandomSigner(); + const signature = sign(signer, data); + + expect(signature).toMatch(/^signature_z/); + expect( + verify(signature, { a: "hello", b: "world" }, getSignerID(signer)) + ).toBe(true); +}); + +test("Invalid signatures don't verify", () => { + const data = { b: "world", a: "hello" }; + const signer = newRandomSigner(); + const signer2 = newRandomSigner(); + const wrongSignature = sign(signer2, data); + + expect(verify(wrongSignature, data, getSignerID(signer))).toBe(false); +}); + +test("encrypting round-trips, but invalid receiver can't unseal", () => { + const data = { b: "world", a: "hello" }; + const sender = newRandomSealer(); + const sealer = newRandomSealer(); + const wrongSealer = newRandomSealer(); + + const nOnceMaterial = { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 }, + } as const; + + const sealed = seal( + data, + sender, + getSealerID(sealer), + nOnceMaterial + ); + + expect( + unseal(sealed, sealer, getSealerID(sender), nOnceMaterial) + ).toEqual(data); + expect( + () => unseal(sealed, wrongSealer, getSealerID(sender), nOnceMaterial) + ).toThrow(/Wrong tag/); + + // trying with wrong sealer secret, by hand + const nOnce = blake3( + new TextEncoder().encode(stableStringify(nOnceMaterial)) + ).slice(0, 24); + const sealer3priv = base58.decode( + wrongSealer.substring("sealerSecret_z".length) + ); + const senderPub = base58.decode( + getSealerID(sender).substring("sealer_z".length) + ); + const sealedBytes = base64url.decode( + sealed.substring("sealed_U".length) + ); + const sharedSecret = x25519.getSharedSecret(sealer3priv, senderPub); + + expect(() => { + const _ = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(sealedBytes); + }).toThrow("Wrong tag"); +}); + +test("Hashing is deterministic", () => { + expect(secureHash({ b: "world", a: "hello" })).toEqual( + secureHash({ a: "hello", b: "world" }) + ); + + expect(shortHash({ b: "world", a: "hello" })).toEqual( + shortHash({ a: "hello", b: "world" }) + ); +}); + +test("Encryption for transactions round-trips", () => { + const { secret } = newRandomKeySecret(); + + const encrypted1 = encryptForTransaction({ a: "hello" }, secret, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 }, + }); + + const encrypted2 = encryptForTransaction({ b: "world" }, secret, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 }, + }); + + const decrypted1 = decryptForTransaction(encrypted1, secret, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 }, + }); + + const decrypted2 = decryptForTransaction(encrypted2, secret, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 }, + }); + + expect([decrypted1, decrypted2]).toEqual([{ a: "hello" }, { b: "world" }]); +}); + +test("Encryption for transactions doesn't decrypt with a wrong key", () => { + const { secret } = newRandomKeySecret(); + const { secret: secret2 } = newRandomKeySecret(); + + const encrypted1 = encryptForTransaction({ a: "hello" }, secret, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 }, + }); + + const encrypted2 = encryptForTransaction({ b: "world" }, secret, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 }, + }); + + const decrypted1 = decryptForTransaction(encrypted1, secret2, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 }, + }); + + const decrypted2 = decryptForTransaction(encrypted2, secret2, { + in: "co_zTEST", + tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 }, + }); + + expect([decrypted1, decrypted2]).toEqual([undefined, undefined]); +}); + +test("Encryption of keySecrets round-trips", () => { + const toEncrypt = newRandomKeySecret(); + const encrypting = newRandomKeySecret(); + + const keys = { + toEncrypt, + encrypting, + }; + + const encrypted = encryptKeySecret(keys); + + const decrypted = decryptKeySecret(encrypted, encrypting.secret); + + expect(decrypted).toEqual(toEncrypt.secret); +}); + +test("Encryption of keySecrets doesn't decrypt with a wrong key", () => { + const toEncrypt = newRandomKeySecret(); + const encrypting = newRandomKeySecret(); + const encryptingWrong = newRandomKeySecret(); + + const keys = { + toEncrypt, + encrypting, + }; + + const encrypted = encryptKeySecret(keys); + + const decrypted = decryptKeySecret(encrypted, encryptingWrong.secret); + + expect(decrypted).toBeUndefined(); +}); diff --git a/src/crypto.ts b/src/crypto.ts new file mode 100644 index 000000000..f3cdb03f9 --- /dev/null +++ b/src/crypto.ts @@ -0,0 +1,353 @@ +import { ed25519, x25519 } from "@noble/curves/ed25519"; +import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa"; +import { JsonValue } from './jsonValue.js'; +import { base58, base64url } from "@scure/base"; +import stableStringify from "fast-json-stable-stringify"; +import { blake3 } from "@noble/hashes/blake3"; +import { randomBytes } from "@noble/ciphers/webcrypto/utils"; +import { AgentID, RawCoID, TransactionID } from './ids.js'; + +export type SignerSecret = `signerSecret_z${string}`; +export type SignerID = `signer_z${string}`; +export type Signature = `signature_z${string}`; + +export type SealerSecret = `sealerSecret_z${string}`; +export type SealerID = `sealer_z${string}`; +export type Sealed = `sealed_U${string}` & { __type: T }; + +export type AgentSecret = `${SealerSecret}/${SignerSecret}`; + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +export function newRandomSigner(): SignerSecret { + return `signerSecret_z${base58.encode( + ed25519.utils.randomPrivateKey() + )}`; +} + +export function signerSecretToBytes(secret: SignerSecret): Uint8Array { + return base58.decode(secret.substring("signerSecret_z".length)); +} + +export function signerSecretFromBytes(bytes: Uint8Array): SignerSecret { + return `signerSecret_z${base58.encode(bytes)}`; +} + +export function getSignerID(secret: SignerSecret): SignerID { + return `signer_z${base58.encode( + ed25519.getPublicKey( + base58.decode(secret.substring("signerSecret_z".length)) + ) + )}`; +} + +export function sign(secret: SignerSecret, message: JsonValue): Signature { + const signature = ed25519.sign( + textEncoder.encode(stableStringify(message)), + base58.decode(secret.substring("signerSecret_z".length)) + ); + return `signature_z${base58.encode(signature)}`; +} + +export function verify( + signature: Signature, + message: JsonValue, + id: SignerID +): boolean { + return ed25519.verify( + base58.decode(signature.substring("signature_z".length)), + textEncoder.encode(stableStringify(message)), + base58.decode(id.substring("signer_z".length)) + ); +} + +export function newRandomSealer(): SealerSecret { + return `sealerSecret_z${base58.encode(x25519.utils.randomPrivateKey())}`; +} + +export function sealerSecretToBytes(secret: SealerSecret): Uint8Array { + return base58.decode(secret.substring("sealerSecret_z".length)); +} + +export function sealerSecretFromBytes(bytes: Uint8Array): SealerSecret { + return `sealerSecret_z${base58.encode(bytes)}`; +} + +export function getSealerID(secret: SealerSecret): SealerID { + return `sealer_z${base58.encode( + x25519.getPublicKey( + base58.decode(secret.substring("sealerSecret_z".length)) + ) + )}`; +} + +export function newRandomAgentSecret(): AgentSecret { + return `${newRandomSealer()}/${newRandomSigner()}`; +} + +export function agentSecretToBytes(secret: AgentSecret): Uint8Array { + const [sealerSecret, signerSecret] = secret.split("/"); + return new Uint8Array([ + ...sealerSecretToBytes(sealerSecret as SealerSecret), + ...signerSecretToBytes(signerSecret as SignerSecret), + ]); +} + +export function agentSecretFromBytes(bytes: Uint8Array): AgentSecret { + const sealerSecret = sealerSecretFromBytes( + bytes.slice(0, 32) + ); + const signerSecret = signerSecretFromBytes( + bytes.slice(32) + ); + return `${sealerSecret}/${signerSecret}`; +} + +export function getAgentID(secret: AgentSecret): AgentID { + const [sealerSecret, signerSecret] = secret.split("/"); + return `${getSealerID( + sealerSecret as SealerSecret + )}/${getSignerID(signerSecret as SignerSecret)}`; +} + +export function getAgentSignerID(agentId: AgentID): SignerID { + return agentId.split("/")[1] as SignerID; +} + +export function getAgentSignerSecret(agentSecret: AgentSecret): SignerSecret { + return agentSecret.split("/")[1] as SignerSecret; +} + +export function getAgentSealerID(agentId: AgentID): SealerID { + return agentId.split("/")[0] as SealerID; +} + +export function getAgentSealerSecret(agentSecret: AgentSecret): SealerSecret { + return agentSecret.split("/")[0] as SealerSecret; +} + +export function seal( + message: T, + from: SealerSecret, + to: SealerID, + nOnceMaterial: { in: RawCoID; tx: TransactionID } +): Sealed { + const nOnce = blake3( + textEncoder.encode(stableStringify(nOnceMaterial)) + ).slice(0, 24); + + const sealerPub = base58.decode(to.substring("sealer_z".length)); + + const senderPriv = base58.decode( + from.substring("sealerSecret_z".length) + ); + + const plaintext = textEncoder.encode(stableStringify(message)); + + const sharedSecret = x25519.getSharedSecret( + senderPriv, + sealerPub + ); + + const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt( + plaintext + ); + + return `sealed_U${base64url.encode( + sealedBytes + )}` as Sealed +} + +export function unseal( + sealed: Sealed, + sealer: SealerSecret, + from: SealerID, + nOnceMaterial: { in: RawCoID; tx: TransactionID } +): T | undefined { + const nOnce = blake3( + textEncoder.encode(stableStringify(nOnceMaterial)) + ).slice(0, 24); + + const sealerPriv = base58.decode( + sealer.substring("sealerSecret_z".length) + ); + + const senderPub = base58.decode(from.substring("sealer_z".length)); + + const sealedBytes = base64url.decode(sealed.substring("sealed_U".length)); + + const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub); + + const plaintext = xsalsa20_poly1305(sharedSecret, nOnce).decrypt( + sealedBytes + ); + + try { + return JSON.parse(textDecoder.decode(plaintext)); + } catch (e) { + console.error("Failed to decrypt/parse sealed message", e); + return undefined; + } +} + +export type Hash = `hash_z${string}`; + +export function secureHash(value: JsonValue): Hash { + return `hash_z${base58.encode( + blake3(textEncoder.encode(stableStringify(value))) + )}`; +} + +export class StreamingHash { + state: ReturnType; + + constructor(fromClone?: ReturnType) { + this.state = fromClone || blake3.create({}); + } + + update(value: JsonValue) { + this.state.update(textEncoder.encode(stableStringify(value))); + } + + digest(): Hash { + const hash = this.state.digest(); + return `hash_z${base58.encode(hash)}`; + } + + clone(): StreamingHash { + return new StreamingHash(this.state.clone()); + } +} + +export type ShortHash = `shortHash_z${string}`; + +export function shortHash(value: JsonValue): ShortHash { + return `shortHash_z${base58.encode( + blake3(textEncoder.encode(stableStringify(value))).slice(0, 19) + )}`; +} + +export type Encrypted< + T extends JsonValue, + N extends JsonValue +> = `encrypted_U${string}` & { __type: T; __nOnceMaterial: N }; + +export type KeySecret = `keySecret_z${string}`; +export type KeyID = `key_z${string}`; + +export function newRandomKeySecret(): { secret: KeySecret; id: KeyID } { + return { + secret: `keySecret_z${base58.encode(randomBytes(32))}`, + id: `key_z${base58.encode(randomBytes(12))}`, + }; +} + +function encrypt( + value: T, + keySecret: KeySecret, + nOnceMaterial: N +): Encrypted { + const keySecretBytes = base58.decode( + keySecret.substring("keySecret_z".length) + ); + const nOnce = blake3( + textEncoder.encode(stableStringify(nOnceMaterial)) + ).slice(0, 24); + + const plaintext = textEncoder.encode(stableStringify(value)); + const ciphertext = xsalsa20(keySecretBytes, nOnce, plaintext); + return `encrypted_U${base64url.encode(ciphertext)}` as Encrypted; +} + +export function encryptForTransaction( + value: T, + keySecret: KeySecret, + nOnceMaterial: { in: RawCoID; tx: TransactionID } +): Encrypted { + return encrypt(value, keySecret, nOnceMaterial); +} + +export function encryptKeySecret(keys: { + toEncrypt: { id: KeyID; secret: KeySecret }; + encrypting: { id: KeyID; secret: KeySecret }; +}): { + encryptedID: KeyID; + encryptingID: KeyID; + encrypted: Encrypted; +} { + const nOnceMaterial = { + encryptedID: keys.toEncrypt.id, + encryptingID: keys.encrypting.id, + }; + + return { + encryptedID: keys.toEncrypt.id, + encryptingID: keys.encrypting.id, + encrypted: encrypt( + keys.toEncrypt.secret, + keys.encrypting.secret, + nOnceMaterial + ), + }; +} + +function decrypt( + encrypted: Encrypted, + keySecret: KeySecret, + nOnceMaterial: N +): T | undefined { + const keySecretBytes = base58.decode( + keySecret.substring("keySecret_z".length) + ); + const nOnce = blake3( + textEncoder.encode(stableStringify(nOnceMaterial)) + ).slice(0, 24); + + const ciphertext = base64url.decode( + encrypted.substring("encrypted_U".length) + ); + const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext); + + try { + return JSON.parse(textDecoder.decode(plaintext)); + } catch (e) { + return undefined; + } +} + +export function decryptForTransaction( + encrypted: Encrypted, + keySecret: KeySecret, + nOnceMaterial: { in: RawCoID; tx: TransactionID } +): T | undefined { + return decrypt(encrypted, keySecret, nOnceMaterial); +} + +export function decryptKeySecret( + encryptedInfo: { + encryptedID: KeyID; + encryptingID: KeyID; + encrypted: Encrypted; + }, + sealingSecret: KeySecret +): KeySecret | undefined { + const nOnceMaterial = { + encryptedID: encryptedInfo.encryptedID, + encryptingID: encryptedInfo.encryptingID, + }; + + return decrypt(encryptedInfo.encrypted, sealingSecret, nOnceMaterial); +} + +export function uniquenessForHeader(): `z${string}` { + return `z${base58.encode(randomBytes(12))}`; +} + +export function createdNowUnique(): {createdAt: `2${string}`, uniqueness: `z${string}`} { + const createdAt = (new Date()).toISOString() as `2${string}`; + return { + createdAt, + uniqueness: uniquenessForHeader(), + } +} \ No newline at end of file diff --git a/src/ids.ts b/src/ids.ts new file mode 100644 index 000000000..d34a2a474 --- /dev/null +++ b/src/ids.ts @@ -0,0 +1,13 @@ +import { AccountIDOrAgentID } from './account.js'; + +export type RawCoID = `co_z${string}`; + +export type TransactionID = { sessionID: SessionID; txIndex: number }; + +export type AgentID = `sealer_z${string}/signer_z${string}`; + +export function isAgentID(id: string): id is AgentID { + return typeof id === "string" && id.startsWith("sealer_") && id.includes("/signer_"); +} + +export type SessionID = `${AccountIDOrAgentID}_session_z${string}`; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..d0b552a29 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,32 @@ +import { CoValue, newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { CoMap } from "./contentTypes/coMap.js"; +import { agentSecretFromBytes, agentSecretToBytes } from "./crypto.js"; +import { connectedPeers } from "./streamUtils.js"; + +import type { SessionID } from "./ids.js"; +import type { CoID, ContentType } from "./contentType.js"; +import type { JsonValue } from "./jsonValue.js"; +import type { SyncMessage } from "./sync.js"; +import type { AgentSecret } from "./crypto.js"; + +type Value = JsonValue | ContentType; + +const internals = { + agentSecretFromBytes, + agentSecretToBytes, + newRandomSessionID, + connectedPeers +}; + +export { LocalNode, CoValue, CoMap, internals }; + +export type { + Value, + JsonValue, + ContentType, + CoID, + AgentSecret, + SessionID, + SyncMessage, +}; diff --git a/src/jsonValue.ts b/src/jsonValue.ts new file mode 100644 index 000000000..1c2474080 --- /dev/null +++ b/src/jsonValue.ts @@ -0,0 +1,6 @@ +import { CoID, ContentType } from './contentType.js'; + +export type JsonAtom = string | number | boolean | null; +export type JsonValue = JsonAtom | JsonArray | JsonObject | CoID; +export type JsonArray = JsonValue[]; +export type JsonObject = { [key: string]: JsonValue; }; diff --git a/src/node.ts b/src/node.ts new file mode 100644 index 000000000..7279cc245 --- /dev/null +++ b/src/node.ts @@ -0,0 +1,324 @@ +import { + AgentSecret, + createdNowUnique, + getAgentID, + getAgentSealerID, + getAgentSealerSecret, + newRandomAgentSecret, + newRandomKeySecret, + seal, +} from "./crypto.js"; +import { CoValue, CoValueHeader, newRandomSessionID } from "./coValue.js"; +import { Team, TeamContent, expectTeamContent } from "./permissions.js"; +import { Peer, SyncManager } from "./sync.js"; +import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js"; +import { CoID, ContentType } from "./contentType.js"; +import { + Account, + AccountMeta, + AccountIDOrAgentID, + accountHeaderForInitialAgentSecret, + GeneralizedControlledAccount, + ControlledAccount, + AnonymousControlledAccount, + AccountID, + Profile, + AccountContent, + ProfileContent, + ProfileMeta, +} from "./account.js"; +import { CoMap } from "./index.js"; + +export class LocalNode { + coValues: { [key: RawCoID]: CoValueState } = {}; + account: GeneralizedControlledAccount; + ownSessionID: SessionID; + sync = new SyncManager(this); + + constructor( + account: GeneralizedControlledAccount, + ownSessionID: SessionID + ) { + this.account = account; + this.ownSessionID = ownSessionID; + } + + static withNewlyCreatedAccount(name: string): { + node: LocalNode; + accountID: AccountID; + accountSecret: AgentSecret; + sessionID: SessionID; + } { + const throwawayAgent = newRandomAgentSecret(); + const setupNode = new LocalNode( + new AnonymousControlledAccount(throwawayAgent), + newRandomSessionID(getAgentID(throwawayAgent)) + ); + + const account = setupNode.createAccount(name); + + const nodeWithAccount = account.node.testWithDifferentAccount( + account, + newRandomSessionID(account.id) + ); + + return { + node: nodeWithAccount, + accountID: account.id, + accountSecret: account.agentSecret, + sessionID: nodeWithAccount.ownSessionID, + }; + } + + static async withLoadedAccount(accountID: AccountID, accountSecret: AgentSecret, sessionID: SessionID, peersToLoadFrom: Peer[]): Promise { + const loadingNode = new LocalNode(new AnonymousControlledAccount(accountSecret), newRandomSessionID(accountID)); + + const accountPromise = loadingNode.load(accountID); + + for (const peer of peersToLoadFrom) { + loadingNode.sync.addPeer(peer); + } + + const account = await accountPromise; + + // since this is all synchronous, we can just swap out nodes for the SyncManager + const node = loadingNode.testWithDifferentAccount(new ControlledAccount(accountSecret, account, loadingNode), sessionID); + node.sync = loadingNode.sync; + node.sync.local = node; + + return node; + } + + createCoValue(header: CoValueHeader): CoValue { + const coValue = new CoValue(header, this); + this.coValues[coValue.id] = { state: "loaded", coValue: coValue }; + + void this.sync.syncCoValue(coValue); + + return coValue; + } + + loadCoValue(id: RawCoID): Promise { + let entry = this.coValues[id]; + if (!entry) { + entry = newLoadingState(); + + this.coValues[id] = entry; + + this.sync.loadFromPeers(id); + } + if (entry.state === "loaded") { + return Promise.resolve(entry.coValue); + } + return entry.done; + } + + async load(id: CoID): Promise { + return (await this.loadCoValue(id)).getCurrentContent() as T; + } + + async loadProfile(id: AccountID): Promise { + const account = await this.load>(id); + const profileID = account.get("profile"); + + if (!profileID) { + throw new Error(`Account ${id} has no profile`); + } + return (await this.loadCoValue(profileID)).getCurrentContent() as Profile; + } + + expectCoValueLoaded(id: RawCoID, expectation?: string): CoValue { + const entry = this.coValues[id]; + if (!entry) { + throw new Error( + `${expectation ? expectation + ": " : ""}Unknown CoValue ${id}` + ); + } + if (entry.state === "loading") { + throw new Error( + `${ + expectation ? expectation + ": " : "" + }CoValue ${id} not yet loaded` + ); + } + return entry.coValue; + } + + expectProfileLoaded(id: AccountID, expectation?: string): Profile { + const account = this.expectCoValueLoaded(id, expectation); + const profileID = expectTeamContent(account.getCurrentContent()).get("profile"); + if (!profileID) { + throw new Error( + `${ + expectation ? expectation + ": " : "" + }Account ${id} has no profile` + ); + } + return this.expectCoValueLoaded(profileID, expectation).getCurrentContent() as Profile; + } + + createAccount(name: string): ControlledAccount { + const agentSecret = newRandomAgentSecret(); + + const account = this.createCoValue( + accountHeaderForInitialAgentSecret(agentSecret) + ).testWithDifferentAccount( + new AnonymousControlledAccount(agentSecret), + newRandomSessionID(getAgentID(agentSecret)) + ); + + const accountAsTeam = new Team(expectTeamContent(account.getCurrentContent()), account.node); + + accountAsTeam.teamMap.edit((editable) => { + editable.set(getAgentID(agentSecret), "admin", "trusting"); + + const readKey = newRandomKeySecret(); + + editable.set( + `${readKey.id}_for_${getAgentID(agentSecret)}`, + seal( + readKey.secret, + getAgentSealerSecret(agentSecret), + getAgentSealerID(getAgentID(agentSecret)), + { + in: account.id, + tx: account.nextTransactionID(), + } + ), + "trusting" + ); + + editable.set("readKey", readKey.id, "trusting"); + }); + + const controlledAccount = new ControlledAccount( + agentSecret, + account.getCurrentContent() as CoMap, + account.node + ); + + const profile = accountAsTeam.createMap({ type: "profile" }); + + profile.edit((editable) => { + editable.set("name", name, "trusting"); + }); + + accountAsTeam.teamMap.edit((editable) => { + editable.set("profile", profile.id, "trusting"); + }); + + return controlledAccount; + } + + resolveAccountAgent(id: AccountIDOrAgentID, expectation?: string): AgentID { + if (isAgentID(id)) { + return id; + } + + const coValue = this.expectCoValueLoaded(id, expectation); + + if ( + coValue.header.type !== "comap" || + coValue.header.ruleset.type !== "team" || + !coValue.header.meta || + !("type" in coValue.header.meta) || + coValue.header.meta.type !== "account" + ) { + throw new Error( + `${ + expectation ? expectation + ": " : "" + }CoValue ${id} is not an account` + ); + } + + return new Account( + coValue.getCurrentContent() as CoMap, + this + ).getCurrentAgentID(); + } + + createTeam(): Team { + const teamCoValue = this.createCoValue({ + type: "comap", + ruleset: { type: "team", initialAdmin: this.account.id }, + meta: null, + ...createdNowUnique(), + }); + + let teamContent = expectTeamContent(teamCoValue.getCurrentContent()); + + teamContent = teamContent.edit((editable) => { + editable.set(this.account.id, "admin", "trusting"); + + const readKey = newRandomKeySecret(); + + editable.set( + `${readKey.id}_for_${this.account.id}`, + seal( + readKey.secret, + this.account.currentSealerSecret(), + this.account.currentSealerID(), + { + in: teamCoValue.id, + tx: teamCoValue.nextTransactionID(), + } + ), + "trusting" + ); + + editable.set("readKey", readKey.id, "trusting"); + }); + + return new Team(teamContent, this); + } + + testWithDifferentAccount( + account: GeneralizedControlledAccount, + ownSessionID: SessionID + ): LocalNode { + const newNode = new LocalNode(account, ownSessionID); + + newNode.coValues = Object.fromEntries( + Object.entries(this.coValues) + .map(([id, entry]) => { + if (entry.state === "loading") { + return undefined; + } + + const newCoValue = new CoValue( + entry.coValue.header, + newNode + ); + + newCoValue.sessions = entry.coValue.sessions; + + return [id, { state: "loaded", coValue: newCoValue }]; + }) + .filter((x): x is Exclude => !!x) + ); + + return newNode; + } +} + +type CoValueState = + | { + state: "loading"; + done: Promise; + resolve: (coValue: CoValue) => void; + } + | { state: "loaded"; coValue: CoValue }; + +export function newLoadingState(): CoValueState { + let resolve: (coValue: CoValue) => void; + + const promise = new Promise((r) => { + resolve = r; + }); + + return { + state: "loading", + done: promise, + resolve: resolve!, + }; +} diff --git a/src/permissions.test.ts b/src/permissions.test.ts new file mode 100644 index 000000000..9c07956e0 --- /dev/null +++ b/src/permissions.test.ts @@ -0,0 +1,1267 @@ +import { newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { expectMap } from "./contentType.js"; +import { expectTeamContent } from "./permissions.js"; +import { + createdNowUnique, + getSealerID, + newRandomKeySecret, + seal, + encryptKeySecret, +} from "./crypto.js"; +import { + newTeam, + newTeamHighLevel, + teamWithTwoAdmins, + teamWithTwoAdminsHighLevel, +} from "./testUtils.js"; + +test("Initial admin can add another admin to a team", () => { + teamWithTwoAdmins(); +}); + +test("Initial admin can add another admin to a team (high level)", () => { + teamWithTwoAdminsHighLevel(); +}); + +test("Added admin can add a third admin to a team", () => { + const { team, otherAdmin, node } = teamWithTwoAdmins(); + + const teamAsOtherAdmin = team.testWithDifferentAccount( + otherAdmin, + newRandomSessionID(otherAdmin.id) + ); + + let otherContent = expectTeamContent(teamAsOtherAdmin.getCurrentContent()); + + expect(otherContent.get(otherAdmin.id)).toEqual("admin"); + + const thirdAdmin = node.createAccount("thirdAdmin"); + + otherContent.edit((editable) => { + editable.set(thirdAdmin.id, "admin", "trusting"); + expect(editable.get(thirdAdmin.id)).toEqual("admin"); + }); + + otherContent = expectTeamContent(teamAsOtherAdmin.getCurrentContent()); + + expect(otherContent.get(thirdAdmin.id)).toEqual("admin"); +}); + +test("Added adming can add a third admin to a team (high level)", () => { + const { team, otherAdmin, node } = teamWithTwoAdminsHighLevel(); + + const teamAsOtherAdmin = team.testWithDifferentAccount( + otherAdmin, + newRandomSessionID(otherAdmin.id) + ); + + const thirdAdmin = node.createAccount("thirdAdmin"); + + teamAsOtherAdmin.addMember(thirdAdmin.id, "admin"); + + expect(teamAsOtherAdmin.teamMap.get(thirdAdmin.id)).toEqual("admin"); +}); + +test("Admins can't demote other admins in a team", () => { + const { team, admin, otherAdmin } = teamWithTwoAdmins(); + + let teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(otherAdmin.id, "writer", "trusting"); + expect(editable.get(otherAdmin.id)).toEqual("admin"); + }); + + teamContent = expectTeamContent(team.getCurrentContent()); + expect(teamContent.get(otherAdmin.id)).toEqual("admin"); + + const teamAsOtherAdmin = team.testWithDifferentAccount( + otherAdmin, + newRandomSessionID(otherAdmin.id) + ); + + let teamContentAsOtherAdmin = expectTeamContent( + teamAsOtherAdmin.getCurrentContent() + ); + + teamContentAsOtherAdmin.edit((editable) => { + editable.set(admin.id, "writer", "trusting"); + expect(editable.get(admin.id)).toEqual("admin"); + }); + + teamContentAsOtherAdmin = expectTeamContent( + teamAsOtherAdmin.getCurrentContent() + ); + + expect(teamContentAsOtherAdmin.get(admin.id)).toEqual("admin"); +}); + +test("Admins can't demote other admins in a team (high level)", () => { + const { team, admin, otherAdmin } = teamWithTwoAdminsHighLevel(); + + const teamAsOtherAdmin = team.testWithDifferentAccount( + otherAdmin, + newRandomSessionID(otherAdmin.id) + ); + + expect(() => teamAsOtherAdmin.addMember(admin.id, "writer")).toThrow( + "Failed to set role" + ); + + expect(teamAsOtherAdmin.teamMap.get(admin.id)).toEqual("admin"); +}); + +test("Admins an add writers to a team, who can't add admins, writers, or readers", () => { + const { team, node } = newTeam(); + const writer = node.createAccount("writer"); + + let teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(writer.id, "writer", "trusting"); + expect(editable.get(writer.id)).toEqual("writer"); + }); + + teamContent = expectTeamContent(team.getCurrentContent()); + expect(teamContent.get(writer.id)).toEqual("writer"); + + const teamAsWriter = team.testWithDifferentAccount( + writer, + newRandomSessionID(writer.id) + ); + + let teamContentAsWriter = expectTeamContent( + teamAsWriter.getCurrentContent() + ); + + expect(teamContentAsWriter.get(writer.id)).toEqual("writer"); + + const otherAgent = node.createAccount("otherAgent"); + + teamContentAsWriter.edit((editable) => { + editable.set(otherAgent.id, "admin", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); + + editable.set(otherAgent.id, "writer", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); + + editable.set(otherAgent.id, "reader", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); + }); + + teamContentAsWriter = expectTeamContent(teamAsWriter.getCurrentContent()); + + expect(teamContentAsWriter.get(otherAgent.id)).toBeUndefined(); +}); + +test("Admins an add writers to a team, who can't add admins, writers, or readers (high level)", () => { + const { team, node } = newTeamHighLevel(); + + const writer = node.createAccount("writer"); + + team.addMember(writer.id, "writer"); + expect(team.teamMap.get(writer.id)).toEqual("writer"); + + const teamAsWriter = team.testWithDifferentAccount( + writer, + newRandomSessionID(writer.id) + ); + + expect(teamAsWriter.teamMap.get(writer.id)).toEqual("writer"); + + const otherAgent = node.createAccount("otherAgent"); + + expect(() => teamAsWriter.addMember(otherAgent.id, "admin")).toThrow( + "Failed to set role" + ); + expect(() => teamAsWriter.addMember(otherAgent.id, "writer")).toThrow( + "Failed to set role" + ); + expect(() => teamAsWriter.addMember(otherAgent.id, "reader")).toThrow( + "Failed to set role" + ); + + expect(teamAsWriter.teamMap.get(otherAgent.id)).toBeUndefined(); +}); + +test("Admins can add readers to a team, who can't add admins, writers, or readers", () => { + const { team, node } = newTeam(); + const reader = node.createAccount("reader"); + + let teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); + }); + + teamContent = expectTeamContent(team.getCurrentContent()); + expect(teamContent.get(reader.id)).toEqual("reader"); + + const teamAsReader = team.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + + let teamContentAsReader = expectTeamContent( + teamAsReader.getCurrentContent() + ); + + expect(teamContentAsReader.get(reader.id)).toEqual("reader"); + + const otherAgent = node.createAccount("otherAgent"); + + teamContentAsReader.edit((editable) => { + editable.set(otherAgent.id, "admin", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); + + editable.set(otherAgent.id, "writer", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); + + editable.set(otherAgent.id, "reader", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); + }); + + teamContentAsReader = expectTeamContent(teamAsReader.getCurrentContent()); + + expect(teamContentAsReader.get(otherAgent.id)).toBeUndefined(); +}); + +test("Admins can add readers to a team, who can't add admins, writers, or readers (high level)", () => { + const { team, node } = newTeamHighLevel(); + + const reader = node.createAccount("reader"); + + team.addMember(reader.id, "reader"); + expect(team.teamMap.get(reader.id)).toEqual("reader"); + + const teamAsReader = team.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + + expect(teamAsReader.teamMap.get(reader.id)).toEqual("reader"); + + const otherAgent = node.createAccount("otherAgent"); + + expect(() => teamAsReader.addMember(otherAgent.id, "admin")).toThrow( + "Failed to set role" + ); + expect(() => teamAsReader.addMember(otherAgent.id, "writer")).toThrow( + "Failed to set role" + ); + expect(() => teamAsReader.addMember(otherAgent.id, "reader")).toThrow( + "Failed to set role" + ); + + expect(teamAsReader.teamMap.get(otherAgent.id)).toBeUndefined(); +}); + +test("Admins can write to an object that is owned by their team", () => { + const { node, team } = newTeam(); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + let childContent = expectMap(childObject.getCurrentContent()); + + childContent.edit((editable) => { + editable.set("foo", "bar", "trusting"); + expect(editable.get("foo")).toEqual("bar"); + }); + + childContent = expectMap(childObject.getCurrentContent()); + + expect(childContent.get("foo")).toEqual("bar"); +}); + +test("Admins can write to an object that is owned by their team (high level)", () => { + const { node, team } = newTeamHighLevel(); + + let childObject = team.createMap(); + + childObject = childObject.edit((editable) => { + editable.set("foo", "bar", "trusting"); + expect(editable.get("foo")).toEqual("bar"); + }); + + expect(childObject.get("foo")).toEqual("bar"); +}); + +test("Writers can write to an object that is owned by their team", () => { + const { node, team } = newTeam(); + + const writer = node.createAccount("writer"); + + expectTeamContent(team.getCurrentContent()).edit((editable) => { + editable.set(writer.id, "writer", "trusting"); + expect(editable.get(writer.id)).toEqual("writer"); + }); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + const childObjectAsWriter = childObject.testWithDifferentAccount( + writer, + newRandomSessionID(writer.id) + ); + + let childContentAsWriter = expectMap( + childObjectAsWriter.getCurrentContent() + ); + + childContentAsWriter.edit((editable) => { + editable.set("foo", "bar", "trusting"); + expect(editable.get("foo")).toEqual("bar"); + }); + + childContentAsWriter = expectMap(childObjectAsWriter.getCurrentContent()); + + expect(childContentAsWriter.get("foo")).toEqual("bar"); +}); + +test("Writers can write to an object that is owned by their team (high level)", () => { + const { node, team } = newTeamHighLevel(); + + const writer = node.createAccount("writer"); + + team.addMember(writer.id, "writer"); + + const childObject = team.createMap(); + + let childObjectAsWriter = expectMap( + childObject.coValue + .testWithDifferentAccount(writer, newRandomSessionID(writer.id)) + .getCurrentContent() + ); + + childObjectAsWriter = childObjectAsWriter.edit((editable) => { + editable.set("foo", "bar", "trusting"); + expect(editable.get("foo")).toEqual("bar"); + }); + + expect(childObjectAsWriter.get("foo")).toEqual("bar"); +}); + +test("Readers can not write to an object that is owned by their team", () => { + const { node, team } = newTeam(); + + const reader = node.createAccount("reader"); + + expectTeamContent(team.getCurrentContent()).edit((editable) => { + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); + }); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + const childObjectAsReader = childObject.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + + let childContentAsReader = expectMap( + childObjectAsReader.getCurrentContent() + ); + + childContentAsReader.edit((editable) => { + editable.set("foo", "bar", "trusting"); + expect(editable.get("foo")).toBeUndefined(); + }); + + childContentAsReader = expectMap(childObjectAsReader.getCurrentContent()); + + expect(childContentAsReader.get("foo")).toBeUndefined(); +}); + +test("Readers can not write to an object that is owned by their team (high level)", () => { + const { node, team } = newTeamHighLevel(); + + const reader = node.createAccount("reader"); + + team.addMember(reader.id, "reader"); + + const childObject = team.createMap(); + + let childObjectAsReader = expectMap( + childObject.coValue + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) + .getCurrentContent() + ); + + childObjectAsReader = childObjectAsReader.edit((editable) => { + editable.set("foo", "bar", "trusting"); + expect(editable.get("foo")).toBeUndefined(); + }); + + expect(childObjectAsReader.get("foo")).toBeUndefined(); +}); + +test("Admins can set team read key and then use it to create and read private transactions in owned objects", () => { + const { node, team, admin } = newTeam(); + + const teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); + const revelation = seal( + readKey, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting"); + + expect(editable.get(`${readKeyID}_for_${admin.id}`)).toEqual( + revelation + ); + + editable.set("readKey", readKeyID, "trusting"); + + expect(editable.get("readKey")).toEqual(readKeyID); + + expect(team.getCurrentReadKey().secret).toEqual(readKey); + }); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + let childContent = expectMap(childObject.getCurrentContent()); + + childContent.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + childContent = expectMap(childObject.getCurrentContent()); + expect(childContent.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key and then use it to create and read private transactions in owned objects (high level)", () => { + const { node, team, admin } = newTeamHighLevel(); + + let childObject = team.createMap(); + + childObject = childObject.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + expect(childObject.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key and then writers can use it to create and read private transactions in owned objects", () => { + const { node, team, admin } = newTeam(); + + const writer = node.createAccount("writer"); + + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); + + const teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(writer.id, "writer", "trusting"); + expect(editable.get(writer.id)).toEqual("writer"); + + const revelation1 = seal( + readKey, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); + + const revelation2 = seal( + readKey, + admin.currentSealerSecret(), + writer.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${writer.id}`, revelation2, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + }); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + const childObjectAsWriter = childObject.testWithDifferentAccount( + writer, + newRandomSessionID(writer.id) + ); + + expect(childObject.getCurrentReadKey().secret).toEqual(readKey); + + let childContentAsWriter = expectMap( + childObjectAsWriter.getCurrentContent() + ); + + childContentAsWriter.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + childContentAsWriter = expectMap(childObjectAsWriter.getCurrentContent()); + + expect(childContentAsWriter.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key and then writers can use it to create and read private transactions in owned objects (high level)", () => { + const { node, team, admin } = newTeamHighLevel(); + + const writer = node.createAccount("writer"); + + team.addMember(writer.id, "writer"); + + const childObject = team.createMap(); + + let childObjectAsWriter = expectMap( + childObject.coValue + .testWithDifferentAccount(writer, newRandomSessionID(writer.id)) + .getCurrentContent() + ); + + childObjectAsWriter = childObjectAsWriter.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + expect(childObjectAsWriter.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read", () => { + const { node, team, admin } = newTeam(); + + const reader = node.createAccount("reader"); + + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); + + const teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); + + const revelation1 = seal( + readKey, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); + + const revelation2 = seal( + readKey, + admin.currentSealerSecret(), + reader.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader.id}`, revelation2, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + }); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + expectMap(childObject.getCurrentContent()).edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + const childObjectAsReader = childObject.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + + expect(childObjectAsReader.getCurrentReadKey().secret).toEqual(readKey); + + const childContentAsReader = expectMap( + childObjectAsReader.getCurrentContent() + ); + + expect(childContentAsReader.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read (high level)", () => { + const { node, team, admin } = newTeamHighLevel(); + + const reader = node.createAccount("reader"); + + team.addMember(reader.id, "reader"); + + let childObject = team.createMap(); + + childObject = childObject.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + const childContentAsReader = expectMap( + childObject.coValue + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) + .getCurrentContent() + ); + + expect(childContentAsReader.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read, even with a separate later revelation for the same read key", () => { + const { node, team, admin } = newTeam(); + + const reader1 = node.createAccount("reader1"); + + const reader2 = node.createAccount("reader2"); + + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); + + const teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(reader1.id, "reader", "trusting"); + expect(editable.get(reader1.id)).toEqual("reader"); + + const revelation1 = seal( + readKey, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); + + const revelation2 = seal( + readKey, + admin.currentSealerSecret(), + reader1.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader1.id}`, revelation2, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + }); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + expectMap(childObject.getCurrentContent()).edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + const childObjectAsReader1 = childObject.testWithDifferentAccount( + reader1, + newRandomSessionID(reader1.id) + ); + + expect(childObjectAsReader1.getCurrentReadKey().secret).toEqual(readKey); + + const childContentAsReader1 = expectMap( + childObjectAsReader1.getCurrentContent() + ); + + expect(childContentAsReader1.get("foo")).toEqual("bar"); + + teamContent.edit((editable) => { + const revelation3 = seal( + readKey, + admin.currentSealerSecret(), + reader2.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader2.id}`, revelation3, "trusting"); + }); + + const childObjectAsReader2 = childObject.testWithDifferentAccount( + reader2, + newRandomSessionID(reader2.id) + ); + + expect(childObjectAsReader2.getCurrentReadKey().secret).toEqual(readKey); + + const childContentAsReader2 = expectMap( + childObjectAsReader2.getCurrentContent() + ); + + expect(childContentAsReader2.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read, even with a separate later revelation for the same read key (high level)", () => { + const { node, team, admin } = newTeamHighLevel(); + + const reader1 = node.createAccount("reader1"); + + const reader2 = node.createAccount("reader2"); + + team.addMember(reader1.id, "reader"); + + let childObject = team.createMap(); + + childObject = childObject.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + const childContentAsReader1 = expectMap( + childObject.coValue + .testWithDifferentAccount(reader1, newRandomSessionID(reader1.id)) + .getCurrentContent() + ); + + expect(childContentAsReader1.get("foo")).toEqual("bar"); + + team.addMember(reader2.id, "reader"); + + const childContentAsReader2 = expectMap( + childObject.coValue + .testWithDifferentAccount(reader2, newRandomSessionID(reader2.id)) + .getCurrentContent() + ); + + expect(childContentAsReader2.get("foo")).toEqual("bar"); +}); + +test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, make another private transaction, and both can be read by the admin", () => { + const { node, team, admin } = newTeam(); + + const teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); + const revelation = seal( + readKey, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID); + expect(team.getCurrentReadKey().secret).toEqual(readKey); + }); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + let childContent = expectMap(childObject.getCurrentContent()); + + childContent.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + childContent = expectMap(childObject.getCurrentContent()); + expect(childContent.get("foo")).toEqual("bar"); + + teamContent.edit((editable) => { + const { secret: readKey2, id: readKeyID2 } = newRandomKeySecret(); + + const revelation = seal( + readKey2, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID2}_for_${admin.id}`, revelation, "trusting"); + + editable.set("readKey", readKeyID2, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID2); + expect(team.getCurrentReadKey().secret).toEqual(readKey2); + }); + + childContent = expectMap(childObject.getCurrentContent()); + expect(childContent.get("foo")).toEqual("bar"); + + childContent.edit((editable) => { + editable.set("foo2", "bar2", "private"); + expect(editable.get("foo2")).toEqual("bar2"); + }); + childContent = expectMap(childObject.getCurrentContent()); + expect(childContent.get("foo")).toEqual("bar"); + expect(childContent.get("foo2")).toEqual("bar2"); +}); + +test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, make another private transaction, and both can be read by the admin (high level)", () => { + const { team } = newTeamHighLevel(); + + let childObject = team.createMap(); + + const firstReadKey = childObject.coValue.getCurrentReadKey(); + + childObject = childObject.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + expect(childObject.get("foo")).toEqual("bar"); + + team.rotateReadKey(); + + expect(childObject.coValue.getCurrentReadKey()).not.toEqual(firstReadKey); + + childObject = childObject.edit((editable) => { + editable.set("foo2", "bar2", "private"); + expect(editable.get("foo2")).toEqual("bar2"); + }); + + expect(childObject.get("foo")).toEqual("bar"); + expect(childObject.get("foo2")).toEqual("bar2"); +}); + +test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, add a reader, make another private transaction in the owned object, and both can be read by the reader", () => { + const { node, team, admin } = newTeam(); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + const teamContent = expectTeamContent(team.getCurrentContent()); + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); + + teamContent.edit((editable) => { + const revelation = seal( + readKey, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID); + expect(team.getCurrentReadKey().secret).toEqual(readKey); + }); + + let childContent = expectMap(childObject.getCurrentContent()); + + childContent.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + childContent = expectMap(childObject.getCurrentContent()); + expect(childContent.get("foo")).toEqual("bar"); + + const reader = node.createAccount("reader"); + + const { secret: readKey2, id: readKeyID2 } = newRandomKeySecret(); + + teamContent.edit((editable) => { + const revelation2 = seal( + readKey2, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID2}_for_${admin.id}`, revelation2, "trusting"); + + const revelation3 = seal( + readKey2, + admin.currentSealerSecret(), + reader.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID2}_for_${reader.id}`, revelation3, "trusting"); + + editable.set( + `${readKeyID}_for_${readKeyID2}`, + encryptKeySecret({ + toEncrypt: { id: readKeyID, secret: readKey }, + encrypting: { id: readKeyID2, secret: readKey2 }, + }).encrypted, + "trusting" + ); + + editable.set("readKey", readKeyID2, "trusting"); + + expect(editable.get("readKey")).toEqual(readKeyID2); + expect(team.getCurrentReadKey().secret).toEqual(readKey2); + + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); + }); + + childContent.edit((editable) => { + editable.set("foo2", "bar2", "private"); + expect(editable.get("foo2")).toEqual("bar2"); + }); + + const childObjectAsReader = childObject.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + + expect(childObjectAsReader.getCurrentReadKey().secret).toEqual(readKey2); + + const childContentAsReader = expectMap( + childObjectAsReader.getCurrentContent() + ); + + expect(childContentAsReader.get("foo")).toEqual("bar"); + expect(childContentAsReader.get("foo2")).toEqual("bar2"); +}); + +test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, add a reader, make another private transaction in the owned object, and both can be read by the reader (high level)", () => { + const { node, team } = newTeamHighLevel(); + + let childObject = team.createMap(); + + const firstReadKey = childObject.coValue.getCurrentReadKey(); + + childObject = childObject.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + expect(childObject.get("foo")).toEqual("bar"); + + team.rotateReadKey(); + + expect(childObject.coValue.getCurrentReadKey()).not.toEqual(firstReadKey); + + const reader = node.createAccount("reader"); + + team.addMember(reader.id, "reader"); + + childObject = childObject.edit((editable) => { + editable.set("foo2", "bar2", "private"); + expect(editable.get("foo2")).toEqual("bar2"); + }); + + const childContentAsReader = expectMap( + childObject.coValue + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) + .getCurrentContent() + ); + + expect(childContentAsReader.get("foo")).toEqual("bar"); + expect(childContentAsReader.get("foo2")).toEqual("bar2"); +}); + +test("Admins can set team read rey, make a private transaction in an owned object, rotate the read key, add two readers, rotate the read key again to kick out one reader, make another private transaction in the owned object, and only the remaining reader can read both transactions", () => { + const { node, team, admin } = newTeam(); + + const childObject = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + const teamContent = expectTeamContent(team.getCurrentContent()); + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); + const reader = node.createAccount("reader"); + + const reader2 = node.createAccount("reader2"); + + teamContent.edit((editable) => { + const revelation1 = seal( + readKey, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); + + const revelation2 = seal( + readKey, + admin.currentSealerSecret(), + reader.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader.id}`, revelation2, "trusting"); + + const revelation3 = seal( + readKey, + admin.currentSealerSecret(), + reader2.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader2.id}`, revelation3, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID); + expect(team.getCurrentReadKey().secret).toEqual(readKey); + + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); + editable.set(reader2.id, "reader", "trusting"); + expect(editable.get(reader2.id)).toEqual("reader"); + }); + + let childContent = expectMap(childObject.getCurrentContent()); + + childContent.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + childContent = expectMap(childObject.getCurrentContent()); + expect(childContent.get("foo")).toEqual("bar"); + + let childObjectAsReader = childObject.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + + expect( + expectMap(childObjectAsReader.getCurrentContent()).get("foo") + ).toEqual("bar"); + + let childObjectAsReader2 = childObject.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + + expect( + expectMap(childObjectAsReader2.getCurrentContent()).get("foo") + ).toEqual("bar"); + + const { secret: readKey2, id: readKeyID2 } = newRandomKeySecret(); + + teamContent.edit((editable) => { + const newRevelation1 = seal( + readKey2, + admin.currentSealerSecret(), + admin.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set( + `${readKeyID2}_for_${admin.id}`, + newRevelation1, + "trusting" + ); + + const newRevelation2 = seal( + readKey2, + admin.currentSealerSecret(), + reader2.currentSealerID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set( + `${readKeyID2}_for_${reader2.id}`, + newRevelation2, + "trusting" + ); + + editable.set("readKey", readKeyID2, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID2); + expect(team.getCurrentReadKey().secret).toEqual(readKey2); + + editable.set(reader.id, "revoked", "trusting"); + // expect(editable.get(reader.id)).toEqual("revoked"); + }); + + expect(childObject.getCurrentReadKey().secret).toEqual(readKey2); + + childContent = expectMap(childObject.getCurrentContent()); + childContent.edit((editable) => { + editable.set("foo2", "bar2", "private"); + expect(editable.get("foo2")).toEqual("bar2"); + }); + + // TODO: make sure these instances of coValues sync between each other so this isn't necessary? + childObjectAsReader = childObject.testWithDifferentAccount( + reader, + newRandomSessionID(reader.id) + ); + childObjectAsReader2 = childObject.testWithDifferentAccount( + reader2, + newRandomSessionID(reader2.id) + ); + + expect( + expectMap(childObjectAsReader.getCurrentContent()).get("foo2") + ).toBeUndefined(); + expect( + expectMap(childObjectAsReader2.getCurrentContent()).get("foo2") + ).toEqual("bar2"); +}); + +test("Admins can set team read rey, make a private transaction in an owned object, rotate the read key, add two readers, rotate the read key again to kick out one reader, make another private transaction in the owned object, and only the remaining reader can read both transactions (high level)", () => { + const { node, team } = newTeamHighLevel(); + + let childObject = team.createMap(); + + childObject = childObject.edit((editable) => { + editable.set("foo", "bar", "private"); + expect(editable.get("foo")).toEqual("bar"); + }); + + expect(childObject.get("foo")).toEqual("bar"); + + team.rotateReadKey(); + + const secondReadKey = childObject.coValue.getCurrentReadKey(); + + const reader = node.createAccount("reader"); + + const reader2 = node.createAccount("reader2"); + + team.addMember(reader.id, "reader"); + team.addMember(reader2.id, "reader"); + + childObject = childObject.edit((editable) => { + editable.set("foo2", "bar2", "private"); + expect(editable.get("foo2")).toEqual("bar2"); + }); + + expect(childObject.get("foo")).toEqual("bar"); + expect(childObject.get("foo2")).toEqual("bar2"); + + team.removeMember(reader.id); + + expect(childObject.coValue.getCurrentReadKey()).not.toEqual(secondReadKey); + + childObject = childObject.edit((editable) => { + editable.set("foo3", "bar3", "private"); + expect(editable.get("foo3")).toEqual("bar3"); + }); + + const childContentAsReader2 = expectMap( + childObject.coValue + .testWithDifferentAccount(reader2, newRandomSessionID(reader2.id)) + .getCurrentContent() + ); + + expect(childContentAsReader2.get("foo")).toEqual("bar"); + expect(childContentAsReader2.get("foo2")).toEqual("bar2"); + expect(childContentAsReader2.get("foo3")).toEqual("bar3"); + + expect( + expectMap( + childObject.coValue + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) + .getCurrentContent() + ).get("foo3") + ).toBeUndefined(); +}); + +test("Can create two owned objects in the same team and they will have different ids", () => { + const { node, team } = newTeam(); + + const childObject1 = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + const childObject2 = node.createCoValue({ + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + ...createdNowUnique(), + }); + + expect(childObject1.id).not.toEqual(childObject2.id); +}); diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100644 index 000000000..cf61e3e30 --- /dev/null +++ b/src/permissions.ts @@ -0,0 +1,392 @@ +import { CoID, ContentType } from "./contentType.js"; +import { CoMap, MapOpPayload } from "./contentTypes/coMap.js"; +import { JsonObject, JsonValue } from "./jsonValue.js"; +import { + Encrypted, + KeyID, + KeySecret, + createdNowUnique, + newRandomKeySecret, + seal, + encryptKeySecret, + getAgentSealerID, + Sealed, +} from "./crypto.js"; +import { + CoValue, + Transaction, + TrustingTransaction, + accountOrAgentIDfromSessionID, +} from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { RawCoID, SessionID, TransactionID, isAgentID } from "./ids.js"; +import { AccountIDOrAgentID, GeneralizedControlledAccount, Profile } from "./account.js"; + +export type PermissionsDef = + | { type: "team"; initialAdmin: AccountIDOrAgentID } + | { type: "ownedByTeam"; team: RawCoID } + | { type: "unsafeAllowAll" }; + +export type Role = "reader" | "writer" | "admin" | "revoked"; + +export function determineValidTransactions( + coValue: CoValue +): { txID: TransactionID; tx: Transaction }[] { + if (coValue.header.ruleset.type === "team") { + const allTrustingTransactionsSorted = Object.entries( + coValue.sessions + ).flatMap(([sessionID, sessionLog]) => { + return sessionLog.transactions + .map((tx, txIndex) => ({ sessionID, txIndex, tx })) + .filter(({ tx }) => { + if (tx.privacy === "trusting") { + return true; + } else { + console.warn("Unexpected private transaction in Team"); + return false; + } + }) as { + sessionID: SessionID; + txIndex: number; + tx: TrustingTransaction; + }[]; + }); + + allTrustingTransactionsSorted.sort((a, b) => { + return a.tx.madeAt - b.tx.madeAt; + }); + + const initialAdmin = coValue.header.ruleset.initialAdmin; + + if (!initialAdmin) { + throw new Error("Team must have initialAdmin"); + } + + const memberState: { [agent: AccountIDOrAgentID]: Role } = {}; + + const validTransactions: { txID: TransactionID; tx: Transaction }[] = + []; + + for (const { + sessionID, + txIndex, + tx, + } of allTrustingTransactionsSorted) { + // console.log("before", { memberState, validTransactions }); + const transactor = accountOrAgentIDfromSessionID(sessionID); + + const change = tx.changes[0] as + | MapOpPayload + | MapOpPayload<"readKey", JsonValue> + | MapOpPayload<"profile", CoID>; + if (tx.changes.length !== 1) { + console.warn("Team transaction must have exactly one change"); + continue; + } + + if (change.op !== "insert") { + console.warn("Team transaction must set a role or readKey"); + continue; + } + + if (change.key === "readKey") { + if (memberState[transactor] !== "admin") { + console.warn("Only admins can set readKeys"); + continue; + } + + validTransactions.push({ txID: { sessionID, txIndex }, tx }); + continue; + } else if (change.key === 'profile') { + if (memberState[transactor] !== "admin") { + console.warn("Only admins can set profile"); + continue; + } + + validTransactions.push({ txID: { sessionID, txIndex }, tx }); + continue; + } else if (isKeyForKeyField(change.key) || isKeyForAccountField(change.key)) { + if (memberState[transactor] !== "admin") { + console.warn("Only admins can reveal keys"); + continue; + } + + // TODO: check validity of agents who the key is revealed to? + + validTransactions.push({ txID: { sessionID, txIndex }, tx }); + continue; + } + + const affectedMember = change.key; + const assignedRole = change.value; + + if ( + change.value !== "admin" && + change.value !== "writer" && + change.value !== "reader" && + change.value !== "revoked" + ) { + console.warn("Team transaction must set a valid role"); + continue; + } + + const isFirstSelfAppointment = + !memberState[transactor] && + transactor === initialAdmin && + change.op === "insert" && + change.key === transactor && + change.value === "admin"; + + if (!isFirstSelfAppointment) { + if (memberState[transactor] !== "admin") { + console.warn( + "Team transaction must be made by current admin" + ); + continue; + } + + if ( + memberState[affectedMember] === "admin" && + affectedMember !== transactor && + assignedRole !== "admin" + ) { + console.warn("Admins can only demote themselves."); + continue; + } + } + + memberState[affectedMember] = change.value; + validTransactions.push({ txID: { sessionID, txIndex }, tx }); + + // console.log("after", { memberState, validTransactions }); + } + + return validTransactions; + } else if (coValue.header.ruleset.type === "ownedByTeam") { + const teamContent = coValue.node + .expectCoValueLoaded( + coValue.header.ruleset.team, + "Determining valid transaction in owned object but its team wasn't loaded" + ) + .getCurrentContent(); + + if (teamContent.type !== "comap") { + throw new Error("Team must be a map"); + } + + return Object.entries(coValue.sessions).flatMap( + ([sessionID, sessionLog]) => { + const transactor = accountOrAgentIDfromSessionID( + sessionID as SessionID + ); + return sessionLog.transactions + .filter((tx) => { + const transactorRoleAtTxTime = teamContent.getAtTime( + transactor, + tx.madeAt + ); + + return ( + transactorRoleAtTxTime === "admin" || + transactorRoleAtTxTime === "writer" + ); + }) + .map((tx, txIndex) => ({ + txID: { sessionID: sessionID as SessionID, txIndex }, + tx, + })); + } + ); + } else if (coValue.header.ruleset.type === "unsafeAllowAll") { + return Object.entries(coValue.sessions).flatMap( + ([sessionID, sessionLog]) => { + return sessionLog.transactions.map((tx, txIndex) => ({ + txID: { sessionID: sessionID as SessionID, txIndex }, + tx, + })); + } + ); + } else { + throw new Error( + "Unknown ruleset type " + (coValue.header.ruleset as any).type + ); + } +} + +export type TeamContent = { + profile: CoID | null; + [key: AccountIDOrAgentID]: Role; + readKey: KeyID; + [revelationFor: `${KeyID}_for_${AccountIDOrAgentID}`]: Sealed; + [oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted< + KeySecret, + { encryptedID: KeyID; encryptingID: KeyID } + >; +}; + +export function expectTeamContent( + content: ContentType +): CoMap { + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + return content as CoMap; +} + +export class Team { + teamMap: CoMap; + node: LocalNode; + + constructor(teamMap: CoMap, node: LocalNode) { + this.teamMap = teamMap; + this.node = node; + } + + get id(): CoID> { + return this.teamMap.id; + } + + addMember(accountID: AccountIDOrAgentID, role: Role) { + this.teamMap = this.teamMap.edit((map) => { + const currentReadKey = this.teamMap.coValue.getCurrentReadKey(); + + if (!currentReadKey.secret) { + throw new Error("Can't add member without read key secret"); + } + + const agent = this.node.resolveAccountAgent( + accountID, + "Expected to know agent to add them to team" + ); + + map.set(accountID, role, "trusting"); + + if (map.get(accountID) !== role) { + throw new Error("Failed to set role"); + } + + map.set( + `${currentReadKey.id}_for_${accountID}`, + seal( + currentReadKey.secret, + this.teamMap.coValue.node.account.currentSealerSecret(), + getAgentSealerID(agent), + { + in: this.teamMap.coValue.id, + tx: this.teamMap.coValue.nextTransactionID(), + } + ), + "trusting" + ); + }); + } + + rotateReadKey() { + const currentlyPermittedReaders = this.teamMap.keys().filter((key) => { + if (key.startsWith("co_") || isAgentID(key)) { + const role = this.teamMap.get(key); + return ( + role === "admin" || role === "writer" || role === "reader" + ); + } else { + return false; + } + }) as AccountIDOrAgentID[]; + + const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey(); + + if (!maybeCurrentReadKey.secret) { + throw new Error( + "Can't rotate read key secret we don't have access to" + ); + } + + const currentReadKey = { + id: maybeCurrentReadKey.id, + secret: maybeCurrentReadKey.secret, + }; + + const newReadKey = newRandomKeySecret(); + + this.teamMap = this.teamMap.edit((map) => { + for (const readerID of currentlyPermittedReaders) { + const reader = this.node.resolveAccountAgent( + readerID, + "Expected to know currently permitted reader" + ); + + map.set( + `${newReadKey.id}_for_${readerID}`, + seal( + newReadKey.secret, + this.teamMap.coValue.node.account.currentSealerSecret(), + getAgentSealerID(reader), + { + in: this.teamMap.coValue.id, + tx: this.teamMap.coValue.nextTransactionID(), + } + ), + "trusting" + ); + } + + map.set( + `${currentReadKey.id}_for_${newReadKey.id}`, + encryptKeySecret({ + encrypting: newReadKey, + toEncrypt: currentReadKey, + }).encrypted, + "trusting" + ); + + map.set("readKey", newReadKey.id, "trusting"); + }); + } + + removeMember(accountID: AccountIDOrAgentID) { + this.teamMap = this.teamMap.edit((map) => { + map.set(accountID, "revoked", "trusting"); + }); + + this.rotateReadKey(); + } + + createMap( + meta?: Meta + ): CoMap { + return this.node + .createCoValue({ + type: "comap", + ruleset: { + type: "ownedByTeam", + team: this.teamMap.id, + }, + meta: meta || null, + ...createdNowUnique(), + }) + .getCurrentContent() as CoMap; + } + + testWithDifferentAccount( + account: GeneralizedControlledAccount, + sessionId: SessionID + ): Team { + return new Team( + expectTeamContent( + this.teamMap.coValue + .testWithDifferentAccount(account, sessionId) + .getCurrentContent() + ), + this.node + ); + } +} + +export function isKeyForKeyField(field: string): field is `${KeyID}_for_${KeyID}` { + return field.startsWith("key_") && field.includes("_for_key"); +} + +export function isKeyForAccountField(field: string): field is `${KeyID}_for_${AccountIDOrAgentID}` { + return field.startsWith("key_") && (field.includes("_for_sealer") || field.includes("_for_co")); +} \ No newline at end of file diff --git a/src/streamUtils.ts b/src/streamUtils.ts new file mode 100644 index 000000000..c3656d60f --- /dev/null +++ b/src/streamUtils.ts @@ -0,0 +1,130 @@ +import { ReadableStream, TransformStream, WritableStream } from "isomorphic-streams"; +import { Peer, PeerID, SyncMessage } from "./sync.js"; + + +export function connectedPeers( + peer1id: PeerID, + peer2id: PeerID, + { + trace = false, peer1role = "peer", peer2role = "peer", + }: { + trace?: boolean; + peer1role?: Peer["role"]; + peer2role?: Peer["role"]; + } = {} +): [Peer, Peer] { + const [inRx1, inTx1] = newStreamPair(); + const [outRx1, outTx1] = newStreamPair(); + + const [inRx2, inTx2] = newStreamPair(); + const [outRx2, outTx2] = newStreamPair(); + + void outRx2 + .pipeThrough( + new TransformStream({ + transform( + chunk: SyncMessage, + controller: { enqueue: (msg: SyncMessage) => void; } + ) { + trace && console.log(`${peer2id} -> ${peer1id}`, JSON.stringify(chunk, null, 2)); + controller.enqueue(chunk); + }, + }) + ) + .pipeTo(inTx1); + + void outRx1 + .pipeThrough( + new TransformStream({ + transform( + chunk: SyncMessage, + controller: { enqueue: (msg: SyncMessage) => void; } + ) { + trace && console.log(`${peer1id} -> ${peer2id}`, JSON.stringify(chunk, null, 2)); + controller.enqueue(chunk); + }, + }) + ) + .pipeTo(inTx2); + + const peer2AsPeer: Peer = { + id: peer2id, + incoming: inRx1, + outgoing: outTx1, + role: peer2role, + }; + + const peer1AsPeer: Peer = { + id: peer1id, + incoming: inRx2, + outgoing: outTx2, + role: peer1role, + }; + + return [peer1AsPeer, peer2AsPeer]; +} + +export function newStreamPair(): [ReadableStream, WritableStream] { + const queue: T[] = []; + let resolveNextItemReady: () => void = () => { }; + let nextItemReady: Promise = new Promise((resolve) => { + resolveNextItemReady = resolve; + }); + + let writerClosed = false; + let readerClosed = false; + + const readable = new ReadableStream({ + async pull(controller) { + let retriesLeft = 3; + while (retriesLeft > 0) { + if (writerClosed) { + controller.close(); + return; + } + retriesLeft--; + if (queue.length > 0) { + controller.enqueue(queue.shift()!); + if (queue.length === 0) { + nextItemReady = new Promise((resolve) => { + resolveNextItemReady = resolve; + }); + } + return; + } else { + await nextItemReady; + } + } + throw new Error( + "Should only use one retry to get next item in queue." + ); + }, + + cancel(reason) { + console.log("Manually closing reader"); + readerClosed = true; + }, + }); + + const writable = new WritableStream({ + write(chunk, controller) { + if (readerClosed) { + console.log("Reader closed, not writing chunk", chunk); + throw new Error("Reader closed, not writing chunk"); + } + queue.push(chunk); + if (queue.length === 1) { + // make sure that await write resolves before corresponding read + setTimeout(() => resolveNextItemReady()); + } + }, + abort(reason) { + console.log("Manually closing writer"); + writerClosed = true; + resolveNextItemReady(); + return Promise.resolve(); + }, + }); + + return [readable, writable]; +} diff --git a/src/sync.test.ts b/src/sync.test.ts new file mode 100644 index 000000000..543b80734 --- /dev/null +++ b/src/sync.test.ts @@ -0,0 +1,1125 @@ +import { newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { Peer, PeerID, SyncMessage } from "./sync.js"; +import { expectMap } from "./contentType.js"; +import { MapOpPayload } from "./contentTypes/coMap.js"; +import { Team } from "./permissions.js"; +import { + ReadableStream, + WritableStream, + TransformStream, +} from "isomorphic-streams"; +import { + randomAnonymousAccountAndSessionID, + shouldNotResolve, +} from "./testUtils.js"; +import { + connectedPeers, + newStreamPair +} from "./streamUtils.js"; +import { AccountID } from "./account.js"; + +test("Node replies with initial tx and header to empty subscribe", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); + + const writer = inTx.getWriter(); + + await writer.write({ + action: "load", + id: map.coValue.id, + header: false, + sessions: {}, + }); + + const reader = outRx.getReader(); + + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + + const mapTellKnownStateMsg = await reader.read(); + expect(mapTellKnownStateMsg.value).toEqual({ + action: "known", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const newContentMsg = await reader.read(); + + expect(newContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: { + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + createdAt: map.coValue.header.createdAt, + uniqueness: map.coValue.header.uniqueness, + }, + new: { + [node.ownSessionID]: { + after: 0, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[0]!.madeAt, + changes: [ + { + op: "insert", + key: "hello", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, + }, + }, + } satisfies SyncMessage); +}); + +test("Node replies with only new tx to subscribe with some known state", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + editable.set("goodbye", "world", "trusting"); + }); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); + + const writer = inTx.getWriter(); + + await writer.write({ + action: "load", + id: map.coValue.id, + header: true, + sessions: { + [node.ownSessionID]: 1, + }, + }); + + const reader = outRx.getReader(); + + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + + const mapTellKnownStateMsg = await reader.read(); + expect(mapTellKnownStateMsg.value).toEqual({ + action: "known", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapNewContentMsg = await reader.read(); + + expect(mapNewContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: undefined, + new: { + [node.ownSessionID]: { + after: 1, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[1]!.madeAt, + changes: [ + { + op: "insert", + key: "goodbye", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, + }, + }, + } satisfies SyncMessage); +}); + +test.todo( + "TODO: node only replies with new tx to subscribe with some known state, even in the depended on coValues" +); + +test("After subscribing, node sends own known state and new txs to peer", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); + + const writer = inTx.getWriter(); + + await writer.write({ + action: "load", + id: map.coValue.id, + header: false, + sessions: { + [node.ownSessionID]: 0, + }, + }); + + const reader = outRx.getReader(); + + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + + const mapTellKnownStateMsg = await reader.read(); + expect(mapTellKnownStateMsg.value).toEqual({ + action: "known", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapNewContentHeaderOnlyMsg = await reader.read(); + + expect(mapNewContentHeaderOnlyMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: {}, + } satisfies SyncMessage); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const mapEditMsg1 = await reader.read(); + + expect(mapEditMsg1.value).toEqual({ + action: "content", + id: map.coValue.id, + new: { + [node.ownSessionID]: { + after: 0, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[0]!.madeAt, + changes: [ + { + op: "insert", + key: "hello", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, + }, + }, + } satisfies SyncMessage); + + map.edit((editable) => { + editable.set("goodbye", "world", "trusting"); + }); + + const mapEditMsg2 = await reader.read(); + + expect(mapEditMsg2.value).toEqual({ + action: "content", + id: map.coValue.id, + new: { + [node.ownSessionID]: { + after: 1, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[1]!.madeAt, + changes: [ + { + op: "insert", + key: "goodbye", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, + }, + }, + } satisfies SyncMessage); +}); + +test("Client replies with known new content to tellKnownState from server", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); + + const reader = outRx.getReader(); + + // expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + + const writer = inTx.getWriter(); + + await writer.write({ + action: "known", + id: map.coValue.id, + header: false, + sessions: { + [node.ownSessionID]: 0, + }, + }); + + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + + const mapTellKnownStateMsg = await reader.read(); + expect(mapTellKnownStateMsg.value).toEqual({ + action: "known", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapNewContentMsg = await reader.read(); + + expect(mapNewContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: { + [node.ownSessionID]: { + after: 0, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[0]!.madeAt, + changes: [ + { + op: "insert", + key: "hello", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, + }, + }, + } satisfies SyncMessage); +}); + +test("No matter the optimistic known state, node respects invalid known state messages and resyncs", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); + + const writer = inTx.getWriter(); + + await writer.write({ + action: "load", + id: map.coValue.id, + header: false, + sessions: { + [node.ownSessionID]: 0, + }, + }); + + const reader = outRx.getReader(); + + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + + const mapTellKnownStateMsg = await reader.read(); + expect(mapTellKnownStateMsg.value).toEqual({ + action: "known", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapNewContentHeaderOnlyMsg = await reader.read(); + + expect(mapNewContentHeaderOnlyMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: {}, + } satisfies SyncMessage); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + map.edit((editable) => { + editable.set("goodbye", "world", "trusting"); + }); + + const _mapEditMsg1 = await reader.read(); + const _mapEditMsg2 = await reader.read(); + + await writer.write({ + action: "known", + isCorrection: true, + id: map.coValue.id, + header: true, + sessions: { + [node.ownSessionID]: 1, + }, + } satisfies SyncMessage); + + const newContentAfterWrongAssumedState = await reader.read(); + + expect(newContentAfterWrongAssumedState.value).toEqual({ + action: "content", + id: map.coValue.id, + header: undefined, + new: { + [node.ownSessionID]: { + after: 1, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[1]!.madeAt, + changes: [ + { + op: "insert", + key: "goodbye", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, + }, + }, + } satisfies SyncMessage); +}); + +test("If we add a peer, but it never subscribes to a coValue, it won't get any messages", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + const [inRx, _inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const reader = outRx.getReader(); + + await expect( + shouldNotResolve(reader.read(), { timeout: 100 }) + ).resolves.toBeUndefined(); +}); + +test("If we add a server peer, all updates to all coValues are sent to it, even if it doesn't subscribe", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + const [inRx, _inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "server", + }); + + const reader = outRx.getReader(); + // expect((await reader.read()).value).toMatchObject({ + // action: "load", + // id: adminID, + // }); + expect((await reader.read()).value).toMatchObject({ + action: "load", + id: team.teamMap.coValue.id, + }); + + const mapSubscribeMsg = await reader.read(); + + expect(mapSubscribeMsg.value).toEqual({ + action: "load", + id: map.coValue.id, + header: true, + sessions: {}, + } satisfies SyncMessage); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapNewContentMsg = await reader.read(); + + expect(mapNewContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: { + [node.ownSessionID]: { + after: 0, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[0]!.madeAt, + changes: [ + { + op: "insert", + key: "hello", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, + }, + }, + } satisfies SyncMessage); +}); + +test("If we add a server peer, newly created coValues are auto-subscribed to", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const [inRx, _inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "server", + }); + + const reader = outRx.getReader(); + // expect((await reader.read()).value).toMatchObject({ + // action: "load", + // id: admin.id, + // }); + expect((await reader.read()).value).toMatchObject({ + action: "load", + id: team.teamMap.coValue.id, + }); + + const map = team.createMap(); + + const mapSubscribeMsg = await reader.read(); + + expect(mapSubscribeMsg.value).toEqual({ + action: "load", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapContentMsg = await reader.read(); + + expect(mapContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: {}, + } satisfies SyncMessage); +}); + +test.todo( + "TODO: when receiving a subscribe response that is behind our optimistic state (due to already sent content), we ignore it" +); + +test("When we connect a new server peer, we try to sync all existing coValues to it", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + const [inRx, _inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "server", + }); + + const reader = outRx.getReader(); + + // const _adminSubscribeMessage = await reader.read(); + const teamSubscribeMessage = await reader.read(); + + expect(teamSubscribeMessage.value).toEqual({ + action: "load", + ...team.teamMap.coValue.knownState(), + } satisfies SyncMessage); + + const secondMessage = await reader.read(); + + expect(secondMessage.value).toEqual({ + action: "load", + ...map.coValue.knownState(), + } satisfies SyncMessage); +}); + +test("When receiving a subscribe with a known state that is ahead of our own, peers should respond with a corresponding subscribe response message", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const map = team.createMap(); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); + + const writer = inTx.getWriter(); + + await writer.write({ + action: "load", + id: map.coValue.id, + header: true, + sessions: { + [node.ownSessionID]: 1, + }, + }); + + const reader = outRx.getReader(); + + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + const mapTellKnownState = await reader.read(); + + expect(mapTellKnownState.value).toEqual({ + action: "known", + ...map.coValue.knownState(), + } satisfies SyncMessage); +}); + +test.skip("When replaying creation and transactions of a coValue as new content, the receiving peer integrates this information", async () => { + // TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work + const [admin, session] = randomAnonymousAccountAndSessionID(); + + const node1 = new LocalNode(admin, session); + + const team = node1.createTeam(); + + const [inRx1, inTx1] = newStreamPair(); + const [outRx1, outTx1] = newStreamPair(); + + node1.sync.addPeer({ + id: "test2", + incoming: inRx1, + outgoing: outTx1, + role: "server", + }); + + const to1 = inTx1.getWriter(); + const from1 = outRx1.getReader(); + + const node2 = new LocalNode(admin, newRandomSessionID(admin.id)); + + const [inRx2, inTx2] = newStreamPair(); + const [outRx2, outTx2] = newStreamPair(); + + node2.sync.addPeer({ + id: "test1", + incoming: inRx2, + outgoing: outTx2, + role: "client", + }); + + const to2 = inTx2.getWriter(); + const from2 = outRx2.getReader(); + + const adminSubscribeMessage = await from1.read(); + expect(adminSubscribeMessage.value).toMatchObject({ + action: "load", + id: admin.id, + }); + const teamSubscribeMsg = await from1.read(); + expect(teamSubscribeMsg.value).toMatchObject({ + action: "load", + id: team.teamMap.coValue.id, + }); + + await to2.write(adminSubscribeMessage.value!); + await to2.write(teamSubscribeMsg.value!); + + // const adminTellKnownStateMsg = await from2.read(); + // expect(adminTellKnownStateMsg.value).toMatchObject(admStateEx(admin.id)); + + const teamTellKnownStateMsg = await from2.read(); + expect(teamTellKnownStateMsg.value).toMatchObject(teamStateEx(team)); + + expect( + node2.sync.peers["test1"]!.optimisticKnownStates[ + team.teamMap.coValue.id + ] + ).toBeDefined(); + + // await to1.write(adminTellKnownStateMsg.value!); + await to1.write(teamTellKnownStateMsg.value!); + + // const adminContentMsg = await from1.read(); + // expect(adminContentMsg.value).toMatchObject(admContEx(admin.id)); + + const teamContentMsg = await from1.read(); + expect(teamContentMsg.value).toMatchObject(teamContentEx(team)); + + // await to2.write(adminContentMsg.value!); + await to2.write(teamContentMsg.value!); + + const map = team.createMap(); + + const mapSubscriptionMsg = await from1.read(); + expect(mapSubscriptionMsg.value).toMatchObject({ + action: "load", + id: map.coValue.id, + }); + + const mapNewContentMsg = await from1.read(); + expect(mapNewContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: {}, + } satisfies SyncMessage); + + await to2.write(mapSubscriptionMsg.value!); + + const mapTellKnownStateMsg = await from2.read(); + expect(mapTellKnownStateMsg.value).toEqual({ + action: "known", + id: map.coValue.id, + header: false, + sessions: {}, + } satisfies SyncMessage); + + expect(node2.coValues[map.coValue.id]?.state).toEqual("loading"); + + await to2.write(mapNewContentMsg.value!); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const mapEditMsg = await from1.read(); + + await to2.write(mapEditMsg.value!); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect( + expectMap( + node2.expectCoValueLoaded(map.coValue.id).getCurrentContent() + ).get("hello") + ).toEqual("world"); +}); + +test.skip("When loading a coValue on one node, the server node it is requested from replies with all the necessary depended on coValues to make it work", async () => { + // TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work + const [admin, session] = randomAnonymousAccountAndSessionID(); + + const node1 = new LocalNode(admin, session); + + const team = node1.createTeam(); + + const map = team.createMap(); + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const node2 = new LocalNode(admin, newRandomSessionID(admin.id)); + + const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2"); + + node1.sync.addPeer(node2asPeer); + node2.sync.addPeer(node1asPeer); + + await node2.loadCoValue(map.coValue.id); + + expect( + expectMap( + node2.expectCoValueLoaded(map.coValue.id).getCurrentContent() + ).get("hello") + ).toEqual("world"); +}); + +test("Can sync a coValue through a server to another client", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + + const client1 = new LocalNode(admin, session); + + const team = client1.createTeam(); + + const map = team.createMap(); + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const [serverUser, serverSession] = randomAnonymousAccountAndSessionID(); + + const server = new LocalNode(serverUser, serverSession); + + const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", { + peer1role: "server", + peer2role: "client", + }); + + client1.sync.addPeer(serverAsPeer); + server.sync.addPeer(client1AsPeer); + + const client2 = new LocalNode(admin, newRandomSessionID(admin.id)); + + const [serverAsOtherPeer, client2AsPeer] = connectedPeers( + "server", + "client2", + { peer1role: "server", peer2role: "client" } + ); + + client2.sync.addPeer(serverAsOtherPeer); + server.sync.addPeer(client2AsPeer); + + const mapOnClient2 = await client2.loadCoValue(map.coValue.id); + + expect(expectMap(mapOnClient2.getCurrentContent()).get("hello")).toEqual( + "world" + ); +}); + +test("Can sync a coValue with private transactions through a server to another client", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + + const client1 = new LocalNode(admin, session); + + const team = client1.createTeam(); + + const map = team.createMap(); + map.edit((editable) => { + editable.set("hello", "world", "private"); + }); + + const [serverUser, serverSession] = randomAnonymousAccountAndSessionID(); + + const server = new LocalNode(serverUser, serverSession); + + const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", { + trace: true, + peer1role: "server", + peer2role: "client", + }); + + client1.sync.addPeer(serverAsPeer); + server.sync.addPeer(client1AsPeer); + + const client2 = new LocalNode(admin, newRandomSessionID(admin.id)); + + const [serverAsOtherPeer, client2AsPeer] = connectedPeers( + "server", + "client2", + { trace: true, peer1role: "server", peer2role: "client" } + ); + + client2.sync.addPeer(serverAsOtherPeer); + server.sync.addPeer(client2AsPeer); + + const mapOnClient2 = await client2.loadCoValue(map.coValue.id); + + expect(expectMap(mapOnClient2.getCurrentContent()).get("hello")).toEqual( + "world" + ); +}); + +test("When a peer's incoming/readable stream closes, we remove the peer", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "server", + }); + + const reader = outRx.getReader(); + // expect((await reader.read()).value).toMatchObject({ + // action: "load", + // id: admin.id, + // }); + expect((await reader.read()).value).toMatchObject({ + action: "load", + id: team.teamMap.coValue.id, + }); + + const map = team.createMap(); + + const mapSubscribeMsg = await reader.read(); + + expect(mapSubscribeMsg.value).toEqual({ + action: "load", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapContentMsg = await reader.read(); + + expect(mapContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: {}, + } satisfies SyncMessage); + + await inTx.abort(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(node.sync.peers["test"]).toBeUndefined(); +}); + +test("When a peer's outgoing/writable stream closes, we remove the peer", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); + + const team = node.createTeam(); + + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); + + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "server", + }); + + const reader = outRx.getReader(); + // expect((await reader.read()).value).toMatchObject({ + // action: "load", + // id: admin.id, + // }); + expect((await reader.read()).value).toMatchObject({ + action: "load", + id: team.teamMap.coValue.id, + }); + + const map = team.createMap(); + + const mapSubscribeMsg = await reader.read(); + + expect(mapSubscribeMsg.value).toEqual({ + action: "load", + ...map.coValue.knownState(), + } satisfies SyncMessage); + + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + + const mapContentMsg = await reader.read(); + + expect(mapContentMsg.value).toEqual({ + action: "content", + id: map.coValue.id, + header: map.coValue.header, + new: {}, + } satisfies SyncMessage); + + reader.releaseLock(); + await outRx.cancel(); + + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(node.sync.peers["test"]).toBeUndefined(); +}); + +test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + + const node1 = new LocalNode(admin, session); + + const team = node1.createTeam(); + + const map = team.createMap(); + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); + + const node2 = new LocalNode(admin, newRandomSessionID(admin.id)); + + const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", { + peer1role: "server", + peer2role: "client", + trace: true, + }); + + node1.sync.addPeer(node2asPeer); + + const mapOnNode2Promise = node2.loadCoValue(map.coValue.id); + + expect(node2.coValues[map.coValue.id]?.state).toEqual("loading"); + + node2.sync.addPeer(node1asPeer); + + const mapOnNode2 = await mapOnNode2Promise; + + expect(expectMap(mapOnNode2.getCurrentContent()).get("hello")).toEqual( + "world" + ); +}); + +function teamContentEx(team: Team) { + return { + action: "content", + id: team.teamMap.coValue.id, + }; +} + +function admContEx(adminID: AccountID) { + return { + action: "content", + id: adminID, + }; +} + +function teamStateEx(team: Team) { + return { + action: "known", + id: team.teamMap.coValue.id, + }; +} + +function admStateEx(adminID: AccountID) { + return { + action: "known", + id: adminID, + }; +} diff --git a/src/sync.ts b/src/sync.ts new file mode 100644 index 000000000..78d0385d6 --- /dev/null +++ b/src/sync.ts @@ -0,0 +1,512 @@ +import { Hash, Signature } from "./crypto.js"; +import { CoValueHeader, Transaction } from "./coValue.js"; +import { CoValue } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { newLoadingState } from "./node.js"; +import { + ReadableStream, + WritableStream, + WritableStreamDefaultWriter, +} from "isomorphic-streams"; +import { RawCoID, SessionID } from "./ids.js"; + +export type CoValueKnownState = { + id: RawCoID; + header: boolean; + sessions: { [sessionID: SessionID]: number }; +}; + +export function emptyKnownState(id: RawCoID): CoValueKnownState { + return { + id, + header: false, + sessions: {}, + }; +} + +export type SyncMessage = + | LoadMessage + | KnownStateMessage + | NewContentMessage + | DoneMessage; + +export type LoadMessage = { + action: "load"; +} & CoValueKnownState; + +export type KnownStateMessage = { + action: "known"; + asDependencyOf?: RawCoID; + isCorrection?: boolean; +} & CoValueKnownState; + +export type NewContentMessage = { + action: "content"; + id: RawCoID; + header?: CoValueHeader; + new: { + [sessionID: SessionID]: SessionNewContent; + }; +}; + +export type SessionNewContent = { + after: number; + newTransactions: Transaction[]; + lastSignature: Signature; +}; +export type DoneMessage = { + action: "done"; + id: RawCoID; +}; + +export type PeerID = string; + +export interface Peer { + id: PeerID; + incoming: ReadableStream; + outgoing: WritableStream; + role: "peer" | "server" | "client"; +} + +export interface PeerState { + id: PeerID; + optimisticKnownStates: { [id: RawCoID]: CoValueKnownState }; + toldKnownState: Set; + incoming: ReadableStream; + outgoing: WritableStreamDefaultWriter; + role: "peer" | "server" | "client"; +} + +export function combinedKnownStates( + stateA: CoValueKnownState, + stateB: CoValueKnownState +): CoValueKnownState { + const sessionStates: CoValueKnownState["sessions"] = {}; + + const allSessions = new Set([ + ...Object.keys(stateA.sessions), + ...Object.keys(stateB.sessions), + ] as SessionID[]); + + for (const sessionID of allSessions) { + const stateAValue = stateA.sessions[sessionID]; + const stateBValue = stateB.sessions[sessionID]; + + sessionStates[sessionID] = Math.max(stateAValue || 0, stateBValue || 0); + } + + return { + id: stateA.id, + header: stateA.header || stateB.header, + sessions: sessionStates, + }; +} + +export class SyncManager { + peers: { [key: PeerID]: PeerState } = {}; + local: LocalNode; + + constructor(local: LocalNode) { + this.local = local; + } + + loadFromPeers(id: RawCoID) { + for (const peer of Object.values(this.peers)) { + peer.outgoing + .write({ + action: "load", + id: id, + header: false, + sessions: {}, + }) + .catch((e) => { + console.error("Error writing to peer", e); + }); + } + } + + async handleSyncMessage(msg: SyncMessage, peer: PeerState) { + // TODO: validate + switch (msg.action) { + case "load": + return await this.handleLoad(msg, peer); + case "known": + if (msg.isCorrection) { + return await this.handleCorrection(msg, peer); + } else { + return await this.handleKnownState(msg, peer); + } + case "content": + return await this.handleNewContent(msg, peer); + case "done": + return await this.handleUnsubscribe(msg); + default: + throw new Error( + `Unknown message type ${ + (msg as { action: "string" }).action + }` + ); + } + } + + async subscribeToIncludingDependencies( + id: RawCoID, + peer: PeerState + ) { + const entry = this.local.coValues[id]; + + if (!entry) { + throw new Error( + "Expected coValue entry on subscribe" + ); + } + + if (entry.state === "loading") { + await this.trySendToPeer(peer, { + action: "load", + id, + header: false, + sessions: {}, + }); + return; + } + + const coValue = entry.coValue; + + for (const id of coValue.getDependedOnCoValues()) { + await this.subscribeToIncludingDependencies(id, peer); + } + + if (!peer.toldKnownState.has(id)) { + peer.toldKnownState.add(id); + await this.trySendToPeer(peer, { + action: "load", + ...coValue.knownState(), + }); + } + } + + async tellUntoldKnownStateIncludingDependencies( + id: RawCoID, + peer: PeerState, + asDependencyOf?: RawCoID + ) { + const coValue = this.local.expectCoValueLoaded(id); + + for (const dependentCoID of coValue.getDependedOnCoValues()) { + await this.tellUntoldKnownStateIncludingDependencies( + dependentCoID, + peer, + asDependencyOf || id + ); + } + + if (!peer.toldKnownState.has(id)) { + await this.trySendToPeer(peer, { + action: "known", + asDependencyOf, + ...coValue.knownState(), + }); + + peer.toldKnownState.add(id); + } + } + + async sendNewContentIncludingDependencies( + id: RawCoID, + peer: PeerState + ) { + const coValue = this.local.expectCoValueLoaded(id); + + for (const id of coValue.getDependedOnCoValues()) { + await this.sendNewContentIncludingDependencies(id, peer); + } + + const newContent = coValue.newContentSince( + peer.optimisticKnownStates[id] + ); + + if (newContent) { + await this.trySendToPeer(peer, newContent); + peer.optimisticKnownStates[id] = combinedKnownStates( + peer.optimisticKnownStates[id] || + emptyKnownState(id), + coValue.knownState() + ); + } + } + + addPeer(peer: Peer) { + const peerState: PeerState = { + id: peer.id, + optimisticKnownStates: {}, + incoming: peer.incoming, + outgoing: peer.outgoing.getWriter(), + toldKnownState: new Set(), + role: peer.role, + }; + this.peers[peer.id] = peerState; + + if (peer.role === "server") { + const initialSync = async () => { + for (const id of Object.keys( + this.local.coValues + ) as RawCoID[]) { + await this.subscribeToIncludingDependencies(id, peerState); + + peerState.optimisticKnownStates[id] = { + id: id, + header: false, + sessions: {}, + }; + } + }; + void initialSync(); + } + + const readIncoming = async () => { + for await (const msg of peerState.incoming) { + try { + await this.handleSyncMessage(msg, peerState); + } catch (e) { + console.error( + `Error reading from peer ${peer.id}`, + JSON.stringify(msg), + e + ); + } + } + console.log("Peer disconnected:", peer.id); + delete this.peers[peer.id]; + }; + + void readIncoming(); + } + + trySendToPeer(peer: PeerState, msg: SyncMessage) { + return peer.outgoing.write(msg).catch((e) => { + console.error(new Error("Error writing to peer, disconnecting", {cause: e})); + delete this.peers[peer.id]; + }); + } + + async handleLoad(msg: LoadMessage, peer: PeerState) { + const entry = this.local.coValues[msg.id]; + + if (!entry || entry.state === "loading") { + if (!entry) { + this.local.coValues[msg.id] = newLoadingState(); + } + + peer.optimisticKnownStates[msg.id] = knownStateIn(msg); + peer.toldKnownState.add(msg.id); + + await this.trySendToPeer(peer, { + action: "known", + id: msg.id, + header: false, + sessions: {}, + }); + + return; + } + + peer.optimisticKnownStates[msg.id] = knownStateIn(msg); + + await this.tellUntoldKnownStateIncludingDependencies( + msg.id, + peer + ); + + await this.sendNewContentIncludingDependencies(msg.id, peer); + } + + async handleKnownState(msg: KnownStateMessage, peer: PeerState) { + let entry = this.local.coValues[msg.id]; + + peer.optimisticKnownStates[msg.id] = combinedKnownStates( + peer.optimisticKnownStates[msg.id] || + emptyKnownState(msg.id), + knownStateIn(msg) + ); + + if (!entry) { + if (msg.asDependencyOf) { + if (this.local.coValues[msg.asDependencyOf]) { + entry = newLoadingState(); + + this.local.coValues[msg.id] = entry; + } else { + throw new Error( + "Expected coValue dependency entry to be created, missing subscribe?" + ); + } + } else { + throw new Error( + "Expected coValue entry to be created, missing subscribe?" + ); + } + } + + if (entry.state === "loading") { + return []; + } + + await this.tellUntoldKnownStateIncludingDependencies( + msg.id, + peer + ); + await this.sendNewContentIncludingDependencies(msg.id, peer); + } + + async handleNewContent(msg: NewContentMessage, peer: PeerState) { + let entry = this.local.coValues[msg.id]; + + if (!entry) { + throw new Error( + "Expected coValue entry to be created, missing subscribe?" + ); + } + + let resolveAfterDone: ((coValue: CoValue) => void) | undefined; + + const peerOptimisticKnownState = + peer.optimisticKnownStates[msg.id]; + + if (!peerOptimisticKnownState) { + throw new Error( + "Expected optimisticKnownState to be set for coValue we receive new content for" + ); + } + + if (entry.state === "loading") { + if (!msg.header) { + throw new Error("Expected header to be sent in first message"); + } + + peerOptimisticKnownState.header = true; + + const coValue = new CoValue(msg.header, this.local); + + resolveAfterDone = entry.resolve; + + entry = { + state: "loaded", + coValue: coValue, + }; + + this.local.coValues[msg.id] = entry; + } + + const coValue = entry.coValue; + + let invalidStateAssumed = false; + + for (const [sessionID, newContentForSession] of Object.entries( + msg.new + ) as [SessionID, SessionNewContent][]) { + const ourKnownTxIdx = + coValue.sessions[sessionID]?.transactions.length; + const theirFirstNewTxIdx = newContentForSession.after; + + if ((ourKnownTxIdx || 0) < theirFirstNewTxIdx) { + invalidStateAssumed = true; + continue; + } + + const alreadyKnownOffset = ourKnownTxIdx + ? ourKnownTxIdx - theirFirstNewTxIdx + : 0; + + const newTransactions = + newContentForSession.newTransactions.slice(alreadyKnownOffset); + + const success = coValue.tryAddTransactions( + sessionID, + newTransactions, + undefined, + newContentForSession.lastSignature + ); + + if (!success) { + console.error("Failed to add transactions", newTransactions); + continue; + } + + peerOptimisticKnownState.sessions[sessionID] = + newContentForSession.after + + newContentForSession.newTransactions.length; + } + + if (resolveAfterDone) { + resolveAfterDone(coValue); + } + + await this.syncCoValue(coValue); + + if (invalidStateAssumed) { + await this.trySendToPeer(peer, { + action: "known", + isCorrection: true, + ...coValue.knownState(), + }); + } + } + + async handleCorrection( + msg: KnownStateMessage, + peer: PeerState + ) { + const coValue = this.local.expectCoValueLoaded(msg.id); + + peer.optimisticKnownStates[msg.id] = combinedKnownStates( + msg, + coValue.knownState() + ); + + const newContent = coValue.newContentSince(msg); + + if (newContent) { + await this.trySendToPeer(peer, newContent); + } + } + + handleUnsubscribe(_msg: DoneMessage) { + throw new Error("Method not implemented."); + } + + async syncCoValue(coValue: CoValue) { + for (const peer of Object.values(this.peers)) { + const optimisticKnownState = peer.optimisticKnownStates[coValue.id]; + + if (optimisticKnownState) { + await this.tellUntoldKnownStateIncludingDependencies( + coValue.id, + peer + ); + await this.sendNewContentIncludingDependencies( + coValue.id, + peer + ); + } else if (peer.role === "server") { + await this.subscribeToIncludingDependencies(coValue.id, peer); + await this.sendNewContentIncludingDependencies( + coValue.id, + peer + ); + } + } + } +} + +function knownStateIn( + msg: + | LoadMessage + | KnownStateMessage +) { + return { + id: msg.id, + header: msg.header, + sessions: msg.sessions, + }; +} diff --git a/src/testUtils.ts b/src/testUtils.ts new file mode 100644 index 000000000..1785bb082 --- /dev/null +++ b/src/testUtils.ts @@ -0,0 +1,99 @@ +import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from "./crypto.js"; +import { newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { expectTeamContent } from "./permissions.js"; +import { AnonymousControlledAccount } from "./account.js"; +import { SessionID } from "./ids.js"; + +export function randomAnonymousAccountAndSessionID(): [AnonymousControlledAccount, SessionID] { + const agentSecret = newRandomAgentSecret(); + + const sessionID = newRandomSessionID(getAgentID(agentSecret)); + + return [new AnonymousControlledAccount(agentSecret), sessionID]; +} + +export function newTeam() { + const [admin, sessionID] = randomAnonymousAccountAndSessionID(); + + const node = new LocalNode(admin, sessionID); + + const team = node.createCoValue({ + type: "comap", + ruleset: { type: "team", initialAdmin: admin.id }, + meta: null, + ...createdNowUnique(), + }); + + const teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(admin.id, "admin", "trusting"); + expect(editable.get(admin.id)).toEqual("admin"); + }); + + return { node, team, admin }; +} + +export function teamWithTwoAdmins() { + const { team, admin, node } = newTeam(); + + const otherAdmin = node.createAccount("otherAdmin"); + + let content = expectTeamContent(team.getCurrentContent()); + + content.edit((editable) => { + editable.set(otherAdmin.id, "admin", "trusting"); + expect(editable.get(otherAdmin.id)).toEqual("admin"); + }); + + content = expectTeamContent(team.getCurrentContent()); + + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + expect(content.get(otherAdmin.id)).toEqual("admin"); + return { team, admin, otherAdmin, node }; +} + +export function newTeamHighLevel() { + const [admin, sessionID] = randomAnonymousAccountAndSessionID(); + + + const node = new LocalNode(admin, sessionID); + + const team = node.createTeam(); + + return { admin, node, team }; +} + +export function teamWithTwoAdminsHighLevel() { + const { admin, node, team } = newTeamHighLevel(); + + const otherAdmin = node.createAccount("otherAdmin"); + + team.addMember(otherAdmin.id, "admin"); + + return { admin, node, team, otherAdmin }; +} + +export function shouldNotResolve( + promise: Promise, + ops: { timeout: number } +): Promise { + return new Promise((resolve, reject) => { + promise + .then((v) => + reject( + new Error( + "Should not have resolved, but resolved to " + + JSON.stringify(v) + ) + ) + ) + .catch(reject); + setTimeout(resolve, ops.timeout); + }); +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..12d1abe71 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "ES2020", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + }, + "include": ["./src/**/*"], +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..774bc2db4 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2786 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + dependencies: + "@babel/highlight" "^7.22.5" + +"@babel/compat-data@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" + integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.9.tgz#bd96492c68822198f33e8a256061da3cf391f58f" + integrity sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.9" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helpers" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.8" + "@babel/types" "^7.22.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.1" + +"@babel/generator@^7.22.7", "@babel/generator@^7.22.9", "@babel/generator@^7.7.2": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d" + integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw== + dependencies: + "@babel/types" "^7.22.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz#f9d0a7aaaa7cd32a3f31c9316a69f5a9bcacb892" + integrity sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.5" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" + integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + +"@babel/helpers@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.6.tgz#8e61d3395a4f0c5a8060f309fb008200969b5ecd" + integrity sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.6" + "@babel/types" "^7.22.5" + +"@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7": + version "7.22.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" + integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" + integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/template@^7.22.5", "@babel/template@^7.3.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8": + version "7.22.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e" + integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.7" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/types" "^7.22.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== + +"@eslint/eslintrc@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" + integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@^8.46.0": + version "8.46.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" + integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== + +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.6.2.tgz#bf1d4101347c23e07c029a1b1ae07d550f5cc541" + integrity sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.6.2" + jest-util "^29.6.2" + slash "^3.0.0" + +"@jest/core@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.6.2.tgz#6f2d1dbe8aa0265fcd4fb8082ae1952f148209c8" + integrity sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg== + dependencies: + "@jest/console" "^29.6.2" + "@jest/reporters" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.5.0" + jest-config "^29.6.2" + jest-haste-map "^29.6.2" + jest-message-util "^29.6.2" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.2" + jest-resolve-dependencies "^29.6.2" + jest-runner "^29.6.2" + jest-runtime "^29.6.2" + jest-snapshot "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" + jest-watcher "^29.6.2" + micromatch "^4.0.4" + pretty-format "^29.6.2" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.6.2.tgz#794c0f769d85e7553439d107d3f43186dc6874a9" + integrity sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q== + dependencies: + "@jest/fake-timers" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + jest-mock "^29.6.2" + +"@jest/expect-utils@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.2.tgz#1b97f290d0185d264dd9fdec7567a14a38a90534" + integrity sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg== + dependencies: + jest-get-type "^29.4.3" + +"@jest/expect@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.2.tgz#5a2ad58bb345165d9ce0a1845bbf873c480a4b28" + integrity sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg== + dependencies: + expect "^29.6.2" + jest-snapshot "^29.6.2" + +"@jest/fake-timers@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.6.2.tgz#fe9d43c5e4b1b901168fe6f46f861b3e652a2df4" + integrity sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA== + dependencies: + "@jest/types" "^29.6.1" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.6.2" + jest-mock "^29.6.2" + jest-util "^29.6.2" + +"@jest/globals@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.6.2.tgz#74af81b9249122cc46f1eb25793617eec69bf21a" + integrity sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/expect" "^29.6.2" + "@jest/types" "^29.6.1" + jest-mock "^29.6.2" + +"@jest/reporters@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.6.2.tgz#524afe1d76da33d31309c2c4a2c8062d0c48780a" + integrity sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.6.2" + jest-util "^29.6.2" + jest-worker "^29.6.2" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" + integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.0.tgz#bd34a05b5737cb1a99d43e1957020ac8e5b9ddb1" + integrity sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.6.2.tgz#fdd11583cd1608e4db3114e8f0cce277bf7a32ed" + integrity sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw== + dependencies: + "@jest/console" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.6.2.tgz#585eff07a68dd75225a7eacf319780cb9f6b9bf4" + integrity sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw== + dependencies: + "@jest/test-result" "^29.6.2" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.2" + slash "^3.0.0" + +"@jest/transform@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.6.2.tgz#522901ebbb211af08835bc3bcdf765ab778094e3" + integrity sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.1" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.2" + jest-regex-util "^29.4.3" + jest-util "^29.6.2" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2" + integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw== + dependencies: + "@jest/schemas" "^29.6.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@noble/ciphers@^0.1.3": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0" + integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ== + +"@noble/curves@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + +"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@scure/base@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@types/babel__core@^7.1.14": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b" + integrity sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.1.tgz#dd6f1d2411ae677dcb2db008c962598be31d6acf" + integrity sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg== + dependencies: + "@babel/types" "^7.20.7" + +"@types/graceful-fs@^4.1.3": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.5.3": + version "29.5.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777" + integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/json-schema@^7.0.12": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/node@*": + version "20.4.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" + integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== + +"@types/semver@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.2.1.tgz#41b79923fee46a745a3a50cba1c33c622aa3c79a" + integrity sha512-iZVM/ALid9kO0+I81pnp1xmYiFyqibAHzrqX4q5YvvVEyJqY+e6rfTXSCsc2jUxGNqJqTfFSSij/NFkZBiBzLw== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.2.1" + "@typescript-eslint/type-utils" "6.2.1" + "@typescript-eslint/utils" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + natural-compare-lite "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.2.1.tgz#e18a31eea1cca8841a565f1701960c8123ed07f9" + integrity sha512-Ld+uL1kYFU8e6btqBFpsHkwQ35rw30IWpdQxgOqOh4NfxSDH6uCkah1ks8R/RgQqI5hHPXMaLy9fbFseIe+dIg== + dependencies: + "@typescript-eslint/scope-manager" "6.2.1" + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/typescript-estree" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.2.1.tgz#b6f43a867b84e5671fe531f2b762e0b68f7cf0c4" + integrity sha512-UCqBF9WFqv64xNsIEPfBtenbfodPXsJ3nPAr55mGPkQIkiQvgoWNo+astj9ZUfJfVKiYgAZDMnM6dIpsxUMp3Q== + dependencies: + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + +"@typescript-eslint/type-utils@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.2.1.tgz#8eb8a2cccdf39cd7cf93e02bd2c3782dc90b0525" + integrity sha512-fTfCgomBMIgu2Dh2Or3gMYgoNAnQm3RLtRp+jP7A8fY+LJ2+9PNpi5p6QB5C4RSP+U3cjI0vDlI3mspAkpPVbQ== + dependencies: + "@typescript-eslint/typescript-estree" "6.2.1" + "@typescript-eslint/utils" "6.2.1" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.2.1.tgz#7fcdeceb503aab601274bf5e210207050d88c8ab" + integrity sha512-528bGcoelrpw+sETlyM91k51Arl2ajbNT9L4JwoXE2dvRe1yd8Q64E4OL7vHYw31mlnVsf+BeeLyAZUEQtqahQ== + +"@typescript-eslint/typescript-estree@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.2.1.tgz#2af6e90c1e91cb725a5fe1682841a3f74549389e" + integrity sha512-G+UJeQx9AKBHRQBpmvr8T/3K5bJa485eu+4tQBxFq0KoT22+jJyzo1B50JDT9QdC1DEmWQfdKsa8ybiNWYsi0Q== + dependencies: + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.2.1.tgz#2aa4279ec13053d05615bcbde2398e1e8f08c334" + integrity sha512-eBIXQeupYmxVB6S7x+B9SdBeB6qIdXKjgQBge2J+Ouv8h9Cxm5dHf/gfAZA6dkMaag+03HdbVInuXMmqFB/lKQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.2.1" + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/typescript-estree" "6.2.1" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.2.1.tgz#442e7c09fe94b715a54ebe30e967987c3c41fbf4" + integrity sha512-iTN6w3k2JEZ7cyVdZJTVJx2Lv7t6zFA8DCrJEHD2mwfc16AEvvBWVhbFh34XyG2NORCd0viIgQY1+u7kPI0WpA== + dependencies: + "@typescript-eslint/types" "6.2.1" + eslint-visitor-keys "^3.4.1" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +babel-jest@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.6.2.tgz#cada0a59e07f5acaeb11cbae7e3ba92aec9c1126" + integrity sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A== + dependencies: + "@jest/transform" "^29.6.2" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.5.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== + dependencies: + babel-plugin-jest-hoist "^29.5.0" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.21.9: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001517: + version "1.0.30001519" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601" + integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +ci-info@^3.2.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + +cjs-module-lexer@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +dedent@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" + integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +electron-to-chromium@^1.4.477: + version "1.4.485" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.485.tgz#fde3ee9ee8112a3414c0dfa545385ad08ec43408" + integrity sha512-1ndQ5IBNEnFirPwvyud69GHL+31FkE09gH/CJ6m3KCbkx3i0EVOrjwz4UNxRmN9H8OVHbC6vMRZGN1yCvjSs9w== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-require-extensions@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-require-extensions/-/eslint-plugin-require-extensions-0.1.3.tgz#394aeab433f996797a6ceba0a3f75640d4846bc8" + integrity sha512-T3c1PZ9PIdI3hjV8LdunfYI8gj017UQjzAnCrxuo3wAjneDbTPHdE3oNWInOjMA+z/aBkUtlW5vC0YepYMZIug== + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" + integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== + +eslint@^8.46.0: + version "8.46.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" + integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.1" + "@eslint/js" "^8.46.0" + "@humanwhocodes/config-array" "^0.11.10" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.2" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521" + integrity sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA== + dependencies: + "@jest/expect-utils" "^29.6.2" + "@types/node" "*" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.6.2" + jest-message-util "^29.6.2" + jest-util "^29.6.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +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== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +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" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +"isomorphic-streams@https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae": + version "1.0.3" + resolved "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae" + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a" + integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.6.2.tgz#1e6ffca60151ac66cad63fce34f443f6b5bb4258" + integrity sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/expect" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.6.2" + jest-matcher-utils "^29.6.2" + jest-message-util "^29.6.2" + jest-runtime "^29.6.2" + jest-snapshot "^29.6.2" + jest-util "^29.6.2" + p-limit "^3.1.0" + pretty-format "^29.6.2" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.6.2.tgz#edb381763398d1a292cd1b636a98bfa5644b8fda" + integrity sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q== + dependencies: + "@jest/core" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/types" "^29.6.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.6.2.tgz#c68723f06b31ca5e63030686e604727d406cd7c3" + integrity sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.6.2" + "@jest/types" "^29.6.1" + babel-jest "^29.6.2" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.6.2" + jest-environment-node "^29.6.2" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.2" + jest-runner "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.6.2" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46" + integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" + +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.6.2.tgz#c9e4b340bcbe838c73adf46b76817b15712d02ce" + integrity sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw== + dependencies: + "@jest/types" "^29.6.1" + chalk "^4.0.0" + jest-get-type "^29.4.3" + jest-util "^29.6.2" + pretty-format "^29.6.2" + +jest-environment-node@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.6.2.tgz#a9ea2cabff39b08eca14ccb32c8ceb924c8bb1ad" + integrity sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/fake-timers" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + jest-mock "^29.6.2" + jest-util "^29.6.2" + +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== + +jest-haste-map@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.2.tgz#298c25ea5255cfad8b723179d4295cf3a50a70d1" + integrity sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA== + dependencies: + "@jest/types" "^29.6.1" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.4.3" + jest-util "^29.6.2" + jest-worker "^29.6.2" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.6.2.tgz#e2b307fee78cab091c37858a98c7e1d73cdf5b38" + integrity sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ== + dependencies: + jest-get-type "^29.4.3" + pretty-format "^29.6.2" + +jest-matcher-utils@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535" + integrity sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ== + dependencies: + chalk "^4.0.0" + jest-diff "^29.6.2" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" + +jest-message-util@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.2.tgz#af7adc2209c552f3f5ae31e77cf0a261f23dc2bb" + integrity sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.6.2" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.2.tgz#ef9c9b4d38c34a2ad61010a021866dad41ce5e00" + integrity sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + jest-util "^29.6.2" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== + +jest-resolve-dependencies@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.2.tgz#36435269b6672c256bcc85fb384872c134cc4cf2" + integrity sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w== + dependencies: + jest-regex-util "^29.4.3" + jest-snapshot "^29.6.2" + +jest-resolve@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.6.2.tgz#f18405fe4b50159b7b6d85e81f6a524d22afb838" + integrity sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.2" + jest-pnp-resolver "^1.2.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.6.2.tgz#89e8e32a8fef24781a7c4c49cd1cb6358ac7fc01" + integrity sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w== + dependencies: + "@jest/console" "^29.6.2" + "@jest/environment" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.4.3" + jest-environment-node "^29.6.2" + jest-haste-map "^29.6.2" + jest-leak-detector "^29.6.2" + jest-message-util "^29.6.2" + jest-resolve "^29.6.2" + jest-runtime "^29.6.2" + jest-util "^29.6.2" + jest-watcher "^29.6.2" + jest-worker "^29.6.2" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.6.2.tgz#692f25e387f982e89ab83270e684a9786248e545" + integrity sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/fake-timers" "^29.6.2" + "@jest/globals" "^29.6.2" + "@jest/source-map" "^29.6.0" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.2" + jest-message-util "^29.6.2" + jest-mock "^29.6.2" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.2" + jest-snapshot "^29.6.2" + jest-util "^29.6.2" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.6.2.tgz#9b431b561a83f2bdfe041e1cab8a6becdb01af9c" + integrity sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.6.2" + graceful-fs "^4.2.9" + jest-diff "^29.6.2" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.6.2" + jest-message-util "^29.6.2" + jest-util "^29.6.2" + natural-compare "^1.4.0" + pretty-format "^29.6.2" + semver "^7.5.3" + +jest-util@^29.0.0, jest-util@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.2.tgz#8a052df8fff2eebe446769fd88814521a517664d" + integrity sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.6.2.tgz#25d972af35b2415b83b1373baf1a47bb266c1082" + integrity sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg== + dependencies: + "@jest/types" "^29.6.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + leven "^3.1.0" + pretty-format "^29.6.2" + +jest-watcher@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.6.2.tgz#77c224674f0620d9f6643c4cfca186d8893ca088" + integrity sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA== + dependencies: + "@jest/test-result" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.6.2" + string-length "^4.0.1" + +jest-worker@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.2.tgz#682fbc4b6856ad0aa122a5403c6d048b83f3fb44" + integrity sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ== + dependencies: + "@types/node" "*" + jest-util "^29.6.2" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.6.2.tgz#3bd55b9fd46a161b2edbdf5f1d1bd0d1eab76c42" + integrity sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg== + dependencies: + "@jest/core" "^29.6.2" + "@jest/types" "^29.6.1" + import-local "^3.0.2" + jest-cli "^29.6.2" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +pretty-format@^29.0.0, pretty-format@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47" + integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg== + dependencies: + "@jest/schemas" "^29.6.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +pure-rand@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + +resolve@^1.20.0: + version "1.22.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" + integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-api-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d" + integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A== + +ts-jest@^29.1.1: + version "29.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" + integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "^7.5.3" + yargs-parser "^21.0.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.2.tgz#891e1a90c5189d8506af64b9ef929fca99ba1ee5" + integrity sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +v8-to-istanbul@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^21.0.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==