diff --git a/deployment/scripts/download_and_import_mongo b/deployment/scripts/download_and_import_mongo index 79aedd89..be4608dd 100755 --- a/deployment/scripts/download_and_import_mongo +++ b/deployment/scripts/download_and_import_mongo @@ -4,61 +4,43 @@ set -u # crash on missing env set -e # stop on any error ENVIRONMENT=$1 + if [ "$ENVIRONMENT" == "" ]; then echo "Usage: $0 " exit 1 fi -# Get the directory where the script is located -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -PROJECT_NAME=$(echo "$SCRIPT_DIR" | rev | cut -d'/' -f3 | rev | tr '-' '_') -DB="${PROJECT_NAME}_${ENVIRONMENT}" - -# Ensure the script runs in api -API_DIR="$SCRIPT_DIR/../../services/api" - -# Save the current directory and change to the target directory -pushd "$API_DIR" > /dev/null || { echo "Failed to change directory to $API_DIR"; exit 1; } - -if [ ! -f "./scripts/anonymize-database.js" ]; then - echo "Expecting ./scripts/anonymize-database.js" - exit 1 -fi +DB="bedrock_${ENVIRONMENT}" bedrock cloud authorize $ENVIRONMENT -API_CLI_POD=`bedrock cloud status $ENVIRONMENT 2> /dev/null | grep api-cli-deployment | grep -i running | awk '{print $1}'` +API_CLI_POD=`kubectl get pods | grep api-cli-deployment | grep -i running | awk '{print $1}'` echo "Using API_CLI pod $API_CLI_POD and database $DB" function exec_remote() { kubectl exec -it $API_CLI_POD -- /bin/bash -c "$*" } -rm -f dump.tar.gz -rm -rf dump -echo "Creating export on CLI pod" -exec_remote "rm -rf /export; mkdir -p /export" +echo "Exporting on CLI pod" + +# Pass through arguments past the env +shift + +# Capture and display the output simultaneously +OUTPUT=$(exec_remote "node scripts/database/prepare-export.js --out=/export $@" | tee /dev/tty) + +# Check if the output contains help message keywords +if echo "$OUTPUT" | grep -q "Usage:"; then + exit 0 +fi + +echo "Transfering export" +rm -rf ./export +kubectl cp $API_CLI_POD:/export ./export -exec_remote "cd /export; mongodump --host=\"mongo:27017\" -d $DB; tar cfzv dump.tar.gz dump" -exec_remote "md5sum /export/dump.tar.gz" -echo "Transfering dump file" -#kubectl cp $API_CLI_POD:/export/dump.tar.gz dump.tar.gz -# Ghetto hack because the above is not stable for large files (EOF during transfer. Due to Kubernetes shortcomings) -# Note if you run into md5 mismatch issues increase the sleep below... -kubectl exec $API_CLI_POD -- bash -c 'cat /export/dump.tar.gz && sleep 20' > dump.tar.gz -md5 dump.tar.gz echo "Cleaning up CLI pod" exec_remote "rm -rf /export" -tar xfzv dump.tar.gz + echo "Restoring dump locally" -cd dump; mongorestore --drop $DB -d $DB; cd .. +mongorestore --drop --gzip --nsInclude="$DB.*" --nsFrom="*.*_sanitized" --nsTo="*.*" ./export echo "Cleaning up locally" -rm -rf dump -rm -f dump.tar.gz - -if [ "$ENVIRONMENT" == "production" ]; then - echo "Anonymizing data" - MONGO_URI=mongodb://localhost/$DB node scripts/anonymize-database.js -fi - -# Return to the original directory -popd > /dev/null \ No newline at end of file +rm -rf ./export \ No newline at end of file diff --git a/services/api/Dockerfile.cli b/services/api/Dockerfile.cli index d5793157..df1c14c5 100644 --- a/services/api/Dockerfile.cli +++ b/services/api/Dockerfile.cli @@ -5,6 +5,8 @@ FROM node:20.12.2-alpine # ignore NODE_ENV and take its production-or-not status from this flag instead. ARG NODE_ENV=production +RUN apk add mongodb-tools; + # Note layers should be ordered from less to more likely to change. # Update & install required packages diff --git a/services/api/package.json b/services/api/package.json index 8957a2df..2dd4a340 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -32,8 +32,10 @@ "@sentry/node": "^7.56.0", "@simplewebauthn/server": "^8.3.5", "bcrypt": "^5.1.0", + "commander": "^12.1.0", "fast-csv": "^5.0.1", "front-matter": "^4.0.2", + "glob": "^11.0.0", "google-auth-library": "^9.2.0", "html-to-text": "^9.0.5", "jsonwebtoken": "^9.0.0", diff --git a/services/api/scripts/database/prepare-export.js b/services/api/scripts/database/prepare-export.js new file mode 100644 index 00000000..f0f03e5b --- /dev/null +++ b/services/api/scripts/database/prepare-export.js @@ -0,0 +1,414 @@ +const os = require('os'); +const path = require('path'); +const process = require('process'); +const fs = require('fs/promises'); + +const { glob } = require('glob'); +const mongoose = require('mongoose'); +const { program } = require('commander'); + +const config = require('@bedrockio/config'); +const logger = require('@bedrockio/logger'); + +const { User } = require('../../src/models'); +const { initialize } = require('../../src/utils/database'); + +program + .description( + ` + Prepares database for export. Sanitizes users table and can perform intelligent + filtering of documents based on refs of type "User". + + Note that multiple user filters will use an $or with the exception of + before/after dates which work together. + ` + ) + .option('-a, --created-after [date]', 'Limit to users created after a certain date. Can be any parseable date.') + .option('-b, --created-before [date]', 'Limit to users created before a certain date. Can be any parseable date.') + .option('-u, --user-id [string...]', 'Limit to users by ID (can be multiple).') + .option('-m, --email [string...]', 'Limit to users by email (can be multiple).') + .option('-e, --exclude [string...]', 'Exclude collections.', []) + .option('-r, --raw [boolean]', 'Skip sanitizations. Only use this when necessary.', false) + .option('-o, --out [string]', 'The directory to export the export to.', 'export'); + +program.parse(process.argv); +const options = program.opts(); + +const MONGO_URI = config.get('MONGO_URI'); + +async function run() { + const db = await initialize(); + + const userIds = await getUserIds(); + + const sanitizations = await getSanitizations(options); + await runSanitizations(db, sanitizations); + + await exec('rm', ['-rf', options.out]); + await exportCollections({ + ...options, + db, + userIds, + sanitizations, + }); +} + +async function getUserIds() { + const { createdAfter, createdBefore, userId: userIds, email: emails } = options; + + const $or = []; + + if (createdAfter || createdBefore) { + const after = parseDate(createdAfter); + const before = parseDate(createdBefore); + + if (after > before) { + throw new Error('"--created-after" cannot occur after "--created-before"'); + } + + $or.push({ + createdAt: { + ...(after && { + $gte: after, + }), + ...(before && { + $lte: before, + }), + }, + }); + } + + if (userIds) { + for (let id of userIds) { + if (!mongoose.isObjectIdOrHexString(id)) { + throw new Error(`Invalid ObjectId ${id}.`); + } + } + $or.push({ + _id: { + $in: userIds, + }, + }); + } + + if (emails) { + $or.push({ + email: { + $in: emails, + }, + }); + } + + if (!$or.length) { + return []; + } + + const data = await User.find({ $or }, { _id: true }).lean(); + + if (!data.length) { + throw new Error('No users found'); + } + + return data.map((u) => { + return String(u._id); + }); +} + +// Sanitization + +async function getSanitizations(options) { + if (options.raw) { + return []; + } + const manual = await getManualSanitizations(); + const auto = await getAutoSanitizations(); + return [...manual, ...auto]; +} + +async function getManualSanitizations() { + const gl = path.resolve(__dirname, 'sanitizations/*.{json,js}'); + const files = await glob(gl); + const result = []; + + for (let file of files) { + let definition = require(file); + if (typeof definition === 'function') { + definition = await definition(); + } + const { collection, pipeline } = definition; + + if (!collection) { + throw new Error('"collection" required in sanitization.'); + } else if (!pipeline) { + throw new Error('"pipeline" required in sanitization.'); + } + + result.push({ + name: getSanitizedName(collection), + ...definition, + }); + } + + return result; +} + +async function getAutoSanitizations() { + const result = []; + + for (let model of Object.values(mongoose.models)) { + const collection = model.collection.name; + + if (isPluginCollection(collection)) { + continue; + } + + const fields = {}; + + for (let [name, path] of Object.entries(model.schema.paths)) { + const sanitize = path.options?.sanitize; + const remove = sanitize === true; + if (sanitize) { + fields[name] = { + $cond: { + if: { + $ne: [`$${name}`, null], + }, + then: remove ? '$$REMOVE' : sanitize, + // Effectively doesn't set the field if it does not exist. + else: '$$REMOVE', + }, + }; + } + } + if (Object.keys(fields).length > 0) { + result.push({ + name: getSanitizedName(collection), + collection, + pipeline: [ + { + $set: fields, + }, + ], + }); + } + } + return result; +} + +async function runSanitizations(db, sanitizations) { + for (let sanitization of sanitizations) { + const { collection, pipeline } = sanitization; + if (!collection) { + throw new Error('"collection" required in sanitization.'); + } else if (!pipeline) { + throw new Error('"pipeline" required in sanitization.'); + } + + const sanitizedName = `${collection}_sanitized`; + + try { + // Attempt to modify the view + await db.db.command({ + collMod: sanitizedName, + viewOn: collection, + pipeline, + }); + } catch { + // View not created yet so create it + await db.createCollection(sanitizedName, { + viewOn: collection, + pipeline, + }); + } + } +} + +function getSanitizedName(collection) { + return `${collection}_sanitized`; +} + +// Exporting + +async function exportCollections(options) { + const { db, exclude, sanitizations, userIds } = options; + + const promises = []; + + for (let model of Object.values(mongoose.models)) { + let collection = model.collection.name; + + const flags = []; + + if (exclude.includes(collection)) { + if (collection === 'users') { + throw new Error('Cannot exclude users.'); + } + continue; + } else if (isPluginCollection(collection)) { + exclude.push(collection); + continue; + } + + const sanitization = sanitizations.find((s) => { + return s.collection === collection; + }); + + if (userIds.length) { + if (collection === 'users') { + const query = { + $or: [ + getFieldQuery('_id', userIds), + { + 'roles.role': { + $in: ['superAdmin'], + }, + }, + ], + }; + const tmpfile = await writeQueryFile(query); + flags.push(`--queryFile=${tmpfile}`); + } else { + const userFields = Object.entries(model.schema.paths) + .filter(([pathName, pathType]) => { + if (!(pathType instanceof mongoose.SchemaTypes.ObjectId)) { + return false; + } else if (pathType.options.ref !== 'User') { + return false; + } + + return pathName; + }) + .map((entry) => { + return entry[0]; + }); + + if (userFields.length) { + const query = { + $or: userFields.map((field) => { + return getFieldQuery(field, userIds); + }), + }; + const tmpfile = await writeQueryFile(query); + flags.push(`--queryFile=${tmpfile}`); + } + } + } + + if (sanitization) { + // Exclude original collection + exclude.push(collection); + + // Use sanitized collection and add flags to export the view. + flags.push('--viewsAsCollections'); + collection = sanitization.name; + } + + if (flags.length) { + promises.push(runExport(['-c', collection, ...flags])); + exclude.push(collection); + } + } + + if (options.raw) { + // Exclude sanitized views which may have been + // previously created if dumping raw documents. + const collections = await db.listCollections(); + for (let collection of collections) { + const { name } = collection; + if (name.endsWith('_sanitized')) { + exclude.push(name); + } + } + } + + promises.push(runExport(getExcludeFlags(exclude))); + + await Promise.all(promises); +} + +async function writeQueryFile(query) { + const dir = os.tmpdir(); + const filename = `query-${Date.now()}.json`; + const tmpfile = path.join(dir, filename); + await fs.writeFile(tmpfile, JSON.stringify(query)); + return tmpfile; +} + +// Dumping + +async function runExport(flags = []) { + const { out } = options; + const args = [MONGO_URI, '--gzip', `--out=${out}`, ...flags]; + await exec('mongodump', args); +} + +function getExcludeFlags(excludes) { + return excludes.map((collection) => { + return `--excludeCollection=${collection}`; + }); +} + +function getFieldQuery(field, ids) { + return { + [field]: { + $in: ids.map((id) => { + return { + // ObjectIds must be passed this way to --query + // in mongodump as they cannot be raw strings. + $oid: id, + }; + }), + }, + }; +} + +// Utils + +function isPluginCollection(collection) { + return collection.startsWith('plugin_'); +} + +function parseDate(str) { + if (str) { + const date = new Date(str); + if (isNaN(date.getTime())) { + throw new Error('Invalid date.'); + } + return date; + } +} + +async function exec(command, args = []) { + return new Promise((resolve, reject) => { + const { spawn } = require('child_process'); + const child = spawn(command, args); + + child.stdout.on('data', (data) => { + process.stdout.write(data); + }); + + child.stderr.on('data', (data) => { + process.stderr.write(data); + }); + + child.on('close', (code) => { + if (code !== 0) { + return reject(new Error(`Process exited with code ${code}`)); + } + resolve(); + }); + + child.on('error', (err) => { + reject(err); + }); + }); +} + +run() + .then(() => { + process.exit(0); + }) + .catch((error) => { + logger.error(`Fatal error: ${error.message}, exiting.`); + process.exit(1); + }); diff --git a/services/api/scripts/database/sanitizations/uploads.json b/services/api/scripts/database/sanitizations/uploads.json new file mode 100644 index 00000000..8c2b4e66 --- /dev/null +++ b/services/api/scripts/database/sanitizations/uploads.json @@ -0,0 +1,12 @@ +{ + "collection": "uploads", + "pipeline": [ + { + "$match": { + "private": { + "$ne": true + } + } + } + ] +} diff --git a/services/api/scripts/database/sanitizations/users.js b/services/api/scripts/database/sanitizations/users.js new file mode 100644 index 00000000..dfd91318 --- /dev/null +++ b/services/api/scripts/database/sanitizations/users.js @@ -0,0 +1,77 @@ +// Note that this is an example of a complex sanitization pipeline. +// For a simple case the "sanitize" key can be set on individual fields +// in a model definition: + +// - When `true` the value will be stripped from all documents. +// - When a string the value will be set to a literal for all documents. + +const bcrypt = require('bcrypt'); + +// Development password. Note this is hard +// coded as this will be run on the actual +// CLI deploymenet pod. +const DEV_PASSWORD = 'development.now'; + +module.exports = async () => { + const salt = await bcrypt.genSalt(12); + const hashedPassword = await bcrypt.hash(DEV_PASSWORD, salt); + + return { + collection: 'users', + pipeline: [ + // Tag admins + { + $set: { + isAdmin: { + $anyElementTrue: { + $map: { + input: '$roles', + as: 'role', + in: { + $in: ['$$role.role', ['superAdmin', 'admin', 'viewer']], + }, + }, + }, + }, + }, + }, + // Obfuscate email/lastName if not admin. + // Set password to dev password for all. + { + $set: { + email: { + $cond: { + if: '$isAdmin', + then: '$email', + else: { + $concat: [ + { + $toString: '$_id', + }, + '@bedrock.foundation', + ], + }, + }, + }, + lastName: { + $cond: { + if: '$isAdmin', + then: '$lastName', + else: 'Doe', + }, + }, + hashedPassword: { + $literal: hashedPassword, + }, + }, + }, + // Unset sensitive fields if any. + // { + // $unset: ['dob'], + // }, + { + $unset: 'isAdmin', + }, + ], + }; +}; diff --git a/services/api/src/models/definitions/application-credential.json b/services/api/src/models/definitions/application-credential.json index f5188299..fd55ce19 100644 --- a/services/api/src/models/definitions/application-credential.json +++ b/services/api/src/models/definitions/application-credential.json @@ -4,6 +4,7 @@ "type": "String", "trim": true, "required": true, + "sanitize": true, "unique": true, "writeAccess": "none" }, diff --git a/services/api/yarn.lock b/services/api/yarn.lock index 2d5e7331..cb27ee66 100644 --- a/services/api/yarn.lock +++ b/services/api/yarn.lock @@ -539,6 +539,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@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" @@ -1678,6 +1690,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -1702,6 +1719,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + anymatch@^3.0.3: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -2248,6 +2270,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + common-tags@^1.4.0: version "1.8.2" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" @@ -2328,6 +2355,15 @@ cross-fetch@^4.0.0: dependencies: node-fetch "^2.6.12" +cross-spawn@^7.0.0: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + 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" @@ -2535,6 +2571,11 @@ duplexify@^4.1.3: readable-stream "^3.1.1" stream-shift "^1.0.2" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -2562,6 +2603,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + encodeurl@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3077,6 +3123,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" @@ -3242,6 +3296,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" + integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3860,6 +3926,13 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +jackspeak@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015" + integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw== + dependencies: + "@isaacs/cliui" "^8.0.2" + jest-changed-files@^29.5.0, jest-changed-files@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" @@ -4642,6 +4715,11 @@ lru-cache@6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^11.0.0: + version "11.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39" + integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4743,6 +4821,13 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + 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" @@ -4781,6 +4866,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -5132,6 +5222,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + pako@~1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -5187,6 +5282,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + path-to-regexp@^6.2.1: version "6.2.2" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36" @@ -5694,6 +5797,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -5808,6 +5916,15 @@ 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@^1.0.2 || 2 || 3 || 4", 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" @@ -5817,6 +5934,15 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.matchall@^4.0.10: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" @@ -5877,6 +6003,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.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@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -5891,6 +6024,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -6406,6 +6546,15 @@ 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: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -6415,6 +6564,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"