diff --git a/.changeset/angry-eels-watch.md b/.changeset/angry-eels-watch.md new file mode 100644 index 0000000000..cd252d5558 --- /dev/null +++ b/.changeset/angry-eels-watch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-rollbar-backend': patch +--- + +Moved `@backstage/test-utils` to `devDependencies`. diff --git a/.changeset/chatty-gifts-fry.md b/.changeset/chatty-gifts-fry.md new file mode 100644 index 0000000000..25f0a70bd9 --- /dev/null +++ b/.changeset/chatty-gifts-fry.md @@ -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 diff --git a/.changeset/curly-bugs-stare.md b/.changeset/curly-bugs-stare.md new file mode 100644 index 0000000000..88e1d86e84 --- /dev/null +++ b/.changeset/curly-bugs-stare.md @@ -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. diff --git a/.changeset/cyan-goats-confess.md b/.changeset/cyan-goats-confess.md new file mode 100644 index 0000000000..0ba500dc7f --- /dev/null +++ b/.changeset/cyan-goats-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Bump `@azure/identity` from `^1.5.0` to `^2.0.1`. diff --git a/.changeset/dar-jag-gar.md b/.changeset/dar-jag-gar.md new file mode 100644 index 0000000000..5989614101 --- /dev/null +++ b/.changeset/dar-jag-gar.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +In-memory cache clients instantiated from the same cache manager now share the same memory space. diff --git a/.changeset/eighty-dancers-heal.md b/.changeset/eighty-dancers-heal.md new file mode 100644 index 0000000000..9286e75d2c --- /dev/null +++ b/.changeset/eighty-dancers-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Update `auth0` and `onelogin` providers to allow for `authHandler` and `signIn.resolver` configuration. diff --git a/.changeset/eleven-pianos-fail.md b/.changeset/eleven-pianos-fail.md new file mode 100644 index 0000000000..5b0aa76ae9 --- /dev/null +++ b/.changeset/eleven-pianos-fail.md @@ -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 diff --git a/.changeset/four-phones-shave.md b/.changeset/four-phones-shave.md new file mode 100644 index 0000000000..1ef58bdadb --- /dev/null +++ b/.changeset/four-phones-shave.md @@ -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, +}; +``` diff --git a/.changeset/four-waves-tan.md b/.changeset/four-waves-tan.md new file mode 100644 index 0000000000..444891d31b --- /dev/null +++ b/.changeset/four-waves-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Internal cleanup of the exports structure diff --git a/.changeset/light-singers-double.md b/.changeset/light-singers-double.md new file mode 100644 index 0000000000..e21017373d --- /dev/null +++ b/.changeset/light-singers-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fix issue with plugin:serve for Plugins not using Lerna monorepo. diff --git a/.changeset/modern-buses-protect.md b/.changeset/modern-buses-protect.md new file mode 100644 index 0000000000..2aed52eca0 --- /dev/null +++ b/.changeset/modern-buses-protect.md @@ -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. diff --git a/.changeset/modern-waves-lay.md b/.changeset/modern-waves-lay.md new file mode 100644 index 0000000000..c0d9804379 --- /dev/null +++ b/.changeset/modern-waves-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Add a comment to the default backend about the fallback 404 handler. diff --git a/.changeset/plenty-flies-repair.md b/.changeset/plenty-flies-repair.md new file mode 100644 index 0000000000..7e9690a7f3 --- /dev/null +++ b/.changeset/plenty-flies-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-rollbar-backend': patch +--- + +Replace the usage of `axios` with `node-fetch` in the Rollbar API diff --git a/.changeset/poor-zoos-invent.md b/.changeset/poor-zoos-invent.md new file mode 100644 index 0000000000..a1c87bd3ad --- /dev/null +++ b/.changeset/poor-zoos-invent.md @@ -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 diff --git a/.changeset/seven-tomatoes-smash.md b/.changeset/seven-tomatoes-smash.md new file mode 100644 index 0000000000..98ae643738 --- /dev/null +++ b/.changeset/seven-tomatoes-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Add support for API auth in DefaultTechDocsCollator diff --git a/.changeset/slimy-socks-pump.md b/.changeset/slimy-socks-pump.md new file mode 100644 index 0000000000..f7252340f1 --- /dev/null +++ b/.changeset/slimy-socks-pump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights': patch +--- + +Export `techInsightsApiRef` and associated types. diff --git a/.changeset/small-points-allow.md b/.changeset/small-points-allow.md new file mode 100644 index 0000000000..2228fc8883 --- /dev/null +++ b/.changeset/small-points-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Bump `typescript-json-schema` from `^0.51.0` to `^0.52.0`. diff --git a/.changeset/spicy-moons-poke.md b/.changeset/spicy-moons-poke.md new file mode 100644 index 0000000000..cc39d629bf --- /dev/null +++ b/.changeset/spicy-moons-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights-backend-module-jsonfc': patch +--- + +ability to add custom operators diff --git a/.changeset/tame-ways-prove.md b/.changeset/tame-ways-prove.md new file mode 100644 index 0000000000..4f625d0a05 --- /dev/null +++ b/.changeset/tame-ways-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add option to build command for minifying the generated code diff --git a/.changeset/twenty-hornets-train.md b/.changeset/twenty-hornets-train.md new file mode 100644 index 0000000000..957bfd00da --- /dev/null +++ b/.changeset/twenty-hornets-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Enforce cookie SSL protection when in production for auth-backend sessions diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9cc5a2378e..a587419d50 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/docs/architecture-decisions/adr013-use-node-fetch.md b/docs/architecture-decisions/adr013-use-node-fetch.md new file mode 100644 index 0000000000..d802988ae4 --- /dev/null +++ b/docs/architecture-decisions/adr013-use-node-fetch.md @@ -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. diff --git a/docs/features/techdocs/configuring-ci-cd.md b/docs/features/techdocs/configuring-ci-cd.md index 0dec7ec24e..96d841eadf 100644 --- a/docs/features/techdocs/configuring-ci-cd.md +++ b/docs/features/techdocs/configuring-ci-cd.md @@ -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 -- 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 --storage-name --entity --directory ./site diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index 318b37f34e..412e3c4598 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -434,7 +434,8 @@ Usage: backstage-cli build [options] Options: --outputs <formats> 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 diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 4750e1d49d..6c4c5261d9 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -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"] } diff --git a/mkdocs.yml b/mkdocs.yml index 625236f737..81dad55b84 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index fb73687f54..ce5b6f5999 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -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( diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index d3a1504187..896c08fb94 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -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, }); } diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index dec0b1a293..d312811396 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -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 }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 94a8104ff1..bef5dfc285 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -138,6 +138,7 @@ export function registerCommands(program: CommanderStatic) { .command('build') .description('Build a package for publishing') .option('--outputs ', 'List of formats to output [types,cjs,esm]') + .option('--minify', 'Minify the generated code') .action(lazy(() => import('./build').then(m => m.default))); program diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index f85db276b2..986e409e58 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -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, diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 28fe6cea76..07fab1fd82 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -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" }, diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index fbeabcff36..08d21e61f7 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -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) diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 9285b7672b..7d50a2bcba 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -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", diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 9ed523c678..af46d874df 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -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; + 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; + signIn?: { + resolver: SignInResolver; + }; +}; + // @public export type AuthHandler = ( 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; + 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; + signIn?: { + resolver: SignInResolver; + }; +}; + // 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 ``` diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 9410a9beee..1f3d47a346 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -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", diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index a3fc77bc02..98f4cd9fa1 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -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, [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); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 4d5d507aa1..ce4b52ef4f 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -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 }); diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index cd1439b399..f54d04d15b 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -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; + refresh?(req: OAuthRefreshRequest): Promise<{ + response: OAuthResponse; + refreshToken?: string; + }>; /** * (Optional) Sign out of the auth provider. diff --git a/plugins/auth-backend/src/providers/atlassian/provider.test.ts b/plugins/auth-backend/src/providers/atlassian/provider.test.ts index 7bed582f81..29241dd17b 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.test.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.test.ts @@ -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', }); }); }); diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index e19f29a3a4..ba402eace0 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -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( 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 { - 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, + }; } } diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 7aa98c3e65..b99bbe86dd 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -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; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class Auth0AuthProvider implements OAuthHandlers { private readonly _strategy: Auth0Strategy; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + 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 { - 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 { - 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 = 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; + + /** + * 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; + }; +}; + +/** @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 = 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, { diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 3a518a6baa..5ad9a739d9 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -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 { - 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) { diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index e418ab22c2..b11ac7f5a1 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -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', }); }); }); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index c7e82cc5ff..b832f4f77b 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -129,26 +129,26 @@ export class GithubAuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - 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), diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index d1a84b43e8..f90de3c75b 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -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', }); }); }); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 6ca405d15f..dedbb7bd5b 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -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 { - 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 { @@ -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, }, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d85fc2de2d..10123bfe6f 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -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 { - 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) { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 9589e97265..3f71dd2c26 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -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'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 429e6691b6..2734878077 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -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='); + }); }); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 2dc259fe74..7e928a2786 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -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 { - 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 { 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( diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 2f9c739860..dc4afbac48 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -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 { + 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, }; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index fe4c042500..e5bfd14f40 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -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 { + 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 { @@ -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, }, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index dfffca9da7..1c74e171ad 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -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 { - 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) { diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 66e8b0bfc5..4b2201e576 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -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; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + 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 { - 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 { - 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 = 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; + + /** + * 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; + }; +}; + +/** @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 = 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, { diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 6a96d421da..deef6ddcf0 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -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 { diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index f861355762..9aafdf1437 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -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), diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index b96f63304a..fd5cf3b9dd 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -137,12 +137,27 @@ describe('', () => { 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 () => { diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 0a46580c12..effd9f5bc6 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -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(); + const { getByTestId, findByTitle, findByText } = await renderWrapped( + , + ); + 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( , ); + 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(); + 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(); + const { getByTestId } = await renderWrapped(); + fireEvent.click(getByTestId('user-picker-owned')); await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); fireEvent.click(screen.getByTestId('user-picker-starred')); await expect( diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx index 7253febd71..128c9d50b9 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx @@ -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( - + @@ -151,7 +150,7 @@ function renderInContext(children: JSX.Element) { - , + , ); } diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index 6cc1817e1c..9fc816bbbf 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -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( - + @@ -71,7 +70,7 @@ const renderProductInsightsCardInTestApp = async ( - , + , ); describe('', () => { diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index fb18c1a68c..efcb15b5a3 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -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 = PropsWithChildren>; export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }]; @@ -172,48 +165,3 @@ export const MockGroupsProvider = ({ ); }; - -export type MockCostInsightsApiProviderProps = PartialPropsWithChildren<{ - identityApi: Partial; - costInsightsApi: Partial; -}>; - -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 ( - - {children} - - ); -}; diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 72fb9a8eb9..def1514909 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -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": [ diff --git a/plugins/rollbar-backend/src/api/RollbarApi.test.ts b/plugins/rollbar-backend/src/api/RollbarApi.test.ts index 66a3955f3a..8936bb1839 100644 --- a/plugins/rollbar-backend/src/api/RollbarApi.test.ts +++ b/plugins/rollbar-backend/src/api/RollbarApi.test.ts @@ -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); + }); + }); }); diff --git a/plugins/rollbar-backend/src/api/RollbarApi.ts b/plugins/rollbar-backend/src/api/RollbarApi.ts index 67d0535942..245164b531 100644 --- a/plugins/rollbar-backend/src/api/RollbarApi.ts +++ b/plugins/rollbar-backend/src/api/RollbarApi.ts @@ -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(response?.data?.result, { deep: true }), - ); + return fetch( + fullUrl, + getRequestHeaders(accessToken || this.accessToken || ''), + ) + .then(response => response.json()) + .then(json => camelcaseKeys(json?.result, { deep: true })); } private async getForProject( diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b7c1a9146c..5c891b1802 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -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 }; ``` diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index b492f8ad2c..1a69f28fb6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -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 * diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 8485e224b2..3bca8e74ed 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -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; +}; ``` diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts new file mode 100644 index 0000000000..8d8ccedbad --- /dev/null +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -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; diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index 3b6fa71d7a..1197f48fed 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -30,3 +30,5 @@ export const templateEntityV1beta3Schema: JSONSchema = v1beta3Schema as Omit< JSONSchema, 'examples' >; + +export * from './TaskSpec'; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 45419e82eb..4f6928e72d 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -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) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index b464e86724..2c9112a77a 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -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", diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 58ec3dfc02..b27f116b96 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -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 (
{ {output && hasLinks(output) && ( )} + diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 3f70f4effd..9d1e7a3e2d 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -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>(() => { + 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 })); }; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 5597082f22..291f680710 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -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; diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx index fe2f6c2327..f5fa6689ef 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx @@ -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', diff --git a/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx b/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx index c8f2e538cb..ec0fc3acb9 100644 --- a/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx +++ b/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx @@ -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'; diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx index 96c86aa16f..3887df814f 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx @@ -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', diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx index 806ee46e60..0ea1a66bf9 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx @@ -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', diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index d46e44c580..bfea548134 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -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: [ diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index ff9622dfb9..d3209f9a9b 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -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'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index ef1eaf650a..8b4da51592 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Dialog, diff --git a/plugins/search/src/components/SearchPage/SearchPage.test.tsx b/plugins/search/src/components/SearchPage/SearchPage.test.tsx index c9980b3bf7..9486196584 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.test.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.test.tsx @@ -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'), diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx index bdd04ad90f..5efbecbe2b 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx @@ -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: [ diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx index b8282d6423..833573f5ee 100644 --- a/plugins/search/src/components/SearchType/SearchType.stories.tsx +++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx @@ -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', diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx deleted file mode 100644 index e413e9505d..0000000000 --- a/plugins/search/src/components/index.tsx +++ /dev/null @@ -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'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 5f0008c425..2cd9130721 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -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, diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index 108b1c8564..aab57fae91 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -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', + }, + ], + }, +} +``` diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index 7093fbfc2b..2e62e7d6a0 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -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; @@ -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; + operators?: Operator[]; }; // @public @@ -86,6 +90,7 @@ export type JsonRulesEngineFactCheckerOptions = { repository: TechInsightsStore; logger: Logger_2; checkRegistry?: TechInsightCheckRegistry; + operators?: Operator[]; }; // @public (undocumented) diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts index 5c33e9e369..c808713c3b 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts @@ -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 = { broken: [ @@ -127,6 +128,49 @@ const testChecks: Record = { }, }, ], + + 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('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(); + }); + }, + ); }); }); diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts index 2623a5c2ee..aef09d5434 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -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; + operators?: Operator[]; }; /** @@ -60,15 +66,27 @@ export class JsonRulesEngineFactChecker private readonly checkRegistry: TechInsightCheckRegistry; 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 { 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 { 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; + operators?: Operator[]; }; /** @@ -330,15 +353,18 @@ export class JsonRulesEngineFactCheckerFactory { private readonly checks: TechInsightJsonRuleCheck[]; private readonly logger: Logger; private readonly checkRegistry?: TechInsightCheckRegistry; + 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, }); } } diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index e7f30a2b30..1849508f63 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -5,12 +5,49 @@ ```ts /// +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; + // (undocumented) + getScorecardsDefinition: ( + type: string, + value: CheckResult[], + ) => CheckResultRenderer | undefined; + // (undocumented) + runChecks(entityParams: EntityName, checks?: Check[]): Promise; +} + +// @public +export const techInsightsApiRef: ApiRef; + // @public (undocumented) export const techInsightsPlugin: BackstagePlugin< { diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index efa5ac16ee..f8382984de 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -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": { diff --git a/plugins/tech-insights/src/api/TechInsightsApi.ts b/plugins/tech-insights/src/api/TechInsightsApi.ts index d3ff9ac2ff..6c5f272e7f 100644 --- a/plugins/tech-insights/src/api/TechInsightsApi.ts +++ b/plugins/tech-insights/src/api/TechInsightsApi.ts @@ -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({ id: 'plugin.techinsights.service', }); +/** + * API client interface for the Tech Insights plugin + * + * @public + */ export interface TechInsightsApi { getScorecardsDefinition: ( type: string, diff --git a/plugins/tech-insights/src/api/types.ts b/plugins/tech-insights/src/api/types.ts index 20071ba0c9..10dfa06420 100644 --- a/plugins/tech-insights/src/api/types.ts +++ b/plugins/tech-insights/src/api/types.ts @@ -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; diff --git a/plugins/tech-insights/src/components/CheckResultRenderer.tsx b/plugins/tech-insights/src/components/CheckResultRenderer.tsx index 60e0fad9c5..d57d547384 100644 --- a/plugins/tech-insights/src/components/CheckResultRenderer.tsx +++ b/plugins/tech-insights/src/components/CheckResultRenderer.tsx @@ -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; diff --git a/plugins/tech-insights/src/index.ts b/plugins/tech-insights/src/index.ts index 273c11fd71..b59ac453ce 100644 --- a/plugins/tech-insights/src/index.ts +++ b/plugins/tech-insights/src/index.ts @@ -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'; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index fa8a1156fe..1d3a3dbc3a 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -123,6 +123,11 @@ export class DefaultTechDocsCollator implements DocumentCollator { techDocsBaseUrl, entityInfo, ), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, ); const searchIndex = await searchIndexResponse.json(); diff --git a/yarn.lock b/yarn.lock index 5ed5806f6f..1e3570b3d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"