Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add the permit-generator github action #69

Closed
wants to merge 47 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
25932b2
feat: add the permit-generator github action
hhio618 Sep 15, 2024
f0b5b9d
feat: add permit-generator script [WIP]
hhio618 Sep 23, 2024
36e3245
feat: add the permit-generator script
hhio618 Sep 24, 2024
b2038b0
feat: post permits to github issues
hhio618 Sep 27, 2024
c3b3c8d
fix: few adjustments
hhio618 Sep 27, 2024
798aa5c
fix: add missing envs
hhio618 Sep 29, 2024
e397fe0
refactor: downgrade to ethers@v5
cohow Sep 15, 2024
787979f
fix: add yarnrc file back
cohow Sep 15, 2024
2e61614
chore: change yarn version
cohow Sep 16, 2024
ba0d046
chore: remove packageManager
cohow Sep 16, 2024
a225135
fix: remove the env decoder
hhio618 Sep 29, 2024
f926e74
fix: type/typo fix
hhio618 Sep 29, 2024
1620035
chore: add logs
hhio618 Sep 29, 2024
0ac5357
fix: extend context env type
hhio618 Sep 29, 2024
917ae26
refactor: switch over action core lib
hhio618 Sep 29, 2024
f828cb3
chore: add log
hhio618 Sep 29, 2024
8540751
refactor: add USERS_AMOUNTS env
hhio618 Sep 29, 2024
133090e
chore: add more logs
hhio618 Sep 29, 2024
f4fb78c
Merge branch 'ubiquity-os:development' into development
hhio618 Sep 30, 2024
a7c05c9
fix: supabase wallets query
hhio618 Oct 3, 2024
7191ee8
feat: use workflow_dispatch runId as issueNodeId
hhio618 Oct 3, 2024
011e368
chore: cleanup
hhio618 Oct 3, 2024
81884f6
fix: add missing env X25519_PRIVATE_KEY
hhio618 Oct 3, 2024
04ecb36
test: hard-code fastest provider
hhio618 Oct 3, 2024
f31251d
fix: pipeline data flow
hhio618 Oct 3, 2024
203433f
fix: report request
hhio618 Oct 3, 2024
153dfe4
fix: report request
hhio618 Oct 3, 2024
55423fb
refactor: runId type
hhio618 Oct 8, 2024
52cb2c3
Merge branch 'ubiquity-os:development' into development
hhio618 Oct 8, 2024
d5f79bc
fix: unit-tests
hhio618 Oct 8, 2024
479ce93
chore: cleanup
hhio618 Oct 8, 2024
b945bdc
feat: use precompiled script
hhio618 Oct 8, 2024
41f85f5
refactor: revert script execution command
hhio618 Oct 8, 2024
b147da4
chore: remove unused ncc dep
hhio618 Oct 8, 2024
d19d962
refactor: rename USERS_AMOUNTS to PAYMENT_REQUESTS
hhio618 Oct 12, 2024
6aaa1ce
feat: add step to log permits data
hhio618 Oct 12, 2024
5e858dd
refactor: remove the contributionType
hhio618 Oct 12, 2024
99d2feb
feat: use uuid for nonce
hhio618 Oct 14, 2024
8e5cd8f
chore: fix knip
hhio618 Oct 15, 2024
4aadfc4
chore: add @types/uuid
hhio618 Oct 15, 2024
2db0505
chore: cleanup
hhio618 Oct 15, 2024
ff0d9d9
chore: fix format:cspell
hhio618 Oct 15, 2024
5b20c35
refactor: remove runId
hhio618 Oct 16, 2024
c22e4c2
refactor: simplify the env vars
hhio618 Oct 16, 2024
c78613a
refactor: rem return to kernel
hhio618 Oct 17, 2024
9fe5fda
Merge branch 'ubiquity-os:development' into development
hhio618 Oct 17, 2024
fe3e60a
chore: cleanup
hhio618 Oct 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/permit-generator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Permit Generator

on:
workflow_dispatch:
inputs:
PAYMENT_REQUESTS:
description: "A JSON array containing usernames and associated amounts"
required: true
# example: '[{"user1": 100}, {"user2": 150}]'

