diff --git a/plugins/auth-backend-module-vmware-csp-provider/package.json b/plugins/auth-backend-module-vmware-csp-provider/package.json index b52d62cfca..7d7b49fc72 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/package.json +++ b/plugins/auth-backend-module-vmware-csp-provider/package.json @@ -28,15 +28,20 @@ "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", - "jwt-decode": "^3.1.0", + "jose": "^4.6.0", "passport-oauth2": "^1.6.1" }, "devDependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", - "@types/jwt-decode": "^3.1.0" + "express": "^4.18.2", + "express-session": "^1.17.3", + "msw": "^2.0.8", + "supertest": "^6.3.3" }, "files": [ "dist" diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..8421c82db7 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts @@ -0,0 +1,473 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { + AuthResolverContext, + encodeOAuthState, + OAuthAuthenticatorAuthenticateInput, + OAuthAuthenticatorRefreshInput, + OAuthAuthenticatorStartInput, + OAuthState, +} from '@backstage/plugin-auth-node'; +import { SignJWT } from 'jose'; +import { rest } from 'msw'; +import { setupServer } from 'msw'; + +import { + vmWareCSPAuthenticator, + vmWareCSPAuthenticatorContext, +} from './authenticator'; + +jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); + +describe('VMwareCloudServicesAuthenticator', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + let oAuthState: OAuthState = { + nonce: 'nonce', + env: 'env', + }; + + const signInInfo: Record = { + given_name: 'Givenname', + family_name: 'Familyname', + context_name: 'orgId', + email: 'user@example.com', + }; + + let idToken: string; + + let authResponse: { + access_token: string; + refresh_token: string; + id_token: typeof idToken; + }; + + let fakeSession: Record; + let authenticatorCtx: vmWareCSPAuthenticatorContext; + + beforeAll(async () => { + idToken = await new SignJWT(signInInfo) + .setProtectedHeader({ alg: 'HS256' }) + .sign(Buffer.from('signing key')); + + authResponse = { + access_token: 'accessToken', + refresh_token: 'refreshToken', + id_token: idToken, + }; + }); + + beforeEach(() => { + server.use( + rest.post( + 'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token', + (req, res, ctx) => + res( + req.headers.get('Authorization') + ? ctx.json(authResponse) + : ctx.status(401), + ), + ), + ); + + authenticatorCtx = vmWareCSPAuthenticator.initialize({ + callbackUrl: 'http://callbackUrl', + config: new ConfigReader({ + clientId: 'placeholderClientId', + organizationId: 'orgId', + }), + }); + }); + + describe('#initialize', () => { + it('fails when organizationId is not configured', () => { + return expect(() => + vmWareCSPAuthenticator.initialize({ + callbackUrl: 'http://callbackUrl', + config: new ConfigReader({ + clientId: 'placeholderClientId', + }), + }), + ).toThrow(`Missing required config value at 'organizationId'`); + }); + }); + + describe('#start', () => { + let startRequest: OAuthAuthenticatorStartInput; + + beforeEach(() => { + fakeSession = {}; + startRequest = { + state: encodeOAuthState(oAuthState), + req: { + query: {}, + session: fakeSession, + }, + } as OAuthAuthenticatorStartInput; + }); + + it('redirects to the Cloud Services Console consent page', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const url = new URL(startResponse.url); + + expect(url.protocol).toBe('https:'); + expect(url.hostname).toBe('console.cloud.vmware.com'); + expect(url.pathname).toBe('/csp/gateway/discovery'); + }); + + it('passes client ID from config', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('client_id')).toBe('placeholderClientId'); + }); + + it('passes organizationId from config', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('orgId')).toBe('orgId'); + }); + + it('passes callback URL', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('redirect_uri')).toBe('http://callbackUrl'); + }); + + it('requests scopes for ID and refresh token', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('scope')).toBe('openid offline_access'); + }); + + it('generates PKCE challenge', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('code_challenge_method')).toBe('S256'); + expect(searchParams.get('code_challenge')).not.toBeNull(); + }); + + it('stores PKCE verifier in session', async () => { + await vmWareCSPAuthenticator.start(startRequest, authenticatorCtx); + + expect( + fakeSession['oauth2:console.cloud.vmware.com'].state.code_verifier, + ).toBeDefined(); + }); + + it('fails when request has no session', () => { + return expect( + vmWareCSPAuthenticator.start( + { + state: encodeOAuthState(oAuthState), + req: { + query: {}, + }, + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ), + ).rejects.toThrow('requires session support'); + }); + + it('adds session ID handle to state param', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const stateParam = new URL(startResponse.url).searchParams.get('state'); + + const state = Object.fromEntries( + new URLSearchParams(Buffer.from(stateParam!, 'hex').toString('utf-8')), + ); + + const { handle } = fakeSession['oauth2:console.cloud.vmware.com'].state; + expect(state.handle).toBe(handle); + }); + }); + + describe('#authenticate', () => { + let resolverContext: jest.Mocked; + let authenticateRequest: OAuthAuthenticatorAuthenticateInput; + + beforeEach(() => { + resolverContext = { + issueToken: jest.fn().mockResolvedValue({ + token: 'defaultBackstageToken', + }), + findCatalogUser: jest.fn(), + signInWithCatalogUser: jest.fn().mockResolvedValue({ + token: 'backstageToken', + }), + }; + + oAuthState = { + code_verifier: 'foo', + handle: 'sessionid', + nonce: 'nonce', + env: 'development', + } as OAuthState; + + fakeSession = { + ['oauth2:console.cloud.vmware.com']: { + state: oAuthState, + }, + }; + + authenticateRequest = { + req: { + query: { + code: 'foo', + state: encodeOAuthState(oAuthState), + } as unknown, + session: fakeSession, + }, + } as OAuthAuthenticatorAuthenticateInput; + }); + + it('stores refresh token in cookie', async () => { + const { + session: { refreshToken }, + } = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + expect(refreshToken).toBe('refreshToken'); + }); + + it('responds with ID token', async () => { + const { session } = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + expect(session.idToken).toBe(idToken); + }); + + it('default transform decodes ID token', async () => { + const result = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + const { profile } = await vmWareCSPAuthenticator.defaultProfileTransform( + result, + resolverContext, + ); + + expect(profile).toStrictEqual({ + email: signInInfo.email, + displayName: `${signInInfo.given_name} ${signInInfo.family_name}`, + }); + }); + + it('default transform fails if claims are missing', async () => { + authenticatorCtx = vmWareCSPAuthenticator.initialize({ + callbackUrl: 'http://callbackUrl', + config: new ConfigReader({ + clientId: 'placeholderClientId', + organizationId: 'myOrgId', + }), + }); + + const result = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + return expect( + vmWareCSPAuthenticator.defaultProfileTransform(result, resolverContext), + ).rejects.toThrow('ID token organizationId mismatch'); + }); + + it('default transform fails if organizationId mismatch', async () => { + const inadequateIdToken: string = await new SignJWT({ sub: 'unusual' }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(Buffer.from('signing key')); + + server.use( + rest.post( + 'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token', + (_, res, ctx) => + res( + ctx.json({ + access_token: 'accessToken', + id_token: inadequateIdToken, + }), + ), + ), + ); + + const result = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + return expect( + vmWareCSPAuthenticator.defaultProfileTransform(result, resolverContext), + ).rejects.toThrow( + 'ID token missing required claims: email, given_name, family_name', + ); + }); + + it('fails when request has no session', () => { + return expect( + vmWareCSPAuthenticator.authenticate( + { + req: { + query: {}, + }, + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ), + ).rejects.toThrow('requires session support'); + }); + + it('fails when request has no authorization code', () => { + return expect( + vmWareCSPAuthenticator.authenticate( + { + req: { + query: {}, + session: fakeSession, + }, + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ), + ).rejects.toThrow('Unexpected redirect'); + }); + }); + + describe('integration between #start and #authenticate', () => { + beforeEach(() => { + fakeSession = { + ['oauth2:console.cloud.vmware.com']: { + state: oAuthState, + }, + }; + }); + + it('state param is compatible', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + { + req: { + query: {}, + session: {}, + }, + state: encodeOAuthState(oAuthState), + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + const { session } = await vmWareCSPAuthenticator.authenticate( + { + req: { + query: { + code: 'authorization_code', + state: searchParams.get('state'), + } as unknown, + session: fakeSession, + }, + } as OAuthAuthenticatorAuthenticateInput, + authenticatorCtx, + ); + + expect(session).toBeDefined(); + expect(session.idToken).toBe(idToken); + }); + }); + + describe('#refresh', () => { + let refreshRequest: OAuthAuthenticatorRefreshInput; + let resolverContext: jest.Mocked; + + beforeEach(() => { + resolverContext = { + issueToken: jest.fn().mockResolvedValue({ + token: 'defaultBackstageToken', + }), + findCatalogUser: jest.fn(), + signInWithCatalogUser: jest.fn().mockResolvedValue({ + token: 'backstageToken', + }), + }; + + refreshRequest = { + req: { + query: { + code: 'foo', + state: 'sessionid', + } as unknown, + session: fakeSession, + }, + } as OAuthAuthenticatorRefreshInput; + }); + + it('gets new refresh token', async () => { + const { + session: { refreshToken }, + } = await vmWareCSPAuthenticator.refresh( + refreshRequest, + authenticatorCtx, + ); + + expect(refreshToken).toBe('refreshToken'); + }); + + it('default transform decodes ID token', async () => { + const result = await vmWareCSPAuthenticator.refresh( + refreshRequest, + authenticatorCtx, + ); + + const { profile } = await vmWareCSPAuthenticator.defaultProfileTransform( + result, + resolverContext, + ); + + expect(profile).toStrictEqual({ + email: signInInfo.email, + displayName: `${signInInfo.given_name} ${signInInfo.family_name}`, + }); + }); + }); +}); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts index bd97111de6..c6c7d8a5c7 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts @@ -22,7 +22,7 @@ import { PassportOAuthDoneCallback, PassportProfile, } from '@backstage/plugin-auth-node'; -import jwtDecoder from 'jwt-decode'; +import { decodeJwt } from 'jose'; import { Metadata, StateStoreStoreCallback, @@ -54,7 +54,7 @@ export const vmWareCSPAuthenticator = createOAuthAuthenticator< ); } - const identity: Record = jwtDecoder(input.session.idToken); + const identity = decodeJwt(input.session.idToken); const missingClaims = [ 'email', 'given_name', @@ -75,7 +75,7 @@ export const vmWareCSPAuthenticator = createOAuthAuthenticator< return { profile: { displayName: `${identity.given_name} ${identity.family_name}`, - email: identity.email, + email: identity.email as string, }, }; }, diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts b/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts new file mode 100644 index 0000000000..3bcf7e4e56 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts @@ -0,0 +1,243 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createHttpServer, + createSpecializedBackend, + DefaultRootHttpRouter, + ExtendedHttpServer, + HostDiscovery, + MiddlewareFactory, +} from '@backstage/backend-app-api'; +import { + BackendFeature, + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { + mockServices, + TestBackend, + TestBackendOptions, +} from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; + +import { authModuleVmwareCspProvider } from './module'; + +const secret = 'secret'; + +function isPromise(value: unknown | Promise): value is Promise { + return ( + typeof value === 'object' && + value !== null && + 'then' in value && + typeof value.then === 'function' + ); +} + +function unwrapFeature( + feature: BackendFeature | (() => BackendFeature), +): BackendFeature { + return typeof feature === 'function' ? feature() : feature; +} + +const defaultServiceFactories = [ + mockServices.cache.factory(), + mockServices.rootConfig.factory(), + mockServices.database.factory(), + mockServices.httpRouter.factory(), + mockServices.identity.factory(), + mockServices.lifecycle.factory(), + mockServices.logger.factory(), + mockServices.permissions.factory(), + mockServices.rootLifecycle.factory(), + mockServices.rootLogger.factory(), + mockServices.scheduler.factory(), + mockServices.tokenManager.factory(), + mockServices.urlReader.factory(), +]; + +async function createBackendWithSession( + options: TestBackendOptions, +): Promise { + const { extensionPoints, ...otherOptions } = options; + + // Unpack input into awaited plain BackendFeatures + const features: BackendFeature[] = await Promise.all( + options.features?.map(async val => { + if (isPromise(val)) { + const { default: feature } = await val; + return unwrapFeature(feature); + } + return unwrapFeature(val); + }) ?? [], + ); + + let server: ExtendedHttpServer; + + const rootHttpRouterFactory = createServiceFactory({ + service: coreServices.rootHttpRouter, + deps: { + config: coreServices.rootConfig, + lifecycle: coreServices.rootLifecycle, + rootLogger: coreServices.rootLogger, + }, + async factory({ config, lifecycle, rootLogger }) { + const router = DefaultRootHttpRouter.create(); + const logger = rootLogger.child({ service: 'rootHttpRouter' }); + + const app = express(); + + const middleware = MiddlewareFactory.create({ config, logger }); + + app.use( + session({ + secret, + resave: false, + saveUninitialized: false, + }), + ); + app.use(router.handler()); + app.use(middleware.notFound()); + app.use(middleware.error()); + + server = await createHttpServer( + app, + { listen: { host: '', port: 0 } }, + { logger }, + ); + + lifecycle.addShutdownHook(() => server.stop(), { logger }); + + await server.start(); + + return router; + }, + }); + + const discoveryFactory = createServiceFactory({ + service: coreServices.discovery, + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + }, + async factory() { + if (!server) { + throw new Error('Test server not started yet'); + } + const port = server.port(); + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { baseUrl: `http://localhost:${port}`, listen: { port } }, + }), + ); + return discovery; + }, + }); + + const backend = createSpecializedBackend({ + ...otherOptions, + defaultServiceFactories: [ + ...defaultServiceFactories, + rootHttpRouterFactory, + discoveryFactory, + ], + }); + + for (const feature of features) { + backend.add(feature); + } + + await backend.start(); + + return Object.assign(backend, { + get server() { + if (!server) { + throw new Error('TestBackend server is not available'); + } + return server; + }, + }); +} + +describe('authModuleVmwareCspProvider', () => { + it('should start', async () => { + const backend = await createBackendWithSession({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleVmwareCspProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + vmwareCloudServices: { + development: { + clientId: 'placeholderClientId', + organizationId: 'orgId', + }, + }, + }, + }, + }, + }), + ], + }); + + const { server } = backend; + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/vmwareCloudServices/start?env=development', + ); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('vmwareCloudServices-nonce', { + domain: 'localhost', + path: '/api/auth/vmwareCloudServices/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://console.cloud.vmware.com'); + expect(startUrl.pathname).toBe('/csp/gateway/discovery'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + client_id: 'placeholderClientId', + redirect_uri: `http://localhost:${server.port()}/api/auth/vmwareCloudServices/handler/frame`, + code_challenge: expect.any(String), + state: expect.any(String), + scope: 'openid offline_access', + orgId: 'orgId', + code_challenge_method: 'S256', + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + handle: expect.any(String), + nonce: decodeURIComponent(nonceCookie.value), + }); + + backend.stop(); + }); +}); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/module.ts b/plugins/auth-backend-module-vmware-csp-provider/src/module.ts index a0dc7ef566..9277d8e661 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/module.ts +++ b/plugins/auth-backend-module-vmware-csp-provider/src/module.ts @@ -31,7 +31,7 @@ export const authModuleVmwareCspProvider = createBackendModule({ deps: { providers: authProvidersExtensionPoint }, async init({ providers }) { providers.registerProvider({ - providerId: 'oauth2', + providerId: 'vmwareCloudServices', factory: createOAuthProviderFactory({ authenticator: vmWareCSPAuthenticator, signInResolverFactories: { diff --git a/yarn.lock b/yarn.lock index f8569c5dc6..242afeb841 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4844,18 +4844,23 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-vmware-csp-provider@workspace:plugins/auth-backend-module-vmware-csp-provider" dependencies: + "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" - "@types/jwt-decode": ^3.1.0 - jwt-decode: ^3.1.0 + express: ^4.18.2 + express-session: ^1.17.3 + jose: ^4.6.0 + msw: ^2.0.8 passport-oauth2: ^1.6.1 + supertest: ^6.3.3 languageName: unknown linkType: soft @@ -10132,6 +10137,33 @@ __metadata: languageName: node linkType: hard +"@bundled-es-modules/cookie@npm:^2.0.0": + version: 2.0.0 + resolution: "@bundled-es-modules/cookie@npm:2.0.0" + dependencies: + cookie: ^0.5.0 + checksum: 53114eabbedda20ba6c63f45dcea35c568616d22adf5d1882cef9761f65ae636bf47e0c66325572cc8e3a335e0257caf5f76ff1287990d9e9265be7bc9767a87 + languageName: node + linkType: hard + +"@bundled-es-modules/js-levenshtein@npm:^2.0.1": + version: 2.0.1 + resolution: "@bundled-es-modules/js-levenshtein@npm:2.0.1" + dependencies: + js-levenshtein: ^1.1.6 + checksum: 13d0cbd2b00e563e09a797559dcff8c7e208c1f71e1787535a3d248f7e3d33ef3f0809b9f498d41788ab5fd399882dcca79917d70d97921b7dde94a282c1b7d8 + languageName: node + linkType: hard + +"@bundled-es-modules/statuses@npm:^1.0.1": + version: 1.0.1 + resolution: "@bundled-es-modules/statuses@npm:1.0.1" + dependencies: + statuses: ^2.0.1 + checksum: bcaa7de192e73056950b5fd20e75140d8d09074b1adc4437924b2051bb02b4dbf568c96e67d53b220fb7d735c3446e2ba746599cb1793ab2d23dd2ef230a8622 + languageName: node + linkType: hard + "@changesets/apply-release-plan@npm:^6.1.4": version: 6.1.4 resolution: "@changesets/apply-release-plan@npm:6.1.4" @@ -13104,6 +13136,13 @@ __metadata: languageName: node linkType: hard +"@mswjs/cookies@npm:^1.1.0": + version: 1.1.0 + resolution: "@mswjs/cookies@npm:1.1.0" + checksum: 1d9be44548907b92ff6acd46795292968661be19f1c04c43fdb2beb98bc7e58b8ffcef3be19d0f2cb58df07a36a6b53b4bbc0ea34e023b7366dbc28ffee90338 + languageName: node + linkType: hard + "@mswjs/interceptors@npm:^0.17.10": version: 0.17.10 resolution: "@mswjs/interceptors@npm:0.17.10" @@ -13120,6 +13159,20 @@ __metadata: languageName: node linkType: hard +"@mswjs/interceptors@npm:^0.25.11": + version: 0.25.12 + resolution: "@mswjs/interceptors@npm:0.25.12" + dependencies: + "@open-draft/deferred-promise": ^2.2.0 + "@open-draft/logger": ^0.3.0 + "@open-draft/until": ^2.0.0 + is-node-process: ^1.2.0 + outvariant: ^1.2.1 + strict-event-emitter: ^0.5.1 + checksum: 0676808700059f55536b51ffe38e9ea07e26b4d9c284fbbdf7a52b7282b7a93703f18d93b109c384205fe5f72b585506d5550cc8f3559267892b01a0f7561d3d + languageName: node + linkType: hard + "@mui/base@npm:5.0.0-beta.24": version: 5.0.0-beta.24 resolution: "@mui/base@npm:5.0.0-beta.24" @@ -14052,6 +14105,23 @@ __metadata: languageName: node linkType: hard +"@open-draft/deferred-promise@npm:^2.2.0": + version: 2.2.0 + resolution: "@open-draft/deferred-promise@npm:2.2.0" + checksum: 7f29d39725bb8ab5b62f89d88a4202ce2439ac740860979f9e3d0015dfe4bc3daddcfa5727fa4eed482fdbee770aa591b1136b98b0a0f0569a65294f35bdf56a + languageName: node + linkType: hard + +"@open-draft/logger@npm:^0.3.0": + version: 0.3.0 + resolution: "@open-draft/logger@npm:0.3.0" + dependencies: + is-node-process: ^1.2.0 + outvariant: ^1.4.0 + checksum: 7adfe3d0ed8ca32333ce2a77f9a93d561ebc89c989eaa9722f1dc8a2d2854f5de1bef6fa6894cdf58e16fa4dd9cfa99444ea1f5cac6eb1518e9247911ed042d5 + languageName: node + linkType: hard + "@open-draft/until@npm:^1.0.3": version: 1.0.3 resolution: "@open-draft/until@npm:1.0.3" @@ -14059,6 +14129,13 @@ __metadata: languageName: node linkType: hard +"@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0": + version: 2.1.0 + resolution: "@open-draft/until@npm:2.1.0" + checksum: 140ea3b16f4a3a6a729c1256050e20a93d408d7aa1e125648ce2665b3c526ed452510c6e4a6f4b15d95fb5e41203fb51510eb8fbc8812d5e5a91880293d66471 + languageName: node + linkType: hard + "@openapi-contrib/openapi-schema-to-json-schema@npm:~3.2.0": version: 3.2.0 resolution: "@openapi-contrib/openapi-schema-to-json-schema@npm:3.2.0" @@ -19081,6 +19158,13 @@ __metadata: languageName: node linkType: hard +"@types/statuses@npm:^2.0.1": + version: 2.0.4 + resolution: "@types/statuses@npm:2.0.4" + checksum: 3a806c3b96d1845e3e7441fbf0839037e95f717334760ddb7c29223c9a34a7206b68e2998631f89f1a1e3ef5b67b15652f6e8fa14987ebd7f6d38587c1bffd18 + languageName: node + linkType: hard + "@types/stoppable@npm:^1.1.0": version: 1.1.3 resolution: "@types/stoppable@npm:1.1.3" @@ -28915,6 +28999,13 @@ __metadata: languageName: node linkType: hard +"headers-polyfill@npm:^4.0.1": + version: 4.0.2 + resolution: "headers-polyfill@npm:4.0.2" + checksum: a95280ed58df429fc86c4f49b21596be3ea3f5f3d790e7d75238668df9b90b292f15a968c7c19ae1db88c0ae036dd1bf363a71b8e771199d82848e2d8b3c6c2e + languageName: node + linkType: hard + "helmet@npm:^6.0.0": version: 6.0.1 resolution: "helmet@npm:6.0.1" @@ -34689,6 +34780,42 @@ __metadata: languageName: node linkType: hard +"msw@npm:^2.0.8": + version: 2.0.8 + resolution: "msw@npm:2.0.8" + dependencies: + "@bundled-es-modules/cookie": ^2.0.0 + "@bundled-es-modules/js-levenshtein": ^2.0.1 + "@bundled-es-modules/statuses": ^1.0.1 + "@mswjs/cookies": ^1.1.0 + "@mswjs/interceptors": ^0.25.11 + "@open-draft/until": ^2.1.0 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + "@types/statuses": ^2.0.1 + chalk: ^4.1.2 + chokidar: ^3.4.2 + graphql: ^16.8.1 + headers-polyfill: ^4.0.1 + inquirer: ^8.2.0 + is-node-process: ^1.2.0 + js-levenshtein: ^1.1.6 + outvariant: ^1.4.0 + path-to-regexp: ^6.2.0 + strict-event-emitter: ^0.5.0 + type-fest: ^2.19.0 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.7.x <= 5.2.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 8737dae4cf516d8c591ad8d3751cef2f8f07d10a1f995b8cd7d19d8e955a29352dd6fc092f04bc0e1d0025663ccc3c64ae7e56e29c8d4a4b13699e3ee4747c46 + languageName: node + linkType: hard + "multer@npm:^1.4.5-lts.1": version: 1.4.5-lts.1 resolution: "multer@npm:1.4.5-lts.1" @@ -41336,7 +41463,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1": +"statuses@npm:2.0.1, statuses@npm:^2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb @@ -41473,6 +41600,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.5.0, strict-event-emitter@npm:^0.5.1": + version: 0.5.1 + resolution: "strict-event-emitter@npm:0.5.1" + checksum: 350480431bc1c28fdb601ef4976c2f8155fc364b4740f9692dd03e5bdd48aafc99a5e021fe655fbd986d0b803e9f3fc5c4b018b35cb838c4690d60f2a26f1cf3 + languageName: node + linkType: hard + "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0"