Merge pull request #5044 from Budibase/fix/cross-tenant-apps-with-session

Fix/cross tenant apps with session
This commit is contained in:
Rory Powell 2022-03-25 11:58:21 +00:00 committed by GitHub
commit 3e5f0c1df7
27 changed files with 346 additions and 3892 deletions

View File

@ -24,6 +24,22 @@
{
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
},
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"plugins": [],
"extends": [
"eslint:recommended"
],
"rules": {
"no-unused-vars": "off",
"no-inner-declarations": "off",
"no-case-declarations": "off",
"no-useless-escape": "off",
"no-undef": "off",
"no-prototype-builtins": "off"
}
}
],
"rules": {

View File

@ -52,9 +52,8 @@ http {
proxy_pass http://{{ address }}:4001;
}
location /app/ {
location /app {
proxy_pass http://{{ address }}:4001;
rewrite ^/app/(.*)$ /$1 break;
}
location /builder {

View File

@ -61,7 +61,6 @@ http {
location /app {
proxy_pass http://$apps:4002;
rewrite ^/app/(.*)$ /$1 break;
}
location = / {

View File

@ -15,7 +15,9 @@
"prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.38.2"
"svelte": "^3.38.2",
"@typescript-eslint/parser": "4.28.0",
"typescript": "4.5.5"
},
"scripts": {
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
@ -31,17 +33,19 @@
"nuke:docker": "lerna run --parallel dev:stack:nuke",
"clean": "lerna clean",
"kill-port": "kill-port 4001",
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
"kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
"test": "lerna run test",
"lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix packages",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
"lint:fix:ts": "lerna run lint:fix",
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream",
"build:specs": "lerna run specs",

View File

@ -1,5 +1,6 @@
const env = require("../environment")
const { Headers } = require("../../constants")
const { SEPARATOR, DocumentTypes } = require("../db/constants")
const cls = require("./FunctionContext")
const { getCouch } = require("../db")
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
@ -42,8 +43,39 @@ exports.doInTenant = (tenantId, task) => {
})
}
/**
* Given an app ID this will attempt to retrieve the tenant ID from it.
* @return {null|string} The tenant ID found within the app ID.
*/
exports.getTenantIDFromAppID = appId => {
if (!appId) {
return null
}
const split = appId.split(SEPARATOR)
const hasDev = split[1] === DocumentTypes.DEV
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
return null
}
if (hasDev) {
return split[2]
} else {
return split[1]
}
}
const setAppTenantId = appId => {
const appTenantId = this.getTenantIDFromAppID(appId) || this.DEFAULT_TENANT_ID
this.updateTenantId(appTenantId)
}
exports.doInAppContext = (appId, task) => {
if (!appId) {
throw new Error("appId is required")
}
return cls.run(() => {
// set the app tenant id
setAppTenantId(appId)
// set the app ID
cls.setOnContext(ContextKeys.APP_ID, appId)

View File

@ -9,11 +9,7 @@ const {
APP_PREFIX,
APP_DEV,
} = require("./constants")
const {
getTenantId,
getTenantIDFromAppID,
getGlobalDBName,
} = require("../tenancy")
const { getTenantId, getGlobalDBName } = require("../tenancy")
const fetch = require("node-fetch")
const { getCouch } = require("./index")
const { getAppMetadata } = require("../cache/appMetadata")
@ -39,7 +35,6 @@ exports.DocumentTypes = DocumentTypes
exports.APP_PREFIX = APP_PREFIX
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
exports.SEPARATOR = SEPARATOR
exports.getTenantIDFromAppID = getTenantIDFromAppID
exports.isDevApp = isDevApp
exports.isProdAppID = isProdAppID
exports.isDevAppID = isDevAppID

View File

@ -1,27 +0,0 @@
const {
isMultiTenant,
updateTenantId,
isTenantIdSet,
DEFAULT_TENANT_ID,
updateAppId,
} = require("../tenancy")
const ContextFactory = require("../context/FunctionContext")
const { getTenantIDFromAppID } = require("../db/utils")
module.exports = () => {
return ContextFactory.getMiddleware(ctx => {
// if not in multi-tenancy mode make sure its default and exit
if (!isMultiTenant()) {
updateTenantId(DEFAULT_TENANT_ID)
return
}
// if tenant ID already set no need to continue
if (isTenantIdSet()) {
return
}
const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
updateTenantId(tenantId)
updateAppId(appId)
})
}

View File

@ -6,7 +6,6 @@ const { authError } = require("./passport/utils")
const authenticated = require("./authenticated")
const auditLog = require("./auditLog")
const tenancy = require("./tenancy")
const appTenancy = require("./appTenancy")
const internalApi = require("./internalApi")
const datasourceGoogle = require("./passport/datasource/google")
const csrf = require("./csrf")
@ -19,7 +18,6 @@ module.exports = {
authenticated,
auditLog,
tenancy,
appTenancy,
authError,
internalApi,
datasource: {

View File

@ -1,6 +1,11 @@
const { getDB } = require("../db")
const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants")
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("../context")
const { SEPARATOR, StaticDatabases } = require("../db/constants")
const {
getTenantId,
DEFAULT_TENANT_ID,
isMultiTenant,
getTenantIDFromAppID,
} = require("../context")
const env = require("../environment")
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
@ -118,26 +123,6 @@ exports.getTenantUser = async identifier => {
}
}
/**
* Given an app ID this will attempt to retrieve the tenant ID from it.
* @return {null|string} The tenant ID found within the app ID.
*/
exports.getTenantIDFromAppID = appId => {
if (!appId) {
return null
}
const split = appId.split(SEPARATOR)
const hasDev = split[1] === DocumentTypes.DEV
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
return null
}
if (hasDev) {
return split[2]
} else {
return split[1]
}
}
exports.isUserInAppTenant = (appId, user = null) => {
let userTenantId
if (user) {
@ -145,7 +130,7 @@ exports.isUserInAppTenant = (appId, user = null) => {
} else {
userTenantId = getTenantId()
}
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
return tenantId === userTenantId
}

View File

@ -3,6 +3,7 @@ const {
SEPARATOR,
ViewNames,
generateGlobalUserID,
getAllApps,
} = require("./db/utils")
const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
@ -20,8 +21,10 @@ const { hash } = require("./hashing")
const userCache = require("./cache/user")
const env = require("./environment")
const { getUserSessions, invalidateSessions } = require("./security/sessions")
const tenancy = require("./tenancy")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
const PROD_APP_PREFIX = "/app/"
function confirmAppId(possibleAppId) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
@ -29,16 +32,35 @@ function confirmAppId(possibleAppId) {
: undefined
}
async function resolveAppUrl(ctx) {
const appUrl = ctx.path.split("/")[2]
let possibleAppUrl = `/${appUrl.toLowerCase()}`
let tenantId = tenancy.getTenantId()
if (!env.SELF_HOSTED && ctx.subdomains.length) {
// always use the tenant id from the url in cloud
tenantId = ctx.subdomains[0]
}
// search prod apps for a url that matches
const apps = await tenancy.doInTenant(tenantId, () =>
getAllApps({ dev: false })
)
const app = apps.filter(
a => a.url && a.url.toLowerCase() === possibleAppUrl
)[0]
return app && app.appId ? app.appId : undefined
}
/**
* Given a request tries to find the appId, which can be located in various places
* @param {object} ctx The main request body to look through.
* @returns {string|undefined} If an appId was found it will be returned.
*/
exports.getAppId = ctx => {
const options = [ctx.headers[Headers.APP_ID], ctx.params.appId]
if (ctx.subdomains) {
options.push(ctx.subdomains[1])
}
exports.getAppIdFromCtx = async ctx => {
// look in headers
const options = [ctx.headers[Headers.APP_ID]]
let appId
for (let option of options) {
appId = confirmAppId(option)
@ -47,16 +69,24 @@ exports.getAppId = ctx => {
}
}
// look in body if can't find it in subdomain
// look in body
if (!appId && ctx.request.body && ctx.request.body.appId) {
appId = confirmAppId(ctx.request.body.appId)
}
// look in the url - dev app
let appPath =
ctx.request.headers.referrer ||
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
if (!appId && appPath.length !== 0) {
if (!appId && appPath.length) {
appId = confirmAppId(appPath[0])
}
// look in the url - prod app
if (!appId && ctx.path.startsWith(PROD_APP_PREFIX)) {
appId = confirmAppId(await resolveAppUrl(ctx))
}
return appId
}

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
{
"globals": {
"emit": true,
"key": true
},
"parser": "@typescript-eslint/parser",
"env": {
"node": true
},
"extends": ["eslint:recommended"],
"rules": {
}
}

View File

@ -21,10 +21,7 @@
"dev:stack:down": "node scripts/dev/manage.js down",
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
"dev:builder": "yarn run dev:stack:up && nodemon",
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
"specs": "node specs/generate.js && openapi-typescript specs/openapi.yaml --output src/definitions/openapi.ts",
"lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint",
"initialise": "node scripts/initialise.js",
"env:multi:enable": "node scripts/multiTenancy.js enable",
"env:multi:disable": "node scripts/multiTenancy.js disable",
@ -151,7 +148,6 @@
"@types/node": "^15.12.4",
"@types/oracledb": "^5.2.1",
"@types/redis": "^4.0.11",
"@typescript-eslint/parser": "4.28.0",
"apidoc": "^0.50.2",
"babel-jest": "^27.0.2",
"copyfiles": "^2.4.1",

View File

@ -7,7 +7,7 @@ const { getAppDB, getAppId } = require("@budibase/backend-core/context")
exports.fetchSelf = async ctx => {
let userId = ctx.user.userId || ctx.user._id
/* istanbul ignore next */
if (!userId) {
if (!userId || !ctx.isAuthenticated) {
ctx.body = {}
return
}

View File

@ -5,7 +5,6 @@ const { resolve, join } = require("../../../utilities/centralPath")
const uuid = require("uuid")
const { ObjectStoreBuckets } = require("../../../constants")
const { processString } = require("@budibase/string-templates")
const { getAllApps } = require("@budibase/backend-core/db")
const {
loadHandlebarsFile,
NODE_MODULES_PATH,
@ -16,7 +15,7 @@ const { clientLibraryPath } = require("../../../utilities")
const { upload } = require("../../../utilities/fileSystem")
const { attachmentsRelativeURL } = require("../../../utilities")
const { DocumentTypes } = require("../../../db/utils")
const { getAppDB, updateAppId } = require("@budibase/backend-core/context")
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
const AWS = require("aws-sdk")
const AWS_REGION = env.AWS_REGION ? env.AWS_REGION : "eu-west-1"
@ -39,21 +38,6 @@ async function prepareUpload({ s3Key, bucket, metadata, file }) {
}
}
async function getAppIdFromUrl(ctx) {
// the "appId" component of the URL can be the id or the custom url
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
// search prod apps for a url that matches, exclude dev where id is always used
const apps = await getAllApps({ dev: false })
const app = apps.filter(
a => a.url && a.url.toLowerCase() === possibleAppUrl
)[0]
const appId = app && app.appId ? app.appId : ctx.params.appId
updateAppId(appId)
return appId
}
exports.serveBuilder = async function (ctx) {
let builderPath = resolve(TOP_LEVEL_PATH, "builder")
await send(ctx, ctx.file, { root: builderPath })
@ -81,10 +65,10 @@ exports.uploadFile = async function (ctx) {
}
exports.serveApp = async function (ctx) {
let appId = await getAppIdFromUrl(ctx)
const App = require("./templates/BudibaseApp.svelte").default
const db = getAppDB({ skip_setup: true })
const appInfo = await db.get(DocumentTypes.APP_METADATA)
let appId = getAppId()
const { head, html, css } = App.render({
title: appInfo.name,

View File

@ -3,7 +3,6 @@ const {
buildAuthMiddleware,
auditLog,
buildTenancyMiddleware,
buildAppTenancyMiddleware,
} = require("@budibase/backend-core/auth")
const currentApp = require("../middleware/currentapp")
const compress = require("koa-compress")
@ -14,6 +13,9 @@ const env = require("../environment")
const router = new Router()
router.get("/health", ctx => (ctx.status = 200))
router.get("/version", ctx => (ctx.body = pkg.version))
router
.use(
compress({
@ -34,8 +36,6 @@ router
}
await next()
})
.use("/health", ctx => (ctx.status = 200))
.use("/version", ctx => (ctx.body = pkg.version))
// re-direct before any middlewares occur
.redirect("/", "/builder")
.use(
@ -52,8 +52,6 @@ router
})
)
.use(currentApp)
// this middleware will try to use the app ID to determine the tenancy
.use(buildAppTenancyMiddleware())
.use(auditLog)
// error handling middleware
@ -74,8 +72,6 @@ router.use(async (ctx, next) => {
}
})
router.get("/health", ctx => (ctx.status = 200))
// authenticated routes
for (let route of mainRoutes) {
router.use(route.routes())

View File

@ -44,8 +44,7 @@ router
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
controller.uploadFile
)
// TODO: this likely needs to be secured in some way
.get("/:appId/:path*", controller.serveApp)
.get("/app/:appUrl/:path*", controller.serveApp)
.post(
"/api/attachments/:datasourceId/url",
authorized(PermissionTypes.TABLE, PermissionLevels.READ),

View File

@ -33,6 +33,7 @@ exports.createLinkView = async () => {
if (doc.type === "link") {
let doc1 = doc.doc1
let doc2 = doc.doc2
// eslint-disable-next-line no-undef
emit([doc1.tableId, doc1.rowId], {
id: doc2.rowId,
thisId: doc1.rowId,
@ -40,6 +41,7 @@ exports.createLinkView = async () => {
})
// if linking to same table can't emit twice
if (doc1.tableId !== doc2.tableId) {
// eslint-disable-next-line no-undef
emit([doc2.tableId, doc2.rowId], {
id: doc1.rowId,
thisId: doc2.rowId,

View File

@ -1,5 +1,5 @@
const {
getAppId,
getAppIdFromCtx,
setCookie,
getCookie,
clearCookie,
@ -17,7 +17,7 @@ const { doInAppContext } = require("@budibase/backend-core/context")
module.exports = async (ctx, next) => {
// try to get the appID from the request
let requestAppId = getAppId(ctx)
let requestAppId = await getAppIdFromCtx(ctx)
// get app cookie if it exists
let appCookie = null
try {
@ -71,21 +71,22 @@ module.exports = async (ctx, next) => {
}
return doInAppContext(appId, async () => {
let noCookieSet = false
let skipCookie = false
// if the user not in the right tenant then make sure they have no permissions
// need to judge this only based on the request app ID,
if (
env.MULTI_TENANCY &&
ctx.user &&
requestAppId &&
!isUserInAppTenant(requestAppId)
!isUserInAppTenant(requestAppId, ctx.user)
) {
// don't error, simply remove the users rights (they are a public user)
delete ctx.user.builder
delete ctx.user.admin
delete ctx.user.roles
ctx.isAuthenticated = false
roleId = BUILTIN_ROLE_IDS.PUBLIC
noCookieSet = true
skipCookie = true
}
ctx.appId = appId
@ -105,7 +106,7 @@ module.exports = async (ctx, next) => {
(requestAppId !== appId ||
appCookie == null ||
appCookie.appId !== requestAppId) &&
!noCookieSet
!skipCookie
) {
setCookie(ctx, { appId }, Cookies.CurrentApp)
}

View File

@ -1,9 +1,9 @@
const { Headers } = require("@budibase/backend-core/constants")
const { getAppId } = require("@budibase/backend-core/utils")
const { getAppIdFromCtx } = require("@budibase/backend-core/utils")
module.exports = function ({ requiresAppId } = {}) {
return async (ctx, next) => {
const appId = getAppId(ctx)
const appId = await getAppIdFromCtx(ctx)
if (requiresAppId && !appId) {
ctx.throw(
400,

View File

@ -35,9 +35,7 @@ class TestConfiguration {
}
executeMiddleware() {
return doInAppContext(APP_ID, () => {
return this.middleware(this.ctx, this.next)
})
return this.middleware(this.ctx, this.next)
}
setUser(user) {

View File

@ -38,7 +38,7 @@ function mockAuthWithNoCookie() {
},
}))
jest.mock("@budibase/backend-core/utils", () => ({
getAppId: jest.fn(),
getAppIdFromCtx: jest.fn(),
setCookie: jest.fn(),
getCookie: jest.fn(),
}))
@ -51,7 +51,7 @@ function mockAuthWithCookie() {
jest.resetModules()
mockWorker()
jest.mock("@budibase/backend-core/utils", () => ({
getAppId: () => {
getAppIdFromCtx: () => {
return "app_test"
},
setCookie: jest.fn(),
@ -143,7 +143,7 @@ describe("Current app middleware", () => {
it("should perform correct when no cookie exists", async () => {
mockReset()
jest.mock("@budibase/backend-core/utils", () => ({
getAppId: () => {
getAppIdFromCtx: () => {
return "app_test"
},
setCookie: jest.fn(),
@ -158,7 +158,7 @@ describe("Current app middleware", () => {
it("lastly check what occurs when cookie doesn't need updated", async () => {
mockReset()
jest.mock("@budibase/backend-core/utils", () => ({
getAppId: () => {
getAppIdFromCtx: () => {
return "app_test"
},
setCookie: jest.fn(),

View File

@ -80,7 +80,7 @@ class TestConfiguration {
return request.body
}
// check if already in a context
if (context.getAppId() == null) {
if (context.getAppId() == null && this.appId !== null) {
return context.doInAppContext(this.appId, async () => {
return run()
})

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
{
"globals": {
"emit": true,
"key": true
},
"parser": "@typescript-eslint/parser",
"env": {
"node": true
},
"extends": ["eslint:recommended"],
"rules": {
}
}

View File

@ -18,9 +18,6 @@
"build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
"dev:stack:init": "node ./scripts/dev/manage.js init",
"dev:builder": "npm run dev:stack:init && nodemon",
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
"lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint",
"test": "jest --runInBand",
"env:multi:enable": "node scripts/multiTenancy.js enable",
"env:multi:disable": "node scripts/multiTenancy.js disable",
@ -67,7 +64,6 @@
"@types/koa": "^2.13.3",
"@types/koa-router": "^7.4.2",
"@types/node": "^15.12.4",
"@typescript-eslint/parser": "4.28.0",
"copyfiles": "^2.4.1",
"eslint": "^6.8.0",
"jest": "^27.0.5",

203
yarn.lock
View File

@ -796,11 +796,32 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@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.stat@^1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@nodelib/fs.walk@^1.2.3":
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"
"@octokit/auth-token@^2.4.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36"
@ -949,6 +970,50 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
"@typescript-eslint/parser@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa"
integrity sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==
dependencies:
"@typescript-eslint/scope-manager" "4.28.0"
"@typescript-eslint/types" "4.28.0"
"@typescript-eslint/typescript-estree" "4.28.0"
debug "^4.3.1"
"@typescript-eslint/scope-manager@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz#6a3009d2ab64a30fc8a1e257a1a320067f36a0ce"
integrity sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==
dependencies:
"@typescript-eslint/types" "4.28.0"
"@typescript-eslint/visitor-keys" "4.28.0"
"@typescript-eslint/types@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0"
integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==
"@typescript-eslint/typescript-estree@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf"
integrity sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==
dependencies:
"@typescript-eslint/types" "4.28.0"
"@typescript-eslint/visitor-keys" "4.28.0"
debug "^4.3.1"
globby "^11.0.3"
is-glob "^4.0.1"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434"
integrity sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==
dependencies:
"@typescript-eslint/types" "4.28.0"
eslint-visitor-keys "^2.0.0"
JSONStream@^1.0.4, JSONStream@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@ -1124,6 +1189,11 @@ array-union@^1.0.1:
dependencies:
array-uniq "^1.0.1"
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==
array-uniq@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@ -1262,6 +1332,13 @@ braces@^2.3.1:
split-string "^3.0.2"
to-regex "^3.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"
btoa-lite@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
@ -1803,6 +1880,13 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "2.1.2"
debug@^4.3.1:
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"
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@ -1908,6 +1992,13 @@ dir-glob@2.0.0:
arrify "^1.0.1"
path-type "^3.0.0"
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"
@ -2278,6 +2369,17 @@ fast-glob@^2.0.2:
merge2 "^1.2.3"
micromatch "^3.1.10"
fast-glob@^3.2.9:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
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.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@ -2288,6 +2390,13 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
dependencies:
reusify "^1.0.4"
figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
@ -2317,6 +2426,13 @@ fill-range@^4.0.0:
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
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"
filter-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
@ -2627,6 +2743,18 @@ globals@^13.6.0, globals@^13.9.0:
dependencies:
type-fest "^0.20.2"
globby@^11.0.3:
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"
globby@^8.0.1:
version "8.0.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d"
@ -2835,6 +2963,11 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@ -3130,6 +3263,11 @@ is-number@^3.0.0:
dependencies:
kind-of "^3.0.2"
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-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@ -3714,7 +3852,7 @@ meow@^8.0.0:
type-fest "^0.18.0"
yargs-parser "^20.2.3"
merge2@^1.2.3:
merge2@^1.2.3, 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==
@ -3738,6 +3876,14 @@ micromatch@^3.1.10:
snapdragon "^0.8.1"
to-regex "^3.0.2"
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"
mime-db@1.51.0:
version "1.51.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
@ -4469,6 +4615,11 @@ path-type@^3.0.0:
dependencies:
pify "^3.0.0"
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==
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@ -4479,6 +4630,11 @@ picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -4637,6 +4793,11 @@ query-string@^6.13.8:
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"
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==
quick-lru@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
@ -4908,6 +5069,11 @@ retry@^0.10.0:
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
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@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@ -4942,6 +5108,13 @@ run-async@^2.2.0:
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
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"
run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
@ -4988,7 +5161,7 @@ semver@^6.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.2.1, semver@^7.3.4:
semver@^7.2.1, semver@^7.3.4, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@ -5063,6 +5236,11 @@ slash@^1.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
@ -5530,6 +5708,13 @@ to-regex-range@^2.1.0:
is-number "^3.0.0"
repeat-string "^1.6.1"
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"
to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@ -5575,11 +5760,18 @@ trim-newlines@^3.0.0:
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
tslib@^1.9.0:
tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@ -5624,6 +5816,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@4.5.5:
version "4.5.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
uglify-js@^3.1.4:
version "3.14.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.3.tgz#c0f25dfea1e8e5323eccf59610be08b6043c15cf"