jobs:
run:
runs-on: ubuntu-latest
permissions: write-all

steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20.10.0"

- name: Install dependencies
run: yarn install --immutable --immutable-cache --check-cache

- name: Process permit requests
id: parse
run: |
echo "Received input: ${{ github.event.inputs.PAYMENT_REQUESTS }}"
npx tsx scripts/github-action-permit-generator.ts permits.txt
shell: bash
env:
X25519_PRIVATE_KEY: ${{ secrets.X25519_PRIVATE_KEY }}
EVM_NETWORK_ID: ${{ secrets.EVM_NETWORK_ID }}
EVM_PRIVATE_KEY: ${{ secrets.EVM_PRIVATE_KEY }}
EVM_TOKEN_ADDRESS: ${{ secrets.EVM_TOKEN_ADDRESS }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL}}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PAYMENT_REQUESTS: ${{ github.event.inputs.PAYMENT_REQUESTS }}

- name: Log permits data
run: |
export PERMITS=$(cat permits.txt)
echo $PERMITS
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"@uniswap/permit2-sdk": "^1.2.0",
"dotenv": "^16.4.4",
"ethers": "^5.7.2",
"libsodium-wrappers": "^0.7.13"
"libsodium-wrappers": "^0.7.13",
"uuid": "^10.0.0"
},
"devDependencies": {
"@commitlint/cli": "^18.6.1",
Expand All @@ -48,6 +49,7 @@
"@jest/types": "29.6.3",
"@types/libsodium-wrappers": "^0.7.8",
"@types/node": "^20.11.19",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"cspell": "^8.4.0",
Expand Down
94 changes: 94 additions & 0 deletions scripts/github-action-permit-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Octokit } from "@octokit/rest";
import { createClient } from "@supabase/supabase-js";
import { createAdapters } from "../src/adapters";
import { Database } from "../src/adapters/supabase/types/database";
import { generatePayoutPermit } from "../src/handlers";
import { Context } from "../src/types/context";
import { PermitGenerationSettings, PermitRequest } from "../src/types/plugin-input";
import * as fs from "fs";

function getEnvVar(key: string) {
return (
process.env[key] ||
(() => {
throw new Error(`Environment variable ${key} is required`);
})()
);
}
/**
* Generates all the permits based on the current github workflow dispatch.
*/
export async function generatePermitsFromGithubWorkflowDispatch() {
const EVM_NETWORK_ID = getEnvVar("EVM_NETWORK_ID");
const EVM_PRIVATE_KEY = getEnvVar("EVM_PRIVATE_KEY");
const EVM_TOKEN_ADDRESS = getEnvVar("EVM_TOKEN_ADDRESS");
const PAYMENT_REQUESTS = getEnvVar("PAYMENT_REQUESTS");
const GITHUB_TOKEN = getEnvVar("GITHUB_TOKEN");
const SUPABASE_URL = getEnvVar("SUPABASE_URL");
const SUPABASE_KEY = getEnvVar("SUPABASE_KEY");

console.log(`Received: ${PAYMENT_REQUESTS}`);
const userAmounts = JSON.parse(PAYMENT_REQUESTS);

// Populate the permitRequests from the user_amounts payload

const permitRequests: PermitRequest[] = userAmounts.flatMap((userObj: { [key: string]: number }) =>
Object.entries(userObj).map(([user, amount]) => ({
type: "ERC20",
username: user,
amount: amount,
tokenAddress: EVM_TOKEN_ADDRESS,
}))
);

const config: PermitGenerationSettings = {
evmNetworkId: Number(EVM_NETWORK_ID),
evmPrivateEncrypted: EVM_PRIVATE_KEY,
permitRequests: permitRequests,
};

const octokit = new Octokit({ auth: GITHUB_TOKEN });
const supabaseClient = createClient<Database>(SUPABASE_URL, SUPABASE_KEY);

const context: Context = {
eventName: "workflow_dispatch",
config: config,
octokit,
payload: userAmounts,
env: undefined,
logger: {
debug(message: unknown, ...optionalParams: unknown[]) {
console.debug(message, ...optionalParams);
},
info(message: unknown, ...optionalParams: unknown[]) {
console.log(message, ...optionalParams);
},
warn(message: unknown, ...optionalParams: unknown[]) {
console.warn(message, ...optionalParams);
},
error(message: unknown, ...optionalParams: unknown[]) {
console.error(message, ...optionalParams);
},
fatal(message: unknown, ...optionalParams: unknown[]) {
console.error(message, ...optionalParams);
},
},
adapters: {} as ReturnType<typeof createAdapters>,
};

context.adapters = createAdapters(supabaseClient, context);

const permits = await generatePayoutPermit(context, config.permitRequests);
const out = Buffer.from(JSON.stringify(permits)).toString("base64");
fs.writeFile(process.argv[2], out, (err) => {
if (err) {
throw err;
}
});
}

