diff --git a/packages/casl-mongoose/package.json b/packages/casl-mongoose/package.json index 469710c4a..0c5ca09b1 100644 --- a/packages/casl-mongoose/package.json +++ b/packages/casl-mongoose/package.json @@ -40,7 +40,7 @@ "license": "MIT", "peerDependencies": { "@casl/ability": "^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0", - "mongoose": "^6.0.0" + "mongoose": "^6.0.13" }, "devDependencies": { "@casl/ability": "^6.0.0", @@ -48,7 +48,7 @@ "@types/jest": "^26.0.22", "chai": "^4.1.0", "chai-spies": "^1.0.0", - "mongoose": "^6.0.0" + "mongoose": "^6.0.13" }, "files": [ "dist", diff --git a/packages/casl-mongoose/spec/accessible_fields.spec.ts b/packages/casl-mongoose/spec/accessible_fields.spec.ts index 70fec709f..5bfd21a59 100644 --- a/packages/casl-mongoose/spec/accessible_fields.spec.ts +++ b/packages/casl-mongoose/spec/accessible_fields.spec.ts @@ -1,13 +1,15 @@ import { defineAbility, Ability } from '@casl/ability' import mongoose from 'mongoose' -import { AccessibleFieldsModel, accessibleFieldsPlugin, AccessibleFieldsDocument } from '../src' +import { accessibleFieldsPlugin, AccessibleFieldsModel } from '../src' describe('Accessible fields plugin', () => { - interface Post extends AccessibleFieldsDocument { + interface Post { title: string; state: string; } - let PostSchema: mongoose.Schema + + type PostModel = AccessibleFieldsModel + let PostSchema: mongoose.Schema beforeEach(() => { PostSchema = new mongoose.Schema({ @@ -21,7 +23,7 @@ describe('Accessible fields plugin', () => { }) it('adds static and instace `accessibleFieldsBy` method', () => { - const Post = mongoose.model>( + const Post = mongoose.model( 'Post', PostSchema.plugin(accessibleFieldsPlugin) ) @@ -32,12 +34,12 @@ describe('Accessible fields plugin', () => { }) describe('`accessibleFieldsBy` method', () => { - let Post: AccessibleFieldsModel + let Post: PostModel describe('by default', () => { beforeEach(() => { PostSchema.plugin(accessibleFieldsPlugin) - Post = mongoose.model>('Post', PostSchema) + Post = mongoose.model('Post', PostSchema) }) it('returns empty array for empty `Ability` instance', () => { @@ -86,28 +88,28 @@ describe('Accessible fields plugin', () => { it('returns fields provided in `only` option specified as string', () => { PostSchema.plugin(accessibleFieldsPlugin, { only: 'title' }) - Post = mongoose.model>('Post', PostSchema) + Post = mongoose.model('Post', PostSchema) expect(Post.accessibleFieldsBy(ability)).toEqual(['title']) }) it('returns fields provided in `only` option specified as array', () => { PostSchema.plugin(accessibleFieldsPlugin, { only: ['title', 'state'] }) - Post = mongoose.model>('Post', PostSchema) + Post = mongoose.model('Post', PostSchema) expect(Post.accessibleFieldsBy(ability)).toEqual(['title', 'state']) }) it('returns all fields except one specified in `except` option as string', () => { PostSchema.plugin(accessibleFieldsPlugin, { except: '_id' }) - Post = mongoose.model>('Post', PostSchema) + Post = mongoose.model('Post', PostSchema) expect(Post.accessibleFieldsBy(ability)).toEqual(['title', 'state', '__v']) }) it('returns all fields except specified in `except` option as array', () => { PostSchema.plugin(accessibleFieldsPlugin, { except: ['_id', '__v'] }) - Post = mongoose.model>('Post', PostSchema) + Post = mongoose.model('Post', PostSchema) expect(Post.accessibleFieldsBy(ability)).toEqual(['title', 'state']) }) diff --git a/packages/casl-mongoose/spec/accessible_records.spec.ts b/packages/casl-mongoose/spec/accessible_records.spec.ts index 4bd64a56c..caaf5ec2c 100644 --- a/packages/casl-mongoose/spec/accessible_records.spec.ts +++ b/packages/casl-mongoose/spec/accessible_records.spec.ts @@ -3,7 +3,7 @@ import mongoose from 'mongoose' import { AccessibleRecordModel, accessibleRecordsPlugin, toMongoQuery } from '../src' describe('Accessible Records Plugin', () => { - interface Post extends mongoose.Document { + interface Post { title: string; state: string; } @@ -79,8 +79,19 @@ describe('Accessible Records Plugin', () => { Post.accessibleBy(ability).where({ title: /test/ }).accessibleBy(ability, 'delete') }) + it('has proper typing for `accessibleBy` methods', () => { + let expectedQueryType: mongoose.Query[], any> + expectedQueryType = Post.find() + expectedQueryType = Post.find().accessibleBy(ability) + expectedQueryType = Post.find().accessibleBy(ability).accessibleBy(ability, 'update') + expectedQueryType = Post.accessibleBy(ability).where({ title: /test/ }).accessibleBy(ability, 'delete') + expectedQueryType = Post.accessibleBy(ability).find() + expectedQueryType = Post.accessibleBy(ability).accessibleBy(ability, 'update').find() + expect(expectedQueryType).not.toBeUndefined() + }) + describe('when ability disallow to perform an action', () => { - let query: mongoose.QueryWithHelpers + let query: mongoose.QueryWithHelpers beforeEach(() => { query = Post.find().accessibleBy(ability, 'notAllowedAction') @@ -89,7 +100,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` for collection request', async () => { await query.exec() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -98,7 +109,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` when `find` is called', async () => { await query.find() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -107,7 +118,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` when callback is passed to `exec`', async () => { await wrapInPromise(cb => query.exec(cb)) .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -116,7 +127,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` for item request', async () => { await query.findOne().exec() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -125,7 +136,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` when `findOne` is called', async () => { await query.findOne() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -134,7 +145,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` for item request when callback is passed to `exec`', async () => { await wrapInPromise(cb => query.findOne().exec(cb)) .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -143,7 +154,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` for count request', async () => { await query.count() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -152,7 +163,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` for count request', async () => { await query.count() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -161,7 +172,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` for `countDocuments` request', async () => { await query.countDocuments() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) @@ -170,7 +181,7 @@ describe('Accessible Records Plugin', () => { it('throws `ForbiddenError` for `countDocuments` request', async () => { await query.estimatedDocumentCount() .then(() => fail('should not execute')) - .catch((error) => { + .catch((error: any) => { expect(error).toBeInstanceOf(ForbiddenError) expect(error.message).toMatch(/cannot execute/i) }) diff --git a/packages/casl-mongoose/src/accessible_fields.ts b/packages/casl-mongoose/src/accessible_fields.ts index e26ebe194..5dc38e948 100644 --- a/packages/casl-mongoose/src/accessible_fields.ts +++ b/packages/casl-mongoose/src/accessible_fields.ts @@ -21,20 +21,31 @@ function fieldsOf(schema: Schema, options: Partial excludedFields.indexOf(field) === -1); } -type GetAccessibleFields = ( +type GetAccessibleFields = ( this: Model | T, ability: U, action?: Normalize['abilities']>[0] ) => string[]; -export interface AccessibleFieldsModel extends Model { +export interface AccessibleFieldsModel< + T, + TQueryHelpers = {}, + TMethods = {}, + TVirtuals = {} +> extends Model, TVirtuals> { accessibleFieldsBy: GetAccessibleFields } -export interface AccessibleFieldsDocument extends Document { - accessibleFieldsBy: GetAccessibleFields +export interface AccessibleFieldDocumentMethods { + accessibleFieldsBy: GetAccessibleFields } +/** + * @deprecated Mongoose recommends against `extends Document`, prefer to use `AccessibleFieldsModel` instead. + * See here: https://mongoosejs.com/docs/typescript.html#using-extends-document + */ +export interface AccessibleFieldsDocument extends Document, AccessibleFieldDocumentMethods {} + function modelFieldsGetter() { let fieldsFrom: PermittedFieldsOptions['fieldsFrom']; return (schema: Schema, options: Partial) => { @@ -52,7 +63,7 @@ function modelFieldsGetter() { export function accessibleFieldsPlugin( schema: Schema, rawOptions?: Partial -) { +): void { const options = { getFields: getSchemaPaths, ...rawOptions }; const fieldsFrom = modelFieldsGetter(); type ModelOrDoc = Model | AccessibleFieldsDocument; diff --git a/packages/casl-mongoose/src/accessible_records.ts b/packages/casl-mongoose/src/accessible_records.ts index bb93ca364..44a63112a 100644 --- a/packages/casl-mongoose/src/accessible_records.ts +++ b/packages/casl-mongoose/src/accessible_records.ts @@ -1,5 +1,5 @@ import { Normalize, AnyMongoAbility, Generics, ForbiddenError, getDefaultErrorMessage } from '@casl/ability'; -import type { Schema, QueryWithHelpers, Model, Document } from 'mongoose'; +import type { Schema, QueryWithHelpers, Model, Document, HydratedDocument } from 'mongoose'; import mongoose from 'mongoose'; import { toMongoQuery } from './mongo'; @@ -49,21 +49,41 @@ function accessibleBy( return this instanceof mongoose.Query ? this.and([query]) : this.where({ $and: [query] }); } -type GetAccessibleRecords = ( +type GetAccessibleRecords = ( ability: U, action?: Normalize['abilities']>[0] -) => QueryWithHelpers>; +) => QueryWithHelpers< +Array, +T, +AccessibleRecordQueryHelpers +>; -type QueryHelpers = { - accessibleBy: GetAccessibleRecords +export type AccessibleRecordQueryHelpers = { + accessibleBy: GetAccessibleRecords< + HydratedDocument, + TQueryHelpers, + TMethods, + TVirtuals + > }; export interface AccessibleRecordModel< - T extends Document, K = unknown -> extends Model> { - accessibleBy: GetAccessibleRecords + T, + TQueryHelpers = {}, + TMethods = {}, + TVirtuals = {} +> extends Model, + TMethods, + TVirtuals> { + accessibleBy: GetAccessibleRecords< + HydratedDocument, + TQueryHelpers, + TMethods, + TVirtuals + > } -export function accessibleRecordsPlugin(schema: Schema) { - schema.query.accessibleBy = accessibleBy; +export function accessibleRecordsPlugin(schema: Schema): void { + (schema.query as Record).accessibleBy = accessibleBy; schema.statics.accessibleBy = accessibleBy; } diff --git a/packages/casl-mongoose/src/index.ts b/packages/casl-mongoose/src/index.ts index f60215db9..1df6c0df0 100644 --- a/packages/casl-mongoose/src/index.ts +++ b/packages/casl-mongoose/src/index.ts @@ -1,9 +1,28 @@ -import { AccessibleFieldsModel, AccessibleFieldsDocument } from './accessible_fields'; -import { AccessibleRecordModel } from './accessible_records'; +import { AccessibleFieldDocumentMethods, AccessibleFieldsModel } from './accessible_fields'; +import { AccessibleRecordModel, AccessibleRecordQueryHelpers } from './accessible_records'; -export type AccessibleModel = - AccessibleRecordModel & AccessibleFieldsModel; +export interface AccessibleModel< + T, + TQueryHelpers = unknown, + TMethods = unknown, + TVirtuals = unknown + > + extends + AccessibleRecordModel, TVirtuals>, + AccessibleFieldsModel, + TVirtuals + >, TMethods, TVirtuals> +{} -export * from './accessible_records'; -export * from './accessible_fields'; -export * from './mongo'; +export { accessibleRecordsPlugin } from './accessible_records'; +export type { AccessibleRecordModel } from './accessible_records'; +export { getSchemaPaths, accessibleFieldsPlugin } from './accessible_fields'; +export type { + AccessibleFieldsModel, + AccessibleFieldsDocument, + AccessibleFieldsOptions +} from './accessible_fields'; +export { toMongoQuery } from './mongo'; diff --git a/packages/casl-mongoose/src/mongo.ts b/packages/casl-mongoose/src/mongo.ts index 3d87785d0..f4969f8e2 100644 --- a/packages/casl-mongoose/src/mongo.ts +++ b/packages/casl-mongoose/src/mongo.ts @@ -1,5 +1,5 @@ import { AnyMongoAbility } from '@casl/ability'; -import { rulesToQuery } from '@casl/ability/extra'; +import { AbilityQuery, rulesToQuery } from '@casl/ability/extra'; function convertToMongoQuery(rule: AnyMongoAbility['rules'][number]) { const conditions = rule.conditions!; @@ -10,6 +10,6 @@ export function toMongoQuery( ability: T, subjectType: Parameters[1], action: Parameters[0] = 'read' -) { +): AbilityQuery | null { return rulesToQuery(ability, action, subjectType, convertToMongoQuery); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8564b1e6..6b1aa7eb2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,14 +98,14 @@ importers: '@types/jest': ^26.0.22 chai: ^4.1.0 chai-spies: ^1.0.0 - mongoose: ^6.0.0 + mongoose: ^6.0.13 devDependencies: '@casl/ability': link:../casl-ability '@casl/dx': link:../dx '@types/jest': 26.0.22 chai: 4.3.4 chai-spies: 1.0.0_chai@4.3.4 - mongoose: 6.0.10 + mongoose: 6.0.13 packages/casl-prisma: specifiers: @@ -4801,8 +4801,8 @@ packages: dependencies: node-int64: 0.4.0 - /bson/4.5.3: - resolution: {integrity: sha512-qVX7LX79Mtj7B3NPLzCfBiCP6RAsjiV8N63DjlaVVpZW+PFoDTxQ4SeDbSpcqgE6mXksM5CAwZnXxxxn/XwC0g==} + /bson/4.6.4: + resolution: {integrity: sha512-TdQ3FzguAu5HKPPlr0kYQCyrYUYh8tFM+CMTpxjNzVzxeiJY00Rtuj3LXLHSgiGvmaWlZ8PE+4KyM2thqE38pQ==} engines: {node: '>=6.9.0'} dependencies: buffer: 5.7.1 @@ -5589,8 +5589,8 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - /denque/2.0.1: - resolution: {integrity: sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==} + /denque/2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} dev: true @@ -8843,31 +8843,31 @@ packages: engines: {node: '>=0.10.0'} dev: false - /mongodb-connection-string-url/2.1.0: - resolution: {integrity: sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==} + /mongodb-connection-string-url/2.5.2: + resolution: {integrity: sha512-tWDyIG8cQlI5k3skB6ywaEA5F9f5OntrKKsT/Lteub2zgwSUlhqEN2inGgBTm8bpYJf8QYBdA/5naz65XDpczA==} dependencies: '@types/whatwg-url': 8.2.1 - whatwg-url: 9.1.0 + whatwg-url: 11.0.0 dev: true - /mongodb/4.1.2: - resolution: {integrity: sha512-pHCKDoOy1h6mVurziJmXmTMPatYWOx8pbnyFgSgshja9Y36Q+caHUzTDY6rrIy9HCSrjnbXmx3pCtvNZHmR8xg==} + /mongodb/4.1.4: + resolution: {integrity: sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==} engines: {node: '>=12.9.0'} dependencies: - bson: 4.5.3 - denque: 2.0.1 - mongodb-connection-string-url: 2.1.0 + bson: 4.6.4 + denque: 2.1.0 + mongodb-connection-string-url: 2.5.2 optionalDependencies: saslprep: 1.0.3 dev: true - /mongoose/6.0.10: - resolution: {integrity: sha512-p/wiEDUXoQuyb/xQx8QW/YGN92ZsojJ5E/DDgMCUU0WOGxc5uhcWoZ7ijLu6Ssjq8UkwVSv+jzkYp4Wbr+NqBg==} + /mongoose/6.0.13: + resolution: {integrity: sha512-/M/YKgx23fCX+j0lwObaHbCibXnMjyWeQrXZf0WaQeS/hL86wQVSmaOxh+kZXfyLOUr+vT2Hl44o50GZHUrKWw==} engines: {node: '>=12.0.0'} dependencies: - bson: 4.5.3 + bson: 4.6.4 kareem: 2.3.2 - mongodb: 4.1.2 + mongodb: 4.1.4 mpath: 0.8.4 mquery: 4.0.0 ms: 2.1.2 @@ -8887,7 +8887,7 @@ packages: resolution: {integrity: sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==} engines: {node: '>=12.0.0'} dependencies: - debug: 4.3.1 + debug: 4.3.4 regexp-clone: 1.0.0 sliced: 1.0.1 transitivePeerDependencies: @@ -10778,7 +10778,7 @@ packages: dev: false /sliced/1.0.1: - resolution: {integrity: sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=} + resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==} dev: true /sockjs/0.3.24: @@ -10849,7 +10849,7 @@ packages: dev: true /sparse-bitfield/3.0.3: - resolution: {integrity: sha1-/0rm5oZWBWuks+eSqzM004JzyhE=} + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} dependencies: memory-pager: 1.5.0 dev: true @@ -11355,6 +11355,13 @@ packages: dependencies: punycode: 2.1.1 + /tr46/3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.1.1 + dev: true + /traverse/0.6.6: resolution: {integrity: sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=} dev: false @@ -11708,6 +11715,11 @@ packages: resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} engines: {node: '>=10.4'} + /webidl-conversions/7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + /webpack-dev-middleware/5.3.0_webpack@5.70.0: resolution: {integrity: sha512-MouJz+rXAm9B1OTOYaJnn6rtD/lWZPy2ufQCH3BPs8Rloh/Du6Jze4p7AeLYHkVi0giJnYLaSGDC7S+GM9arhg==} engines: {node: '>= 12.13.0'} @@ -11874,6 +11886,14 @@ packages: /whatwg-mimetype/2.3.0: resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + /whatwg-url/11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + /whatwg-url/7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -11890,14 +11910,6 @@ packages: tr46: 2.1.0 webidl-conversions: 6.1.0 - /whatwg-url/9.1.0: - resolution: {integrity: sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==} - engines: {node: '>=12'} - dependencies: - tr46: 2.1.0 - webidl-conversions: 6.1.0 - dev: true - /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: