Init auth-backend-module-openshift-provider

Signed-off-by: Yannik Daellenbach <git@daellenbach.org>
This commit is contained in:
Yannik Daellenbach
2025-08-12 15:03:11 +02:00
parent a0757f057f
commit eb772f5f18
14 changed files with 890 additions and 38 deletions
+1
View File
@@ -37,6 +37,7 @@
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^",
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,5 @@
# @backstage/plugin-auth-backend-module-openshift-provider
The openshift-provider backend module for the auth plugin.
_This plugin was created through the Backstage CLI_
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-auth-backend-module-openshift-provider
title: '@backstage/plugin-auth-backend-module-openshift-provider'
description: The OpenShift backend module for the auth plugin.
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: auth-maintainers
@@ -0,0 +1,44 @@
/*
* Copyright 2025 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 { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
/** @visibility frontend */
openshift?: {
[authEnv: string]: {
clientId: string;
/**
* @visibility secret
*/
clientSecret: string;
authorizationUrl: string;
tokenUrl: string;
callbackUrl?: string;
openshiftApiServerUrl: string;
signIn?: {
resolvers: Array<{
resolver: 'displayNameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}>;
};
sessionDuration?: HumanDuration | string;
};
};
};
};
}
@@ -0,0 +1,26 @@
/*
* Copyright 2025 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 { createBackend } from '@backstage/backend-defaults';
import authPlugin from '@backstage/plugin-auth-backend';
import authModuleOpenShiftProvider from '../src';
const backend = createBackend();
backend.add(authPlugin);
backend.add(authModuleOpenShiftProvider);
backend.start();
@@ -0,0 +1,55 @@
{
"name": "@backstage/plugin-auth-backend-module-openshift-provider",
"version": "0.0.0",
"description": "The OpenShift backend module for the auth plugin.",
"backstage": {
"role": "backend-plugin-module",
"pluginId": "auth",
"pluginPackage": "@backstage/plugin-auth-backend"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/auth-backend-module-openshift-provider"
},
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist",
"config.d.ts"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/types": "workspace:^",
"passport-oauth2": "^1.8.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"express": "^4.18.2",
"msw": "^2.7.3",
"supertest": "^7.1.0"
},
"configSchema": "config.d.ts"
}
@@ -0,0 +1,28 @@
## API Report File for "@backstage/plugin-auth-backend-module-openshift-provider"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { OAuthAuthenticator } from '@backstage/plugin-auth-node';
import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node';
import { PassportProfile } from '@backstage/plugin-auth-node';
// @public (undocumented)
const authModuleOpenshiftProvider: BackendFeature;
export default authModuleOpenshiftProvider;
// @public (undocumented)
export const openshiftAuthenticator: OAuthAuthenticator<
OpenShiftAuthenticatorContext,
PassportProfile
>;
// @public (undocumented)
export interface OpenShiftAuthenticatorContext {
// (undocumented)
helper: PassportOAuthAuthenticatorHelper;
// (undocumented)
openshiftApiServerUrl: string;
}
```
@@ -0,0 +1,338 @@
/*
* Copyright 2025 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 { setupServer } from 'msw/node';
import {
decodeOAuthState,
encodeOAuthState,
} from '@backstage/plugin-auth-node';
import { registerMswTestHooks } from '@backstage/backend-test-utils';
import { http, HttpResponse } from 'msw';
import { openshiftAuthenticator } from './authenticator';
import { ConfigReader } from '@backstage/config';
import {
OAuthState,
OAuthAuthenticatorStartInput,
OAuthAuthenticatorAuthenticateInput,
} from '@backstage/plugin-auth-node';
import express from 'express';
describe('openshiftAuthenticator', () => {
let implementation: any;
let oauthState: OAuthState;
const mswServer = setupServer();
registerMswTestHooks(mswServer);
beforeEach(() => {
mswServer.use(
http.post('https://openshift.test/oauth/token', () => {
return HttpResponse.json({
access_token: 'accessToken',
scope: 'user:full',
expires_in: 60 * 60 * 24,
});
}),
http.get(
'https://api.openshift.test/apis/user.openshift.io/v1/users/~',
async () => {
return HttpResponse.json({
kind: 'User',
apiVersion: 'user.openshift.io/v1',
metadata: {
name: 'alice',
uid: 'ca993628-8817-4a3b-9811-be4a34c60bf4',
resourceVersion: '1',
creationTimestamp: '2022-01-11T13:10:45Z',
managedFields: [],
},
fullName: 'Alice Adams',
identities: ['SSO:id'],
groups: ['system:authenticated', 'system:authenticated:oauth'],
});
},
),
http.delete(
'https://api.openshift.test/apis/oauth.openshift.io/v1/oauthaccesstokens/:id',
({ params }) => {
const { id } = params;
if (typeof id !== 'string') {
return new Response(null, { status: 401 });
}
if (!id.startsWith('sha256~')) {
return new Response(null, { status: 401 });
}
return new Response(null, { status: 200 });
},
),
);
implementation = openshiftAuthenticator.initialize({
callbackUrl: 'https://backstage.test/callback',
config: new ConfigReader({
clientId: 'clientId',
clientSecret: 'clientSecret',
authorizationUrl: 'https://openshift.test/oauth/authorize',
tokenUrl: 'https://openshift.test/oauth/token',
openshiftApiServerUrl: 'https://api.openshift.test',
}),
});
oauthState = {
nonce: 'nonce',
env: 'env',
};
});
afterEach(() => {
jest.clearAllMocks();
});
describe('#start', () => {
let fakeSession: Record<string, any>;
let startRequest: OAuthAuthenticatorStartInput;
beforeEach(() => {
fakeSession = {};
startRequest = {
state: encodeOAuthState(oauthState),
req: {
method: 'GET',
url: 'test',
session: fakeSession,
},
} as unknown as OAuthAuthenticatorStartInput;
});
it('initiates authorization code grant', async () => {
const startResponse = await openshiftAuthenticator.start(
startRequest,
implementation,
);
const { searchParams } = new URL(startResponse.url);
expect(searchParams.get('response_type')).toBe('code');
});
it('passes client ID from config', async () => {
const startResponse = await openshiftAuthenticator.start(
startRequest,
implementation,
);
const { searchParams } = new URL(startResponse.url);
expect(searchParams.get('client_id')).toBe('clientId');
});
it('passes callback URL from config', async () => {
const startResponse = await openshiftAuthenticator.start(
startRequest,
implementation,
);
const { searchParams } = new URL(startResponse.url);
expect(searchParams.get('redirect_uri')).toBe(
'https://backstage.test/callback',
);
});
it('encodes OAuth state in query param', async () => {
const startResponse = await openshiftAuthenticator.start(
startRequest,
implementation,
);
const { searchParams } = new URL(startResponse.url);
const stateParam = searchParams.get('state');
const decodedState = decodeOAuthState(stateParam!);
expect(decodedState).toMatchObject(oauthState);
});
});
describe('#authenticate', () => {
let handlerRequest: OAuthAuthenticatorAuthenticateInput;
beforeEach(() => {
handlerRequest = {
req: {
method: 'GET',
query: {
code: 'authorization_code',
state: encodeOAuthState(oauthState),
},
session: {
'oauth2:openshift': {
state: encodeOAuthState(oauthState),
},
},
} as unknown as express.Request,
};
});
it('exchanges authorization code for access token', async () => {
const authenticatorResult = await openshiftAuthenticator.authenticate(
handlerRequest,
implementation,
);
const accessToken = authenticatorResult.session.accessToken;
expect(accessToken).toEqual('accessToken');
});
it('returns granted scope', async () => {
const authenticatorResult = await openshiftAuthenticator.authenticate(
handlerRequest,
implementation,
);
const responseScope = authenticatorResult.session.scope;
expect(responseScope).toEqual('user:full');
});
it('returns a default session.tokentype field', async () => {
const authenticatorResult = await openshiftAuthenticator.authenticate(
handlerRequest,
implementation,
);
const tokenType = authenticatorResult.session.tokenType;
expect(tokenType).toEqual('bearer');
});
it('returns displayName', async () => {
const authenticatorResult = await openshiftAuthenticator.authenticate(
handlerRequest,
implementation,
);
expect(authenticatorResult).toMatchObject({
fullProfile: {
displayName: 'alice',
},
});
});
it('should store access token as refresh token', async () => {
const authenticatorResult = await openshiftAuthenticator.authenticate(
handlerRequest,
implementation,
);
expect(authenticatorResult.session.refreshToken).toBe(
authenticatorResult.session.accessToken,
);
});
});
describe('#refresh', () => {
it('gets new refresh token (access token)', async () => {
const refreshResponse = await openshiftAuthenticator.refresh(
{
scope: 'user:full',
refreshToken: 'access-token',
req: {} as express.Request,
},
implementation,
);
expect(refreshResponse.session.refreshToken).toBe('access-token');
});
it('should throw error when invalid access token was provided', async () => {
mswServer.use(
http.get(
'https://api.openshift.test/apis/user.openshift.io/v1/users/~',
async () => {
return HttpResponse.json(
{
kind: 'Status',
apiVersion: 'v1',
metadata: {},
status: 'Failure',
message: 'Unauthorized',
reason: 'Unauthorized',
code: 401,
},
{
status: 401,
},
);
},
),
);
await expect(
openshiftAuthenticator.refresh(
{
scope: 'user:full',
refreshToken: 'invalid-access-token',
req: {} as express.Request,
},
implementation,
),
).rejects.toThrow('HTTP error! Status: 401');
});
});
describe('#logout', () => {
it('should delete valid access token', async () => {
await expect(
openshiftAuthenticator.logout?.(
{
refreshToken: 'access-token',
req: {} as express.Request,
},
implementation,
),
).resolves.not.toThrow();
});
it('should throw when refresh token is not set', async () => {
await expect(
openshiftAuthenticator.logout?.(
{
req: {} as express.Request,
},
implementation,
),
).rejects.toThrow();
});
it('should throw when access cannot be deleted', async () => {
mswServer.use(
http.delete(
'https://api.openshift.test/apis/oauth.openshift.io/v1/oauthaccesstokens/:id',
() => {
return new Response(null, { status: 401 });
},
),
);
await expect(
openshiftAuthenticator.logout?.(
{
refreshToken: 'access-token',
req: {} as express.Request,
},
implementation,
),
).rejects.toThrow();
});
});
});
@@ -0,0 +1,184 @@
/*
* Copyright 2025 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 {
createOAuthAuthenticator,
PassportOAuthAuthenticatorHelper,
PassportOAuthDoneCallback,
PassportProfile,
} from '@backstage/plugin-auth-node';
import { createHash } from 'node:crypto';
import OAuth2Strategy from 'passport-oauth2';
import { z } from 'zod';
/** @public */
export interface OpenShiftAuthenticatorContext {
openshiftApiServerUrl: string;
helper: PassportOAuthAuthenticatorHelper;
}
/** @private
* Schema for user.openshift.io/v1,
* see https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/user_and_group_apis/user-user-openshift-io-v1#user-user-openshift-io-v1
*/
const OpenShiftUser = z.object({
metadata: z.object({
name: z.string(),
}),
});
/** @public */
export const openshiftAuthenticator = createOAuthAuthenticator<
OpenShiftAuthenticatorContext,
PassportProfile
>({
defaultProfileTransform:
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
scopes: {
required: ['user:full'],
},
initialize({ callbackUrl, config }) {
const clientId = config.getString('clientId');
const clientSecret = config.getString('clientSecret');
const authorizationUrl = config.getString('authorizationUrl');
const tokenUrl = config.getString('tokenUrl');
const openshiftApiServerUrl = config.getString('openshiftApiServerUrl');
// userUrl: `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`,
const strategy = new OAuth2Strategy(
{
clientID: clientId,
clientSecret: clientSecret,
callbackURL: callbackUrl,
authorizationURL: authorizationUrl,
tokenURL: tokenUrl,
passReqToCallback: false,
},
(
accessToken: any,
refreshToken: string,
params: any,
fullProfile: PassportProfile,
done: PassportOAuthDoneCallback,
) => {
done(undefined, { fullProfile, params, accessToken }, { refreshToken });
},
);
strategy.userProfile = function userProfile(
accessToken: string,
done: (err?: unknown, profile?: any) => void,
): void {
this._oauth2.useAuthorizationHeaderforGET(true);
this._oauth2.get(
`${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`,
accessToken,
(error, data, _) => {
if (error !== null && error.statusCode !== 200) {
done(new Error(`HTTP error! Status: ${error.statusCode}`));
return;
}
if (!data) {
done(new Error('No data provided!'));
return;
}
if (typeof data !== 'string') {
done(new Error('Data of type Buffer is not supported!'));
return;
}
const user = OpenShiftUser.parse(JSON.parse(data));
done(null, { displayName: user.metadata.name });
},
);
};
return {
openshiftApiServerUrl,
helper: PassportOAuthAuthenticatorHelper.from(strategy),
};
},
async start(input, { helper }) {
return helper.start(input, {
accessType: 'offline',
prompt: 'consent',
});
},
async authenticate(input, { helper }) {
// Same workaround as the GitHub provider; see https://github.com/backstage/backstage/issues/25383
const { fullProfile, session } = await helper.authenticate(input);
session.refreshToken = session.accessToken;
session.refreshTokenExpiresInSeconds = session.expiresInSeconds;
return { fullProfile, session };
},
async refresh(input, { helper }) {
// Because the session is refreshed on login, this override is crucial,
// see https://github.com/backstage/backstage/issues/25383
const accessToken = input.refreshToken;
const fullProfile = await helper.fetchProfile(accessToken).catch(error => {
if (error.oauthError?.statusCode === 401) {
throw new Error('Invalid access token');
}
throw error;
});
return {
fullProfile,
session: {
accessToken,
tokenType: 'bearer',
scope: input.scope,
refreshToken: input.refreshToken,
},
};
},
async logout(input, { openshiftApiServerUrl, helper }) {
// Due to the implementation of createOAuthRouteHandlers, only the refresh token is set.
// In this provider, the refresh token actually IS the access token.
const accessToken = input.refreshToken;
if (!accessToken) {
throw new Error('access token/refresh token needs to be set for logout');
}
// Check if access token is still valid.
try {
await helper.fetchProfile(accessToken);
} catch {
// Invalid token, no need to delete OAuthAccessToken.
return;
}
// Calculate token name, see:
// https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/oauth_apis/oauthaccesstoken-oauth-openshift-io-v1#apis-oauth-openshift-io-v1-oauthaccesstokens
const tokenName = createHash('sha256')
.update(accessToken.slice('sha256~'.length))
.digest()
.toString('base64url');
const response = await fetch(
`${openshiftApiServerUrl}/apis/oauth.openshift.io/v1/oauthaccesstokens/sha256~${tokenName}`,
{ method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` } },
);
if (response.status === 401) {
throw new Error('unauthorized');
}
},
});
@@ -0,0 +1,25 @@
/*
* Copyright 2025 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.
*/
/**
* The openshift-provider backend module for the auth plugin.
*
* @packageDocumentation
*/
export {
openshiftAuthenticator,
type OpenShiftAuthenticatorContext,
} from './authenticator';
export { authModuleOpenshiftProvider as default } from './module';
@@ -0,0 +1,45 @@
/*
* Copyright 2025 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 { createBackendModule } from '@backstage/backend-plugin-api';
import {
authProvidersExtensionPoint,
createOAuthProviderFactory,
} from '@backstage/plugin-auth-node';
import { openshiftAuthenticator } from './authenticator';
import { openshiftSignInResolvers } from './resolvers';
/** @public */
export const authModuleOpenshiftProvider = createBackendModule({
pluginId: 'auth',
moduleId: 'openshift-provider',
register(reg) {
reg.registerInit({
deps: { providers: authProvidersExtensionPoint },
async init({ providers }) {
providers.registerProvider({
providerId: 'openshift',
factory: createOAuthProviderFactory({
authenticator: openshiftAuthenticator,
signInResolverFactories: {
...openshiftSignInResolvers,
},
}),
});
},
});
},
});
@@ -0,0 +1,68 @@
/*
* Copyright 2025 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 {
createSignInResolverFactory,
OAuthAuthenticatorResult,
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { z } from 'zod';
export namespace openshiftSignInResolvers {
export const displayNameMatchingUserEntityName = createSignInResolverFactory({
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
) => {
const { displayName } = info.profile;
if (!displayName) {
throw new Error(
`OpenShift user profile does not contain a displayName`,
);
}
const userRef = stringifyEntityRef({
kind: 'User',
name: displayName,
namespace: DEFAULT_NAMESPACE,
});
return await ctx.signInWithCatalogUser(
{ entityRef: userRef },
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: displayName } }
: undefined,
},
);
};
},
});
}
+60 -38
View File
@@ -4120,6 +4120,27 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-auth-backend-module-openshift-provider@workspace:^, @backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider":
version: 0.0.0-use.local
resolution: "@backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider"
dependencies:
"@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/plugin-auth-backend": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/types": "workspace:^"
express: "npm:^4.18.2"
msw: "npm:^2.7.3"
passport-oauth2: "npm:^1.8.0"
supertest: "npm:^7.1.0"
zod: "npm:^3.24.2"
languageName: unknown
linkType: soft
"@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider":
version: 0.0.0-use.local
resolution: "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider"
@@ -11081,9 +11102,9 @@ __metadata:
languageName: node
linkType: hard
"@mswjs/interceptors@npm:^0.39.1":
version: 0.39.2
resolution: "@mswjs/interceptors@npm:0.39.2"
"@mswjs/interceptors@npm:^0.37.0":
version: 0.37.1
resolution: "@mswjs/interceptors@npm:0.37.1"
dependencies:
"@open-draft/deferred-promise": "npm:^2.2.0"
"@open-draft/logger": "npm:^0.3.0"
@@ -11091,7 +11112,7 @@ __metadata:
is-node-process: "npm:^1.2.0"
outvariant: "npm:^1.4.3"
strict-event-emitter: "npm:^0.5.1"
checksum: 10/faaa95d636363a197f125c32066457fa74d5063d8ccae4c9c0e0510179060d92b1faf8640df45a0623e0bf42a30d610c83364a58e0eb0ca412c87b2e835936c1
checksum: 10/332d8aa50beb4834ccbda6a800ca00b1204adc0eba23e1c1f7bb9f4e564a92707e563f7a2424d4a8607404ec91424e5d8c34a87c250b191ca7b24dff12eba2c5
languageName: node
linkType: hard
@@ -26393,10 +26414,10 @@ __metadata:
languageName: node
linkType: hard
"component-emitter@npm:^1.3.1":
version: 1.3.1
resolution: "component-emitter@npm:1.3.1"
checksum: 10/94550aa462c7bd5a61c1bc480e28554aa306066930152d1b1844a0dd3845d4e5db7e261ddec62ae184913b3e59b55a2ad84093b9d3596a8f17c341514d6c483d
"component-emitter@npm:^1.3.0":
version: 1.3.0
resolution: "component-emitter@npm:1.3.0"
checksum: 10/dfc1ec2e7aa2486346c068f8d764e3eefe2e1ca0b24f57506cd93b2ae3d67829a7ebd7cc16e2bf51368fac2f45f78fcff231718e40b1975647e4a86be65e1d05
languageName: node
linkType: hard
@@ -27620,15 +27641,15 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0":
version: 4.4.1
resolution: "debug@npm:4.4.1"
"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0":
version: 4.4.0
resolution: "debug@npm:4.4.0"
dependencies:
ms: "npm:^2.1.3"
peerDependenciesMeta:
supports-color:
optional: true
checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe
checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367
languageName: node
linkType: hard
@@ -30049,6 +30070,7 @@ __metadata:
"@backstage/plugin-auth-backend": "workspace:^"
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^"
@@ -31103,7 +31125,7 @@ __metadata:
languageName: node
linkType: hard
"formidable@npm:^3.5.4":
"formidable@npm:^3.5.1":
version: 3.5.4
resolution: "formidable@npm:3.5.4"
dependencies:
@@ -38633,15 +38655,15 @@ __metadata:
languageName: node
linkType: hard
"msw@npm:^2.0.0, msw@npm:^2.0.8":
version: 2.10.4
resolution: "msw@npm:2.10.4"
"msw@npm:^2.0.0, msw@npm:^2.0.8, msw@npm:^2.7.3":
version: 2.7.3
resolution: "msw@npm:2.7.3"
dependencies:
"@bundled-es-modules/cookie": "npm:^2.0.1"
"@bundled-es-modules/statuses": "npm:^1.0.1"
"@bundled-es-modules/tough-cookie": "npm:^0.1.6"
"@inquirer/confirm": "npm:^5.0.0"
"@mswjs/interceptors": "npm:^0.39.1"
"@mswjs/interceptors": "npm:^0.37.0"
"@open-draft/deferred-promise": "npm:^2.2.0"
"@open-draft/until": "npm:^2.1.0"
"@types/cookie": "npm:^0.6.0"
@@ -38662,7 +38684,7 @@ __metadata:
optional: true
bin:
msw: cli/index.js
checksum: 10/e2f25dda1aba66c7444c29c41d3157cb15c0332055ab7ebfb74ef4b506e7b90098cf37c577768edb5b2b2dbf0d6ed6a7a3ca8ee6da3d72df5a25823d82f33316
checksum: 10/f193329a68fc22e477a6f8504aa44a92bd12847f2eeac1dfbd8ec1cc43ff293112ec067de1c7fe312ba02beecb313fb00aeeebf5817432b57af2d796b2dff2fa
languageName: node
linkType: hard
@@ -40612,7 +40634,7 @@ __metadata:
languageName: node
linkType: hard
"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0":
"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0, passport-oauth2@npm:^1.8.0":
version: 1.8.0
resolution: "passport-oauth2@npm:1.8.0"
dependencies:
@@ -42268,7 +42290,7 @@ __metadata:
languageName: node
linkType: hard
"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4":
"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4":
version: 6.14.0
resolution: "qs@npm:6.14.0"
dependencies:
@@ -46507,30 +46529,30 @@ __metadata:
languageName: node
linkType: hard
"superagent@npm:^10.2.3":
version: 10.2.3
resolution: "superagent@npm:10.2.3"
"superagent@npm:^9.0.1":
version: 9.0.2
resolution: "superagent@npm:9.0.2"
dependencies:
component-emitter: "npm:^1.3.1"
component-emitter: "npm:^1.3.0"
cookiejar: "npm:^2.1.4"
debug: "npm:^4.3.7"
debug: "npm:^4.3.4"
fast-safe-stringify: "npm:^2.1.1"
form-data: "npm:^4.0.4"
formidable: "npm:^3.5.4"
form-data: "npm:^4.0.0"
formidable: "npm:^3.5.1"
methods: "npm:^1.1.2"
mime: "npm:2.6.0"
qs: "npm:^6.11.2"
checksum: 10/377bf938e68927dd772169c5285be27872bf6e84fac01c52bcd9396bc5b348c9ded8f8be54649510ec09a67bc5096055847b37cb01b3bca0eb06ff1856170e35
qs: "npm:^6.11.0"
checksum: 10/d3c0c9051ceec84d5b431eaa410ad81bcd53255cea57af1fc66d683a24c34f3ba4761b411072a9bf489a70e3d5b586a78a0e6f2eac6a561067e7d196ddab0907
languageName: node
linkType: hard
"supertest@npm:^7.0.0":
version: 7.1.4
resolution: "supertest@npm:7.1.4"
"supertest@npm:^7.0.0, supertest@npm:^7.1.0":
version: 7.1.0
resolution: "supertest@npm:7.1.0"
dependencies:
methods: "npm:^1.1.2"
superagent: "npm:^10.2.3"
checksum: 10/ecb5d41f2b62b257dbdcabac245c32b8e8fb264fe2636dd85c2c883569d23dc14adc0a471abb84187cbdb49bc36ad870ad355b4a0b85973f510fd57fc229e6cc
superagent: "npm:^9.0.1"
checksum: 10/20069f739a44821dfa4f7f397b9086ef31a358366331138f97945eedb2e231796e7c55b032125d3bd12f9839f089fbb809893dbc0f98edc57e12333b9f42b726
languageName: node
linkType: hard
@@ -50029,10 +50051,10 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.22.4, zod@npm:^3.23.8":
version: 3.25.76
resolution: "zod@npm:3.25.76"
checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995
"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.2":
version: 3.25.67
resolution: "zod@npm:3.25.67"
checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa
languageName: node
linkType: hard