generatePermitsFromGithubWorkflowDispatch()
.then((result) => console.log(`result: ${result}`))
.catch((error) => {
console.error(error);
});
7 changes: 5 additions & 2 deletions src/adapters/supabase/helpers/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ export class Wallet extends Super {
throw error;
}

console.info("Successfully fetched wallet", { userId, address: data.wallets?.address });
return data.wallets?.address;
// Check if wallets is an array, if so, return the first element's address
const address = Array.isArray(data.wallets) ? data.wallets[0]?.address : data.wallets?.address;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should only be a single wallet associated per user. Why did you check for arrays

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep getting an array here. Even though the docs say wallets should be an object, it’s probably showing up as an array because Supabase treats it like a one-to-many relationship, even if the user only has a single wallet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whilefoo @rndquu can you concur this is correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you use single() to retrieve only one? The type should be properly deduced.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, keep getting an array.
Here's the code:

  async getWalletByUserId(userId: number) {
    const { data, error } = await this.supabase.from("users").select("wallets(*)").eq("id", userId).single();
    if (error) {
      console.error("Failed to get wallet", { userId, error });
      throw error;
    }

    console.info("Result: ", { wallets: data.wallets });
    // Check if wallets is an array, if so, return the first element's address
    const address = Array.isArray(data.wallets) ? data.wallets[0]?.address : data.wallets?.address;

    console.info("Successfully fetched wallet", { userId, address: address });
    return address;
  }

Results after run:

Result:  {
  wallets: [
    {
      address: '0x6321286F9B73f427C72e1f9F1bC6b3d25eF06605',
      user_id: 1272158,
      wallet_id: 1
    }
  ]
}
Successfully fetched wallet {
  userId: 1272158,
  address: '0x6321286F9B73f427C72e1f9F1bC6b3d25eF06605'
}

Copy link
Contributor

@whilefoo whilefoo Oct 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it's correct for that to be an array because it's querying an user with wallets as a relation. if you wanted to get just one record you would need to query the wallet table directly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whilefoo The code is already in the base branch but isn't working without the changes in this PR. Should I move the hotfix to a separate PR?


console.info("Successfully fetched wallet", { userId, address: address });
return address;
}

async upsertWallet(userId: number, address: string) {
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/generate-erc20-permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Context, Logger } from "../types/context";
import { PermitReward, TokenType } from "../types";
import { decrypt, parseDecryptedPrivateKey } from "../utils";
import { getFastestProvider } from "../utils/get-fastest-provider";
import { v4 as uuid } from "uuid";

