test: Add module test, fix providerId & remove jwt-decode dep

Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com>
This commit is contained in:
Carlos Esteban Lopez
2023-11-17 15:54:59 -05:00
parent dc69df8d73
commit 28fe984163
6 changed files with 864 additions and 9 deletions
@@ -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"
@@ -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<string, string> = {
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<string, any>;
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<AuthResolverContext>;
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<AuthResolverContext>;
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}`,
});
});
});
});
@@ -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<string, string> = 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,
},
};
},
@@ -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<T>(value: unknown | Promise<T>): value is Promise<T> {
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<TExtensionPoints extends any[]>(
options: TestBackendOptions<TExtensionPoints>,
): Promise<TestBackend> {
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();
});
});
@@ -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: {
+137 -3
View File
@@ -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"