diff --git a/package.json b/package.json index d768f01..aeff635 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/aio-cli-plugin-api-mesh", - "version": "5.5.0", + "version": "5.6.0", "description": "Adobe I/O CLI plugin to develop and manage API mesh sources", "keywords": [ "oclif-plugin" @@ -38,13 +38,13 @@ "version": "oclif-dev readme && git add README.md" }, "dependencies": { - "@adobe-apimesh/mesh-builder": "2.2.0", + "@adobe-apimesh/mesh-builder": "2.3.1", "@adobe/aio-cli-lib-console": "^5.0.0", "@adobe/aio-lib-core-config": "^5.0.0", "@adobe/aio-lib-core-logging": "^3.0.0", "@adobe/aio-lib-env": "^3.0.0", "@adobe/aio-lib-ims": "^7.0.1", - "@adobe/plugin-hooks": "0.3.4", + "@adobe/plugin-hooks": "0.4.0", "@adobe/plugin-on-fetch": "0.1.1", "@adobe/plugin-source-headers": "^0.0.2", "@envelop/disable-introspection": "^6.0.0", diff --git a/src/meshArtifact.js b/src/meshArtifact.js index 5ab34d6..f85f403 100644 --- a/src/meshArtifact.js +++ b/src/meshArtifact.js @@ -70,8 +70,8 @@ function resolveComposerAsTypeScriptModule(data) { * ``` */ function resolveComposerAsJavaScriptModule(data) { - return data.replace( - /"composer":\s*"((?!https:\/\/)[^#]+)#([^"]+)"/, + return data.replaceAll( + /"composer":\s*"((?!https:\/\/)[^#]+)#([^"]+)"/g, `"module": __importStar(require("$1")), "fn": "$2"`, ); } diff --git a/src/server.js b/src/server.js index 1c65ce4..7e9e437 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,7 @@ import { getMesh } from '@graphql-mesh/runtime'; +import { KvStateApiImpl } from './state'; + const { getCorsOptions } = require('./cors'); const { createYoga } = require('graphql-yoga'); const { GraphQLError } = require('graphql/error'); @@ -46,13 +48,14 @@ async function getBuiltMesh(meshArtifacts, meshConfig) { } const buildServer = async (loggerInstance, env, meshArtifacts, meshConfig) => { - const { Secret: secret } = env; + const { SECRETS } = env; const tenantMesh = await getBuiltMesh(meshArtifacts, meshConfig); - const meshSecrets = loadMeshSecrets(loggerInstance, secret); + const meshSecrets = loadMeshSecrets(loggerInstance, SECRETS); return await buildYogaServer(env, tenantMesh, meshConfig, meshSecrets); }; async function buildYogaServer(env, tenantMesh, meshConfig, meshSecrets) { + const stateApi = new KvStateApiImpl(env, meshConfig); const secretsProxy = new Proxy(meshSecrets, getSecretsHandler); return createYoga({ plugins: tenantMesh.plugins, @@ -61,6 +64,7 @@ async function buildYogaServer(env, tenantMesh, meshConfig, meshSecrets) { context: initialContext => ({ ...initialContext, secrets: secretsProxy, + state: stateApi, }), maskedErrors: { maskError: maskError, diff --git a/src/state.js b/src/state.js new file mode 100644 index 0000000..54cfd58 --- /dev/null +++ b/src/state.js @@ -0,0 +1,155 @@ +import { GraphQLError } from 'graphql/error'; + +/** + * Whether string is empty. + * @param value {string | null | undefined} String value. + */ +const isEmptyString = value => { + return value === '' || value === null || value === undefined; +}; + +/** + * Get byte length of a string. + * @param value {string} String value. + */ +const getByteLength = value => { + return new TextEncoder().encode(value).length; +}; + +/** + * Abstract class for state API implementations. + */ +class StateApiAbstract { + #env; + + constructor(env) { + this.#env = env; + } + + getEnv() { + return this.#env; + } + + /** + * Validate a key. + * @param key {string} Key to validate. + */ + validateKey(key) { + if (isEmptyString(key)) { + throw new Error(`Invalid key: ${key}`); + } + const keyBytes = getByteLength(key); + const env = this.getEnv(); + if (keyBytes > env.MAX_STATE_KEY_SIZE_BYTES) { + throw new Error( + `Key ${key} exceeds maximum key size of ${env.MAX_STATE_KEY_SIZE_BYTES}. Received ${keyBytes} B.`, + ); + } + } + + /** + * Validate a value + * @param key {string} Key. + * @param value {string} Value to validate. + */ + validateValue(key, value) { + if (isEmptyString(value)) { + throw new Error(`Invalid value for key: ${key}`); + } + const valueBytes = getByteLength(value); + const env = this.getEnv(); + if (valueBytes > env.MAX_STATE_VALUE_SIZE_BYTES) { + throw new Error( + `Key ${key} value exceeds maximum key size of ${env.MAX_STATE_VALUE_SIZE_BYTES}. Received ${valueBytes} B.`, + ); + } + } + + /** + * Get the TTL in milliseconds based on the provided config or environment defaults. + * @param config {{ ttl?: number } | undefined} Configuration object that may contain a TTL value. + */ + getTtl(config) { + const env = this.getEnv(); + // Default to maximum ttl if not provided + let useTtl = config?.ttl || env.MAX_STATE_TTL_SECONDS; + // Set TTL to max when over maximum TTL + if (useTtl > env.MAX_STATE_TTL_SECONDS) { + useTtl = env.MAX_STATE_TTL_SECONDS; + } + // Set TTL to min seconds when under minimum TTL + if (useTtl < env.MIN_STATE_TTL_SECONDS) { + useTtl = env.MIN_STATE_TTL_SECONDS; + } + return useTtl; + } +} + +/** + * State API implementation using Cloudflare's KV as the backing store. KV is a distributed key-value store that allows + * for storing and retrieving key-value pairs across different worker instances with eventual consistency. + */ +class KvStateApiImpl extends StateApiAbstract { + meshConfig; + + constructor(env, meshConfig) { + super(env); + this.meshConfig = meshConfig; + } + + /** + * Ensure that the KV namespace is configured in the environment. + * @throws {GraphQLError} when the KV namespace is not configured. + */ + ensureKvConfigured() { + if (!this.getEnv().MESH_KV_NAMESPACE || !this.meshConfig?.state?.enabled) { + throw new GraphQLError( + 'Context state is not configured for this mesh. Please check your mesh configuration and try again.', + { + extensions: { + code: 'ERROR_CONTEXT_STATE_NOT_CONFIGURED', + }, + }, + ); + } + } + + /** + * Get a value by key. + * @param key {key} Key to retrieve. + * @return {Promise} + */ + async get(key) { + this.ensureKvConfigured(); + this.validateKey(key); + return this.getEnv().MESH_KV_NAMESPACE.get(key); + } + + /** + * Put a key-value pair with optional TTL. + * @param key {string} Key to store. + * @param value {string} Value to store. + * @param config {{ ttl?: number } | undefined} Optional configuration object that may contain a TTL value in seconds. + * @return {Promise} + */ + async put(key, value, config) { + this.ensureKvConfigured(); + this.validateKey(key); + this.validateValue(key, value); + const ttl = this.getTtl(config); + return this.getEnv().MESH_KV_NAMESPACE.put(key, value, { expirationTtl: ttl }); + } + + /** + * Delete a key-value pair. + * @param key {string} Key to delete. + * @return {Promise} + */ + async delete(key) { + this.ensureKvConfigured(); + this.validateKey(key); + return this.getEnv().MESH_KV_NAMESPACE.delete(key); + } +} + +module.exports = { KvStateApiImpl }; diff --git a/src/templates/gitignore b/src/templates/gitignore index aa15a7b..52cdab7 100644 --- a/src/templates/gitignore +++ b/src/templates/gitignore @@ -1,5 +1,13 @@ +# Dependencies node_modules + +# Temporary files +.wrangler +.mesh +tenantFiles +tempfiles mesh-artifact meshes -tempfiles -*.txt + +# Template files from @adobe/aio-cli-plugin-api-mesh +wrangler.toml diff --git a/src/templates/package.json b/src/templates/package.json index 5d0aff7..451171f 100644 --- a/src/templates/package.json +++ b/src/templates/package.json @@ -26,7 +26,7 @@ "@graphql-mesh/soap": "0.14.25", "@graphql-mesh/http": "^0.96.9", "graphql": "^16.6.0", - "@adobe/plugin-hooks": "0.3.3", + "@adobe/plugin-hooks": "0.4.0", "@adobe/plugin-on-fetch": "0.1.1", "eslint-plugin-no-loops": "^0.3.0", "eslint-plugin-node": "^11.1.0", diff --git a/src/templates/wrangler.toml b/src/templates/wrangler.toml index f73ae11..0180614 100644 --- a/src/templates/wrangler.toml +++ b/src/templates/wrangler.toml @@ -1,7 +1,6 @@ # Tenant worker core definition. For platform workers metadata configs are used to set bindings. name = "tenant-worker-core" compatibility_date = "2024-06-03" -node_compat = false find_additional_modules = true base_dir = ".mesh" @@ -9,6 +8,13 @@ rules = [ { type = "CommonJS", globs = ["tenantFiles/**/*.js"] } ] +kv_namespaces = [ + { binding = "MESH_KV_NAMESPACE", id = "local-kv" } +] -[[migrations]] -tag = "v1" +[vars] +# State configuration +MAX_STATE_KEY_SIZE_BYTES="512" # 512 bytes +MAX_STATE_VALUE_SIZE_BYTES="1048576" # 1 MiB +MIN_STATE_TTL_SECONDS="60" # 1 minute +MAX_STATE_TTL_SECONDS="604800" # 7 days diff --git a/src/utils.js b/src/utils.js index 547f7c8..861c205 100644 --- a/src/utils.js +++ b/src/utils.js @@ -227,6 +227,38 @@ function getFilesInMeshConfig(data, meshConfigName) { filesList.push(filename); } } + + if (plugin.hooks.afterAll) { + const composer = plugin.hooks.afterAll.composer; + if (composer && !fileURLRegex.test(composer)) { + const [filename] = composer.split('#'); + filesList.push(filename); + } + } + + if (plugin.hooks.beforeSource) { + Object.values(plugin.hooks.beforeSource).forEach(beforeSourceHooks => { + beforeSourceHooks?.forEach(hook => { + const composer = hook.composer; + if (composer && !fileURLRegex.test(composer)) { + const [filename] = composer.split('#'); + filesList.push(filename); + } + }); + }); + } + + if (plugin.hooks.afterSource) { + Object.values(plugin.hooks.afterSource).forEach(afterSourceHooks => { + afterSourceHooks?.forEach(hook => { + const composer = hook.composer; + if (composer && !fileURLRegex.test(composer)) { + const [filename] = composer.split('#'); + filesList.push(filename); + } + }); + }); + } } }); diff --git a/src/wranglerCli.js b/src/wranglerCli.js index 3244aca..b092247 100644 --- a/src/wranglerCli.js +++ b/src/wranglerCli.js @@ -2,6 +2,7 @@ const { spawn } = require('child_process'); const { readSecretsFile } = require('./serverUtils'); const packageData = require('../package.json'); const { join } = require('node:path'); +const fs = require('node:fs'); /** * Starts the wrangler dev server @@ -13,7 +14,10 @@ const { join } = require('node:path'); const start = (command, port, debug, inspectPort) => { const wranglerPackageNumber = packageData.dependencies.wrangler; const wranglerVersion = `wrangler@${wranglerPackageNumber.replace(/^[\^~]/, '')}`; - const wranglerToml = join(__dirname, '..', 'wrangler.toml'); + // Project wrangler.toml must be used to avoid issues w/ Wrangler. Ensure it is up to date. + const wranglerTomlTemplate = join(__dirname, '..', 'wrangler.toml'); + const wranglerToml = join(process.cwd(), 'wrangler.toml'); + fs.cpSync(wranglerTomlTemplate, wranglerToml); const meshDir = '.mesh'; const secrets = readSecretsFile(meshDir); const entrypoint = join(__dirname, 'worker.js'); @@ -29,7 +33,7 @@ const start = (command, port, debug, inspectPort) => { '--show-interactive-dev-session', 'false', '--var', - `Secret:${JSON.stringify(secrets)}`, + `SECRETS:${JSON.stringify(secrets)}`, '--port', port, '--inspector-port', diff --git a/wrangler.toml b/wrangler.toml index dc0e05a..0180614 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -8,6 +8,13 @@ rules = [ { type = "CommonJS", globs = ["tenantFiles/**/*.js"] } ] +kv_namespaces = [ + { binding = "MESH_KV_NAMESPACE", id = "local-kv" } +] -[[migrations]] -tag = "v1" +[vars] +# State configuration +MAX_STATE_KEY_SIZE_BYTES="512" # 512 bytes +MAX_STATE_VALUE_SIZE_BYTES="1048576" # 1 MiB +MIN_STATE_TTL_SECONDS="60" # 1 minute +MAX_STATE_TTL_SECONDS="604800" # 7 days diff --git a/yarn.lock b/yarn.lock index 5d0d9a9..f71124b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@adobe-apimesh/mesh-builder@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@adobe-apimesh/mesh-builder/-/mesh-builder-2.2.0.tgz#946bd7e2e1ea53af1363cd1fd088d7921763c7c7" - integrity sha512-IupQHm1kup/5KJKaYCZzxqE1hypWvmCItwqKMn5xMBedbsqDHW4eYmF2f/0+hEVIHcgPPb/+OfGn/LxMQ+rQFQ== +"@adobe-apimesh/mesh-builder@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@adobe-apimesh/mesh-builder/-/mesh-builder-2.3.1.tgz#72e0cc90bf145078eeae7597aa8d4551592805b2" + integrity sha512-r0FR2AFYk5ZmIMWivfTmA3D/k3wRJZAFb5ur3mR3+t3LK6z091PfHcm9jBJV2pkNLmJV8C7xy35YMZHJD2O2Jg== dependencies: "@fastify/request-context" "^4.1.0" eslint "^8.39.0" @@ -170,12 +170,13 @@ joi "^17.4.2" lodash.clonedeep "^4.5.0" -"@adobe/plugin-hooks@0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@adobe/plugin-hooks/-/plugin-hooks-0.3.4.tgz#e1a743c496c87833dd3b0a73e9d3287fcc3f26d9" - integrity sha512-YjGWJeuU04FLpl6BsWzuM6K1/LxCL1ZfleUt9OhC+NDgNhLJx+BMr6SGmoDWbNSl9MZT1JvBLZQ6KWjuhJWZUw== +"@adobe/plugin-hooks@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@adobe/plugin-hooks/-/plugin-hooks-0.4.0.tgz#2bdac7cea274d96f22a67f4acdadbf071a2afb98" + integrity sha512-iYZCc9kFflkkpz1EnKugIcANjjD1pn3j01gaFWvUxvbJYARUpn3uP0MA0kJCpkkVo5TbJHlxWu/CbX+lfDLAKQ== dependencies: - "@graphql-mesh/utils" "^0.43.4" + "@graphql-mesh/cross-helpers" "^0.4.10" + "@graphql-mesh/utils" "0.43.20" await-timeout "^1.1.1" make-cancellable-promise "^1.1.0" node-fetch "^2" @@ -1586,6 +1587,14 @@ react-native-fs "2.20.0" react-native-path "0.0.5" +"@graphql-mesh/cross-helpers@^0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@graphql-mesh/cross-helpers/-/cross-helpers-0.4.10.tgz#a998699cdbf8ced55052beaa26bf17ca8a097a65" + integrity sha512-7xmYM4P3UCmhx1pqU3DY4xWNydMBSXdzlHJ2wQPoM/s+l7tuWhxdtvmFmy12VkvZYtMAHkCpYvQokscWctUyrA== + dependencies: + "@graphql-tools/utils" "^10.8.0" + path-browserify "1.0.1" + "@graphql-mesh/graphql@0.34.13": version "0.34.13" resolved "https://registry.yarnpkg.com/@graphql-mesh/graphql/-/graphql-0.34.13.tgz#17d20228b82b80dca571fc19d4c4e2cce50d8664" @@ -1745,15 +1754,6 @@ json-pointer "0.6.2" lodash.get "4.4.2" -"@graphql-mesh/string-interpolation@0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@graphql-mesh/string-interpolation/-/string-interpolation-0.4.4.tgz#82a41f4d02863f28a12dfef48af58501a976cf26" - integrity sha512-IotswBYZRaPswOebcr2wuOFuzD3dHIJxVEkPiiQubqjUIR8HhQI22XHJv0WNiQZ65z8NR9+GYWwEDIc2JRCNfQ== - dependencies: - dayjs "1.11.7" - json-pointer "0.6.2" - lodash.get "4.4.2" - "@graphql-mesh/transform-encapsulate@0.4.21": version "0.4.21" resolved "https://registry.yarnpkg.com/@graphql-mesh/transform-encapsulate/-/transform-encapsulate-0.4.21.tgz#4b65f267992d605c04c2a037460f2f2ee03ad177" @@ -1931,19 +1931,6 @@ lodash.topath "4.5.2" tiny-lru "8.0.2" -"@graphql-mesh/utils@^0.43.4": - version "0.43.23" - resolved "https://registry.yarnpkg.com/@graphql-mesh/utils/-/utils-0.43.23.tgz#01dc33fe73f835931d7be35915402b7dde4e6e4e" - integrity sha512-3ZrgLGGE61geHzBnX/mXcBzl/hJ1bmTut4kYKkesT72HiFSX6N5YKXlGxh33NtvYWrU8rV86/2Fi+u4yA+y2fQ== - dependencies: - "@graphql-mesh/string-interpolation" "0.4.4" - "@graphql-tools/delegate" "9.0.31" - dset "3.1.2" - js-yaml "4.1.0" - lodash.get "4.4.2" - lodash.topath "4.5.2" - tiny-lru "8.0.2" - "@graphql-tools/batch-delegate@8.4.1": version "8.4.1" resolved "https://registry.yarnpkg.com/@graphql-tools/batch-delegate/-/batch-delegate-8.4.1.tgz#e4757df8cb6e985fedbd6315a7b2721190b988ef" @@ -2004,7 +1991,7 @@ tslib "^2.4.0" value-or-promise "1.0.11" -"@graphql-tools/batch-execute@^8.5.18", "@graphql-tools/batch-execute@^8.5.19", "@graphql-tools/batch-execute@^8.5.22": +"@graphql-tools/batch-execute@^8.5.18", "@graphql-tools/batch-execute@^8.5.22": version "8.5.22" resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.5.22.tgz#a742aa9d138fe794e786d8fb6429665dc7df5455" integrity sha512-hcV1JaY6NJQFQEwCKrYhpfLK8frSXDbtNMoTur98u10Cmecy1zrqNKSqhEyGetpgHxaJRqszGzKeI3RuroDN6A== @@ -2051,19 +2038,6 @@ tslib "^2.5.0" value-or-promise "^1.0.12" -"@graphql-tools/delegate@9.0.31": - version "9.0.31" - resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-9.0.31.tgz#e147f25c3deaf9ce161867986da129f48ecc7190" - integrity sha512-kQ08ssI9RMC3ENBCcwR/vP+IAvi5oWiyLBlydlS62N/UoIEHi1AgjT4dPkIlCXy/U/f2l6ETbsWCcdoN/2SQ7A== - dependencies: - "@graphql-tools/batch-execute" "^8.5.19" - "@graphql-tools/executor" "^0.0.17" - "@graphql-tools/schema" "^9.0.18" - "@graphql-tools/utils" "^9.2.1" - dataloader "^2.2.2" - tslib "^2.5.0" - value-or-promise "^1.0.12" - "@graphql-tools/delegate@9.0.8": version "9.0.8" resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-9.0.8.tgz#aa792f419a041de0c6341eaecf9694cf6f16f76f" @@ -2149,17 +2123,6 @@ tslib "^2.4.0" value-or-promise "1.0.12" -"@graphql-tools/executor@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-0.0.17.tgz#4f48693fd3fa2980c67ee0a1d1c2049942aa28e5" - integrity sha512-DVKyMclsNY8ei14FUrR4jn24VHB3EuFldD8yGWrcJ8cudSh47sknznvXN6q0ffqDeAf0IlZSaBCHrOTBqA7OfQ== - dependencies: - "@graphql-tools/utils" "^9.2.1" - "@graphql-typed-document-node/core" "3.2.0" - "@repeaterjs/repeater" "3.0.4" - tslib "^2.4.0" - value-or-promise "1.0.12" - "@graphql-tools/executor@^0.0.20": version "0.0.20" resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-0.0.20.tgz#d51d159696e839522dd49d936636af251670e425" @@ -2414,6 +2377,17 @@ dset "^3.1.2" tslib "^2.4.0" +"@graphql-tools/utils@^10.8.0": + version "10.9.1" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.9.1.tgz#ff4067256f2080db0c66f4475858f29a8da65ecf" + integrity sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + "@whatwg-node/promise-helpers" "^1.0.0" + cross-inspect "1.0.1" + dset "^3.1.4" + tslib "^2.4.0" + "@graphql-tools/utils@^8.8.0": version "8.13.1" resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.13.1.tgz#b247607e400365c2cd87ff54654d4ad25a7ac491" @@ -3650,6 +3624,13 @@ fast-querystring "^1.1.1" tslib "^2.6.3" +"@whatwg-node/promise-helpers@^1.0.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz#3b54987ad6517ef6db5920c66a6f0dada606587d" + integrity sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA== + dependencies: + tslib "^2.6.3" + "@whatwg-node/router@0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@whatwg-node/router/-/router-0.3.0.tgz#70002afb4a8ef4b544894c8344933e9559c0ba40" @@ -5017,6 +4998,11 @@ dset@^3.1.2: resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.3.tgz#c194147f159841148e8e34ca41f638556d9542d2" integrity sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ== +dset@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248" + integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA== + duplexify@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" @@ -5710,9 +5696,9 @@ expect@^29.7.0: jest-util "^29.7.0" exsolve@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.4.tgz#7de5c75af82ecd15998328fbf5f2295883be3a39" - integrity sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e" + integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw== external-editor@^3.0.3: version "3.1.0" @@ -10329,9 +10315,9 @@ ua-parser-js@^1.0.35: integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== ufo@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" - integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== uglify-js@^3.1.4: version "3.17.4"