export interface Payload {
evmNetworkId: number;
Expand Down Expand Up @@ -55,7 +56,7 @@ export async function generateErc20PermitSignature(
} else if ("pull_request" in contextOrPayload.payload) {
issueNodeId = contextOrPayload.payload.pull_request.node_id;
} else {
throw new Error("Issue Id is missing");
issueNodeId = uuid();
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/handlers/generate-erc721-permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export async function generateErc721PermitSignature(
_repositoryName = contextOrPermitPayload.repositoryName;
_userId = contextOrPermitPayload.userId;
} else {
if (!contextOrPermitPayload.env) {
throw new Error(`env is undefined`);
}
const { NFT_MINTER_PRIVATE_KEY, NFT_CONTRACT_ADDRESS } = contextOrPermitPayload.env;
const { evmNetworkId } = contextOrPermitPayload.config;
const adapters = contextOrPermitPayload.adapters;
Expand Down
2 changes: 1 addition & 1 deletion src/types/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ export interface Context<T extends WebhookEventName = SupportedEvents> {
octokit: InstanceType<typeof Octokit>;
adapters: ReturnType<typeof createAdapters>;
config: PermitGenerationSettings;
env: Env;
env?: Env;
logger: Logger;
}
36 changes: 17 additions & 19 deletions src/utils/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ export async function decrypt(encryptedText: string, x25519PrivateKey: string):
await sodium.ready;

const publicKey = await getPublicKey(x25519PrivateKey);

const binaryPublic = sodium.from_base64(publicKey, sodium.base64_variants.URLSAFE_NO_PADDING);
const binaryPrivate = sodium.from_base64(x25519PrivateKey, sodium.base64_variants.URLSAFE_NO_PADDING);
const binaryEncryptedText = sodium.from_base64(encryptedText, sodium.base64_variants.URLSAFE_NO_PADDING);

const decryptedText = sodium.crypto_box_seal_open(binaryEncryptedText, binaryPublic, binaryPrivate, "text");

return decryptedText;
return sodium.crypto_box_seal_open(binaryEncryptedText, binaryPublic, binaryPrivate, "text");
}

/**
Expand All @@ -36,40 +34,40 @@ export async function getPublicKey(x25519PrivateKey: string): Promise<string> {
* 1. Private key
* 2. Organization id where this private key is allowed to be used
* 3. Repository id where this private key is allowed to be used
*
* The issue with "plain" encryption of wallet private keys is that if partner accidentally shares
* his encrypted private key then a malicious user will be able to use that leaked private key
*
* The issue with "plain" encryption of wallet private keys is that if partner accidentally shares
* his encrypted private key then a malicious user will be able to use that leaked private key
* in another organization with permits generated from a leaked partner's wallet.
*
*
* Partner private key (`evmPrivateEncrypted` config param in `conversation-rewards` plugin) supports 3 formats:
* 1. PRIVATE_KEY
* 2. PRIVATE_KEY:GITHUB_ORGANIZATION_ID
* 3. PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID
*
*
* Format "PRIVATE_KEY" can be used only for `ubiquity` and `ubiquibot` organizations. It is
* kept for backwards compatibility in order not to update private key formats for our existing
* values set in the `evmPrivateEncrypted` param.
*
*
* Format "PRIVATE_KEY:GITHUB_ORGANIZATION_ID" restricts in which particular organization this private
* key can be used. It can be set either in the organization wide config either in the repository wide one.
*
* Format "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" restricts organization and a particular
*
* Format "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" restricts organization and a particular
* repository where private key is allowed to be used.
*
* @param decryptedPrivateKey Decrypted private key string (in any of the 3 different formats)
*
* @param decryptedPrivateKey Decrypted private key string (in any of the 3 different formats)
* @returns Parsed private key object: private key, organization id and repository id
*/
export function parseDecryptedPrivateKey(decryptedPrivateKey: string) {
let result: {
privateKey: string | null,
allowedOrganizationId: number | null,
allowedRepositoryId: number | null,
const result: {
privateKey: string | null;
allowedOrganizationId: number | null;
allowedRepositoryId: number | null;
} = {
privateKey: null,
allowedOrganizationId: null,
allowedRepositoryId: null,
};

// split private key
const privateKeyParts = decryptedPrivateKey.split(":");

Expand Down
41 changes: 13 additions & 28 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==

"@types/uuid@^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==

"@types/ws@^8.5.10":
version "8.5.12"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
Expand Down Expand Up @@ -6157,16 +6162,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"

"string-width-cjs@npm:string-width@^4.2.0":
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"

string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.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==
Expand Down Expand Up @@ -6238,14 +6234,7 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"

"strip-ansi-cjs@npm: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-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm: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==
Expand Down Expand Up @@ -6655,6 +6644,11 @@ util-deprecate@^1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==

uuid@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==

uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
Expand Down Expand Up @@ -6779,16 +6773,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==

"wrap-ansi-cjs@npm: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"

wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.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==
Expand Down