Merge branch 'master' into feat/search-facet-by-type

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-12-29 10:43:33 +01:00
99 changed files with 1662 additions and 886 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-rollbar-backend': patch
---
Moved `@backstage/test-utils` to `devDependencies`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Replaces the usage of `got` with `node-fetch` in the `getUserPhoto` method of the Microsoft provider
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
Fixed an accidental re-export of `@backstage/test-utils` that broke this plugin in the most recent release.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/techdocs-common': patch
---
Bump `@azure/identity` from `^1.5.0` to `^2.0.1`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
In-memory cache clients instantiated from the same cache manager now share the same memory space.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Update `auth0` and `onelogin` providers to allow for `authHandler` and `signIn.resolver` configuration.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Fixed bug on refresh token on Okta provider, now it gets the refresh token and it sends it into providerInfo
+29
View File
@@ -0,0 +1,29 @@
---
'@backstage/plugin-auth-backend': minor
---
Avoid ever returning OAuth refresh tokens back to the client, and always exchange refresh tokens for a new one when available for all providers.
This comes with a breaking change to the TypeScript API for custom auth providers. The `refresh` method of `OAuthHandlers` implementation must now return a `{ response, refreshToken }` object rather than a direct response. Existing `refresh` implementations are typically migrated by changing an existing return expression that looks like this:
```ts
return await this.handleResult({
fullProfile,
params,
accessToken,
refreshToken,
});
```
Into the following:
```ts
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search': patch
---
Internal cleanup of the exports structure
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fix issue with plugin:serve for Plugins not using Lerna monorepo.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-react': patch
---
When a user has zero owned entities when viewing an entity kind in the catalog
page, it will be automatically redirected to see all the entities. Furthermore,
for the kind User and Group there are no longer the owned selector.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Add a comment to the default backend about the fallback 404 handler.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-rollbar-backend': patch
---
Replace the usage of `axios` with `node-fetch` in the Rollbar API
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-common': patch
---
Support navigating back to pre-filled templates to update inputs of scaffolder tasks for resubmission
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-backend': patch
---
Add support for API auth in DefaultTechDocsCollator
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-insights': patch
---
Export `techInsightsApiRef` and associated types.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config-loader': patch
---
Bump `typescript-json-schema` from `^0.51.0` to `^0.52.0`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-insights-backend-module-jsonfc': patch
---
ability to add custom operators
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Add option to build command for minifying the generated code
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Enforce cookie SSL protection when in production for auth-backend sessions
+1
View File
@@ -43,3 +43,4 @@
/.changeset/techdocs-* @backstage/techdocs-core
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core
/plugins/apache-airflow @backstage/reviewers @cmpadden
/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7
@@ -0,0 +1,71 @@
---
id: adrs-adr013
title: 'ADR013: Proper use of HTTP fetching libraries'
# prettier-ignore
description: Architecture Decision Record (ADR) for the proper use of fetchApiRef, node-fetch, and cross-fetch for data fetching.
---
## Context
Using multiple HTTP packages for data fetching increases the complexity and the
support burden of keeping said package up to date.
## Decision
Backend (node) packages should use the `node-fetch` package for HTTP data
fetching. Example:
```ts
import fetch from 'node-fetch';
import { ResponseError } from '@backstage/errors';
const response = await fetch('https://example.com/api/v1/users.json');
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const users = await response.json();
```
Frontend plugins and packages should prefer to use the
[`fetchApiRef`](https://backstage.io/docs/reference/core-plugin-api.fetchapiref).
It uses `cross-fetch` internally. Example:
```ts
import { useApi } from '@backstage/core-plugin-api';
const { fetch } = useApi(fetchApiRef);
const response = await fetch('https://example.com/api/v1/users.json');
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const users = await response.json();
```
Isomorphic packages should have a dependency on the `cross-fetch` package for
mocking and type definitions. Preferably, classes and functions in isomorphic
packages should accept an argument of type `typeof fetch` to let callers supply
their preferred implementation of `fetch`. This lets them adorn the calls with
auth or other information, and track metrics etc, in a cross-platform way.
Example:
```ts
import crossFetch from 'cross-fetch';
export class MyClient {
private readonly fetch: typeof crossFetch;
constructor(options: { fetch?: typeof crossFetch }) {
this.fetch = options.fetch || crossFetch;
}
async users() {
return await this.fetch('https://example.com/api/v1/users.json');
}
}
```
## Consequences
We will gradually transition away from third party packages such as `axios`,
`got` and others. Once we have transitioned to `node-fetch` we will add lint
rules to enforce this decision.
+5 -7
View File
@@ -10,7 +10,7 @@ TechDocs reads the static generated documentation files from a cloud storage
bucket (GCS, AWS S3, etc.). The documentation site is generated on the CI/CD
workflow associated with the repository containing the documentation files. This
document explains the steps needed to generate docs on CI and publish to a cloud
storage using [`techdocs-cli`](https://github.com/backstage/techdocs-cli).
storage using [`techdocs-cli`](./cli.md).
The steps here target all kinds of CI providers (GitHub Actions, CircleCI,
Jenkins, etc.). Specific tools for individual providers will also be made
@@ -40,9 +40,8 @@ techdocs-cli publish --publisher-type awsS3 --storage-name <bucket/container> --
That's it!
Take a look at
[`techdocs-cli` README](https://github.com/backstage/techdocs-cli) for the
complete command reference, details, and options.
Take a look at [`techdocs-cli`](./cli.md) for the complete command reference,
details, and options.
## Steps
@@ -74,7 +73,7 @@ Install [`npx`](https://www.npmjs.com/package/npx) to use it for running
`techdocs-cli`. Or you can install using `npm install -g @techdocs/cli`.
We are going to use the
[`techdocs-cli generate`](https://github.com/backstage/techdocs-cli#generate-techdocs-site-from-a-documentation-project)
[`techdocs-cli generate`](./cli.md#generate-techdocs-site-from-a-documentation-project)
command in this step.
```sh
@@ -93,8 +92,7 @@ necessary authentication environment variables.
- [AWS authentication](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html)
And then run the
[`techdocs-cli publish`](https://github.com/backstage/techdocs-cli#publish-generated-techdocs-sites)
command.
[`techdocs-cli publish`](./cli.md#publish-generated-techdocs-sites) command.
```sh
npx @techdocs/cli publish --publisher-type <awsS3|googleGcs> --storage-name <bucket/container> --entity <namespace/kind/name> --directory ./site
+2 -1
View File
@@ -434,7 +434,8 @@ Usage: backstage-cli build [options]
Options:
--outputs &lt;formats&gt; List of formats to output [types,cjs,esm]
-h, --help display help for command
--minify Minify the generated code
-h, --help display help for command
```
## lint
+2 -1
View File
@@ -287,7 +287,8 @@
"architecture-decisions/adrs-adr009",
"architecture-decisions/adrs-adr010",
"architecture-decisions/adrs-adr011",
"architecture-decisions/adrs-adr012"
"architecture-decisions/adrs-adr012",
"architecture-decisions/adrs-adr013"
],
"FAQ": ["FAQ"]
}
+2
View File
@@ -186,6 +186,8 @@ nav:
- ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md'
- ADR010 - Luxon Date Library: 'architecture-decisions/adr010-luxon-date-library.md'
- ADR011 - Plugin Package Structure: 'architecture-decisions/adr011-plugin-package-structure.md'
- ADR012 - Plugin Package Structure: 'architecture-decisions/adr012-use-luxon-locale-and-date-presets.md'
- ADR013 - Plugin Package Structure: 'architecture-decisions/adr013-use-node-fetch.md'
- Support:
- Backstage Project Structure: 'support/project-structure.md'
- Glossary: glossary.md
+16
View File
@@ -147,6 +147,22 @@ describe('CacheManager', () => {
});
});
it('shares memory across multiple instances of the memory client', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const plugin = 'test-plugin';
// Instantiate two in-memory clients.
manager.forPlugin(plugin).getClient({ defaultTtl: 10 });
manager.forPlugin(plugin).getClient({ defaultTtl: 10 });
const cache = Keyv as unknown as jest.Mock;
const mockCall2 = cache.mock.calls.splice(-1)[0][0];
const mockCall1 = cache.mock.calls.splice(-1)[0][0];
// Note: .toBe() checks referential identity of object instances.
expect(mockCall1.store).toBe(mockCall2.store);
});
it('returns a memcache client when configured', () => {
const expectedHost = '127.0.0.1:11211';
const manager = CacheManager.fromConfig(
+8
View File
@@ -42,6 +42,13 @@ export class CacheManager {
none: this.getNoneClient,
};
/**
* Shared memory store for the in-memory cache client. Sharing the same Map
* instance ensures get/set/delete operations hit the same store, regardless
* of where/when a client is instantiated.
*/
private readonly memoryStore = new Map();
private readonly logger: Logger;
private readonly store: keyof CacheManager['storeFactories'];
private readonly connection: string;
@@ -133,6 +140,7 @@ export class CacheManager {
return new Keyv({
namespace: pluginId,
ttl: defaultTtl,
store: this.memoryStore,
});
}
+1 -1
View File
@@ -33,5 +33,5 @@ export default async (cmd: Command) => {
outputs = new Set([Output.types, Output.esm, Output.cjs]);
}
await buildPackage({ outputs });
await buildPackage({ outputs, minify: cmd.minify });
};
+1
View File
@@ -138,6 +138,7 @@ export function registerCommands(program: CommanderStatic) {
.command('build')
.description('Build a package for publishing')
.option('--outputs <formats>', 'List of formats to output [types,cjs,esm]')
.option('--minify', 'Minify the generated code')
.action(lazy(() => import('./build').then(m => m.default)));
program
+11 -3
View File
@@ -44,9 +44,17 @@ export async function loadCliConfig(options: Options) {
const project = new Project(paths.targetDir);
const packages = await project.getPackages();
const localPackageNames = options.fromPackage
? findPackages(packages, options.fromPackage)
: packages.map((p: any) => p.name);
let localPackageNames;
if (options.fromPackage) {
if (packages.length) {
localPackageNames = findPackages(packages, options.fromPackage);
} else {
// No packages: it means that it's not a monorepo (e.g. standalone plugin)
localPackageNames = [options.fromPackage];
}
} else {
localPackageNames = packages.map((p: any) => p.name);
}
const schema = await loadConfigSchema({
dependencies: localPackageNames,
+1 -1
View File
@@ -42,7 +42,7 @@
"json-schema-merge-allof": "^0.8.1",
"json-schema-traverse": "^1.0.0",
"node-fetch": "^2.6.1",
"typescript-json-schema": "^0.51.0",
"typescript-json-schema": "^0.52.0",
"yaml": "^1.9.2",
"yup": "^0.32.9"
},
@@ -81,6 +81,8 @@ async function main() {
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use('/search', await search(searchEnv));
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
+1 -1
View File
@@ -36,7 +36,7 @@
"url": "https://github.com/backstage/backstage/issues"
},
"dependencies": {
"@azure/identity": "^1.5.0",
"@azure/identity": "^2.0.1",
"@azure/storage-blob": "^12.5.0",
"@backstage/backend-common": "^0.10.0",
"@backstage/catalog-model": "^0.9.7",
+41 -10
View File
@@ -27,10 +27,13 @@ export class AtlassianAuthProvider implements OAuthHandlers {
// (undocumented)
handler(req: express.Request): Promise<{
response: OAuthResponse;
refreshToken: string;
refreshToken: string | undefined;
}>;
// (undocumented)
refresh(req: OAuthRefreshRequest): Promise<OAuthResponse>;
refresh(req: OAuthRefreshRequest): Promise<{
response: OAuthResponse;
refreshToken: string | undefined;
}>;
// Warning: (ae-forgotten-export) The symbol "RedirectInfo" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -47,6 +50,14 @@ export type AtlassianProviderOptions = {
};
};
// @public (undocumented)
export type Auth0ProviderOptions = {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver<OAuthResult>;
};
};
// @public
export type AuthHandler<AuthResult> = (
input: AuthResult,
@@ -219,6 +230,11 @@ export const createAtlassianProvider: (
options?: AtlassianProviderOptions | undefined,
) => AuthProviderFactory;
// @public (undocumented)
export const createAuth0Provider: (
options?: Auth0ProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -282,6 +298,11 @@ export const createOktaProvider: (
_options?: OktaProviderOptions | undefined,
) => AuthProviderFactory;
// @public (undocumented)
export const createOneLoginProvider: (
options?: OneLoginProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -470,7 +491,10 @@ export interface OAuthHandlers {
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
refresh?(req: OAuthRefreshRequest): Promise<OAuthResponse>;
refresh?(req: OAuthRefreshRequest): Promise<{
response: OAuthResponse;
refreshToken?: string;
}>;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
@@ -485,7 +509,6 @@ export type OAuthProviderInfo = {
idToken?: string;
expiresInSeconds?: number;
scope: string;
refreshToken?: string;
};
// Warning: (ae-missing-release-tag) "OAuthProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -572,6 +595,14 @@ export type OktaProviderOptions = {
};
};
// @public (undocumented)
export type OneLoginProviderOptions = {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver<OAuthResult>;
};
};
// Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -677,11 +708,11 @@ export type WebMessageResponse =
//
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
// src/providers/github/provider.d.ts:71:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// src/providers/github/provider.d.ts:71:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// src/providers/github/provider.d.ts:71:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// src/providers/github/provider.d.ts:71:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name
// src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
// src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
// src/providers/github/provider.d.ts:74:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// src/providers/github/provider.d.ts:74:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// src/providers/github/provider.d.ts:74:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// src/providers/github/provider.d.ts:74:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name
// src/providers/github/provider.d.ts:74:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
// src/providers/github/provider.d.ts:81:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
```
-1
View File
@@ -46,7 +46,6 @@
"express-promise-router": "^4.1.0",
"express-session": "^1.17.1",
"fs-extra": "9.1.0",
"got": "^11.5.2",
"helmet": "^4.0.0",
"jose": "^1.27.1",
"jwt-decode": "^3.1.0",
@@ -57,7 +57,10 @@ describe('OAuthAdapter', () => {
};
}
async refresh() {
return mockResponseData;
return {
response: mockResponseData,
refreshToken: 'token',
};
}
}
const providerInstance = new MyAuthProvider();
@@ -257,7 +260,10 @@ describe('OAuthAdapter', () => {
});
it('correctly populates incomplete identities', async () => {
const mockRefresh = jest.fn<Promise<OAuthResponse>, [express.Request]>();
const mockRefresh = jest.fn<
Promise<{ response: OAuthResponse }>,
[express.Request]
>();
const oauthProvider = new OAuthAdapter(
{
@@ -291,10 +297,12 @@ describe('OAuthAdapter', () => {
// Without a token
mockRefresh.mockResolvedValueOnce({
...mockResponseData,
backstageIdentity: {
id: 'foo',
token: '',
response: {
...mockResponseData,
backstageIdentity: {
id: 'foo',
token: '',
},
},
});
await oauthProvider.refresh(mockRequest, mockResponse);
@@ -315,10 +323,12 @@ describe('OAuthAdapter', () => {
// With a token
mockRefresh.mockResolvedValueOnce({
...mockResponseData,
backstageIdentity: {
id: 'foo',
token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`,
response: {
...mockResponseData,
backstageIdentity: {
id: 'foo',
token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`,
},
},
});
await oauthProvider.refresh(mockRequest, mockResponse);
@@ -212,19 +212,15 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
const forwardReq = Object.assign(req, { scope, refreshToken });
// get new access_token
const response = await this.handlers.refresh(
forwardReq as OAuthRefreshRequest,
);
const { response, refreshToken: newRefreshToken } =
await this.handlers.refresh(forwardReq as OAuthRefreshRequest);
const backstageIdentity = await this.populateIdentity(
response.backstageIdentity,
);
if (
response.providerInfo.refreshToken &&
response.providerInfo.refreshToken !== refreshToken
) {
this.setRefreshTokenCookie(res, response.providerInfo.refreshToken);
if (newRefreshToken && newRefreshToken !== refreshToken) {
this.setRefreshTokenCookie(res, newRefreshToken);
}
res.status(200).json({ ...response, backstageIdentity });
+4 -5
View File
@@ -79,10 +79,6 @@ export type OAuthProviderInfo = {
* Scopes granted for the access token.
*/
scope: string;
/**
* A refresh token issued for the signed in user
*/
refreshToken?: string;
};
export type OAuthState = {
@@ -130,7 +126,10 @@ export interface OAuthHandlers {
* @param {string} refreshToken
* @param {string} scope
*/
refresh?(req: OAuthRefreshRequest): Promise<OAuthResponse>;
refresh?(req: OAuthRefreshRequest): Promise<{
response: OAuthResponse;
refreshToken?: string;
}>;
/**
* (Optional) Sign out of the auth provider.
@@ -78,20 +78,22 @@ describe('createAtlassianProvider', () => {
refreshToken: 'wacka',
},
});
const { response } = await provider.handler({} as any);
expect(response).toEqual({
providerInfo: {
accessToken: 'accessToken',
expiresInSeconds: 123,
idToken: 'idToken',
scope: 'scope',
refreshToken: 'wacka',
},
profile: {
email: 'conrad@example.com',
displayName: 'Conrad',
picture: 'http://google.com/lols',
const result = await provider.handler({} as any);
expect(result).toEqual({
response: {
providerInfo: {
accessToken: 'accessToken',
expiresInSeconds: 123,
idToken: 'idToken',
scope: 'scope',
},
profile: {
email: 'conrad@example.com',
displayName: 'Conrad',
picture: 'http://google.com/lols',
},
},
refreshToken: 'wacka',
});
});
@@ -127,20 +129,22 @@ describe('createAtlassianProvider', () => {
],
});
const response = await provider.refresh({} as any);
const result = await provider.refresh({} as any);
expect(response).toEqual({
profile: {
displayName: 'Mocked User',
email: 'mockuser@gmail.com',
picture: 'http://google.com/lols',
},
providerInfo: {
accessToken: 'a.b.c',
idToken: 'my-id',
refreshToken: 'dont-forget-to-send-refresh',
scope: 'read_user',
expect(result).toEqual({
response: {
profile: {
displayName: 'Mocked User',
email: 'mockuser@gmail.com',
picture: 'http://google.com/lols',
},
providerInfo: {
accessToken: 'a.b.c',
idToken: 'my-id',
scope: 'read_user',
},
},
refreshToken: 'dont-forget-to-send-refresh',
});
});
});
@@ -107,9 +107,7 @@ export class AtlassianAuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result } = await executeFrameHandlerStrategy<OAuthResult>(
req,
this._strategy,
@@ -117,7 +115,7 @@ export class AtlassianAuthProvider implements OAuthHandlers {
return {
response: await this.handleResult(result),
refreshToken: result.refreshToken ?? '',
refreshToken: result.refreshToken,
};
}
@@ -128,7 +126,6 @@ export class AtlassianAuthProvider implements OAuthHandlers {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
refreshToken: result.refreshToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
@@ -152,28 +149,27 @@ export class AtlassianAuthProvider implements OAuthHandlers {
return response;
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const {
accessToken,
params,
refreshToken: newRefreshToken,
} = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, params, refreshToken } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: newRefreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
}
@@ -36,7 +36,15 @@ import {
makeProfileInfo,
PassportDoneCallback,
} from '../../lib/passport';
import { RedirectInfo, AuthProviderFactory } from '../types';
import {
RedirectInfo,
AuthProviderFactory,
AuthHandler,
SignInResolver,
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { TokenIssuer } from '../../identity';
import { Logger } from 'winston';
type PrivateInfo = {
refreshToken: string;
@@ -44,12 +52,27 @@ type PrivateInfo = {
export type Auth0AuthProviderOptions = OAuthProviderOptions & {
domain: string;
signInResolver?: SignInResolver<OAuthResult>;
authHandler: AuthHandler<OAuthResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
};
export class Auth0AuthProvider implements OAuthHandlers {
private readonly _strategy: Auth0Strategy;
private readonly signInResolver?: SignInResolver<OAuthResult>;
private readonly authHandler: AuthHandler<OAuthResult>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: Auth0AuthProviderOptions) {
this.signInResolver = options.signInResolver;
this.authHandler = options.authHandler;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
this._strategy = new Auth0Strategy(
{
clientID: options.clientId,
@@ -90,88 +113,144 @@ export class Auth0AuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
>(req, this._strategy);
const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
return {
response: await this.populateIdentity({
profile,
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
}),
response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
const profile = makeProfileInfo(fullProfile, params.id_token);
return this.populateIdentity({
providerInfo: {
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
},
profile,
});
}),
refreshToken,
};
}
// Use this function to grab the user profile info from the token
// Then populate the profile with it
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
if (!profile.email) {
throw new Error('Profile does not contain an email');
const response: OAuthResponse = {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
profile,
};
if (this.signInResolver) {
response.backstageIdentity = await this.signInResolver(
{
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
);
}
const id = profile.email.split('@')[0];
return { ...response, backstageIdentity: { id, token: '' } };
return response;
}
}
export type Auth0ProviderOptions = {};
const defaultSignInResolver: SignInResolver<OAuthResult> = async info => {
const { profile } = info;
if (!profile.email) {
throw new Error('Profile does not contain an email');
}
const id = profile.email.split('@')[0];
return { id, token: '' };
};
/** @public */
export type Auth0ProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth response
* into the profile that will be presented to the user.
*/
authHandler?: AuthHandler<OAuthResult>;
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
signIn?: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<OAuthResult>;
};
};
/** @public */
export const createAuth0Provider = (
_options?: Auth0ProviderOptions,
options?: Auth0ProviderOptions,
): AuthProviderFactory => {
return ({ providerId, globalConfig, config, tokenIssuer }) =>
return ({
providerId,
globalConfig,
config,
tokenIssuer,
catalogApi,
logger,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const domain = envConfig.getString('domain');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
? options.authHandler
: async ({ fullProfile, params }) => ({
profile: makeProfileInfo(fullProfile, params.id_token),
});
const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver;
const provider = new Auth0AuthProvider({
clientId,
clientSecret,
callbackUrl,
domain,
authHandler,
signInResolver,
tokenIssuer,
catalogIdentityClient,
logger,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
@@ -138,9 +138,7 @@ export class BitbucketAuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -152,22 +150,25 @@ export class BitbucketAuthProvider implements OAuthHandlers {
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: req.refreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: BitbucketOAuthResult) {
@@ -316,24 +316,26 @@ describe('GithubAuthProvider', () => {
],
});
const response = await provider.refresh({} as any);
const result = await provider.refresh({} as any);
expect(response).toEqual({
backstageIdentity: {
id: 'mockuser',
token: 'token-for-mockuser',
},
profile: {
displayName: 'Mocked User',
email: 'mockuser@gmail.com',
picture: undefined,
},
providerInfo: {
accessToken: 'a.b.c',
refreshToken: 'dont-forget-to-send-refresh',
expiresInSeconds: 123,
scope: 'read_user',
expect(result).toEqual({
response: {
backstageIdentity: {
id: 'mockuser',
token: 'token-for-mockuser',
},
profile: {
displayName: 'Mocked User',
email: 'mockuser@gmail.com',
picture: undefined,
},
providerInfo: {
accessToken: 'a.b.c',
expiresInSeconds: 123,
scope: 'read_user',
},
},
refreshToken: 'dont-forget-to-send-refresh',
});
});
});
@@ -129,26 +129,26 @@ export class GithubAuthProvider implements OAuthHandlers {
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const {
accessToken,
refreshToken: newRefreshToken,
params,
} = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: newRefreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: GithubOAuthResult) {
@@ -158,7 +158,6 @@ export class GithubAuthProvider implements OAuthHandlers {
const response: OAuthResponse = {
providerInfo: {
accessToken: result.accessToken,
refreshToken: result.refreshToken, // GitHub expires the old refresh token when used
scope: result.params.scope,
expiresInSeconds:
expiresInStr === undefined ? undefined : Number(expiresInStr),
@@ -184,23 +184,25 @@ describe('GitlabAuthProvider', () => {
],
});
const response = await provider.refresh({} as any);
const result = await provider.refresh({} as any);
expect(response).toEqual({
backstageIdentity: {
id: 'mockuser',
},
profile: {
displayName: 'Mocked User',
email: 'mockuser@gmail.com',
picture: 'http://gitlab.com/lols',
},
providerInfo: {
accessToken: 'a.b.c',
idToken: 'my-id',
refreshToken: 'dont-forget-to-send-refresh',
scope: 'read_user',
expect(result).toEqual({
response: {
backstageIdentity: {
id: 'mockuser',
},
profile: {
displayName: 'Mocked User',
email: 'mockuser@gmail.com',
picture: 'http://gitlab.com/lols',
},
providerInfo: {
accessToken: 'a.b.c',
idToken: 'my-id',
scope: 'read_user',
},
},
refreshToken: 'dont-forget-to-send-refresh',
});
});
});
@@ -132,9 +132,7 @@ export class GitlabAuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -146,28 +144,26 @@ export class GitlabAuthProvider implements OAuthHandlers {
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const {
accessToken,
refreshToken: newRefreshToken,
params,
} = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: newRefreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: OAuthResult): Promise<OAuthResponse> {
@@ -177,7 +173,6 @@ export class GitlabAuthProvider implements OAuthHandlers {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
refreshToken: result.refreshToken, // GitLab expires the old refresh token when used
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
@@ -113,9 +113,7 @@ export class GoogleAuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -127,22 +125,26 @@ export class GoogleAuthProvider implements OAuthHandlers {
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: req.refreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: OAuthResult) {
+5 -3
View File
@@ -14,6 +14,10 @@
* limitations under the License.
*/
export * from './atlassian';
export * from './auth0';
export * from './aws-alb';
export * from './bitbucket';
export * from './github';
export * from './gitlab';
export * from './google';
@@ -21,9 +25,7 @@ export * from './microsoft';
export * from './oauth2';
export * from './oidc';
export * from './okta';
export * from './bitbucket';
export * from './atlassian';
export * from './aws-alb';
export * from './onelogin';
export * from './saml';
export { factories as defaultAuthProviderFactories } from './factories';
@@ -20,6 +20,9 @@ import { OAuthResult } from '../../lib/oauth';
import { getVoidLogger } from '@backstage/backend-common';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const mockFrameHandler = jest.spyOn(
helpers,
@@ -28,8 +31,62 @@ const mockFrameHandler = jest.spyOn(
() => Promise<{ result: OAuthResult; privateInfo: any }>
>;
const mockResult = {
result: {
fullProfile: {
emails: [
{
type: 'work',
value: 'conrad@example.com',
},
],
displayName: 'Conrad',
name: {
familyName: 'Ribas',
givenName: 'Francisco',
},
id: 'conrad',
provider: 'microsoft',
photos: [
{
value: 'some-data',
},
],
},
params: {
id_token: 'idToken',
scope: 'scope',
expires_in: 123,
},
accessToken: 'accessToken',
},
privateInfo: {
refreshToken: 'wacka',
},
};
const server = setupServer();
setupRequestMockHandlers(server);
const setupHandlers = () => {
server.use(
rest.get(
'https://graph.microsoft.com/v1.0/me/photos/*',
async (_, res, ctx) => {
const imageBuffer = new Uint8Array([104, 111, 119, 100, 121]).buffer;
return res(
ctx.set('Content-Length', imageBuffer.byteLength.toString()),
ctx.set('Content-Type', 'image/jpeg'),
ctx.body(imageBuffer),
);
},
),
);
};
describe('createMicrosoftProvider', () => {
it('should auth', async () => {
setupHandlers();
const tokenIssuer = {
issueToken: jest.fn(),
listPublicKeys: jest.fn(),
@@ -55,39 +112,7 @@ describe('createMicrosoftProvider', () => {
callbackUrl: 'mock',
});
mockFrameHandler.mockResolvedValueOnce({
result: {
fullProfile: {
emails: [
{
type: 'work',
value: 'conrad@example.com',
},
],
displayName: 'Conrad',
name: {
familyName: 'Ribas',
givenName: 'Francisco',
},
id: 'conrad',
provider: 'microsoft',
photos: [
{
value: 'some-data',
},
],
},
params: {
id_token: 'idToken',
scope: 'scope',
expires_in: 123,
},
accessToken: 'accessToken',
},
privateInfo: {
refreshToken: 'wacka',
},
});
mockFrameHandler.mockResolvedValueOnce(mockResult);
const { response } = await provider.handler({} as any);
expect(response).toEqual({
providerInfo: {
@@ -103,4 +128,45 @@ describe('createMicrosoftProvider', () => {
},
});
});
it('should return the base64 encoded photo data of the profile', async () => {
setupHandlers();
const tokenIssuer = {
issueToken: jest.fn(),
listPublicKeys: jest.fn(),
};
const catalogIdentityClient = {
findUser: jest.fn(),
};
const provider = new MicrosoftAuthProvider({
logger: getVoidLogger(),
catalogIdentityClient:
catalogIdentityClient as unknown as CatalogIdentityClient,
tokenIssuer: tokenIssuer as unknown as TokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: {
email: fullProfile.emails![0]!.value,
displayName: fullProfile.displayName,
picture: 'http://microsoft.com/lols',
},
}),
clientId: 'mock',
clientSecret: 'mock',
callbackUrl: 'mock',
// define resolver to return user `info` for photo validation
signInResolver: async (info, _) => {
return {
id: 'user.name',
token: 'token',
info: info,
};
},
});
mockFrameHandler.mockResolvedValueOnce(mockResult);
const { response } = await provider.handler({} as any);
const overloadedIdentity = response.backstageIdentity as any;
const photo = overloadedIdentity.info.result.fullProfile.photos[0];
expect(photo.value).toEqual('data:image/jpeg;base64,aG93ZHk=');
});
});
@@ -45,7 +45,7 @@ import {
SignInResolver,
} from '../types';
import { Logger } from 'winston';
import got from 'got';
import fetch from 'node-fetch';
type PrivateInfo = {
refreshToken: string;
@@ -104,9 +104,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -118,24 +116,27 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: req.refreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: OAuthResult) {
@@ -173,19 +174,17 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
private getUserPhoto(accessToken: string): Promise<string | undefined> {
return new Promise(resolve => {
got
.get('https://graph.microsoft.com/v1.0/me/photos/48x48/$value', {
encoding: 'binary',
responseType: 'buffer',
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.then(photoData => {
const photoURL = `data:image/jpeg;base64,${Buffer.from(
photoData.body,
fetch('https://graph.microsoft.com/v1.0/me/photos/48x48/$value', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.then(response => response.arrayBuffer())
.then(arrayBuffer => {
const imageUrl = `data:image/jpeg;base64,${Buffer.from(
arrayBuffer,
).toString('base64')}`;
resolve(photoURL);
resolve(imageUrl);
})
.catch(error => {
this.logger.warn(
@@ -127,9 +127,7 @@ export class OAuth2AuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -141,29 +139,27 @@ export class OAuth2AuthProvider implements OAuthHandlers {
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
async refresh(req: OAuthRefreshRequest) {
const refreshTokenResponse = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const {
accessToken,
params,
refreshToken: updatedRefreshToken,
} = refreshTokenResponse;
const { accessToken, params, refreshToken } = refreshTokenResponse;
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: updatedRefreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: OAuthResult) {
@@ -175,7 +171,6 @@ export class OAuth2AuthProvider implements OAuthHandlers {
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
refreshToken: result.refreshToken,
},
profile,
};
@@ -112,34 +112,31 @@ export class OidcAuthProvider implements OAuthHandlers {
return await executeRedirectStrategy(req, strategy, options);
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken?: string }> {
async handler(req: express.Request) {
const { strategy } = await this.implementation;
const strategyResponse = await executeFrameHandlerStrategy<
const { result, privateInfo } = await executeFrameHandlerStrategy<
OidcAuthResult,
PrivateInfo
>(req, strategy);
const {
result: { userinfo, tokenset },
privateInfo,
} = strategyResponse;
const identityResponse = await this.handleResult({ tokenset, userinfo });
return {
response: identityResponse,
response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
async refresh(req: OAuthRefreshRequest) {
const { client } = await this.implementation;
const tokenset = await client.refresh(req.refreshToken);
if (!tokenset.access_token) {
throw new Error('Refresh failed');
}
const profile = await client.userinfo(tokenset.access_token);
return this.handleResult({ tokenset, userinfo: profile });
const userinfo = await client.userinfo(tokenset.access_token);
return {
response: await this.handleResult({ tokenset, userinfo }),
refreshToken: tokenset.refresh_token,
};
}
private async setupStrategy(options: Options): Promise<OidcImpl> {
@@ -190,7 +187,6 @@ export class OidcAuthProvider implements OAuthHandlers {
providerInfo: {
idToken: result.tokenset.id_token,
accessToken: result.tokenset.access_token!,
refreshToken: result.tokenset.refresh_token,
scope: result.tokenset.scope!,
expiresInSeconds: result.tokenset.expires_in,
},
@@ -133,9 +133,7 @@ export class OktaAuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -147,24 +145,27 @@ export class OktaAuthProvider implements OAuthHandlers {
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: req.refreshToken,
});
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: OAuthResult) {
@@ -36,7 +36,15 @@ import {
executeFetchUserProfileStrategy,
PassportDoneCallback,
} from '../../lib/passport';
import { RedirectInfo, AuthProviderFactory } from '../types';
import {
RedirectInfo,
AuthProviderFactory,
AuthHandler,
SignInResolver,
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
type PrivateInfo = {
refreshToken: string;
@@ -44,12 +52,27 @@ type PrivateInfo = {
export type Options = OAuthProviderOptions & {
issuer: string;
signInResolver?: SignInResolver<OAuthResult>;
authHandler: AuthHandler<OAuthResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
};
export class OneLoginProvider implements OAuthHandlers {
private readonly _strategy: any;
private readonly signInResolver?: SignInResolver<OAuthResult>;
private readonly authHandler: AuthHandler<OAuthResult>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: Options) {
this.signInResolver = options.signInResolver;
this.authHandler = options.authHandler;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
this._strategy = new OneLoginStrategy(
{
issuer: options.issuer,
@@ -89,86 +112,144 @@ export class OneLoginProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
>(req, this._strategy);
const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
return {
response: await this.populateIdentity({
profile,
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
}),
response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
const profile = makeProfileInfo(fullProfile, params.id_token);
return this.populateIdentity({
providerInfo: {
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
},
profile,
});
}),
refreshToken,
};
}
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
if (!profile.email) {
throw new Error('OIDC profile contained no email');
const response: OAuthResponse = {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
profile,
};
if (this.signInResolver) {
response.backstageIdentity = await this.signInResolver(
{
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
);
}
const id = profile.email.split('@')[0];
return { ...response, backstageIdentity: { id, token: '' } };
return response;
}
}
export type OneLoginProviderOptions = {};
const defaultSignInResolver: SignInResolver<OAuthResult> = async info => {
const { profile } = info;
if (!profile.email) {
throw new Error('OIDC profile contained no email');
}
const id = profile.email.split('@')[0];
return { id, token: '' };
};
/** @public */
export type OneLoginProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth response
* into the profile that will be presented to the user.
*/
authHandler?: AuthHandler<OAuthResult>;
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
signIn?: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<OAuthResult>;
};
};
/** @public */
export const createOneLoginProvider = (
_options?: OneLoginProviderOptions,
options?: OneLoginProviderOptions,
): AuthProviderFactory => {
return ({ providerId, globalConfig, config, tokenIssuer }) =>
return ({
providerId,
globalConfig,
config,
tokenIssuer,
catalogApi,
logger,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const issuer = envConfig.getString('issuer');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
? options.authHandler
: async ({ fullProfile, params }) => ({
profile: makeProfileInfo(fullProfile, params.id_token),
});
const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver;
const provider = new OneLoginProvider({
clientId,
clientSecret,
callbackUrl,
issuer,
authHandler,
signInResolver,
tokenIssuer,
catalogIdentityClient,
logger,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
+9 -1
View File
@@ -68,7 +68,15 @@ export async function createRouter(
if (secret) {
router.use(cookieParser(secret));
// TODO: Configure the server-side session storage. The default MemoryStore is not designed for production
router.use(session({ secret, saveUninitialized: false, resave: false }));
const enforceCookieSSL = authUrl.startsWith('https');
router.use(
session({
secret,
saveUninitialized: false,
resave: false,
cookie: { secure: enforceCookieSSL },
}),
);
router.use(passport.initialize());
router.use(passport.session());
} else {
@@ -122,26 +122,26 @@ export const UserListPicker = ({
const classes = useStyles();
const configApi = useApi(configApiRef);
const orgName = configApi.getOptionalString('organization.name') ?? 'Company';
const { filters, updateFilters, backendEntities, queryParameters } =
useEntityListProvider();
// Remove group items that aren't in availableFilters and exclude
// any now-empty groups.
const userAndGroupFilterIds = ['starred', 'all'];
const filterGroups = getFilterGroups(orgName)
.map(filterGroup => ({
...filterGroup,
items: filterGroup.items.filter(
({ id }) => !availableFilters || availableFilters.includes(id),
items: filterGroup.items.filter(({ id }) =>
// TODO: avoid hardcoding kinds here
['group', 'user'].some(kind => kind === queryParameters.kind)
? userAndGroupFilterIds.includes(id)
: !availableFilters || availableFilters.includes(id),
),
}))
.filter(({ items }) => !!items.length);
const { filters, updateFilters, backendEntities, queryParameters } =
useEntityListProvider();
const { isStarredEntity } = useStarredEntities();
const { isOwnedEntity } = useEntityOwnership();
const [selectedUserFilter, setSelectedUserFilter] = useState(
[queryParameters.user].flat()[0] ?? initialFilter,
);
// Static filters; used for generating counts of potentially unselected kinds
const ownedFilter = useMemo(
@@ -153,6 +153,19 @@ export const UserListPicker = ({
[isOwnedEntity, isStarredEntity],
);
// To show proper counts for each section, apply all other frontend filters _except_ the user
// filter that's controlled by this picker.
const [entitiesWithoutUserFilter, setEntitiesWithoutUserFilter] =
useState(backendEntities);
const totalOwnedUserEntities = entitiesWithoutUserFilter.filter(entity =>
ownedFilter.filterEntity(entity),
).length;
const [selectedUserFilter, setSelectedUserFilter] = useState(
totalOwnedUserEntities > 0
? [queryParameters.user].flat()[0] ?? initialFilter
: 'all',
);
useEffect(() => {
updateFilters({
user: selectedUserFilter
@@ -165,10 +178,6 @@ export const UserListPicker = ({
});
}, [selectedUserFilter, isOwnedEntity, isStarredEntity, updateFilters]);
// To show proper counts for each section, apply all other frontend filters _except_ the user
// filter that's controlled by this picker.
const [entitiesWithoutUserFilter, setEntitiesWithoutUserFilter] =
useState(backendEntities);
useEffect(() => {
const filterFn = reduceEntityFilters(
compact(Object.values({ ...filters, user: undefined })),
@@ -179,9 +188,7 @@ export const UserListPicker = ({
function getFilterCount(id: UserListFilterKind) {
switch (id) {
case 'owned':
return entitiesWithoutUserFilter.filter(entity =>
ownedFilter.filterEntity(entity),
).length;
return totalOwnedUserEntities;
case 'starred':
return entitiesWithoutUserFilter.filter(entity =>
starredFilter.filterEntity(entity),
@@ -137,12 +137,27 @@ describe('<EntityListProvider />', () => {
const { result, waitFor } = renderHook(() => useEntityListProvider(), {
wrapper,
initialProps: {
userFilter: 'owned',
userFilter: 'all',
},
});
await waitFor(() => !!result.current.entities.length);
expect(result.current.backendEntities.length).toBe(2);
expect(result.current.entities.length).toBe(1);
act(() =>
result.current.updateFilters({
user: new UserListFilter(
'owned',
entity => entity.metadata.name === 'component-1',
() => true,
),
}),
);
await waitFor(() => {
expect(result.current.backendEntities.length).toBe(2);
expect(result.current.entities.length).toBe(1);
expect(mockCatalogApi.getEntities).toHaveBeenCalledTimes(1);
});
});
it('resolves query param filter values', async () => {
@@ -189,12 +189,14 @@ describe('CatalogPage', () => {
c => c.tagName === 'SPAN',
);
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']);
}, 20_000);
it('should render the default actions of an item in the grid', async () => {
const { findByTitle, findByText } = await renderWrapped(<CatalogPage />);
const { getByTestId, findByTitle, findByText } = await renderWrapped(
<CatalogPage />,
);
fireEvent.click(getByTestId('user-picker-owned'));
expect(await findByText(/Owned \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/View/)).toBeInTheDocument();
expect(await findByTitle(/Edit/)).toBeInTheDocument();
@@ -221,9 +223,10 @@ describe('CatalogPage', () => {
},
];
const { findByTitle, findByText } = await renderWrapped(
const { getByTestId, findByTitle, findByText } = await renderWrapped(
<CatalogPage actions={actions} />,
);
fireEvent.click(getByTestId('user-picker-owned'));
expect(await findByText(/Owned \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/Foo Action/)).toBeInTheDocument();
expect(await findByTitle(/Bar Action/)).toBeInTheDocument();
@@ -235,6 +238,7 @@ describe('CatalogPage', () => {
// https://github.com/mbrn/material-table/issues/1293
it('should render', async () => {
const { findByText, getByTestId } = await renderWrapped(<CatalogPage />);
fireEvent.click(getByTestId('user-picker-owned'));
await expect(findByText(/Owned \(1\)/)).resolves.toBeInTheDocument();
fireEvent.click(getByTestId('user-picker-all'));
await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument();
@@ -250,7 +254,8 @@ describe('CatalogPage', () => {
// this test is for fixing the bug after favoriting an entity, the matching
// entities defaulting to "owned" filter and not based on the selected filter
it('should render the correct entities filtered on the selected filter', async () => {
await renderWrapped(<CatalogPage />);
const { getByTestId } = await renderWrapped(<CatalogPage />);
fireEvent.click(getByTestId('user-picker-owned'));
await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument();
fireEvent.click(screen.getByTestId('user-picker-starred'));
await expect(
@@ -15,13 +15,12 @@
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { ProductInsights } from './ProductInsights';
import { ProductInsightsOptions } from '../../api';
import { costInsightsApiRef, ProductInsightsOptions } from '../../api';
import {
mockDefaultLoadingState,
MockConfigProvider,
MockCostInsightsApiProvider,
MockCurrencyProvider,
MockFilterProvider,
MockBillingDateProvider,
@@ -139,7 +138,7 @@ const costInsightsApi = {
function renderInContext(children: JSX.Element) {
return renderInTestApp(
<MockCostInsightsApiProvider costInsightsApi={costInsightsApi}>
<TestApiProvider apis={[[costInsightsApiRef, costInsightsApi]]}>
<MockConfigProvider>
<MockFilterProvider>
<MockCurrencyProvider>
@@ -151,7 +150,7 @@ function renderInContext(children: JSX.Element) {
</MockCurrencyProvider>
</MockFilterProvider>
</MockConfigProvider>
</MockCostInsightsApiProvider>,
</TestApiProvider>,
);
}
@@ -15,15 +15,14 @@
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { ProductInsightsCard } from './ProductInsightsCard';
import { CostInsightsApi } from '../../api';
import { CostInsightsApi, costInsightsApiRef } from '../../api';
import {
createMockEntity,
mockDefaultLoadingState,
MockComputeEngine,
MockConfigProvider,
MockCostInsightsApiProvider,
MockCurrencyProvider,
MockBillingDateProvider,
MockScrollProvider,
@@ -55,7 +54,7 @@ const renderProductInsightsCardInTestApp = async (
onSelectAsync = jest.fn(() => Promise.resolve(mockProductCost)),
) =>
await renderInTestApp(
<MockCostInsightsApiProvider costInsightsApi={costInsightsApi(entity)}>
<TestApiProvider apis={[[costInsightsApiRef, costInsightsApi(entity)]]}>
<MockConfigProvider>
<MockCurrencyProvider>
<MockLoadingProvider state={mockDefaultLoadingState}>
@@ -71,7 +70,7 @@ const renderProductInsightsCardInTestApp = async (
</MockLoadingProvider>
</MockCurrencyProvider>
</MockConfigProvider>
</MockCostInsightsApiProvider>,
</TestApiProvider>,
);
describe('<ProductInsightsCard/>', () => {
@@ -15,7 +15,6 @@
*/
import React, { PropsWithChildren } from 'react';
import { costInsightsApiRef, CostInsightsApi } from '../api';
import { LoadingContext, LoadingContextProps } from '../hooks/useLoading';
import { GroupsContext, GroupsContextProps } from '../hooks/useGroups';
import { FilterContext, FilterContextProps } from '../hooks/useFilters';
@@ -28,12 +27,6 @@ import {
import { ScrollContext, ScrollContextProps } from '../hooks/useScroll';
import { Group, Duration } from '../types';
// TODO(Rugvip): Could be good to have a clear place to put test utils that is linted accordingly
// eslint-disable-next-line import/no-extraneous-dependencies
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
// eslint-disable-next-line import/no-extraneous-dependencies
import { TestApiProvider } from '@backstage/test-utils';
type PartialPropsWithChildren<T> = PropsWithChildren<Partial<T>>;
export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }];
@@ -172,48 +165,3 @@ export const MockGroupsProvider = ({
</GroupsContext.Provider>
);
};
export type MockCostInsightsApiProviderProps = PartialPropsWithChildren<{
identityApi: Partial<IdentityApi>;
costInsightsApi: Partial<CostInsightsApi>;
}>;
export const MockCostInsightsApiProvider = ({
children,
...context
}: MockCostInsightsApiProviderProps) => {
const defaultIdentityApi: IdentityApi = {
getProfile: jest.fn(),
getIdToken: jest.fn(),
getUserId: jest.fn(),
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
};
const defaultCostInsightsApi: CostInsightsApi = {
getAlerts: jest.fn(),
getDailyMetricData: jest.fn(),
getGroupDailyCost: jest.fn(),
getGroupProjects: jest.fn(),
getLastCompleteBillingDate: jest.fn(),
getProductInsights: jest.fn(),
getProjectDailyCost: jest.fn(),
getUserGroups: jest.fn(),
};
return (
<TestApiProvider
apis={[
[identityApiRef, { ...defaultIdentityApi, ...context.identityApi }],
[
costInsightsApiRef,
{ ...defaultCostInsightsApi, ...context.costInsightsApi },
],
]}
>
{children}
</TestApiProvider>
);
};
+3 -1
View File
@@ -34,7 +34,6 @@
"@backstage/backend-common": "^0.10.0",
"@backstage/config": "^0.1.10",
"@types/express": "^4.17.6",
"axios": "^0.24.0",
"camelcase-keys": "^6.2.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
@@ -44,12 +43,15 @@
"helmet": "^4.0.0",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
"node-fetch": "^2.6.1",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/test-utils": "^0.2.0",
"@types/supertest": "^2.0.8",
"msw": "^0.36.3",
"supertest": "^6.1.3"
},
"files": [
@@ -14,7 +14,12 @@
* limitations under the License.
*/
import { getRequestHeaders } from './RollbarApi';
import { getRequestHeaders, RollbarApi } from './RollbarApi';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { getVoidLogger } from '@backstage/backend-common';
import { RollbarProject } from './types';
describe('RollbarApi', () => {
describe('getRequestHeaders', () => {
@@ -26,4 +31,31 @@ describe('RollbarApi', () => {
});
});
});
describe('getAllProjects', () => {
const server = setupServer();
setupRequestMockHandlers(server);
const mockBaseUrl = 'https://api.rollbar.com/api/1';
const mockProjects: RollbarProject[] = [
{ id: 123, name: 'abc', accountId: 1, status: 'enabled' },
{ id: 456, name: 'xyz', accountId: 1, status: 'enabled' },
];
const setupHandlers = () => {
server.use(
rest.get(`${mockBaseUrl}/projects`, (_, res, ctx) => {
return res(ctx.json({ result: mockProjects }));
}),
);
};
it('should return all projects with a name attribute', async () => {
setupHandlers();
const api = new RollbarApi('my-access-token', getVoidLogger());
const projects = await api.getAllProjects();
expect(projects).toEqual(mockProjects);
});
});
});
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import axios from 'axios';
import { Logger } from 'winston';
import camelcaseKeys from 'camelcase-keys';
import { buildQuery } from '../util';
@@ -25,6 +24,7 @@ import {
RollbarProjectAccessToken,
RollbarTopActiveItem,
} from './types';
import fetch from 'node-fetch';
const baseUrl = 'https://api.rollbar.com/api/1';
@@ -110,11 +110,12 @@ export class RollbarApi {
this.logger.info(`Calling Rollbar REST API, ${fullUrl}`);
}
return axios
.get(fullUrl, getRequestHeaders(accessToken || this.accessToken || ''))
.then(response =>
camelcaseKeys<T>(response?.data?.result, { deep: true }),
);
return fetch(
fullUrl,
getRequestHeaders(accessToken || this.accessToken || ''),
)
.then(response => response.json())
.then(json => camelcaseKeys<T>(json?.result, { deep: true }));
}
private async getForProject<T>(
+8 -48
View File
@@ -25,7 +25,11 @@ import { Schema } from 'jsonschema';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { ScmIntegrations } from '@backstage/integration';
import { SpawnOptionsWithoutStdio } from 'child_process';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common';
import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateEntityV1beta2 } from '@backstage/catalog-model';
import { TemplateMetadata } from '@backstage/plugin-scaffolder-common';
import { UrlReader } from '@backstage/backend-common';
import { Writable } from 'stream';
@@ -432,52 +436,11 @@ export type TaskSecrets = {
token: string | undefined;
};
// @public
export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3;
export { TaskSpec };
// @public
export interface TaskSpecV1beta2 {
// (undocumented)
apiVersion: 'backstage.io/v1beta2';
// (undocumented)
baseUrl?: string;
// (undocumented)
metadata?: TemplateMetadata;
// (undocumented)
output: {
[name: string]: string;
};
// (undocumented)
steps: Array<{
id: string;
name: string;
action: string;
input?: JsonObject;
if?: string | boolean;
}>;
// (undocumented)
values: JsonObject;
}
export { TaskSpecV1beta2 };
// @public
export interface TaskSpecV1beta3 {
// (undocumented)
apiVersion: 'scaffolder.backstage.io/v1beta3';
// (undocumented)
baseUrl?: string;
// (undocumented)
metadata?: TemplateMetadata;
// (undocumented)
output: {
[name: string]: JsonValue;
};
// (undocumented)
parameters: JsonObject;
// Warning: (ae-forgotten-export) The symbol "TaskStep" needs to be exported by the entry point index.d.ts
//
// (undocumented)
steps: TaskStep[];
}
export { TaskSpecV1beta3 };
// @public
export interface TaskState {
@@ -573,8 +536,5 @@ export class TemplateActionRegistry {
): void;
}
// @public
export type TemplateMetadata = {
name: string;
};
export { TemplateMetadata };
```
@@ -15,6 +15,21 @@
*/
import { JsonValue, JsonObject } from '@backstage/types';
import {
TaskSpec,
TaskStep,
TemplateMetadata,
TaskSpecV1beta2,
TaskSpecV1beta3,
} from '@backstage/plugin-scaffolder-common';
export type {
TaskSpec,
TaskStep,
TemplateMetadata,
TaskSpecV1beta2,
TaskSpecV1beta3,
};
/**
* Status
@@ -69,64 +84,6 @@ export type SerializedTaskEvent = {
createdAt: string;
};
/**
* TemplateMetadata
*
* @public
*/
export type TemplateMetadata = {
name: string;
};
/**
* TaskSpecV1beta2
*
* @public
*/
export interface TaskSpecV1beta2 {
apiVersion: 'backstage.io/v1beta2';
baseUrl?: string;
values: JsonObject;
steps: Array<{
id: string;
name: string;
action: string;
input?: JsonObject;
if?: string | boolean;
}>;
output: { [name: string]: string };
metadata?: TemplateMetadata;
}
export interface TaskStep {
id: string;
name: string;
action: string;
input?: JsonObject;
if?: string | boolean;
}
/**
* TaskSpecV1beta3
*
* @public
*/
export interface TaskSpecV1beta3 {
apiVersion: 'scaffolder.backstage.io/v1beta3';
baseUrl?: string;
parameters: JsonObject;
steps: TaskStep[];
output: { [name: string]: JsonValue };
metadata?: TemplateMetadata;
}
/**
* TaskSpec
*
* @public
*/
export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3;
/**
* TaskSecrets
*
+59
View File
@@ -6,6 +6,60 @@
import { Entity } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/types';
import { JSONSchema } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
// @public
export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3;
// @public
export interface TaskSpecV1beta2 {
// (undocumented)
apiVersion: 'backstage.io/v1beta2';
// (undocumented)
baseUrl?: string;
// (undocumented)
metadata?: TemplateMetadata;
// (undocumented)
output: {
[name: string]: string;
};
// (undocumented)
steps: TaskStep[];
// (undocumented)
values: JsonObject;
}
// @public
export interface TaskSpecV1beta3 {
// (undocumented)
apiVersion: 'scaffolder.backstage.io/v1beta3';
// (undocumented)
baseUrl?: string;
// (undocumented)
metadata?: TemplateMetadata;
// (undocumented)
output: {
[name: string]: JsonValue;
};
// (undocumented)
parameters: JsonObject;
// (undocumented)
steps: TaskStep[];
}
// @public
export interface TaskStep {
// (undocumented)
action: string;
// (undocumented)
id: string;
// (undocumented)
if?: string | boolean;
// (undocumented)
input?: JsonObject;
// (undocumented)
name: string;
}
// @public (undocumented)
export interface TemplateEntityV1beta3 extends Entity {
@@ -33,4 +87,9 @@ export interface TemplateEntityV1beta3 extends Entity {
// @public (undocumented)
export const templateEntityV1beta3Schema: JSONSchema;
// @public
export type TemplateMetadata = {
name: string;
};
```
+74
View File
@@ -0,0 +1,74 @@
/*
* Copyright 2021 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 { JsonValue, JsonObject } from '@backstage/types';
/**
* TemplateMetadata
*
* @public
*/
export type TemplateMetadata = {
name: string;
};
/**
* TaskStep
*
* @public
*/
export interface TaskStep {
id: string;
name: string;
action: string;
input?: JsonObject;
if?: string | boolean;
}
/**
* TaskSpecV1beta2
*
* @public
*/
export interface TaskSpecV1beta2 {
apiVersion: 'backstage.io/v1beta2';
baseUrl?: string;
values: JsonObject;
steps: TaskStep[];
output: { [name: string]: string };
metadata?: TemplateMetadata;
}
/**
* TaskSpecV1beta3
*
* @public
*/
export interface TaskSpecV1beta3 {
apiVersion: 'scaffolder.backstage.io/v1beta3';
baseUrl?: string;
parameters: JsonObject;
steps: TaskStep[];
output: { [name: string]: JsonValue };
metadata?: TemplateMetadata;
}
/**
* TaskSpec
*
* @public
*/
export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3;
+2
View File
@@ -30,3 +30,5 @@ export const templateEntityV1beta3Schema: JSONSchema = v1beta3Schema as Omit<
JSONSchema,
'examples'
>;
export * from './TaskSpec';
+1 -1
View File
@@ -21,12 +21,12 @@ import { IconButton } from '@material-ui/core';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { JSONSchema } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
import { Observable } from '@backstage/types';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { TemplateEntityV1beta2 } from '@backstage/catalog-model';
// Warning: (ae-missing-release-tag) "createScaffolderFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+1
View File
@@ -40,6 +40,7 @@
"@backstage/integration": "^0.6.10",
"@backstage/integration-react": "^0.1.16",
"@backstage/plugin-catalog-react": "^0.6.8",
"@backstage/plugin-scaffolder-common": "^0.1.1",
"@backstage/theme": "^0.2.14",
"@backstage/types": "^0.1.1",
"@material-ui/core": "^4.12.2",
@@ -22,8 +22,10 @@ import {
Page,
LogViewer,
} from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import {
Button,
CircularProgress,
Paper,
StepButton,
@@ -40,9 +42,11 @@ import Check from '@material-ui/icons/Check';
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
import classNames from 'classnames';
import { DateTime, Interval } from 'luxon';
import qs from 'qs';
import React, { memo, useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router';
import { generatePath, useNavigate, useParams } from 'react-router';
import { useInterval } from 'react-use';
import { rootRouteRef } from '../../routes';
import { Status, TaskOutput } from '../../types';
import { useTaskEventStream } from '../hooks/useEventStream';
import { TaskPageLinks } from './TaskPageLinks';
@@ -56,8 +60,8 @@ const useStyles = makeStyles((theme: Theme) =>
width: '100%',
},
button: {
marginTop: theme.spacing(1),
marginRight: theme.spacing(1),
marginBottom: theme.spacing(2),
marginLeft: theme.spacing(2),
},
actionsContainer: {
marginBottom: theme.spacing(2),
@@ -215,6 +219,9 @@ const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean =>
!!(entityRef || remoteUrl || links.length > 0);
export const TaskPage = () => {
const classes = useStyles();
const navigate = useNavigate();
const rootLink = useRouteRef(rootRouteRef);
const [userSelectedStepId, setUserSelectedStepId] = useState<
string | undefined
>(undefined);
@@ -266,6 +273,26 @@ export const TaskPage = () => {
const { output } = taskStream;
const handleStartOver = () => {
if (!taskStream.task || !taskStream.task?.spec.metadata?.name) {
navigate(generatePath(rootLink()));
}
const formData =
taskStream.task!.spec.apiVersion === 'backstage.io/v1beta2'
? taskStream.task!.spec.values
: taskStream.task!.spec.parameters;
navigate(
generatePath(
`${rootLink()}/templates/:templateName?${qs.stringify({ formData })}`,
{
templateName: taskStream.task!.spec.metadata!.name,
},
),
);
};
return (
<Page themeId="home">
<Header
@@ -297,6 +324,15 @@ export const TaskPage = () => {
{output && hasLinks(output) && (
<TaskPageLinks output={output} />
)}
<Button
className={classes.button}
onClick={handleStartOver}
disabled={!completed}
variant="contained"
color="primary"
>
Start Over
</Button>
</Paper>
</Grid>
<Grid item xs={9}>
@@ -16,6 +16,7 @@
import { JsonObject, JsonValue } from '@backstage/types';
import { LinearProgress } from '@material-ui/core';
import { FormValidation, IChangeEvent } from '@rjsf/core';
import qs from 'qs';
import React, { useCallback, useState } from 'react';
import { generatePath, Navigate, useNavigate } from 'react-router';
import { useParams } from 'react-router-dom';
@@ -120,7 +121,13 @@ export const TemplatePage = ({
const navigate = useNavigate();
const rootLink = useRouteRef(rootRouteRef);
const { schema, loading, error } = useTemplateParameterSchema(templateName);
const [formState, setFormState] = useState({});
const [formState, setFormState] = useState<Record<string, any>>(() => {
const query = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
});
return query.formData ?? {};
});
const handleFormReset = () => setFormState({});
const handleChange = useCallback(
(e: IChangeEvent) => setFormState(e.formData),
@@ -129,6 +136,18 @@ export const TemplatePage = ({
const handleCreate = async () => {
const id = await scaffolderApi.scaffold(templateName, formState);
const formParams = qs.stringify(
{ formData: formState },
{ addQueryPrefix: true },
);
const newUrl = `${window.location.pathname}${formParams}`;
// We use direct history manipulation since useSearchParams and
// useNavigate in react-router-dom cause unnecessary extra rerenders.
// Also make sure to replace the state rather than pushing to avoid
// extra back/forward slots.
window.history?.replaceState(null, document.title, newUrl);
navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId: id }));
};
+2 -11
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JSONSchema } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
export type Status = 'open' | 'processing' | 'failed' | 'completed' | 'skipped';
export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
@@ -39,18 +39,9 @@ export type Stage = {
endedAt?: string;
};
export type ScaffolderStep = {
id: string;
name: string;
action: string;
parameters?: { [name: string]: JsonValue };
};
export type ScaffolderTask = {
id: string;
spec: {
steps: ScaffolderStep[];
};
spec: TaskSpec;
status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled';
lastHeartbeatAt: string;
createdAt: string;
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import React from 'react';
import { Button } from '@backstage/core-components';
import { Grid } from '@material-ui/core';
import FindInPageIcon from '@material-ui/icons/FindInPage';
import GroupIcon from '@material-ui/icons/Group';
import { Button } from '@backstage/core-components';
import { DefaultResultListItem } from '../index';
import React from 'react';
import { MemoryRouter } from 'react-router';
import { DefaultResultListItem } from './DefaultResultListItem';
export default {
title: 'Plugins/Search/DefaultResultListItem',
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Grid } from '@material-ui/core';
import React, { useEffect, useState } from 'react';
import { useDebounce } from 'react-use';
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { Grid, makeStyles, Paper } from '@material-ui/core';
import React, { ComponentType } from 'react';
import { Paper, Grid, makeStyles } from '@material-ui/core';
import { SearchBar } from '../index';
import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchBar } from './SearchBar';
export default {
title: 'Plugins/Search/SearchBar',
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import React, { ComponentType } from 'react';
import { Grid, Paper } from '@material-ui/core';
import { SearchFilter } from '../index';
import React, { ComponentType } from 'react';
import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchFilter } from './SearchFilter';
export default {
title: 'Plugins/Search/SearchFilter',
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import React, { ComponentType } from 'react';
import { Button } from '@material-ui/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { SearchModal } from '../index';
import { useSearch } from '../SearchContext';
import { Button } from '@material-ui/core';
import React, { ComponentType } from 'react';
import { rootRouteRef } from '../../plugin';
import { useSearch } from '../SearchContext';
import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchModal } from './SearchModal';
const mockResults = {
results: [
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { screen } from '@testing-library/react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
Dialog,
@@ -18,7 +18,7 @@ import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { useLocation, useOutlet } from 'react-router';
import { useSearch } from '../SearchContext';
import { SearchPage } from './';
import { SearchPage } from './SearchPage';
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
@@ -14,12 +14,13 @@
* limitations under the License.
*/
import React, { ComponentType } from 'react';
import { List, ListItem } from '@material-ui/core';
import { SearchResult, DefaultResultListItem } from '../index';
import { MemoryRouter } from 'react-router';
import { Link } from '@backstage/core-components';
import { List, ListItem } from '@material-ui/core';
import React, { ComponentType } from 'react';
import { MemoryRouter } from 'react-router';
import { DefaultResultListItem } from '../DefaultResultListItem';
import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchResult } from './SearchResult';
const mockResults = {
results: [
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { ComponentType } from 'react';
import { Grid, Paper } from '@material-ui/core';
import CatalogIcon from '@material-ui/icons/MenuBook';
import DocsIcon from '@material-ui/icons/Description';
import UsersGroupsIcon from '@material-ui/icons/Person';
import { SearchType } from '../index';
import React, { ComponentType } from 'react';
import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchType } from './SearchType';
export default {
title: 'Plugins/Search/SearchType',
-29
View File
@@ -1,29 +0,0 @@
/*
* Copyright 2020 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.
*/
export * from './DefaultResultListItem';
export * from './Filters';
export * from './SearchBar';
export * from './SearchContext';
export * from './SearchFilter';
export * from './SearchModal';
export * from './SearchPage';
export * from './SearchResult';
export * from './SearchResultPager';
export * from './SearchType';
export * from './SidebarSearch';
export * from './SidebarSearchModal';
export * from './HomePageComponent';
+20 -22
View File
@@ -22,32 +22,30 @@
export { searchApiRef } from './apis';
export type { SearchApi } from './apis';
export {
Filters,
FiltersButton,
SearchBar,
SearchBarBase,
SearchContextProvider,
SearchFilter,
SearchFilterNext,
SearchModal,
SearchPage as Router,
SearchResultPager,
SearchType,
SidebarSearch,
useSearch,
} from './components';
export { Filters, FiltersButton } from './components/Filters';
export type { FiltersState } from './components/Filters';
export type { HomePageSearchBarProps } from './components/HomePageComponent';
export { SearchBar, SearchBarBase } from './components/SearchBar';
export type {
SearchModalProps,
SidebarSearchModalProps,
HomePageSearchBarProps,
SidebarSearchProps,
FiltersState,
SearchBarProps,
SearchBarBaseProps,
SearchBarProps,
} from './components/SearchBar';
export { SearchContextProvider, useSearch } from './components/SearchContext';
export { SearchFilter, SearchFilterNext } from './components/SearchFilter';
export { SearchModal } from './components/SearchModal';
export type { SearchModalProps } from './components/SearchModal';
export { SearchPage as Router } from './components/SearchPage';
export { SearchResultPager } from './components/SearchResultPager';
export { SearchType } from './components/SearchType';
export type {
SearchTypeAccordionProps,
SearchTypeProps,
} from './components';
} from './components/SearchType';
export { SidebarSearch } from './components/SidebarSearch';
export type { SidebarSearchProps } from './components/SidebarSearch';
export type { SidebarSearchModalProps } from './components/SidebarSearchModal';
export {
DefaultResultListItem,
HomePageSearchBar,
@@ -85,3 +85,34 @@ export const exampleCheck: TechInsightJsonRuleCheck = {
},
};
```
# Custom operators
json-rules-engine supports a limited [number of built-in operators](https://github.com/CacheControl/json-rules-engine/blob/master/docs/rules.md#operators) that can be used in conditions. You can add your own operators by adding them to the `operators` array in the `JsonRulesEngineFactCheckerFactory` constructor. For example:
```diff
+ import { Operator } from 'json-rules-engine';
const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({
checks: [],
logger,
+ operators: [ new Operator("startsWith", (a, b) => a.startsWith(b) ]
})
```
And you can then use it in your checks like this:
```js
...
rule: {
conditions: {
any: [
{
fact: 'version',
operator: 'startsWith',
value: '12',
},
],
},
}
```
@@ -8,6 +8,7 @@ import { CheckResponse } from '@backstage/plugin-tech-insights-common';
import { CheckValidationResponse } from '@backstage/plugin-tech-insights-node';
import { FactChecker } from '@backstage/plugin-tech-insights-node';
import { Logger as Logger_2 } from 'winston';
import { Operator } from 'json-rules-engine';
import { TechInsightCheck } from '@backstage/plugin-tech-insights-node';
import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-node';
import { TechInsightsStore } from '@backstage/plugin-tech-insights-node';
@@ -50,6 +51,7 @@ export class JsonRulesEngineFactChecker
repository,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerOptions);
// (undocumented)
getChecks(): Promise<TechInsightJsonRuleCheck[]>;
@@ -68,6 +70,7 @@ export class JsonRulesEngineFactCheckerFactory {
checks,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerFactoryOptions);
// (undocumented)
construct(repository: TechInsightsStore): JsonRulesEngineFactChecker;
@@ -78,6 +81,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = {
checks: TechInsightJsonRuleCheck[];
logger: Logger_2;
checkRegistry?: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
operators?: Operator[];
};
// @public
@@ -86,6 +90,7 @@ export type JsonRulesEngineFactCheckerOptions = {
repository: TechInsightsStore;
logger: Logger_2;
checkRegistry?: TechInsightCheckRegistry<any>;
operators?: Operator[];
};
// @public (undocumented)
@@ -24,6 +24,7 @@ import {
} from '../index';
import { getVoidLogger } from '@backstage/backend-common';
import { TechInsightJsonRuleCheck } from '../types';
import { Operator } from 'json-rules-engine';
const testChecks: Record<string, TechInsightJsonRuleCheck[]> = {
broken: [
@@ -127,6 +128,49 @@ const testChecks: Record<string, TechInsightJsonRuleCheck[]> = {
},
},
],
customOperator: [
{
id: 'customOperatorTestCheck',
name: 'customOperatorTestCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
description: 'Check For Testing using Custom Operator',
factIds: ['test-factretriever'],
rule: {
conditions: {
all: [
{
fact: 'testnumberfact',
operator: 'isDivisibleBy',
value: 2,
},
],
},
},
},
],
invalidCustomOperator: [
{
id: 'invalidCustomOperatorTestCheck',
name: 'invalidCustomOperatorTestCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
description:
'Check For Testing using a Custom Operator that is not registered',
factIds: ['test-factretriever'],
rule: {
conditions: {
all: [
{
fact: 'testnumberfact',
operator: 'isOdd',
value: 2,
},
],
},
},
},
],
};
const latestSchemasMock = jest.fn().mockImplementation(() => [
@@ -166,6 +210,9 @@ describe('JsonRulesEngineFactChecker', () => {
const factChecker = new JsonRulesEngineFactCheckerFactory({
checkRegistry: mockCheckRegistry,
checks: [],
operators: [
new Operator<number, number>('isDivisibleBy', (a, b) => a % b === 0),
],
logger: getVoidLogger(),
}).construct(mockRepository);
@@ -234,6 +281,42 @@ describe('JsonRulesEngineFactChecker', () => {
});
});
it('should use custom operators when defined', async () => {
const results = await factChecker.runChecks('a/a/a', ['customOperator']);
expect(results).toHaveLength(1);
expect(results[0]).toMatchObject({
facts: {
testnumberfact: {
value: 3,
type: 'integer',
description: '',
},
},
result: false,
check: {
id: 'customOperatorTestCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
name: 'customOperatorTestCheck',
description: 'Check For Testing using Custom Operator',
factIds: ['test-factretriever'],
rule: {
conditions: {
all: [
{
fact: 'testnumberfact',
factResult: 3,
operator: 'isDivisibleBy',
result: false,
value: 2,
},
],
priority: 1,
},
},
},
});
});
it('should gracefully handle multiple check at once', async () => {
const results = await factChecker.runChecks('a/a/a', [
'simple',
@@ -307,17 +390,22 @@ describe('JsonRulesEngineFactChecker', () => {
});
describe('when validating checks', () => {
it('should succeed on valid rules', async () => {
const validationResponse = await factChecker.validate(
testChecks.simple[0],
);
expect(validationResponse.valid).toBeTruthy();
});
it('should fail on broken rules', async () => {
const validationResponse = await factChecker.validate(
testChecks.broken[0],
);
expect(validationResponse.valid).toBeFalsy();
[testChecks.simple[0], testChecks.customOperator[0]].forEach(check => {
it(`should succeed on valid rule: ${check.name}`, async () => {
const validationResponse = await factChecker.validate(check);
expect(validationResponse.valid).toBeTruthy();
});
});
[testChecks.broken[0], testChecks.invalidCustomOperator[0]].forEach(
check => {
it(`should fail on broken rules: ${check.name}`, async () => {
const validationResponse = await factChecker.validate(
testChecks.broken[0],
);
expect(validationResponse.valid).toBeFalsy();
});
},
);
});
});
@@ -23,11 +23,16 @@ import {
CheckValidationResponse,
} from '@backstage/plugin-tech-insights-node';
import { FactResponse } from '@backstage/plugin-tech-insights-common';
import { Engine, EngineResult, TopLevelCondition } from 'json-rules-engine';
import {
Engine,
EngineResult,
Operator,
TopLevelCondition,
} from 'json-rules-engine';
import { DefaultCheckRegistry } from './CheckRegistry';
import { Logger } from 'winston';
import { pick } from 'lodash';
import Ajv from 'ajv';
import Ajv, { SchemaObject } from 'ajv';
import * as validationSchema from './validation-schema.json';
import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants';
@@ -46,6 +51,7 @@ export type JsonRulesEngineFactCheckerOptions = {
repository: TechInsightsStore;
logger: Logger;
checkRegistry?: TechInsightCheckRegistry<any>;
operators?: Operator[];
};
/**
@@ -60,15 +66,27 @@ export class JsonRulesEngineFactChecker
private readonly checkRegistry: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
private repository: TechInsightsStore;
private readonly logger: Logger;
private readonly validationSchema: SchemaObject;
private readonly operators: Operator[];
constructor({
checks,
repository,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerOptions) {
this.repository = repository;
this.logger = logger;
this.operators = operators || [];
this.validationSchema = JSON.parse(JSON.stringify(validationSchema));
this.operators.forEach(op => {
this.validationSchema.definitions.condition.properties.operator.anyOf.push(
{ const: op.name },
);
});
checks.forEach(check => this.validate(check));
this.checkRegistry =
checkRegistry ??
@@ -80,6 +98,10 @@ export class JsonRulesEngineFactChecker
checks?: string[],
): Promise<JsonRuleBooleanCheckResult[]> {
const engine = new Engine();
this.operators.forEach(op => {
engine.addOperator(op);
});
const techInsightChecks = checks
? await this.checkRegistry.getAll(checks)
: await this.checkRegistry.list();
@@ -125,7 +147,7 @@ export class JsonRulesEngineFactChecker
check: TechInsightJsonRuleCheck,
): Promise<CheckValidationResponse> {
const ajv = new Ajv({ verbose: true });
const validator = ajv.compile(validationSchema);
const validator = ajv.compile(this.validationSchema);
const isValidToSchema = validator(check.rule);
if (check.type !== JSON_RULE_ENGINE_CHECK_TYPE) {
const msg = `Only ${JSON_RULE_ENGINE_CHECK_TYPE} checks can be registered to this fact checker`;
@@ -317,6 +339,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = {
checks: TechInsightJsonRuleCheck[];
logger: Logger;
checkRegistry?: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
operators?: Operator[];
};
/**
@@ -330,15 +353,18 @@ export class JsonRulesEngineFactCheckerFactory {
private readonly checks: TechInsightJsonRuleCheck[];
private readonly logger: Logger;
private readonly checkRegistry?: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
private readonly operators?: Operator[];
constructor({
checks,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerFactoryOptions) {
this.logger = logger;
this.checks = checks;
this.checkRegistry = checkRegistry;
this.operators = operators;
}
/**
@@ -352,6 +378,7 @@ export class JsonRulesEngineFactCheckerFactory {
logger: this.logger,
checkRegistry: this.checkRegistry,
repository,
operators: this.operators,
});
}
}
+37
View File
@@ -5,12 +5,49 @@
```ts
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { CheckResult } from '@backstage/plugin-tech-insights-common';
import { EntityName } from '@backstage/catalog-model';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
// @public
export type Check = {
id: string;
type: string;
name: string;
description: string;
factIds: string[];
};
// @public
export type CheckResultRenderer = {
type: string;
title: string;
description: string;
component: React_2.ReactElement;
};
// @public (undocumented)
export const EntityTechInsightsScorecardContent: () => JSX.Element;
// @public
export interface TechInsightsApi {
// (undocumented)
getAllChecks(): Promise<Check[]>;
// (undocumented)
getScorecardsDefinition: (
type: string,
value: CheckResult[],
) => CheckResultRenderer | undefined;
// (undocumented)
runChecks(entityParams: EntityName, checks?: Check[]): Promise<CheckResult[]>;
}
// @public
export const techInsightsApiRef: ApiRef<TechInsightsApi>;
// @public (undocumented)
export const techInsightsPlugin: BackstagePlugin<
{
+1
View File
@@ -35,6 +35,7 @@
"react-use": "^17.2.4"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
@@ -20,10 +20,20 @@ import { Check } from './types';
import { CheckResultRenderer } from '../components/CheckResultRenderer';
import { EntityName } from '@backstage/catalog-model';
/**
* {@link @backstage/core-plugin-api#ApiRef} for the {@link TechInsightsApi}
*
* @public
*/
export const techInsightsApiRef = createApiRef<TechInsightsApi>({
id: 'plugin.techinsights.service',
});
/**
* API client interface for the Tech Insights plugin
*
* @public
*/
export interface TechInsightsApi {
getScorecardsDefinition: (
type: string,
+6
View File
@@ -13,6 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Represents a single check defined on the TechInsights backend.
*
* @public
*/
export type Check = {
id: string;
type: string;
@@ -18,6 +18,11 @@ import { CheckResult } from '@backstage/plugin-tech-insights-common';
import React from 'react';
import { BooleanCheck } from './BooleanCheck';
/**
* Defines a react component that is responsible for rendering a results of a given type.
*
* @public
*/
export type CheckResultRenderer = {
type: string;
title: string;
+5
View File
@@ -17,3 +17,8 @@ export {
techInsightsPlugin,
EntityTechInsightsScorecardContent,
} from './plugin';
export { techInsightsApiRef } from './api/TechInsightsApi';
export type { TechInsightsApi } from './api/TechInsightsApi';
export type { Check } from './api/types';
export type { CheckResultRenderer } from './components/CheckResultRenderer';
@@ -123,6 +123,11 @@ export class DefaultTechDocsCollator implements DocumentCollator {
techDocsBaseUrl,
entityInfo,
),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
const searchIndex = await searchIndexResponse.json();
+175 -199
View File
@@ -127,15 +127,7 @@
resolved "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz#dcccebb88406e5c76e0e1d52e8cc4c43a68b3ee7"
integrity sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==
"@azure/core-auth@^1.1.3":
version "1.1.4"
resolved "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.1.4.tgz#af9a334acf3cb9c49e6013e6caf6dc9d43476030"
integrity sha512-+j1embyH1jqf04AIfJPdLafd5SC1y6z1Jz4i+USR1XkTp6KM8P5u4/AjmWMVoEQdM/M29PJcRDZcCEWjK9S1bw==
dependencies:
"@azure/abort-controller" "^1.0.0"
tslib "^2.0.0"
"@azure/core-auth@^1.3.0":
"@azure/core-auth@^1.1.3", "@azure/core-auth@^1.3.0":
version "1.3.2"
resolved "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.2.tgz#6a2c248576c26df365f6c7881ca04b7f6d08e3d0"
integrity sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==
@@ -225,6 +217,14 @@
"@opentelemetry/api" "^1.0.0"
tslib "^2.2.0"
"@azure/core-tracing@1.0.0-preview.13":
version "1.0.0-preview.13"
resolved "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz#55883d40ae2042f6f1e12b17dd0c0d34c536d644"
integrity sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==
dependencies:
"@opentelemetry/api" "^1.0.1"
tslib "^2.2.0"
"@azure/core-tracing@1.0.0-preview.9":
version "1.0.0-preview.9"
resolved "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz#84f3b85572013f9d9b85e1e5d89787aa180787eb"
@@ -234,29 +234,35 @@
"@opentelemetry/api" "^0.10.2"
tslib "^2.0.0"
"@azure/identity@^1.5.0":
version "1.5.0"
resolved "https://registry.npmjs.org/@azure/identity/-/identity-1.5.0.tgz#0ac832b95adaac00b4718d92b43b2c9c5ab42d2d"
integrity sha512-djgywuWtX6720seqNOPmGM1hY54oHnjRT0MLIOzacMARTZuEtAIaFFvMPBlUIMQdtSGhdjH+/MS1/9PE8j83eA==
"@azure/core-util@^1.0.0-beta.1":
version "1.0.0-beta.1"
resolved "https://registry.npmjs.org/@azure/core-util/-/core-util-1.0.0-beta.1.tgz#2efd2c74b4b0a38180369f50fe274a3c4cd36e98"
integrity sha512-pS6cup979/qyuyNP9chIybK2qVkJ3MarbY/bx3JcGKE6An6dRweLnsfJfU2ydqUI/B51Rjnn59ajHIhCUTwWZw==
dependencies:
tslib "^2.0.0"
"@azure/identity@^2.0.1":
version "2.0.1"
resolved "https://registry.npmjs.org/@azure/identity/-/identity-2.0.1.tgz#31107506371e520bc874647a9e4384cfd2f85103"
integrity sha512-gdGGuLKlKIQaf2RefA84keoBfmWfiAntbW2SzcdKvwLSGzsio/qkyY3sYUpXRz/sqLDxguuimgZukp7TPgwIlg==
dependencies:
"@azure/abort-controller" "^1.0.0"
"@azure/core-auth" "^1.3.0"
"@azure/core-client" "^1.0.0"
"@azure/core-rest-pipeline" "^1.1.0"
"@azure/core-tracing" "1.0.0-preview.12"
"@azure/core-tracing" "1.0.0-preview.13"
"@azure/core-util" "^1.0.0-beta.1"
"@azure/logger" "^1.0.0"
"@azure/msal-node" "1.0.0-beta.6"
"@azure/msal-browser" "^2.16.0"
"@azure/msal-common" "^4.5.1"
"@azure/msal-node" "^1.3.0"
"@types/stoppable" "^1.1.0"
axios "^0.21.1"
events "^3.0.0"
jws "^4.0.0"
msal "^1.0.2"
open "^7.0.0"
qs "^6.7.0"
open "^8.0.0"
stoppable "^1.1.0"
tslib "^2.0.0"
tslib "^2.2.0"
uuid "^8.3.0"
optionalDependencies:
keytar "^7.3.0"
"@azure/logger@^1.0.0":
version "1.0.1"
@@ -265,36 +271,33 @@
dependencies:
tslib "^2.0.0"
"@azure/msal-common@^4.0.0":
version "4.4.0"
resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.4.0.tgz#818526042f78838ebc332fb735e7de64d8bccb45"
integrity sha512-Qrs33Ctt2KM7NxArFPIUKc8UbIcm7zYxJFdJeQ9k7HKBhVk3e88CUz1Mw33cS/Jr+YA1H02OAzHg++bJ+4SFyQ==
"@azure/msal-browser@^2.16.0":
version "2.20.0"
resolved "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.20.0.tgz#78e34395048c4a8842400d4168b2fb3bdd3c854e"
integrity sha512-Fl8boo38fPNlEm84fRCulbTfHJo+Z/i+1gcdJTG+PqmrkMOUVTdpkwznGh6ZQdAM34uumEgzukmqMr8lVKrytA==
dependencies:
"@azure/msal-common" "^5.2.0"
"@azure/msal-common@^4.5.1":
version "4.5.1"
resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.5.1.tgz#f35af8b634ae24aebd0906deb237c0db1afa5826"
integrity sha512-/i5dXM+QAtO+6atYd5oHGBAx48EGSISkXNXViheliOQe+SIFMDo3gSq3lL54W0suOSAsVPws3XnTaIHlla0PIQ==
dependencies:
debug "^4.1.1"
"@azure/msal-common@^5.0.1":
version "5.0.1"
resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-5.0.1.tgz#234030b340cc63575e84190b96a8a336b69c7698"
integrity sha512-CmPR3XM9+CGUu7V/+bAwDxyN6XqWJJhVLmv7utT3sbgay4l5roVXsD1t4wURTs8PwzxmmnJOrhvvGhoDxUW69g==
"@azure/msal-common@^5.2.0":
version "5.2.0"
resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-5.2.0.tgz#49440e04f4d0961fc5a1a1718fbe5e4eae2db5db"
integrity sha512-oVc4soy5MEZOp9NvCDqBk57mtiUTJXQQ8Z8S/4UiRQP8RG8snuCFQUs9xxdIfvl2FWIvgiBz+SMByyjTaRX42Q==
dependencies:
debug "^4.1.1"
"@azure/msal-node@1.0.0-beta.6":
version "1.0.0-beta.6"
resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz#da6bc3a3a861057c85586055960e069f162548ee"
integrity sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==
"@azure/msal-node@^1.1.0", "@azure/msal-node@^1.3.0":
version "1.4.0"
resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.4.0.tgz#660685804fbdc533b10cc699f16323e27ec582c6"
integrity sha512-Ek6hqOFUi5QEAxZ55awM8y1N+9SzS9Qh8ijF4RDLtFuHzqP7xXmMnVC1lae45FlH55DUOo7dg/smuDJnb4kw6g==
dependencies:
"@azure/msal-common" "^4.0.0"
axios "^0.21.1"
jsonwebtoken "^8.5.1"
uuid "^8.3.0"
"@azure/msal-node@^1.1.0":
version "1.3.2"
resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.3.2.tgz#17665397c04b9cad57b42eb6e21d5d6f35d9b83a"
integrity sha512-aKU2lVRKhZa1IJ/Za/Ir6qlythQ3FHz0g0px3SbM4iC1otyr3ANS4mIn/6fmkpZDIHc8eAgJh2KMep1Yn2zpig==
dependencies:
"@azure/msal-common" "^5.0.1"
"@azure/msal-common" "^5.2.0"
axios "^0.21.4"
jsonwebtoken "^8.5.1"
uuid "^8.3.0"
@@ -4963,7 +4966,7 @@
"@types/set-cookie-parser" "^2.4.0"
set-cookie-parser "^2.4.6"
"@mswjs/interceptors@^0.12.6":
"@mswjs/interceptors@^0.12.6", "@mswjs/interceptors@^0.12.7":
version "0.12.7"
resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909"
integrity sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg==
@@ -5437,6 +5440,11 @@
resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.1.tgz#03c72f548431da5820a0c8864d1401e348e7e79f"
integrity sha512-H5Djcc2txGAINgf3TNaq4yFofYSIK3722PM89S/3R8FuI/eqi1UscajlXk7EBkG9s2pxss/q6SHlpturaavXaw==
"@opentelemetry/api@^1.0.1":
version "1.0.4"
resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz#a167e46c10d05a07ab299fc518793b0cff8f6924"
integrity sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==
"@opentelemetry/context-base@^0.10.2":
version "0.10.2"
resolved "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz#55bea904b2b91aa8a8675df9eaba5961bddb1def"
@@ -7011,11 +7019,6 @@
resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
"@tsconfig/node16@^1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1"
integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA==
"@tsconfig/node16@^1.0.2":
version "1.0.2"
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
@@ -7853,9 +7856,9 @@
"@types/node" "*"
"@types/mock-fs@^4.10.0", "@types/mock-fs@^4.13.0":
version "4.13.0"
resolved "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.0.tgz#b8b01cd2db588668b2532ecd21b1babd3fffb2c0"
integrity sha512-FUqxhURwqFtFBCuUj3uQMp7rPSQs//b3O9XecAVxhqS9y4/W8SIJEZFq2mmpnFVZBXwR/2OyPLE97CpyYiB8Mw==
version "4.13.1"
resolved "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.1.tgz#9201554ceb23671badbfa8ac3f1fa9e0706305be"
integrity sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA==
dependencies:
"@types/node" "*"
@@ -10032,13 +10035,6 @@ axios@^0.21.1, axios@^0.21.4:
dependencies:
follow-redirects "^1.14.0"
axios@^0.24.0:
version "0.24.0"
resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
dependencies:
follow-redirects "^1.14.4"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -11145,6 +11141,14 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@4.1.1, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^1.0.0, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -11164,14 +11168,6 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
change-case-all@1.0.14:
version "1.0.14"
resolved "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.14.tgz#bac04da08ad143278d0ac3dda7eccd39280bfba1"
@@ -13232,10 +13228,10 @@ delegates@^1.0.0:
resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
denque@^1.4.1:
version "1.5.0"
resolved "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
denque@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a"
integrity sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==
depd@^1.1.2, depd@~1.1.2:
version "1.1.2"
@@ -14689,11 +14685,6 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
expect@^24.8.0:
version "24.9.0"
resolved "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
@@ -15284,7 +15275,7 @@ fn.name@1.x.x:
resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.4:
follow-redirects@^1.0.0, follow-redirects@^1.14.0:
version "1.14.6"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
@@ -15840,11 +15831,6 @@ gitconfiglocal@^1.0.0:
dependencies:
ini "^1.3.2"
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -16132,7 +16118,7 @@ google-p12-pem@^3.0.3:
dependencies:
node-forge "^0.10.0"
got@^11.5.2, got@^11.8.0, got@^11.8.2:
got@^11.8.0, got@^11.8.2:
version "11.8.2"
resolved "https://registry.npmjs.org/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
@@ -17003,10 +16989,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
dependencies:
safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
iconv-lite@^0.6.2, iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
@@ -17278,7 +17264,7 @@ inquirer@^8.0.0:
strip-ansi "^6.0.0"
through "^2.3.6"
inquirer@^8.1.1:
inquirer@^8.1.1, inquirer@^8.2.0:
version "8.2.0"
resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a"
integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==
@@ -19266,14 +19252,6 @@ kafkajs@^1.16.0-beta.6:
resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-1.16.0-beta.21.tgz#5736bcef7b505714642a82d6dc0d1507fc0ae817"
integrity sha512-6iarOOnKTaei0EK+a+K2V/bBA7YgvpA69tZwnVF85PxGlvoG/wqKpfRNh2Mb04uiNTEwBYNEIO7hAFElEM6/AA==
keytar@^7.3.0:
version "7.7.0"
resolved "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2"
integrity sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==
dependencies:
node-addon-api "^3.0.0"
prebuild-install "^6.0.0"
keyv-memcache@^1.2.5:
version "1.2.7"
resolved "https://registry.npmjs.org/keyv-memcache/-/keyv-memcache-1.2.7.tgz#b8a43eeecdb11ad8f4d6d64abd4298d014c74955"
@@ -21290,11 +21268,6 @@ mkdirp-classic@^0.5.2:
resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz#54c441ce4c96cd7790e10b41a87aa51068ecab2b"
integrity sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==
mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp-infer-owner@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316"
@@ -21386,13 +21359,6 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msal@^1.0.2:
version "1.4.4"
resolved "https://registry.npmjs.org/msal/-/msal-1.4.4.tgz#3f9b5a4442aa711c12ab8e88b8ed89b293f99711"
integrity sha512-aOBD/L6jAsizDFzKxxvXxH0FEDjp6Inr3Ufi/Y2o7KCFKN+akoE2sLeszEb/0Y3VxHxK0F0ea7xQ/HHTomKivw==
dependencies:
tslib "^1.9.3"
msw@^0.35.0:
version "0.35.0"
resolved "https://registry.npmjs.org/msw/-/msw-0.35.0.tgz#18a4ceb6c822ef226a30421d434413bc45030d38"
@@ -21419,6 +21385,32 @@ msw@^0.35.0:
type-fest "^1.2.2"
yargs "^17.0.1"
msw@^0.36.3:
version "0.36.3"
resolved "https://registry.npmjs.org/msw/-/msw-0.36.3.tgz#7feb243a5fcf563806d45edc027bc36144741170"
integrity sha512-Itzp/QhKaleZoslXDrNik3ramW9ynqzOdbwydX2ehBSSaZd5QoiAl/bHYcV33R6CEZcJgIX1N4s+G6XkF/bhkA==
dependencies:
"@mswjs/cookies" "^0.1.6"
"@mswjs/interceptors" "^0.12.7"
"@open-draft/until" "^1.0.3"
"@types/cookie" "^0.4.1"
"@types/inquirer" "^8.1.3"
"@types/js-levenshtein" "^1.1.0"
chalk "4.1.1"
chokidar "^3.4.2"
cookie "^0.4.1"
graphql "^15.5.1"
headers-utils "^3.0.2"
inquirer "^8.2.0"
is-node-process "^1.0.1"
js-levenshtein "^1.1.6"
node-fetch "^2.6.1"
path-to-regexp "^6.2.0"
statuses "^2.0.0"
strict-event-emitter "^0.2.0"
type-fest "^1.2.2"
yargs "^17.3.0"
multicast-dns-service-types@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
@@ -21458,13 +21450,13 @@ mv@~2:
rimraf "~2.4.0"
mysql2@^2.2.5:
version "2.2.5"
resolved "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz#72624ffb4816f80f96b9c97fedd8c00935f9f340"
integrity sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g==
version "2.3.3"
resolved "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz#944f3deca4b16629052ff8614fbf89d5552545a0"
integrity sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==
dependencies:
denque "^1.4.1"
denque "^2.0.1"
generate-function "^2.3.1"
iconv-lite "^0.6.2"
iconv-lite "^0.6.3"
long "^4.0.0"
lru-cache "^6.0.0"
named-placeholders "^1.1.2"
@@ -21538,11 +21530,6 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
native-url@^0.2.6:
version "0.2.6"
resolved "https://registry.npmjs.org/native-url/-/native-url-0.2.6.tgz#ca1258f5ace169c716ff44eccbddb674e10399ae"
@@ -21618,13 +21605,6 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-abi@^2.21.0:
version "2.30.0"
resolved "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz#8be53bf3e7945a34eea10e0fc9a5982776cf550b"
integrity sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==
dependencies:
semver "^5.4.1"
node-abort-controller@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e"
@@ -22037,7 +22017,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2, npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -22283,7 +22263,7 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
open@^7.0.0, open@^7.0.2, open@^7.0.3:
open@^7.0.2, open@^7.0.3:
version "7.3.1"
resolved "https://registry.npmjs.org/open/-/open-7.3.1.tgz#111119cb919ca1acd988f49685c4fdd0f4755356"
integrity sha512-f2wt9DCBKKjlFbjzGb8MOAW8LH8F0mrs1zc7KTjAJ9PZNQbfenzWbNP1VZJvw6ICMG9r14Ah6yfwPn7T7i646A==
@@ -22291,10 +22271,10 @@ open@^7.0.0, open@^7.0.2, open@^7.0.3:
is-docker "^2.0.0"
is-wsl "^2.1.1"
open@^8.0.9:
version "8.2.1"
resolved "https://registry.npmjs.org/open/-/open-8.2.1.tgz#82de42da0ccbf429bc12d099dad2e0975e14e8af"
integrity sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==
open@^8.0.0, open@^8.0.9:
version "8.4.0"
resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==
dependencies:
define-lazy-prop "^2.0.0"
is-docker "^2.1.1"
@@ -23033,6 +23013,11 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
path-to-regexp@^6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -23744,25 +23729,6 @@ postgres-interval@^1.1.0:
dependencies:
xtend "^4.0.0"
prebuild-install@^6.0.0:
version "6.1.3"
resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.3.tgz#8ea1f9d7386a0b30f7ef20247e36f8b2b82825a2"
integrity sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==
dependencies:
detect-libc "^1.0.3"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^2.21.0"
npmlog "^4.0.1"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^3.0.3"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
precond@0.2:
version "0.2.3"
resolved "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"
@@ -24189,7 +24155,7 @@ qs@6.9.6:
resolved "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee"
integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==
qs@^6.10.0, qs@^6.10.1, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4, qs@^6.9.6:
qs@^6.10.0, qs@^6.10.1, qs@^6.9.1, qs@^6.9.4, qs@^6.9.6:
version "6.10.1"
resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
@@ -25874,6 +25840,11 @@ safe-stable-stringify@^1.1.0:
resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a"
integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==
safe-stable-stringify@^2.2.0:
version "2.3.1"
resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73"
integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@@ -27005,6 +26976,15 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string.prototype.matchall@^4.0.0 || ^3.0.1", string.prototype.matchall@^4.0.6:
version "4.0.6"
resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa"
@@ -27102,6 +27082,13 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.0:
version "7.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@@ -27517,7 +27504,7 @@ tar-fs@2.0.0:
pump "^3.0.0"
tar-stream "^2.0.0"
tar-fs@^2.0.0, tar-fs@^2.1.1:
tar-fs@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@@ -28134,23 +28121,7 @@ ts-log@^2.2.3:
resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.3.tgz#4da5640fe25a9fb52642cd32391c886721318efb"
integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w==
ts-node@^10.0.0:
version "10.0.0"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be"
integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg==
dependencies:
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
ts-node@^10.2.1, ts-node@^10.4.0:
ts-node@^10.0.0, ts-node@^10.2.1, ts-node@^10.4.0:
version "10.4.0"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7"
integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==
@@ -28369,33 +28340,33 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript-json-schema@^0.51.0:
version "0.51.0"
resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.51.0.tgz#e2abff69b8564c98c0edef2c13d55ef10fd71427"
integrity sha512-POhWbUNs2oaBti1W9k/JwS+uDsaZD9J/KQiZ/iXRQEOD0lTn9VmshIls9tn+A9X6O+smPjeEz5NEy6WTkCCzrQ==
typescript-json-schema@^0.52.0:
version "0.52.0"
resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.52.0.tgz#954560ec90e5486e8f7a5b7706ec59286a708e29"
integrity sha512-3ZdHzx116gZ+D9LmMl5/+d1G3Rpt8baWngKzepYWHnXbAa8Winv64CmFRqLlMKneE1c40yugYDFcWdyX1FjGzQ==
dependencies:
"@types/json-schema" "^7.0.9"
"@types/node" "^16.9.2"
glob "^7.1.7"
json-stable-stringify "^1.0.1"
safe-stable-stringify "^2.2.0"
ts-node "^10.2.1"
typescript "~4.2.3"
typescript "~4.4.4"
yargs "^17.1.1"
typescript@^4.0.3, typescript@~4.2.3:
version "4.2.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
typescript@^4.0.3, typescript@~4.5.2:
version "4.5.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
typescript@~4.3.5:
version "4.3.5"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
typescript@~4.5.2:
version "4.5.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
typescript@~4.4.4:
version "4.4.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
ua-parser-js@^0.7.18:
version "0.7.28"
@@ -29949,6 +29920,11 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^21.0.0:
version "21.0.0"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55"
integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==
yargs-parser@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-3.2.0.tgz#5081355d19d9d0c8c5d81ada908cb4e6d186664f"
@@ -29987,20 +29963,7 @@ yargs@^16.1.1, yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.0.0, yargs@^17.0.1:
version "17.0.1"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.1.1:
yargs@^17.0.0, yargs@^17.0.1, yargs@^17.1.1:
version "17.2.1"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea"
integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==
@@ -30013,6 +29976,19 @@ yargs@^17.1.1:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.3.0:
version "17.3.0"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.3.0.tgz#295c4ffd0eef148ef3e48f7a2e0f58d0e4f26b1c"
integrity sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^21.0.0"
yargs@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/yargs/-/yargs-5.0.0.tgz#3355144977d05757dbb86d6e38ec056123b3a66e"