Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha';
+import { ServiceFactory } from '@backstage/backend-plugin-api';
+
+// @alpha (undocumented)
+export const featureDiscoveryServiceFactory: () => ServiceFactory<
+ FeatureDiscoveryService,
+ 'root'
+>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md
index 675a7755fc..0fa1ff376e 100644
--- a/packages/backend-app-api/api-report.md
+++ b/packages/backend-app-api/api-report.md
@@ -43,7 +43,7 @@ import { UrlReader } from '@backstage/backend-common';
// @public (undocumented)
export interface Backend {
// (undocumented)
- add(feature: BackendFeature): void;
+ add(feature: BackendFeature | (() => BackendFeature)): void;
// (undocumented)
start(): Promise
;
// (undocumented)
@@ -81,7 +81,7 @@ export function createSpecializedBackend(
// @public (undocumented)
export interface CreateSpecializedBackendOptions {
// (undocumented)
- services: ServiceFactoryOrFunction[];
+ defaultServiceFactories: ServiceFactoryOrFunction[];
}
// @public (undocumented)
diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json
index d1aa511794..82e32311aa 100644
--- a/packages/backend-app-api/package.json
+++ b/packages/backend-app-api/package.json
@@ -1,17 +1,30 @@
{
"name": "@backstage/backend-app-api",
"description": "Core API used by Backstage backend apps",
- "version": "0.5.0-next.1",
+ "version": "0.5.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
- "access": "public",
- "main": "dist/index.cjs.js",
- "types": "dist/index.d.ts"
+ "access": "public"
},
"backstage": {
"role": "node-library"
},
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
"homepage": "https://backstage.io",
"repository": {
"type": "git",
@@ -36,6 +49,7 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/cli-common": "workspace:^",
+ "@backstage/cli-node": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
@@ -73,6 +87,7 @@
"@types/node-forge": "^1.3.0",
"@types/stoppable": "^1.1.0",
"http-errors": "^2.0.0",
+ "mock-fs": "^5.2.0",
"supertest": "^6.1.3"
},
"files": [
diff --git a/packages/backend-app-api/src/alpha.ts b/packages/backend-app-api/src/alpha.ts
new file mode 100644
index 0000000000..a334214738
--- /dev/null
+++ b/packages/backend-app-api/src/alpha.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { featureDiscoveryServiceFactory } from './alpha/featureDiscoveryServiceFactory';
diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts
new file mode 100644
index 0000000000..5d4b1fc551
--- /dev/null
+++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import mockFs from 'mock-fs';
+import { resolve as resolvePath, dirname } from 'path';
+import { startTestBackend, mockServices } from '@backstage/backend-test-utils';
+import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory';
+import {
+ coreServices,
+ createServiceFactory,
+} from '@backstage/backend-plugin-api';
+
+const rootDir = dirname(process.argv[1]);
+
+describe('featureDiscoveryServiceFactory', () => {
+ beforeEach(() => {
+ mockFs({
+ [rootDir]: {
+ 'package.json': JSON.stringify({
+ name: 'example-app',
+ dependencies: {
+ 'detected-plugin': '0.0.0',
+ 'detected-module': '0.0.0',
+ },
+ }),
+ },
+ [resolvePath(rootDir, 'node_modules/detected-plugin')]: {
+ 'package.json': JSON.stringify({
+ name: 'detected-plugin',
+ main: 'index.js',
+ backstage: {
+ role: 'backend-plugin',
+ },
+ }),
+ 'index.js': `
+ const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api');
+ exports.detectedPlugin = createBackendPlugin({
+ pluginId: 'detected',
+ register(env) {
+ env.registerInit({
+ deps: { identity: coreServices.identity },
+ async init({ identity }) {
+ identity.getIdentity('detected-plugin');
+ },
+ });
+ },
+ });
+ `,
+ },
+ [resolvePath(rootDir, 'node_modules/detected-module')]: {
+ 'package.json': JSON.stringify({
+ name: 'detected-module',
+ main: 'index.js',
+ backstage: {
+ role: 'backend-module',
+ },
+ }),
+ 'index.js': `
+ const { createBackendModule, coreServices } = require('@backstage/backend-plugin-api');
+ exports.detectedModuleDerp = createBackendModule({
+ pluginId: 'detected',
+ moduleId: 'derp',
+ register(env) {
+ env.registerInit({
+ deps: { identity: coreServices.identity },
+ async init({ identity }) {
+ identity.getIdentity('detected-module');
+ },
+ });
+ },
+ });
+ `,
+ },
+ });
+ });
+
+ afterEach(() => {
+ mockFs.restore();
+ });
+
+ it('should detect plugin and module packages', async () => {
+ const fn = jest.fn().mockResolvedValue({});
+
+ await startTestBackend({
+ features: [
+ createServiceFactory({
+ service: coreServices.identity,
+ deps: {},
+ factory: () => ({ getIdentity: fn }),
+ }),
+ featureDiscoveryServiceFactory(),
+ mockServices.rootConfig.factory({
+ data: { backend: { packages: 'all' } },
+ }),
+ ],
+ });
+
+ expect(fn).toHaveBeenCalledWith('detected-plugin');
+ expect(fn).toHaveBeenCalledWith('detected-module');
+ });
+});
diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts
new file mode 100644
index 0000000000..f9ee7edc5f
--- /dev/null
+++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ BackendFeature,
+ RootConfigService,
+ coreServices,
+ createServiceFactory,
+} from '@backstage/backend-plugin-api';
+import {
+ featureDiscoveryServiceRef,
+ FeatureDiscoveryService,
+} from '@backstage/backend-plugin-api/alpha';
+import { resolve as resolvePath, dirname } from 'path';
+import fs from 'fs-extra';
+import { BackstagePackageJson } from '@backstage/cli-node';
+
+const LOADED_PACKAGE_ROLES = ['backend-plugin', 'backend-module'];
+
+/** @internal */
+async function findClosestPackageDir(
+ searchDir: string,
+): Promise {
+ let path = searchDir;
+
+ // Some confidence check to avoid infinite loop
+ for (let i = 0; i < 1000; i++) {
+ const packagePath = resolvePath(path, 'package.json');
+ const exists = await fs.pathExists(packagePath);
+ if (exists) {
+ return path;
+ }
+
+ const newPath = dirname(path);
+ if (newPath === path) {
+ return undefined;
+ }
+ path = newPath;
+ }
+
+ throw new Error(
+ `Iteration limit reached when searching for root package.json at ${searchDir}`,
+ );
+}
+
+/** @internal */
+class PackageDiscoveryService implements FeatureDiscoveryService {
+ constructor(private readonly config: RootConfigService) {}
+
+ async getBackendFeatures(): Promise<{ features: Array }> {
+ if (this.config.getOptionalString('backend.packages') !== 'all') {
+ return { features: [] };
+ }
+
+ const packageDir = await findClosestPackageDir(process.argv[1]);
+ if (!packageDir) {
+ throw new Error('Package discovery failed to find package.json');
+ }
+ const { dependencies } = require(resolvePath(
+ packageDir,
+ 'package.json',
+ )) as BackstagePackageJson;
+ const dependencyNames = Object.keys(dependencies || {});
+
+ const features: BackendFeature[] = [];
+
+ for (const name of dependencyNames) {
+ const depPkg = require(require.resolve(`${name}/package.json`, {
+ paths: [packageDir],
+ })) as BackstagePackageJson;
+ if (!LOADED_PACKAGE_ROLES.includes(depPkg?.backstage?.role ?? '')) {
+ continue;
+ }
+ const depModule = require(require.resolve(name, { paths: [packageDir] }));
+ for (const exportValue of Object.values(depModule)) {
+ if (isBackendFeature(exportValue)) {
+ features.push(exportValue);
+ }
+ if (isBackendFeatureFactory(exportValue)) {
+ features.push(exportValue());
+ }
+ }
+ }
+
+ return { features };
+ }
+}
+
+/** @alpha */
+export const featureDiscoveryServiceFactory = createServiceFactory({
+ service: featureDiscoveryServiceRef,
+ deps: {
+ config: coreServices.rootConfig,
+ },
+ factory({ config }) {
+ return new PackageDiscoveryService(config);
+ },
+});
+
+function isBackendFeature(value: unknown): value is BackendFeature {
+ return (
+ !!value &&
+ typeof value === 'object' &&
+ (value as BackendFeature).$$type === '@backstage/BackendFeature'
+ );
+}
+
+function isBackendFeatureFactory(
+ value: unknown,
+): value is () => BackendFeature {
+ return (
+ !!value &&
+ typeof value === 'function' &&
+ (value as any).$$type === '@backstage/BackendFeatureFactory'
+ );
+}
diff --git a/packages/backend-app-api/src/lib/DependencyGraph.test.ts b/packages/backend-app-api/src/lib/DependencyGraph.test.ts
new file mode 100644
index 0000000000..65efac0252
--- /dev/null
+++ b/packages/backend-app-api/src/lib/DependencyGraph.test.ts
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { DependencyGraph } from './DependencyGraph';
+
+describe('DependencyGraph', () => {
+ it('should be empty', async () => {
+ const empty = DependencyGraph.fromMap({});
+ expect(empty.findUnsatisfiedDeps()).toEqual([]);
+ expect(empty.detectCircularDependency()).toBeUndefined();
+ await expect(
+ empty.parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual([]);
+ });
+
+ describe('detectCircularDependency', () => {
+ it('should return undefined with not deps', () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: {},
+ 2: {},
+ 3: {},
+ 4: {},
+ }).detectCircularDependency(),
+ ).toBeUndefined();
+ });
+
+ it('should return undefined with no circular deps', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { consumes: ['a'], provides: ['b', 'c'] },
+ 3: { consumes: ['b'] },
+ 4: { consumes: ['c'] },
+ }).detectCircularDependency(),
+ ).toBeUndefined();
+ });
+
+ it('should detect an immediate circular dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['a'] },
+ }).detectCircularDependency(),
+ ).toEqual(['1', '1']);
+ });
+
+ it('should detect a small circular dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['b'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ }).detectCircularDependency(),
+ ).toEqual(['1', '2', '1']);
+ });
+
+ it('should detect a larger distant circular dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a', 'e'] },
+ 3: { provides: ['c'], consumes: ['b'] },
+ 4: { provides: ['d', 'e'], consumes: ['c', 'a'] },
+ }).detectCircularDependency(),
+ ).toEqual(['2', '3', '4', '2']);
+ });
+ });
+
+ describe('findUnsatisfiedDeps', () => {
+ it('should return nothing with no deps', () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: {},
+ 2: {},
+ 3: {},
+ 4: {},
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+ });
+
+ it('should return nothing when all deps are satisfied', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { consumes: ['a'], provides: ['b', 'c'] },
+ 3: { consumes: ['b'] },
+ 4: { consumes: ['c'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+ });
+
+ it('should find a single unsatisfied dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([{ value: '1', unsatisfied: ['a'] }]);
+ });
+
+ it('should handle circular dependencies', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'], provides: ['a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'], provides: ['b'] },
+ 2: { consumes: ['b'], provides: ['a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'] },
+ 2: { consumes: ['b'], provides: ['c'] },
+ 3: { consumes: ['c'], provides: ['a', 'b'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+ });
+
+ it('should find multiple unsatisfied deps for one node', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['b'] },
+ 2: { provides: ['b'], consumes: ['a', 'd', 'e'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([{ value: '2', unsatisfied: ['d', 'e'] }]);
+ });
+
+ it('should find multiple unsatisfied deps for multiple nodes', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a', 'd', 'e'] },
+ 3: { provides: [], consumes: ['b'] },
+ 4: { provides: [], consumes: ['c', 'a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([
+ { value: '2', unsatisfied: ['d', 'e'] },
+ { value: '4', unsatisfied: ['c'] },
+ ]);
+ });
+ });
+
+ describe('parallelTopologicalTraversal', () => {
+ it('should traverse with no deps', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: {},
+ 2: {},
+ 3: {},
+ 4: {},
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['1', '2', '3', '4']);
+ });
+
+ it('should traverse with a few deps', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { consumes: ['a'], provides: ['b', 'c'] },
+ 3: { consumes: ['b'] },
+ 4: { consumes: ['c'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['1', '2', '3', '4']);
+ });
+
+ it('should traverse in reverse', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['c'] },
+ 2: { provides: ['c'], consumes: ['b'] },
+ 3: { provides: ['b'], consumes: ['a'] },
+ 4: { provides: ['a'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['4', '3', '2', '1']);
+ });
+
+ it('should execute in parallel', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ 3: { provides: ['c'], consumes: ['a'] },
+ 4: { consumes: ['b'] },
+ 5: { consumes: ['c'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['1', '2', '3', '4', '5']);
+
+ // Same as above, but with 2 being delayed
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ 3: { provides: ['c'], consumes: ['a'] },
+ 4: { consumes: ['b'] },
+ 5: { consumes: ['c'] },
+ }).parallelTopologicalTraversal(async id => {
+ // When delaying 2 we expect 3 and 5 to complete before 2 and 4
+ if (id === '2') {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ return id;
+ }),
+ ).resolves.toEqual(['1', '3', '5', '2', '4']);
+ });
+
+ it('should detect circular dependencies', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['a'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).rejects.toThrow('Circular dependency detected');
+
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['b'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).rejects.toThrow('Circular dependency detected');
+
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['c'], consumes: ['a', 'b'] },
+ 3: { provides: ['b'], consumes: ['a', 'c'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).rejects.toThrow('Circular dependency detected');
+ });
+ });
+});
diff --git a/packages/backend-app-api/src/lib/DependencyGraph.ts b/packages/backend-app-api/src/lib/DependencyGraph.ts
new file mode 100644
index 0000000000..8fd679f555
--- /dev/null
+++ b/packages/backend-app-api/src/lib/DependencyGraph.ts
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+interface NodeInput {
+ value: T;
+ consumes?: Iterable;
+ provides?: Iterable;
+}
+
+/** @internal */
+class Node {
+ static from(input: NodeInput) {
+ return new Node(
+ input.value,
+ input.consumes ? new Set(input.consumes) : new Set(),
+ input.provides ? new Set(input.provides) : new Set(),
+ );
+ }
+
+ private constructor(
+ readonly value: T,
+ readonly consumes: Set,
+ readonly provides: Set,
+ ) {}
+}
+
+/**
+ * Internal helper to help validate and traverse a dependency graph.
+ * @internal
+ */
+export class DependencyGraph {
+ static fromMap(
+ nodes: Record, 'value'>>,
+ ): DependencyGraph {
+ return this.fromIterable(
+ Object.entries(nodes).map(([key, node]) => ({
+ value: String(key),
+ ...node,
+ })),
+ );
+ }
+
+ static fromIterable(
+ nodeInputs: Iterable>,
+ ): DependencyGraph {
+ const nodes = new Array>();
+ for (const nodeInput of nodeInputs) {
+ nodes.push(Node.from(nodeInput));
+ }
+
+ return new DependencyGraph(nodes);
+ }
+
+ #nodes: Array>;
+ #allProvided: Set;
+
+ private constructor(nodes: Array>) {
+ this.#nodes = nodes;
+ this.#allProvided = new Set();
+
+ for (const node of this.#nodes.values()) {
+ for (const produced of node.provides) {
+ this.#allProvided.add(produced);
+ }
+ }
+ }
+
+ // Find all nodes that consume dependencies that are not provided by any other node
+ findUnsatisfiedDeps(): Array<{ value: T; unsatisfied: string[] }> {
+ const unsatisfiedDependencies = [];
+ for (const node of this.#nodes.values()) {
+ const unsatisfied = Array.from(node.consumes).filter(
+ id => !this.#allProvided.has(id),
+ );
+ if (unsatisfied.length > 0) {
+ unsatisfiedDependencies.push({ value: node.value, unsatisfied });
+ }
+ }
+ return unsatisfiedDependencies;
+ }
+
+ // Detect circular dependencies within the graph, returning the path of nodes that
+ // form a cycle, with the same node as the first and last element of the array.
+ detectCircularDependency(): T[] | undefined {
+ for (const startNode of this.#nodes) {
+ const visited = new Set>();
+ const stack = new Array<[node: Node, path: T[]]>([
+ startNode,
+ [startNode.value],
+ ]);
+
+ while (stack.length > 0) {
+ const [node, path] = stack.pop()!;
+ if (visited.has(node)) {
+ continue;
+ }
+ visited.add(node);
+ for (const produced of node.provides) {
+ const consumerNodes = this.#nodes.filter(other =>
+ other.consumes.has(produced),
+ );
+ for (const consumer of consumerNodes) {
+ if (consumer === startNode) {
+ return [...path, startNode.value];
+ }
+ if (!visited.has(consumer)) {
+ stack.push([consumer, [...path, consumer.value]]);
+ }
+ }
+ }
+ }
+ }
+ return undefined;
+ }
+
+ /**
+ * Traverses the dependency graph in topological order, calling the provided
+ * function for each node and waiting for it to resolve.
+ *
+ * The nodes are traversed in parallel, but in such a way that no node is
+ * visited before all of its dependencies.
+ *
+ * Dependencies of nodes that are not produced by any other nodes will be ignored.
+ */
+ async parallelTopologicalTraversal(
+ fn: (value: T) => Promise,
+ ): Promise {
+ const allProvided = this.#allProvided;
+ const producedSoFar = new Set();
+ const waiting = new Set(this.#nodes.values());
+ const visited = new Set>();
+ const results = new Array();
+ let inFlight = 0; // Keep track of how many callbacks are in flight, so that we know if we got stuck
+
+ // Find all nodes that have no dependencies that have not already been produced by visited nodes
+ async function processMoreNodes() {
+ if (waiting.size === 0) {
+ return;
+ }
+ const nodesToProcess = [];
+ for (const node of waiting) {
+ let ready = true;
+ for (const consumed of node.consumes) {
+ if (allProvided.has(consumed) && !producedSoFar.has(consumed)) {
+ ready = false;
+ continue;
+ }
+ }
+ if (ready) {
+ nodesToProcess.push(node);
+ }
+ }
+
+ for (const node of nodesToProcess) {
+ waiting.delete(node);
+ }
+
+ if (nodesToProcess.length === 0 && inFlight === 0) {
+ // We expect the caller to check for circular dependencies before
+ // traversal, so this error should never happen
+ throw new Error('Circular dependency detected');
+ }
+
+ await Promise.all(nodesToProcess.map(processNode));
+ }
+
+ // Process an individual node, and then add its produced dependencies to the set of available products
+ async function processNode(node: Node) {
+ visited.add(node);
+ inFlight += 1;
+
+ const result = await fn(node.value);
+ results.push(result);
+
+ node.provides.forEach(produced => producedSoFar.add(produced));
+ inFlight -= 1;
+ await processMoreNodes();
+ }
+
+ await processMoreNodes();
+
+ return results;
+ }
+}
diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts
index 7af9de44da..d771c5dd11 100644
--- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts
+++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts
@@ -61,8 +61,7 @@ describe('schedulerFactory', () => {
});
await startTestBackend({
- features: [plugin()],
- services: [subject],
+ features: [plugin(), subject],
});
});
});
diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
index b05194f594..791b9b1c3a 100644
--- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
+++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
@@ -20,10 +20,15 @@ import {
coreServices,
createBackendPlugin,
createBackendModule,
+ createExtensionPoint,
} from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
-import { ServiceRegistry } from './ServiceRegistry';
-import { rootLifecycleServiceFactory } from '../services/implementations';
+
+import {
+ lifecycleServiceFactory,
+ loggerServiceFactory,
+ rootLifecycleServiceFactory,
+} from '../services/implementations';
const rootRef = createServiceRef<{ x: number }>({
id: '1',
@@ -44,12 +49,23 @@ class MockLogger {
}
}
+const baseFactories = [
+ lifecycleServiceFactory(),
+ rootLifecycleServiceFactory(),
+ createServiceFactory({
+ service: coreServices.rootLogger,
+ deps: {},
+ factory: () => new MockLogger(),
+ })(),
+ loggerServiceFactory(),
+];
+
describe('BackendInitializer', () => {
it('should initialize root scoped services', async () => {
const rootFactory = jest.fn();
const pluginFactory = jest.fn();
- const registry = new ServiceRegistry([
+ const services = [
createServiceFactory({
service: rootRef,
deps: {},
@@ -66,17 +82,76 @@ describe('BackendInitializer', () => {
deps: {},
factory: () => new MockLogger(),
})(),
- ]);
+ ];
- const init = new BackendInitializer(registry);
+ const init = new BackendInitializer(services);
await init.start();
expect(rootFactory).toHaveBeenCalled();
expect(pluginFactory).not.toHaveBeenCalled();
});
+ it('should initialize modules with extension points', async () => {
+ expect.assertions(3);
+
+ const extensionPoint = createExtensionPoint<{ values: string[] }>({
+ id: 'a',
+ });
+ const init = new BackendInitializer(baseFactories);
+
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modA',
+ register(reg) {
+ reg.registerInit({
+ deps: { extension: extensionPoint },
+ async init({ extension }) {
+ expect(extension.values).toEqual(['b']);
+ extension.values.push('a');
+ },
+ });
+ },
+ })(),
+ );
+
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modB',
+ register(reg) {
+ const values = ['b'];
+ reg.registerExtensionPoint(extensionPoint, { values });
+ reg.registerInit({
+ deps: {},
+ async init() {
+ expect(values).toEqual(['b', 'a', 'c']);
+ },
+ });
+ },
+ })(),
+ );
+
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modC',
+ register(reg) {
+ reg.registerInit({
+ deps: { extension: extensionPoint },
+ async init({ extension }) {
+ expect(extension.values).toEqual(['b', 'a']);
+ extension.values.push('c');
+ },
+ });
+ },
+ })(),
+ );
+ await init.start();
+ });
+
it('should forward errors when plugins fail to start', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendPlugin({
pluginId: 'test',
@@ -96,7 +171,7 @@ describe('BackendInitializer', () => {
});
it('should forward errors when modules fail to start', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendModule({
pluginId: 'test',
@@ -117,7 +192,7 @@ describe('BackendInitializer', () => {
});
it('should reject duplicate plugins', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendPlugin({
pluginId: 'test',
@@ -146,7 +221,7 @@ describe('BackendInitializer', () => {
});
it('should reject duplicate modules', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendModule({
pluginId: 'test',
@@ -175,4 +250,78 @@ describe('BackendInitializer', () => {
"Module 'mod' for plugin 'test' is already registered",
);
});
+
+ it('should reject modules with circular dependencies', async () => {
+ const extA = createExtensionPoint({ id: 'a' });
+ const extB = createExtensionPoint({ id: 'b' });
+ const init = new BackendInitializer([
+ rootLifecycleServiceFactory(),
+ createServiceFactory({
+ service: coreServices.rootLogger,
+ deps: {},
+ factory: () => new MockLogger(),
+ })(),
+ ]);
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modA',
+ register(reg) {
+ reg.registerExtensionPoint(extA, 'a');
+ reg.registerInit({
+ deps: { ext: extB },
+ async init() {},
+ });
+ },
+ })(),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modB',
+ register(reg) {
+ reg.registerExtensionPoint(extB, 'b');
+ reg.registerInit({
+ deps: { ext: extA },
+ async init() {},
+ });
+ },
+ })(),
+ );
+ await expect(init.start()).rejects.toThrow(
+ "Circular dependency detected for modules of plugin 'test', 'modA' -> 'modB' -> 'modA'",
+ );
+ });
+
+ it('should reject modules that depend on extension points other plugins', async () => {
+ const init = new BackendInitializer(baseFactories);
+ const extA = createExtensionPoint({ id: 'a' });
+ init.add(
+ createBackendPlugin({
+ pluginId: 'testA',
+ register(reg) {
+ reg.registerExtensionPoint(extA, 'a');
+ reg.registerInit({
+ deps: {},
+ async init() {},
+ });
+ },
+ })(),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'testB',
+ moduleId: 'mod',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extA },
+ async init() {},
+ });
+ },
+ })(),
+ );
+ await expect(init.start()).rejects.toThrow(
+ "Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'",
+ );
+ });
});
diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts
index ec4b2a79ac..93d2f6942a 100644
--- a/packages/backend-app-api/src/wiring/BackendInitializer.ts
+++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts
@@ -19,6 +19,7 @@ import {
ExtensionPoint,
coreServices,
ServiceRef,
+ ServiceFactory,
} from '@backstage/backend-plugin-api';
import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory';
import { BackendPluginLifecycleImpl } from '../services/implementations/lifecycle/lifecycleServiceFactory';
@@ -26,7 +27,10 @@ import { EnumerableServiceHolder, ServiceOrExtensionPoint } from './types';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-forbidden-package-imports
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
-import { ForwardedError } from '@backstage/errors';
+import { ForwardedError, ConflictError } from '@backstage/errors';
+import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha';
+import { DependencyGraph } from '../lib/DependencyGraph';
+import { ServiceRegistry } from './ServiceRegistry';
export interface BackendRegisterInit {
consumes: Set;
@@ -40,11 +44,16 @@ export interface BackendRegisterInit {
export class BackendInitializer {
#startPromise?: Promise;
#features = new Array();
- #extensionPoints = new Map, unknown>();
- #serviceHolder: EnumerableServiceHolder;
+ #extensionPoints = new Map<
+ ExtensionPoint,
+ { impl: unknown; pluginId: string }
+ >();
+ #serviceHolder: EnumerableServiceHolder | undefined;
+ #providedServiceFactories = new Array();
+ #defaultApiFactories: ServiceFactory[];
- constructor(serviceHolder: EnumerableServiceHolder) {
- this.#serviceHolder = serviceHolder;
+ constructor(defaultApiFactories: ServiceFactory[]) {
+ this.#defaultApiFactories = defaultApiFactories;
}
async #getInitDeps(
@@ -55,13 +64,16 @@ export class BackendInitializer {
const missingRefs = new Set();
for (const [name, ref] of Object.entries(deps)) {
- const extensionPoint = this.#extensionPoints.get(
- ref as ExtensionPoint,
- );
- if (extensionPoint) {
- result.set(name, extensionPoint);
+ const ep = this.#extensionPoints.get(ref as ExtensionPoint);
+ if (ep) {
+ if (ep.pluginId !== pluginId) {
+ throw new Error(
+ `Extension point registered for plugin '${ep.pluginId}' may not be used by module for plugin '${pluginId}'`,
+ );
+ }
+ result.set(name, ep.impl);
} else {
- const impl = await this.#serviceHolder.get(
+ const impl = await this.#serviceHolder!.get(
ref as ServiceRef,
pluginId,
);
@@ -87,18 +99,44 @@ export class BackendInitializer {
if (this.#startPromise) {
throw new Error('feature can not be added after the backend has started');
}
+ this.#addFeature(feature);
+ }
+
+ #addFeature(feature: BackendFeature) {
if (feature.$$type !== '@backstage/BackendFeature') {
throw new Error(
`Failed to add feature, invalid type '${feature.$$type}'`,
);
}
- const internalFeature = feature as InternalBackendFeature;
- if (internalFeature.version !== 'v1') {
+
+ if (isServiceFactory(feature)) {
+ if (feature.service.id === coreServices.pluginMetadata.id) {
+ throw new Error(
+ `The ${coreServices.pluginMetadata.id} service cannot be overridden`,
+ );
+ }
+ if (
+ this.#providedServiceFactories.find(
+ sf => sf.service.id === feature.service.id,
+ )
+ ) {
+ throw new Error(
+ `Duplicate service implementations provided for ${feature.service.id}`,
+ );
+ }
+ this.#providedServiceFactories.push(feature);
+ } else if (isInternalBackendFeature(feature)) {
+ if (feature.version !== 'v1') {
+ throw new Error(
+ `Failed to add feature, invalid version '${feature.version}'`,
+ );
+ }
+ this.#features.push(feature);
+ } else {
throw new Error(
- `Failed to add feature, invalid version '${internalFeature.version}'`,
+ `Failed to add feature, invalid feature ${JSON.stringify(feature)}`,
);
}
- this.#features.push(internalFeature);
}
async start(): Promise {
@@ -129,6 +167,23 @@ export class BackendInitializer {
}
async #doStart(): Promise {
+ this.#serviceHolder = new ServiceRegistry([
+ ...this.#defaultApiFactories,
+ ...this.#providedServiceFactories,
+ ]);
+
+ const featureDiscovery = await this.#serviceHolder.get(
+ featureDiscoveryServiceRef,
+ 'root',
+ );
+
+ if (featureDiscovery) {
+ const { features } = await featureDiscovery.getBackendFeatures();
+ for (const feature of features) {
+ this.#addFeature(feature);
+ }
+ }
+
// Initialize all root scoped services
for (const ref of this.#serviceHolder.getServiceRefs()) {
if (ref.scope === 'root') {
@@ -144,14 +199,17 @@ export class BackendInitializer {
for (const r of feature.getRegistrations()) {
const provides = new Set>();
- if (r.type === 'plugin') {
+ if (r.type === 'plugin' || r.type === 'module') {
for (const [extRef, extImpl] of r.extensionPoints) {
if (this.#extensionPoints.has(extRef)) {
throw new Error(
`ExtensionPoint with ID '${extRef.id}' is already registered`,
);
}
- this.#extensionPoints.set(extRef, extImpl);
+ this.#extensionPoints.set(extRef, {
+ impl: extImpl,
+ pluginId: r.pluginId,
+ });
provides.add(extRef);
}
}
@@ -193,21 +251,41 @@ export class BackendInitializer {
await Promise.all(
allPluginIds.map(async pluginId => {
// Modules are initialized before plugins, so that they can provide extension to the plugin
- const modules = moduleInits.get(pluginId) ?? [];
- await Promise.all(
- Array.from(modules).map(async ([moduleId, moduleInit]) => {
- const moduleDeps = await this.#getInitDeps(
- moduleInit.init.deps,
- pluginId,
+ const modules = moduleInits.get(pluginId);
+ if (modules) {
+ const tree = DependencyGraph.fromIterable(
+ Array.from(modules).map(([moduleId, moduleInit]) => ({
+ value: { moduleId, moduleInit },
+ // Relationships are reversed at this point since we're only interested in the extension points.
+ // If a modules provides extension point A we want it to be initialized AFTER all modules
+ // that depend on extension point A, so that they can provide their extensions.
+ consumes: Array.from(moduleInit.provides).map(p => p.id),
+ provides: Array.from(moduleInit.consumes).map(c => c.id),
+ })),
+ );
+ const circular = tree.detectCircularDependency();
+ if (circular) {
+ throw new ConflictError(
+ `Circular dependency detected for modules of plugin '${pluginId}', ${circular
+ .map(({ moduleId }) => `'${moduleId}'`)
+ .join(' -> ')}`,
);
- await moduleInit.init.func(moduleDeps).catch(error => {
- throw new ForwardedError(
- `Module '${moduleId}' for plugin '${pluginId}' startup failed`,
- error,
+ }
+ await tree.parallelTopologicalTraversal(
+ async ({ moduleId, moduleInit }) => {
+ const moduleDeps = await this.#getInitDeps(
+ moduleInit.init.deps,
+ pluginId,
);
- });
- }),
- );
+ await moduleInit.init.func(moduleDeps).catch(error => {
+ throw new ForwardedError(
+ `Module '${moduleId}' for plugin '${pluginId}' startup failed`,
+ error,
+ );
+ });
+ },
+ );
+ }
// Once all modules have been initialized, we can initialize the plugin itself
const pluginInit = pluginInits.get(pluginId);
@@ -272,7 +350,7 @@ export class BackendInitializer {
// Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this
async #getRootLifecycleImpl(): Promise {
- const lifecycleService = await this.#serviceHolder.get(
+ const lifecycleService = await this.#serviceHolder!.get(
coreServices.rootLifecycle,
'root',
);
@@ -285,7 +363,7 @@ export class BackendInitializer {
async #getPluginLifecycleImpl(
pluginId: string,
): Promise {
- const lifecycleService = await this.#serviceHolder.get(
+ const lifecycleService = await this.#serviceHolder!.get(
coreServices.lifecycle,
pluginId,
);
@@ -295,3 +373,15 @@ export class BackendInitializer {
throw new Error('Unexpected plugin lifecycle service implementation');
}
}
+
+function isServiceFactory(feature: BackendFeature): feature is ServiceFactory {
+ return !!(feature as ServiceFactory).service;
+}
+
+function isInternalBackendFeature(
+ feature: BackendFeature,
+): feature is InternalBackendFeature {
+ return (
+ typeof (feature as InternalBackendFeature).getRegistrations === 'function'
+ );
+}
diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts
index c53f47b685..0ea55bc1c7 100644
--- a/packages/backend-app-api/src/wiring/BackstageBackend.ts
+++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts
@@ -14,22 +14,19 @@
* limitations under the License.
*/
-import { ServiceFactory, BackendFeature } from '@backstage/backend-plugin-api';
+import { BackendFeature, ServiceFactory } from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
-import { ServiceRegistry } from './ServiceRegistry';
import { Backend } from './types';
export class BackstageBackend implements Backend {
- #services: ServiceRegistry;
#initializer: BackendInitializer;
- constructor(apiFactories: ServiceFactory[]) {
- this.#services = new ServiceRegistry(apiFactories);
- this.#initializer = new BackendInitializer(this.#services);
+ constructor(defaultServiceFactories: ServiceFactory[]) {
+ this.#initializer = new BackendInitializer(defaultServiceFactories);
}
- add(feature: BackendFeature): void {
- this.#initializer.add(feature);
+ add(feature: BackendFeature | (() => BackendFeature)): void {
+ this.#initializer.add(typeof feature === 'function' ? feature() : feature);
}
async start(): Promise {
diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts
index 0e1d8614f9..d2f36339fa 100644
--- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts
+++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts
@@ -31,7 +31,7 @@ const sf1 = createServiceFactory({
async factory() {
return { x: 1 };
},
-})();
+});
const ref2 = createServiceRef<{ x: number }>({
scope: 'root',
@@ -43,14 +43,14 @@ const sf2 = createServiceFactory({
async factory() {
return { x: 2 };
},
-})();
+});
const sf2b = createServiceFactory({
service: ref2,
deps: {},
async factory() {
return { x: 22 };
},
-})();
+});
const refDefault1 = createServiceRef<{ x: number }>({
id: '1',
@@ -61,7 +61,7 @@ const refDefault1 = createServiceRef<{ x: number }>({
async factory() {
return { x: 10 };
},
- })(),
+ }),
});
const refDefault2a = createServiceRef<{ x: number }>({
@@ -95,7 +95,7 @@ describe('ServiceRegistry', () => {
});
it('should return an implementation for a registered ref', async () => {
- const registry = new ServiceRegistry([sf1]);
+ const registry = new ServiceRegistry([sf1()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 });
await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 });
expect(await registry.get(ref1, 'catalog')).toBe(
@@ -110,7 +110,7 @@ describe('ServiceRegistry', () => {
});
it('should handle multiple factories with different serviceRefs', async () => {
- const registry = new ServiceRegistry([sf1, sf2]);
+ const registry = new ServiceRegistry([sf1(), sf2()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
x: 1,
@@ -131,7 +131,7 @@ describe('ServiceRegistry', () => {
return { x: 2 };
},
});
- const registry = new ServiceRegistry([factory(), sf1]);
+ const registry = new ServiceRegistry([factory(), sf1()]);
await expect(registry.get(ref2, 'catalog')).rejects.toThrow(
"Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.",
);
@@ -145,7 +145,7 @@ describe('ServiceRegistry', () => {
return { x: rootDep.x };
},
});
- const registry = new ServiceRegistry([factory(), sf2]);
+ const registry = new ServiceRegistry([factory(), sf2()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
x: 2,
});
@@ -160,7 +160,7 @@ describe('ServiceRegistry', () => {
return { x: rootDep.x };
},
});
- const registry = new ServiceRegistry([factory(), sf2]);
+ const registry = new ServiceRegistry([factory(), sf2()]);
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
x: 2,
});
@@ -182,7 +182,7 @@ describe('ServiceRegistry', () => {
});
it('should use the last factory for each ref', async () => {
- const registry = new ServiceRegistry([sf2, sf2b]);
+ const registry = new ServiceRegistry([sf2(), sf2b()]);
await expect(registry.get(ref2, 'catalog')).resolves.toEqual({
x: 22,
});
@@ -196,7 +196,7 @@ describe('ServiceRegistry', () => {
});
it('should not use the defaultFactory from the ref if provided to the registry', async () => {
- const registry = new ServiceRegistry([sf1]);
+ const registry = new ServiceRegistry([sf1()]);
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
x: 1,
});
diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts
index de26619816..d9d7120dbd 100644
--- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts
+++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts
@@ -39,7 +39,7 @@ function toInternalServiceFactory(
factory: ServiceFactory,
): InternalServiceFactory {
const f = factory as InternalServiceFactory;
- if (f.$$type !== '@backstage/ServiceFactory') {
+ if (f.$$type !== '@backstage/BackendFeature') {
throw new Error(`Invalid service factory, bad type '${f.$$type}'`);
}
if (f.version !== 'v1') {
@@ -49,10 +49,10 @@ function toInternalServiceFactory(
}
const pluginMetadataServiceFactory = createServiceFactory(
- (options: { pluginId: string }) => ({
+ (options?: { pluginId: string }) => ({
service: coreServices.pluginMetadata,
deps: {},
- factory: async () => ({ getId: () => options.pluginId }),
+ factory: async () => ({ getId: () => options?.pluginId! }),
}),
);
diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts
index dc9a0cc085..1fd7fcbbec 100644
--- a/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts
+++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts
@@ -22,13 +22,15 @@ import { createSpecializedBackend } from './createSpecializedBackend';
describe('createSpecializedBackend', () => {
it('should create a backend without services', () => {
- expect(() => createSpecializedBackend({ services: [] })).not.toThrow();
+ expect(() =>
+ createSpecializedBackend({ defaultServiceFactories: [] }),
+ ).not.toThrow();
});
it('should throw on duplicate service implementations', () => {
expect(() =>
createSpecializedBackend({
- services: [
+ defaultServiceFactories: [
createServiceFactory({
service: coreServices.rootLifecycle,
deps: {},
@@ -55,7 +57,7 @@ describe('createSpecializedBackend', () => {
it('should throw when providing a plugin metadata service implementation', () => {
expect(() =>
createSpecializedBackend({
- services: [
+ defaultServiceFactories: [
createServiceFactory({
service: coreServices.pluginMetadata,
deps: {},
diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
index 15e51f7c4c..f5c4d8b152 100644
--- a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
+++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
@@ -24,7 +24,7 @@ import { Backend, CreateSpecializedBackendOptions } from './types';
export function createSpecializedBackend(
options: CreateSpecializedBackendOptions,
): Backend {
- const services = options.services.map(sf =>
+ const services = options.defaultServiceFactories.map(sf =>
typeof sf === 'function' ? sf() : sf,
);
diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts
index ffedb40324..552a69a792 100644
--- a/packages/backend-app-api/src/wiring/types.ts
+++ b/packages/backend-app-api/src/wiring/types.ts
@@ -25,7 +25,7 @@ import {
* @public
*/
export interface Backend {
- add(feature: BackendFeature): void;
+ add(feature: BackendFeature | (() => BackendFeature)): void;
start(): Promise;
stop(): Promise;
}
@@ -34,7 +34,7 @@ export interface Backend {
* @public
*/
export interface CreateSpecializedBackendOptions {
- services: ServiceFactoryOrFunction[];
+ defaultServiceFactories: ServiceFactoryOrFunction[];
}
export interface ServiceHolder {
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md
index f0c6d75478..030223606f 100644
--- a/packages/backend-common/CHANGELOG.md
+++ b/packages/backend-common/CHANGELOG.md
@@ -1,5 +1,38 @@
# @backstage/backend-common
+## 0.19.2
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- 443afcf7f567: To improve performance, `GerritUrlReader.readTree()` now uses Gitiles to fetch an archive instead of cloning the repository.
+ If `gitilesBaseUrl` is not configured, `readTree` still uses Git to clone the repository.
+
+ Added `stripFirstDirectory` option to `ReadTreeResponseFactory.fromTarArchive()`, allowing to disable stripping first directory
+ for `tar` archives.
+
+- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result.
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0
+ - @backstage/config-loader@1.4.0
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/integration@1.6.0
+ - @backstage/integration-aws-node@0.1.5
+ - @backstage/backend-dev-utils@0.1.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 0.19.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/config-loader@1.4.0-next.1
+
## 0.19.2-next.1
### Patch Changes
diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md
index fae3e2117d..f8e535c8a7 100644
--- a/packages/backend-common/api-report.md
+++ b/packages/backend-common/api-report.md
@@ -610,7 +610,9 @@ export interface ReadTreeResponseFactory {
): Promise;
// (undocumented)
fromTarArchive(
- options: ReadTreeResponseFactoryOptions,
+ options: ReadTreeResponseFactoryOptions & {
+ stripFirstDirectory?: boolean;
+ },
): Promise;
// (undocumented)
fromZipArchive(
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index e18369b7e7..fd9b9cb0df 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
- "version": "0.19.2-next.1",
+ "version": "0.19.2",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-common/src/reading/GerritUrlReader.test.ts b/packages/backend-common/src/reading/GerritUrlReader.test.ts
index 4e0b97fc58..9de9aac33d 100644
--- a/packages/backend-common/src/reading/GerritUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GerritUrlReader.test.ts
@@ -37,14 +37,16 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
+const cloneMock = jest.fn(() => Promise.resolve());
jest.mock('../scm', () => ({
Git: {
fromAuth: () => ({
- clone: jest.fn(() => Promise.resolve({})),
+ clone: cloneMock,
}),
},
}));
+// Gerrit processor without a gitilesBaseUrl configured
const gerritProcessor = new GerritUrlReader(
new GerritIntegration(
readGerritIntegrationConfig(
@@ -57,6 +59,21 @@ const gerritProcessor = new GerritUrlReader(
'/tmp',
);
+// Gerrit processor with a gitilesBaseUrl configured.
+// Use to test readTree with Gitiles archive download.
+const gerritProcessorWithGitiles = new GerritUrlReader(
+ new GerritIntegration(
+ readGerritIntegrationConfig(
+ new ConfigReader({
+ host: 'gerrit.com',
+ gitilesBaseUrl: 'https://gerrit.com/gitiles',
+ }),
+ ),
+ ),
+ { treeResponseFactory },
+ '/tmp',
+);
+
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
return GerritUrlReader.factory({
config: new ConfigReader(config),
@@ -217,9 +234,17 @@ describe('GerritUrlReader', () => {
path.resolve(__dirname, '__fixtures__/gerrit/branch-info-response.txt'),
);
const treeUrl = 'https://gerrit.com/app/web/+/refs/heads/master/';
+ const treeUrlGitiles =
+ 'https://gerrit.com/gitiles/app/web/+/refs/heads/master/';
const etag = '52432507a70b677b5674b019c9a46b2e9f29d0a1';
- const mkdocsContent = 'great content';
+ const mkdocsContent = 'a repo fetched using git clone';
const mdContent = 'doc';
+ const repoArchiveBuffer = fs.readFileSync(
+ path.resolve(__dirname, '__fixtures__/gerrit/gerrit-master.tar.gz'),
+ );
+ const repoArchiveDocsBuffer = fs.readFileSync(
+ path.resolve(__dirname, '__fixtures__/gerrit/gerrit-master-docs.tar.gz'),
+ );
beforeEach(() => {
mockFs({
@@ -229,6 +254,35 @@ describe('GerritUrlReader', () => {
});
const spy = jest.spyOn(fs, 'mkdtemp');
spy.mockImplementation(() => '/tmp/gerrit-clone-123abc');
+
+ worker.use(
+ rest.get(
+ 'https://gerrit.com/gitiles/app/web/\\+archive/refs/heads/master.tar.gz',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/x-gzip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=web-refs/heads/master.tar.gz',
+ ),
+ ctx.body(repoArchiveBuffer),
+ ),
+ ),
+ rest.get(
+ 'https://gerrit.com/gitiles/app/web/\\+archive/refs/heads/master/docs.tar.gz',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/x-gzip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=web-refs/heads/master-docs.tar.gz',
+ ),
+ ctx.body(repoArchiveDocsBuffer),
+ ),
+ ),
+ );
});
afterEach(() => {
@@ -236,7 +290,32 @@ describe('GerritUrlReader', () => {
jest.clearAllMocks();
});
- it('reads the wanted files correctly.', async () => {
+ it('reads the wanted files correctly using gitiles.', async () => {
+ worker.use(
+ rest.get(branchAPIUrl, (_, res, ctx) => {
+ return res(ctx.status(200), ctx.body(branchAPIresponse));
+ }),
+ );
+
+ const response = await gerritProcessorWithGitiles.readTree(
+ treeUrlGitiles,
+ );
+
+ expect(response.etag).toBe(etag);
+
+ const files = await response.files();
+ expect(files.length).toBe(2);
+
+ const docsYaml = await files[0].content();
+ expect(docsYaml.toString()).toBe('# Test\n');
+
+ const mdFile = await files[1].content();
+ expect(mdFile.toString()).toBe('site_name: Test\n');
+
+ expect(cloneMock).not.toHaveBeenCalled();
+ });
+
+ it('reads the wanted files correctly using git clone.', async () => {
worker.use(
rest.get(branchAPIUrl, (_, res, ctx) => {
return res(ctx.status(200), ctx.body(branchAPIresponse));
@@ -255,6 +334,8 @@ describe('GerritUrlReader', () => {
const mdFile = await files[1].content();
expect(mdFile.toString()).toBe(mdContent);
+
+ expect(cloneMock).toHaveBeenCalled();
});
it('throws NotModifiedError for matching etags.', async () => {
@@ -291,7 +372,29 @@ describe('GerritUrlReader', () => {
await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow(Error);
});
- it('should returns wanted files with a subpath', async () => {
+ it('should returns wanted files with a subpath using gitiles', async () => {
+ worker.use(
+ rest.get(branchAPIUrl, (_, res, ctx) => {
+ return res(ctx.status(200), ctx.body(branchAPIresponse));
+ }),
+ );
+
+ const response = await gerritProcessorWithGitiles.readTree(
+ `${treeUrlGitiles}/docs`,
+ );
+
+ expect(response.etag).toBe(etag);
+
+ const files = await response.files();
+ expect(files.length).toBe(1);
+
+ const mdFile = await files[0].content();
+ expect(mdFile.toString()).toBe('# Test\n');
+
+ expect(cloneMock).not.toHaveBeenCalled();
+ });
+
+ it('should returns wanted files with a subpath using git clone', async () => {
worker.use(
rest.get(branchAPIUrl, (_, res, ctx) => {
return res(ctx.status(200), ctx.body(branchAPIresponse));
@@ -307,6 +410,8 @@ describe('GerritUrlReader', () => {
const mdFile = await files[0].content();
expect(mdFile.toString()).toBe(mdContent);
+
+ expect(cloneMock).toHaveBeenCalled();
});
});
});
diff --git a/packages/backend-common/src/reading/GerritUrlReader.ts b/packages/backend-common/src/reading/GerritUrlReader.ts
index 8ac5452a4c..f4a5fdb9f5 100644
--- a/packages/backend-common/src/reading/GerritUrlReader.ts
+++ b/packages/backend-common/src/reading/GerritUrlReader.ts
@@ -14,16 +14,17 @@
* limitations under the License.
*/
-import { Git } from '../scm';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import {
GerritIntegration,
- getGerritCloneRepoUrl,
+ ScmIntegrations,
+ buildGerritGitilesArchiveUrl,
getGerritBranchApiUrl,
+ getGerritCloneRepoUrl,
getGerritFileContentsApiUrl,
getGerritRequestOptions,
- parseGerritJsonResponse,
parseGerritGitilesUrl,
+ parseGerritJsonResponse,
} from '@backstage/integration';
import { Base64Decode } from 'base64-stream';
import concatStream from 'concat-stream';
@@ -31,20 +32,20 @@ import fs from 'fs-extra';
import fetch, { Response } from 'node-fetch';
import os from 'os';
import { join as joinPath } from 'path';
+import { Readable, pipeline as pipelineCb } from 'stream';
import tar from 'tar';
-import { pipeline as pipelineCb, Readable } from 'stream';
import { promisify } from 'util';
+import { Git } from '../scm';
import {
- ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
ReadTreeResponseFactory,
ReadUrlOptions,
ReadUrlResponse,
+ ReaderFactory,
SearchResponse,
UrlReader,
} from './types';
-import { ScmIntegrations } from '@backstage/integration';
const pipeline = promisify(pipelineCb);
@@ -59,6 +60,8 @@ const createTemporaryDirectory = async (workDir: string): Promise =>
* way we are depending on that there is a Gitiles installation somewhere
* that we can link to. It is perfectly possible to integrate Gerrit with
* Backstage without Gitiles since all API calls goes directly to Gerrit.
+ * However if Gitiles is configured, readTree will use it to fetch
+ * an archive instead of cloning the repository.
*
* The "host" variable in the config is the Gerrit host. The address where
* Gitiles is installed may be on the same host but it could be on a
@@ -125,6 +128,7 @@ export class GerritUrlReader implements UrlReader {
} catch (e) {
throw new Error(`Unable to read gerrit file ${url}, ${e}`);
}
+
if (response.ok) {
let responseBody: string;
return {
@@ -152,7 +156,6 @@ export class GerritUrlReader implements UrlReader {
url: string,
options?: ReadTreeOptions,
): Promise {
- const { filePath } = parseGerritGitilesUrl(this.integration.config, url);
const apiUrl = getGerritBranchApiUrl(this.integration.config, url);
let response: Response;
try {
@@ -180,6 +183,30 @@ export class GerritUrlReader implements UrlReader {
throw new NotModifiedError();
}
+ if (
+ this.integration.config.gitilesBaseUrl !== this.integration.config.baseUrl
+ ) {
+ return this.readTreeFromGitiles(url, branchInfo.revision, options);
+ }
+ return this.readTreeFromGitClone(url, branchInfo.revision, options);
+ }
+
+ async search(): Promise {
+ throw new Error('GerritReader does not implement search');
+ }
+
+ toString() {
+ const { host, password } = this.integration.config;
+ return `gerrit{host=${host},authed=${Boolean(password)}}`;
+ }
+
+ private async readTreeFromGitClone(
+ url: string,
+ revision: string,
+ options?: ReadTreeOptions,
+ ) {
+ const { filePath } = parseGerritGitilesUrl(this.integration.config, url);
+
const git = Git.fromAuth({
username: this.integration.config.username,
password: this.integration.config.password,
@@ -192,7 +219,7 @@ export class GerritUrlReader implements UrlReader {
await git.clone({
url: cloneUrl,
dir: joinPath(tempDir, 'repo'),
- ref: branchInfo.revision,
+ ref: revision,
depth: 1,
});
@@ -206,7 +233,7 @@ export class GerritUrlReader implements UrlReader {
return await this.deps.treeResponseFactory.fromTarArchive({
stream: tarArchive,
subpath: filePath === '/' ? undefined : filePath,
- etag: branchInfo.revision,
+ etag: revision,
filter: options?.filter,
});
} catch (error) {
@@ -216,12 +243,47 @@ export class GerritUrlReader implements UrlReader {
}
}
- async search(): Promise {
- throw new Error('GerritReader does not implement search');
- }
+ private async readTreeFromGitiles(
+ url: string,
+ revision: string,
+ options?: ReadTreeOptions,
+ ) {
+ const { branch, filePath, project } = parseGerritGitilesUrl(
+ this.integration.config,
+ url,
+ );
+ const archiveUrl = buildGerritGitilesArchiveUrl(
+ this.integration.config,
+ project,
+ branch,
+ filePath,
+ );
+ const archiveResponse = await fetch(archiveUrl, {
+ ...getGerritRequestOptions(this.integration.config),
+ // TODO(freben): The signal cast is there because pre-3.x versions of
+ // node-fetch have a very slightly deviating AbortSignal type signature.
+ // The difference does not affect us in practice however. The cast can
+ // be removed after we support ESM for CLI dependencies and migrate to
+ // version 3 of node-fetch.
+ // https://github.com/backstage/backstage/issues/8242
+ signal: options?.signal as any,
+ });
- toString() {
- const { host, password } = this.integration.config;
- return `gerrit{host=${host},authed=${Boolean(password)}}`;
+ if (archiveResponse.status === 404) {
+ throw new NotFoundError(`Not found: ${archiveUrl}`);
+ }
+
+ if (!archiveResponse.ok) {
+ throw new Error(
+ `${url} could not be read as ${archiveUrl}, ${archiveResponse.status} ${archiveResponse.statusText}`,
+ );
+ }
+
+ return await this.deps.treeResponseFactory.fromTarArchive({
+ stream: archiveResponse.body as unknown as Readable,
+ etag: revision,
+ filter: options?.filter,
+ stripFirstDirectory: false,
+ });
}
}
diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz
new file mode 100644
index 0000000000..425cf0876b
Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz differ
diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz
new file mode 100644
index 0000000000..8a27502734
Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz differ
diff --git a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts
index 11a5ef168a..90c58c0efa 100644
--- a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts
+++ b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts
@@ -37,7 +37,9 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory {
constructor(private readonly workDir: string) {}
async fromTarArchive(
- options: ReadTreeResponseFactoryOptions,
+ options: ReadTreeResponseFactoryOptions & {
+ stripFirstDirectory?: boolean;
+ },
): Promise {
return new TarArchiveResponse(
options.stream,
@@ -45,6 +47,7 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory {
this.workDir,
options.etag,
options.filter,
+ options.stripFirstDirectory ?? true,
);
}
diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
index e1529c196a..294ec22609 100644
--- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
+++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
@@ -44,6 +44,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
private readonly workDir: string,
public readonly etag: string,
private readonly filter?: (path: string, info: { size: number }) => boolean,
+ private readonly stripFirstDirectory: boolean = true,
) {
if (subPath) {
if (!subPath.endsWith('/')) {
@@ -81,7 +82,9 @@ export class TarArchiveResponse implements ReadTreeResponse {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
- const relativePath = stripFirstDirectoryFromPath(entry.path);
+ const relativePath = this.stripFirstDirectory
+ ? stripFirstDirectoryFromPath(entry.path)
+ : entry.path;
if (this.subPath) {
if (!relativePath.startsWith(this.subPath)) {
@@ -148,7 +151,10 @@ export class TarArchiveResponse implements ReadTreeResponse {
// Equivalent of tar --strip-components=N
// When no subPath is given, remove just 1 top level directory
- const strip = this.subPath ? this.subPath.split('/').length : 1;
+ let strip = this.subPath ? this.subPath.split('/').length : 1;
+ if (!this.stripFirstDirectory) {
+ strip--;
+ }
let filterError: Error | undefined = undefined;
await pipeline(
@@ -164,7 +170,9 @@ export class TarArchiveResponse implements ReadTreeResponse {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
- const relativePath = stripFirstDirectoryFromPath(path);
+ const relativePath = this.stripFirstDirectory
+ ? stripFirstDirectoryFromPath(path)
+ : path;
if (this.subPath && !relativePath.startsWith(this.subPath)) {
return false;
}
diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts
index 7f4d407fd0..90a3c6287e 100644
--- a/packages/backend-common/src/reading/types.ts
+++ b/packages/backend-common/src/reading/types.ts
@@ -116,7 +116,13 @@ export type FromReadableArrayOptions = Array<{
*/
export interface ReadTreeResponseFactory {
fromTarArchive(
- options: ReadTreeResponseFactoryOptions,
+ options: ReadTreeResponseFactoryOptions & {
+ /**
+ * Strip the first parent directory of a tar archive.
+ * Defaults to true.
+ */
+ stripFirstDirectory?: boolean;
+ },
): Promise;
fromZipArchive(
options: ReadTreeResponseFactoryOptions,
diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md
index 089f80d745..af86313d11 100644
--- a/packages/backend-defaults/CHANGELOG.md
+++ b/packages/backend-defaults/CHANGELOG.md
@@ -1,5 +1,40 @@
# @backstage/backend-defaults
+## 0.2.0
+
+### Minor Changes
+
+- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system.
+- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated:
+
+ ```ts
+ const backend = createBackend({ services: [myCustomServiceFactory] });
+ ```
+
+ To instead pass the service factory via `backend.add(...)`:
+
+ ```ts
+ const backend = createBackend();
+ backend.add(customRootLoggerServiceFactory);
+ ```
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-app-api@0.5.0
+ - @backstage/backend-plugin-api@0.6.0
+
+## 0.2.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+
## 0.2.0-next.1
### Minor Changes
diff --git a/packages/backend-defaults/api-report.md b/packages/backend-defaults/api-report.md
index 6cc58de663..ab9cc5775c 100644
--- a/packages/backend-defaults/api-report.md
+++ b/packages/backend-defaults/api-report.md
@@ -4,14 +4,7 @@
```ts
import { Backend } from '@backstage/backend-app-api';
-import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api';
// @public (undocumented)
-export function createBackend(options?: CreateBackendOptions): Backend;
-
-// @public (undocumented)
-export interface CreateBackendOptions {
- // (undocumented)
- services?: ServiceFactoryOrFunction[];
-}
+export function createBackend(): Backend;
```
diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json
index 5a89bb51f0..48464ac81e 100644
--- a/packages/backend-defaults/package.json
+++ b/packages/backend-defaults/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-defaults",
"description": "Backend defaults used by Backstage backend apps",
- "version": "0.2.0-next.1",
+ "version": "0.2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts
index de04d4e350..0b36b57069 100644
--- a/packages/backend-defaults/src/CreateBackend.test.ts
+++ b/packages/backend-defaults/src/CreateBackend.test.ts
@@ -22,60 +22,63 @@ import { createBackend } from './CreateBackend';
describe('createBackend', () => {
it('should not throw when overriding a default service implementation', () => {
- expect(() =>
- createBackend({
- services: [
- createServiceFactory({
- service: coreServices.rootLifecycle,
- deps: {},
- factory: async () => ({
- addStartupHook: () => {},
- addShutdownHook: () => {},
- }),
+ const backend = createBackend();
+
+ expect(() => {
+ backend.add(
+ createServiceFactory({
+ service: coreServices.rootLifecycle,
+ deps: {},
+ factory: async () => ({
+ addStartupHook: () => {},
+ addShutdownHook: () => {},
}),
- ],
- }),
- ).not.toThrow();
+ }),
+ );
+ }).not.toThrow();
});
it('should throw on duplicate service implementations', () => {
- expect(() =>
- createBackend({
- services: [
- createServiceFactory({
- service: coreServices.rootLifecycle,
- deps: {},
- factory: async () => ({
- addStartupHook: () => {},
- addShutdownHook: () => {},
- }),
- }),
- createServiceFactory({
- service: coreServices.rootLifecycle,
- deps: {},
- factory: async () => ({
- addStartupHook: () => {},
- addShutdownHook: () => {},
- }),
- }),
- ],
+ const backend = createBackend();
+
+ backend.add(
+ createServiceFactory({
+ service: coreServices.rootLifecycle,
+ deps: {},
+ factory: async () => ({
+ addStartupHook: () => {},
+ addShutdownHook: () => {},
+ }),
}),
- ).toThrow(
+ );
+
+ expect(() => {
+ backend.add(
+ createServiceFactory({
+ service: coreServices.rootLifecycle,
+ deps: {},
+ factory: async () => ({
+ addStartupHook: () => {},
+ addShutdownHook: () => {},
+ }),
+ }),
+ );
+ }).toThrow(
'Duplicate service implementations provided for core.rootLifecycle',
);
});
it('should throw when providing a plugin metadata service implementation', () => {
+ const backend = createBackend();
+
expect(() =>
- createBackend({
- services: [
- createServiceFactory({
- service: coreServices.pluginMetadata,
- deps: {},
- factory: async () => ({ getId: () => 'test' }),
- }),
- ],
- }),
+ backend.add(
+ createServiceFactory({
+ service: coreServices.pluginMetadata,
+ deps: {},
+ factory: async () => ({ getId: () => 'test' }),
+ }),
+ ),
).toThrow('The core.pluginMetadata service cannot be overridden');
});
});
diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts
index 3db013a290..c07d1cc38a 100644
--- a/packages/backend-defaults/src/CreateBackend.ts
+++ b/packages/backend-defaults/src/CreateBackend.ts
@@ -33,10 +33,6 @@ import {
urlReaderServiceFactory,
identityServiceFactory,
} from '@backstage/backend-app-api';
-import {
- ServiceFactory,
- ServiceFactoryOrFunction,
-} from '@backstage/backend-plugin-api';
export const defaultServiceFactories = [
cacheServiceFactory(),
@@ -59,27 +55,6 @@ export const defaultServiceFactories = [
/**
* @public
*/
-export interface CreateBackendOptions {
- services?: ServiceFactoryOrFunction[];
-}
-
-/**
- * @public
- */
-export function createBackend(options?: CreateBackendOptions): Backend {
- const services = new Array();
-
- // Highest priority: Services passed directly to createBackend
- const providedServices = (options?.services ?? []).map(sf =>
- typeof sf === 'function' ? sf() : sf,
- );
- services.push(...providedServices);
-
- // Lowest priority: Default services that are not already provided by environment or directly to createBackend
- const defaultServices = defaultServiceFactories.filter(
- sf => !services.some(({ service }) => service.id === sf.service.id),
- );
- services.push(...defaultServices);
-
- return createSpecializedBackend({ services });
+export function createBackend(): Backend {
+ return createSpecializedBackend({ defaultServiceFactories });
}
diff --git a/packages/backend-defaults/src/index.ts b/packages/backend-defaults/src/index.ts
index 833878744e..d3e36cf0fb 100644
--- a/packages/backend-defaults/src/index.ts
+++ b/packages/backend-defaults/src/index.ts
@@ -25,5 +25,4 @@
// TODO(Rugvip): Remove this once backend-common is no longer used by backend-app-api
import '@backstage/backend-common';
-export type { CreateBackendOptions } from './CreateBackend';
export { createBackend } from './CreateBackend';
diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md
index a5eb6fe466..2c0fefb9f3 100644
--- a/packages/backend-next/CHANGELOG.md
+++ b/packages/backend-next/CHANGELOG.md
@@ -1,5 +1,70 @@
# example-backend-next
+## 0.0.14
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-techdocs@0.1.4
+ - @backstage/plugin-search-backend-module-catalog@0.1.4
+ - @backstage/plugin-search-backend-module-explore@0.1.4
+ - @backstage/plugin-azure-devops-backend@0.3.27
+ - @backstage/plugin-kubernetes-backend@0.11.3
+ - @backstage/plugin-lighthouse-backend@0.2.4
+ - @backstage/plugin-permission-backend@0.5.23
+ - @backstage/plugin-scaffolder-backend@1.16.0
+ - @backstage/backend-defaults@0.2.0
+ - @backstage/plugin-devtools-backend@0.1.3
+ - @backstage/plugin-techdocs-backend@1.6.5
+ - @backstage/plugin-catalog-backend@1.12.0
+ - @backstage/plugin-badges-backend@0.2.3
+ - @backstage/plugin-search-backend@1.4.0
+ - @backstage/plugin-proxy-backend@0.3.0
+ - @backstage/plugin-todo-backend@0.2.0
+ - @backstage/plugin-app-backend@0.3.48
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0
+ - @backstage/plugin-entity-feedback-backend@0.1.6
+ - @backstage/plugin-search-backend-node@1.2.4
+ - @backstage/plugin-linguist-backend@0.4.0
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/backend-tasks@0.5.5
+ - @backstage/plugin-adr-backend@0.3.6
+ - @backstage/plugin-permission-node@0.7.11
+ - @backstage/plugin-permission-common@0.7.7
+
+## 0.0.14-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-techdocs@0.1.4-next.2
+ - @backstage/plugin-search-backend-module-catalog@0.1.4-next.2
+ - @backstage/plugin-search-backend-module-explore@0.1.4-next.2
+ - @backstage/plugin-scaffolder-backend@1.15.2-next.2
+ - @backstage/plugin-catalog-backend@1.12.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/plugin-proxy-backend@0.3.0-next.2
+ - @backstage/backend-tasks@0.5.5-next.2
+ - @backstage/plugin-app-backend@0.3.48-next.2
+ - @backstage/plugin-linguist-backend@0.4.0-next.2
+ - @backstage/plugin-techdocs-backend@1.6.5-next.2
+ - @backstage/backend-defaults@0.2.0-next.2
+ - @backstage/plugin-adr-backend@0.3.6-next.2
+ - @backstage/plugin-azure-devops-backend@0.3.27-next.2
+ - @backstage/plugin-badges-backend@0.2.3-next.2
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2
+ - @backstage/plugin-devtools-backend@0.1.3-next.2
+ - @backstage/plugin-entity-feedback-backend@0.1.6-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.3-next.2
+ - @backstage/plugin-lighthouse-backend@0.2.4-next.2
+ - @backstage/plugin-permission-backend@0.5.23-next.2
+ - @backstage/plugin-permission-node@0.7.11-next.2
+ - @backstage/plugin-search-backend@1.4.0-next.2
+ - @backstage/plugin-search-backend-node@1.2.4-next.2
+ - @backstage/plugin-todo-backend@0.2.0-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
## 0.0.14-next.1
### Patch Changes
diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json
index af4f483db0..c2b923ed26 100644
--- a/packages/backend-next/package.json
+++ b/packages/backend-next/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend-next",
- "version": "0.0.14-next.1",
+ "version": "0.0.14",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -26,6 +26,7 @@
},
"dependencies": {
"@backstage/backend-defaults": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/plugin-adr-backend": "workspace:^",
"@backstage/plugin-app-backend": "workspace:^",
diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts
index bbb7290c73..f3b1ea06f8 100644
--- a/packages/backend-next/src/index.ts
+++ b/packages/backend-next/src/index.ts
@@ -36,7 +36,6 @@ import { badgesPlugin } from '@backstage/plugin-badges-backend';
import { azureDevOpsPlugin } from '@backstage/plugin-azure-devops-backend';
import { linguistPlugin } from '@backstage/plugin-linguist-backend';
import { devtoolsPlugin } from '@backstage/plugin-devtools-backend';
-import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { adrPlugin } from '@backstage/plugin-adr-backend';
import { lighthousePlugin } from '@backstage/plugin-lighthouse-backend';
import { proxyPlugin } from '@backstage/plugin-proxy-backend';
@@ -58,20 +57,7 @@ backend.add(devtoolsPlugin());
backend.add(entityFeedbackPlugin());
// Linguist
-const linguistSchedule: TaskScheduleDefinition = {
- frequency: { minutes: 2 },
- timeout: { minutes: 15 },
- initialDelay: { seconds: 15 },
-};
-
-backend.add(
- linguistPlugin({
- schedule: linguistSchedule,
- age: { days: 30 },
- batchSize: 2,
- useSourceLocation: false,
- }),
-);
+backend.add(linguistPlugin());
// Todo
backend.add(todoPlugin());
diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md
index 3eb95afe95..fc3e52fa92 100644
--- a/packages/backend-openapi-utils/CHANGELOG.md
+++ b/packages/backend-openapi-utils/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/backend-openapi-utils
+## 0.0.3
+
+### Patch Changes
+
+- ebeb77586975: Add a new `createRouter` method for generating an `express` router that validates against your spec. Also fixes a bug with the query parameters type resolution.
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/errors@1.2.1
+
## 0.0.3-next.1
### Patch Changes
diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json
index 9123d6dd77..102d8c36d6 100644
--- a/packages/backend-openapi-utils/package.json
+++ b/packages/backend-openapi-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-openapi-utils",
"description": "OpenAPI typescript support.",
- "version": "0.0.3-next.1",
+ "version": "0.0.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md
index 510797672c..9ea8b95c03 100644
--- a/packages/backend-plugin-api/CHANGELOG.md
+++ b/packages/backend-plugin-api/CHANGELOG.md
@@ -1,5 +1,47 @@
# @backstage/backend-plugin-api
+## 0.6.0
+
+### Minor Changes
+
+- c49785f00cab: **BREAKING**: It is no longer possible to declare options as being required with `createServiceFactory`.
+- 629cbd194a87: **BREAKING**: Renamed `coreServices.config` to `coreServices.rootConfig`.
+- 51987dbdaf87: **BREAKING**: Removed the ability to define options for plugins and modules. Existing options should be migrated to instead use either static configuration or extension points.
+- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system.
+
+### Patch Changes
+
+- c7aa4ff1793c: Allow modules to register extension points.
+- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export.
+- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated:
+
+ ```ts
+ const backend = createBackend({ services: [myCustomServiceFactory] });
+ ```
+
+ To instead pass the service factory via `backend.add(...)`:
+
+ ```ts
+ const backend = createBackend();
+ backend.add(customRootLoggerServiceFactory);
+ ```
+
+- Updated dependencies
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/backend-tasks@0.5.5
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+ - @backstage/plugin-permission-common@0.7.7
+
+## 0.6.0-next.2
+
+### Patch Changes
+
+- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export.
+- Updated dependencies
+ - @backstage/backend-tasks@0.5.5-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
## 0.6.0-next.1
### Minor Changes
diff --git a/packages/backend-plugin-api/alpha-api-report.md b/packages/backend-plugin-api/alpha-api-report.md
new file mode 100644
index 0000000000..81b378a671
--- /dev/null
+++ b/packages/backend-plugin-api/alpha-api-report.md
@@ -0,0 +1,24 @@
+## API Report File for "@backstage/backend-plugin-api"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { ServiceRef } from '@backstage/backend-plugin-api';
+
+// @alpha (undocumented)
+export interface FeatureDiscoveryService {
+ // (undocumented)
+ getBackendFeatures(): Promise<{
+ features: Array;
+ }>;
+}
+
+// @alpha
+export const featureDiscoveryServiceRef: ServiceRef<
+ FeatureDiscoveryService,
+ 'root'
+>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md
index a1ae6284c6..17cc521499 100644
--- a/packages/backend-plugin-api/api-report.md
+++ b/packages/backend-plugin-api/api-report.md
@@ -31,6 +31,11 @@ export interface BackendModuleConfig {
// @public
export interface BackendModuleRegistrationPoints {
+ // (undocumented)
+ registerExtensionPoint(
+ ref: ExtensionPoint,
+ impl: TExtensionPoint,
+ ): void;
// (undocumented)
registerInit<
Deps extends {
@@ -114,14 +119,14 @@ export namespace coreServices {
}
// @public
-export function createBackendModule(
- config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig),
-): (...params: TOptions) => BackendFeature;
+export function createBackendModule(
+ config: BackendModuleConfig,
+): () => BackendFeature;
// @public
-export function createBackendPlugin(
- config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig),
-): (...params: TOptions) => BackendFeature;
+export function createBackendPlugin(
+ config: BackendPluginConfig,
+): () => BackendFeature;
// @public
export function createExtensionPoint(
@@ -152,18 +157,6 @@ export function createServiceFactory<
config: (options?: TOpts) => RootServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-// @public
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends {
- [name in string]: ServiceRef;
- },
- TOpts extends object | undefined = undefined,
->(
- config: (options: TOpts) => RootServiceFactoryConfig,
-): (options: TOpts) => ServiceFactory;
-
// @public
export function createServiceFactory<
TService,
@@ -192,23 +185,6 @@ export function createServiceFactory<
) => PluginServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-// @public
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends {
- [name in string]: ServiceRef;
- },
- TContext = undefined,
- TOpts extends object | undefined = undefined,
->(
- config:
- | PluginServiceFactoryConfig
- | ((
- options: TOpts,
- ) => PluginServiceFactoryConfig),
-): (options: TOpts) => ServiceFactory;
-
// @public
export function createServiceRef(
config: ServiceRefConfig,
@@ -433,9 +409,7 @@ export type SearchResponseFile = {
export interface ServiceFactory<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
-> {
- // (undocumented)
- $$type: '@backstage/ServiceFactory';
+> extends BackendFeature {
// (undocumented)
service: ServiceRef;
}
diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json
index 5d0bfe08fb..44a9d5c69c 100644
--- a/packages/backend-plugin-api/package.json
+++ b/packages/backend-plugin-api/package.json
@@ -1,17 +1,30 @@
{
"name": "@backstage/backend-plugin-api",
"description": "Core API used by Backstage backend plugins",
- "version": "0.6.0-next.1",
+ "version": "0.6.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
- "access": "public",
- "main": "dist/index.cjs.js",
- "types": "dist/index.d.ts"
+ "access": "public"
},
"backstage": {
"role": "node-library"
},
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
"homepage": "https://backstage.io",
"repository": {
"type": "git",
diff --git a/packages/backend-plugin-api/src/alpha.ts b/packages/backend-plugin-api/src/alpha.ts
new file mode 100644
index 0000000000..baee739f49
--- /dev/null
+++ b/packages/backend-plugin-api/src/alpha.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ BackendFeature,
+ createServiceRef,
+} from '@backstage/backend-plugin-api';
+
+/** @alpha */
+export interface FeatureDiscoveryService {
+ getBackendFeatures(): Promise<{ features: Array }>;
+}
+
+/**
+ * An optional service that can be used to dynamically load in additional BackendFeatures at runtime.
+ * @alpha
+ */
+export const featureDiscoveryServiceRef =
+ createServiceRef({
+ id: 'core.featureDiscovery',
+ scope: 'root',
+ });
diff --git a/packages/backend-plugin-api/src/index.ts b/packages/backend-plugin-api/src/index.ts
index 452fa046f9..8e0b23d20b 100644
--- a/packages/backend-plugin-api/src/index.ts
+++ b/packages/backend-plugin-api/src/index.ts
@@ -21,4 +21,5 @@
*/
export * from './services';
+export type { BackendFeature } from './types';
export * from './wiring';
diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts
index d3e9b9dd4e..4d0432505e 100644
--- a/packages/backend-plugin-api/src/services/system/types.test.ts
+++ b/packages/backend-plugin-api/src/services/system/types.test.ts
@@ -26,7 +26,7 @@ interface TestOptions {
function unused(..._any: any[]) {}
describe('createServiceFactory', () => {
- it('should create a sync factory with no options', () => {
+ it('should create a sync factory', () => {
const metaFactory = createServiceFactory({
service: ref,
deps: {},
@@ -51,7 +51,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a sync root factory with no options', () => {
+ it('should create a sync root factory', () => {
const metaFactory = createServiceFactory({
service: rootDep,
deps: {},
@@ -75,7 +75,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with no options', () => {
+ it('should create a factory', () => {
const metaFactory = createServiceFactory({
service: ref,
deps: {},
@@ -100,7 +100,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with optional options', () => {
+ it('should create a factory with options', () => {
const metaFactory = createServiceFactory((_opts?: { x: number }) => ({
service: ref,
deps: {},
@@ -124,7 +124,8 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with required options', () => {
+ it('should not be allowed to require options', () => {
+ // @ts-expect-error
const metaFactory = createServiceFactory((_opts: { x: number }) => ({
service: ref,
deps: {},
@@ -134,23 +135,9 @@ describe('createServiceFactory', () => {
},
}));
expect(metaFactory).toEqual(expect.any(Function));
-
- // @ts-expect-error
- metaFactory('string');
- // @ts-expect-error
- metaFactory({});
- metaFactory({ x: 1 });
- // @ts-expect-error
- metaFactory({ x: 1, y: 2 });
- // @ts-expect-error
- metaFactory(null);
- // @ts-expect-error
- metaFactory(undefined);
- // @ts-expect-error
- metaFactory();
});
- it('should create a factory with optional options as interface', () => {
+ it('should create a factory with options as interface', () => {
const metaFactory = createServiceFactory((_opts?: TestOptions) => ({
service: ref,
deps: {},
@@ -174,32 +161,6 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with required options as interface', () => {
- const metaFactory = createServiceFactory((_opts: TestOptions) => ({
- service: ref,
- deps: {},
- async createRootContext() {},
- async factory() {
- return 'x';
- },
- }));
- expect(metaFactory).toEqual(expect.any(Function));
-
- // @ts-expect-error
- metaFactory('string');
- // @ts-expect-error
- metaFactory({});
- metaFactory({ x: 1 });
- // @ts-expect-error
- metaFactory({ x: 1, y: 2 });
- // @ts-expect-error
- metaFactory(null);
- // @ts-expect-error
- metaFactory(undefined);
- // @ts-expect-error
- metaFactory();
- });
-
it('should create root scoped factory with dependencies', () => {
const metaFactory = createServiceFactory({
service: createServiceRef({ id: 'foo', scope: 'root' }),
@@ -226,7 +187,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create root scoped factory with dependencies and optional options', () => {
+ it('should create root scoped factory with dependencies and options', () => {
const metaFactory = createServiceFactory((_options?: TestOptions) => ({
service: createServiceRef({ id: 'foo', scope: 'root' }),
deps: {
@@ -325,49 +286,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create factory with required options and dependencies', () => {
- const metaFactory = createServiceFactory((_opts: TestOptions) => ({
- service: ref,
- deps: {
- root: rootDep,
- plugin: pluginDep,
- },
- async createRootContext({ root }) {
- const root1: number = root;
- // @ts-expect-error
- const root2: string = root;
- unused(root1, root2);
- return { root };
- },
- async factory({ plugin }, { root }) {
- const root1: number = root;
- // @ts-expect-error
- const root2: string = root;
- const plugin3: boolean = plugin;
- // @ts-expect-error
- const plugin4: number = plugin;
- unused(root1, root2, plugin3, plugin4);
- return 'x';
- },
- }));
- expect(metaFactory).toEqual(expect.any(Function));
-
- // @ts-expect-error
- metaFactory('string');
- // @ts-expect-error
- metaFactory({});
- metaFactory({ x: 1 });
- // @ts-expect-error
- metaFactory({ x: 1, y: 2 });
- // @ts-expect-error
- metaFactory(null);
- // @ts-expect-error
- metaFactory(undefined);
- // @ts-expect-error
- metaFactory();
- });
-
- it('should create factory with optional options and dependencies', () => {
+ it('should create factory with options and dependencies', () => {
const metaFactory = createServiceFactory((_opts?: TestOptions) => ({
service: ref,
deps: {
diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts
index 190ef7c13e..bea53dc1ed 100644
--- a/packages/backend-plugin-api/src/services/system/types.ts
+++ b/packages/backend-plugin-api/src/services/system/types.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { BackendFeature } from '../../types';
+
/**
* TODO
*
@@ -51,9 +53,7 @@ export type ServiceRef<
export interface ServiceFactory<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
-> {
- $$type: '@backstage/ServiceFactory';
-
+> extends BackendFeature {
service: ServiceRef;
}
@@ -191,20 +191,6 @@ export function createServiceFactory<
>(
config: (options?: TOpts) => RootServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-/**
- * Creates a root scoped service factory with required options.
- *
- * @public
- * @param config - The service factory configuration.
- */
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends { [name in string]: ServiceRef },
- TOpts extends object | undefined = undefined,
->(
- config: (options: TOpts) => RootServiceFactoryConfig,
-): (options: TOpts) => ServiceFactory;
/**
* Creates a plugin scoped service factory without options.
*
@@ -237,25 +223,6 @@ export function createServiceFactory<
options?: TOpts,
) => PluginServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-/**
- * Creates a plugin scoped service factory with required options.
- *
- * @public
- * @param config - The service factory configuration.
- */
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends { [name in string]: ServiceRef },
- TContext = undefined,
- TOpts extends object | undefined = undefined,
->(
- config:
- | PluginServiceFactoryConfig
- | ((
- options: TOpts,
- ) => PluginServiceFactoryConfig),
-): (options: TOpts) => ServiceFactory;
export function createServiceFactory<
TService,
TImpl extends TService,
@@ -281,7 +248,7 @@ export function createServiceFactory<
if (anyConf.service.scope === 'root') {
const c = anyConf as RootServiceFactoryConfig;
return {
- $$type: '@backstage/ServiceFactory',
+ $$type: '@backstage/BackendFeature',
version: 'v1',
service: c.service,
deps: c.deps,
@@ -295,7 +262,7 @@ export function createServiceFactory<
TDeps
>;
return {
- $$type: '@backstage/ServiceFactory',
+ $$type: '@backstage/BackendFeature',
version: 'v1',
service: c.service,
...('createRootContext' in c
diff --git a/packages/backend-plugin-api/src/types.ts b/packages/backend-plugin-api/src/types.ts
new file mode 100644
index 0000000000..59f1106fab
--- /dev/null
+++ b/packages/backend-plugin-api/src/types.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** @internal */
+export interface BackendFeatureFactory<
+ TOptions extends [options?: object] = [],
+> {
+ (...options: TOptions): BackendFeature;
+ $$type: '@backstage/BackendFeatureFactory';
+}
+
+/** @public */
+export interface BackendFeature {
+ // NOTE: This type is opaque in order to simplify future API evolution.
+ $$type: '@backstage/BackendFeature';
+}
diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/factories.test.ts
index ae199d84e4..1fd05a1605 100644
--- a/packages/backend-plugin-api/src/wiring/factories.test.ts
+++ b/packages/backend-plugin-api/src/wiring/factories.test.ts
@@ -33,22 +33,19 @@ describe('createExtensionPoint', () => {
describe('createBackendPlugin', () => {
it('should create a BackendPlugin', () => {
- const plugin = createBackendPlugin((_options: { a: string }) => ({
+ const plugin = createBackendPlugin({
pluginId: 'x',
register(r) {
r.registerInit({ deps: {}, async init() {} });
},
- }));
+ });
expect(plugin).toBeDefined();
- expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin({ a: 'a' })).toEqual({
+ expect(plugin()).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
- expect(
- (plugin({ a: 'a' }) as InternalBackendFeature).getRegistrations(),
- ).toEqual([
+ expect((plugin() as InternalBackendFeature).getRegistrations()).toEqual([
{
type: 'plugin',
pluginId: 'x',
@@ -60,95 +57,32 @@ describe('createBackendPlugin', () => {
},
]);
// @ts-expect-error
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
- });
-
- it('should create plugins with optional options', () => {
- const plugin = createBackendPlugin((_options?: { a: string }) => ({
- pluginId: 'x',
- register() {},
- }));
- expect(plugin).toBeDefined();
expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
- });
-
- it('should create plugins without options', () => {
- const plugin = createBackendPlugin({
- pluginId: 'x',
- register() {},
- });
- expect(plugin).toBeDefined();
- // @ts-expect-error
- expect(plugin({ a: 'a' })).toBeDefined();
- // @ts-expect-error
- expect(plugin({})).toBeDefined();
- });
-
- it('should create a BackendPlugin with options as interface', () => {
- interface TestOptions {
- a: string;
- }
- const plugin = createBackendPlugin((_options: TestOptions) => ({
- pluginId: 'x',
- register() {},
- }));
- expect(plugin).toBeDefined();
- expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin({ a: 'a' })).toEqual({
- $$type: '@backstage/BackendFeature',
- version: 'v1',
- getRegistrations: expect.any(Function),
- });
- // @ts-expect-error
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
- });
-
- it('should create plugins with optional options as interface', () => {
- interface TestOptions {
- a: string;
- }
- const plugin = createBackendPlugin((_options?: TestOptions) => ({
- pluginId: 'x',
- register() {},
- }));
- expect(plugin).toBeDefined();
- expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
});
});
describe('createBackendModule', () => {
it('should create a BackendModule', () => {
- const mod = createBackendModule((_options: { a: string }) => ({
+ const mod = createBackendModule({
pluginId: 'x',
moduleId: 'y',
register(r) {
r.registerInit({ deps: {}, async init() {} });
},
- }));
+ });
expect(mod).toBeDefined();
- expect(mod({ a: 'a' })).toBeDefined();
- expect(mod({ a: 'a' })).toEqual({
+ expect(mod()).toBeDefined();
+ expect(mod()).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
- expect(
- (mod({ a: 'a' }) as InternalBackendFeature).getRegistrations(),
- ).toEqual([
+ expect((mod() as InternalBackendFeature).getRegistrations()).toEqual([
{
type: 'module',
pluginId: 'x',
moduleId: 'y',
+ extensionPoints: [],
init: {
deps: expect.any(Object),
func: expect.any(Function),
@@ -156,72 +90,6 @@ describe('createBackendModule', () => {
},
]);
// @ts-expect-error
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
- });
-
- it('should create modules with optional options', () => {
- const mod = createBackendModule((_options?: { a: string }) => ({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- }));
- expect(mod).toBeDefined();
expect(mod({ a: 'a' })).toBeDefined();
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
- });
-
- it('should create modules without options', () => {
- const mod = createBackendModule({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- });
- expect(mod).toBeDefined();
- // @ts-expect-error
- expect(mod({ a: 'a' })).toBeDefined();
- // @ts-expect-error
- expect(mod({})).toBeDefined();
- });
-
- it('should create a BackendModule as interface', () => {
- interface TestOptions {
- a: string;
- }
- const mod = createBackendModule((_options: TestOptions) => ({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- }));
- expect(mod).toBeDefined();
- expect(mod({ a: 'a' })).toBeDefined();
- expect(mod({ a: 'a' })).toEqual({
- $$type: '@backstage/BackendFeature',
- version: 'v1',
- getRegistrations: expect.any(Function),
- });
- // @ts-expect-error
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
- });
-
- it('should create modules with optional options as interface', () => {
- interface TestOptions {
- a: string;
- }
- const mod = createBackendModule((_options?: TestOptions) => ({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- }));
- expect(mod).toBeDefined();
- expect(mod({ a: 'a' })).toBeDefined();
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
});
});
diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts
index 928c933af0..9fa0f87bab 100644
--- a/packages/backend-plugin-api/src/wiring/factories.ts
+++ b/packages/backend-plugin-api/src/wiring/factories.ts
@@ -14,12 +14,11 @@
* limitations under the License.
*/
+import { BackendFeature, BackendFeatureFactory } from '../types';
import {
BackendModuleRegistrationPoints,
BackendPluginRegistrationPoints,
- BackendFeature,
ExtensionPoint,
- InternalBackendFeature,
InternalBackendModuleRegistration,
InternalBackendPluginRegistration,
} from './types';
@@ -85,13 +84,10 @@ export interface BackendPluginConfig {
* @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins}
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
*/
-export function createBackendPlugin(
- config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig),
-): (...params: TOptions) => BackendFeature {
- const configCallback = typeof config === 'function' ? config : () => config;
- return (...options: TOptions): InternalBackendFeature => {
- const c = configCallback(...options);
-
+export function createBackendPlugin(
+ config: BackendPluginConfig,
+): () => BackendFeature {
+ const factory: BackendFeatureFactory = () => {
let registrations: InternalBackendPluginRegistration[];
return {
@@ -106,7 +102,7 @@ export function createBackendPlugin(
let init: InternalBackendPluginRegistration['init'] | undefined =
undefined;
- c.register({
+ config.register({
registerExtensionPoint(ext, impl) {
if (init) {
throw new Error(
@@ -128,14 +124,14 @@ export function createBackendPlugin(
if (!init) {
throw new Error(
- `registerInit was not called by register in ${c.pluginId}`,
+ `registerInit was not called by register in ${config.pluginId}`,
);
}
registrations = [
{
type: 'plugin',
- pluginId: c.pluginId,
+ pluginId: config.pluginId,
extensionPoints,
init,
},
@@ -144,6 +140,9 @@ export function createBackendPlugin(
},
};
};
+ factory.$$type = '@backstage/BackendFeatureFactory';
+
+ return factory;
}
/**
@@ -155,13 +154,14 @@ export function createBackendPlugin(
*/
export interface BackendModuleConfig {
/**
- * The ID of this plugin.
+ * Should exactly match the `id` of the plugin that the module extends.
*
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
*/
pluginId: string;
+
/**
- * Should exactly match the `id` of the plugin that the module extends.
+ * The ID of this module, used to identify the module and ensure that it is not installed twice.
*/
moduleId: string;
register(reg: BackendModuleRegistrationPoints): void;
@@ -174,13 +174,10 @@ export interface BackendModuleConfig {
* @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules}
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
*/
-export function createBackendModule(
- config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig),
-): (...params: TOptions) => BackendFeature {
- const configCallback = typeof config === 'function' ? config : () => config;
- return (...options: TOptions): InternalBackendFeature => {
- const c = configCallback(...options);
-
+export function createBackendModule(
+ config: BackendModuleConfig,
+): () => BackendFeature {
+ const factory: BackendFeatureFactory = () => {
let registrations: InternalBackendModuleRegistration[];
return {
@@ -190,10 +187,20 @@ export function createBackendModule(
if (registrations) {
return registrations;
}
+ const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] =
+ [];
let init: InternalBackendModuleRegistration['init'] | undefined =
undefined;
- c.register({
+ config.register({
+ registerExtensionPoint(ext, impl) {
+ if (init) {
+ throw new Error(
+ 'registerExtensionPoint called after registerInit',
+ );
+ }
+ extensionPoints.push([ext, impl]);
+ },
registerInit(regInit) {
if (init) {
throw new Error('registerInit must only be called once');
@@ -207,15 +214,16 @@ export function createBackendModule(
if (!init) {
throw new Error(
- `registerInit was not called by register in ${c.moduleId} module for ${c.pluginId}`,
+ `registerInit was not called by register in ${config.moduleId} module for ${config.pluginId}`,
);
}
registrations = [
{
type: 'module',
- pluginId: c.pluginId,
- moduleId: c.moduleId,
+ pluginId: config.pluginId,
+ moduleId: config.moduleId,
+ extensionPoints,
init,
},
];
@@ -223,4 +231,7 @@ export function createBackendModule(
},
};
};
+ factory.$$type = '@backstage/BackendFeatureFactory';
+
+ return factory;
}
diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts
index 9cb767b8ac..49f15c0b55 100644
--- a/packages/backend-plugin-api/src/wiring/index.ts
+++ b/packages/backend-plugin-api/src/wiring/index.ts
@@ -27,6 +27,5 @@ export {
export type {
BackendModuleRegistrationPoints,
BackendPluginRegistrationPoints,
- BackendFeature,
ExtensionPoint,
} from './types';
diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts
index c81ee3ceb7..55d43ccb7c 100644
--- a/packages/backend-plugin-api/src/wiring/types.ts
+++ b/packages/backend-plugin-api/src/wiring/types.ts
@@ -15,6 +15,7 @@
*/
import { ServiceRef } from '../services/system/types';
+import { BackendFeature } from '../types';
/**
* TODO
@@ -59,6 +60,10 @@ export interface BackendPluginRegistrationPoints {
* @public
*/
export interface BackendModuleRegistrationPoints {
+ registerExtensionPoint(
+ ref: ExtensionPoint,
+ impl: TExtensionPoint,
+ ): void;
registerInit(options: {
deps: {
[name in keyof Deps]: ServiceRef | ExtensionPoint;
@@ -67,12 +72,6 @@ export interface BackendModuleRegistrationPoints {
}): void;
}
-/** @public */
-export interface BackendFeature {
- // NOTE: This type is opaque in order to simplify future API evolution.
- $$type: '@backstage/BackendFeature';
-}
-
/** @internal */
export interface InternalBackendFeature extends BackendFeature {
version: 'v1';
@@ -97,6 +96,7 @@ export interface InternalBackendModuleRegistration {
pluginId: string;
moduleId: string;
type: 'module';
+ extensionPoints: Array, unknown]>;
init: {
deps: Record | ExtensionPoint>;
func(deps: Record): Promise;
diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md
index 05e51c661c..11a039e053 100644
--- a/packages/backend-tasks/CHANGELOG.md
+++ b/packages/backend-tasks/CHANGELOG.md
@@ -1,5 +1,24 @@
# @backstage/backend-tasks
+## 0.5.5
+
+### Patch Changes
+
+- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 0.5.5-next.2
+
+### Patch Changes
+
+- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.2
+
## 0.5.5-next.1
### Patch Changes
diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json
index 9dd2c9e557..ca9129078e 100644
--- a/packages/backend-tasks/package.json
+++ b/packages/backend-tasks/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
- "version": "0.5.5-next.1",
+ "version": "0.5.5",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md
index 78a8e7cd6d..d6eec9f7ed 100644
--- a/packages/backend-test-utils/CHANGELOG.md
+++ b/packages/backend-test-utils/CHANGELOG.md
@@ -1,5 +1,44 @@
# @backstage/backend-test-utils
+## 0.2.0
+
+### Minor Changes
+
+- b9c57a4f857e: **BREAKING**: Renamed `mockServices.config` to `mockServices.rootConfig`.
+- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated:
+
+ ```ts
+ const backend = createBackend({ services: [myCustomServiceFactory] });
+ ```
+
+ To instead pass the service factory via `backend.add(...)`:
+
+ ```ts
+ const backend = createBackend();
+ backend.add(customRootLoggerServiceFactory);
+ ```
+
+### Patch Changes
+
+- ae9304818136: Add needed constants and constructs to support PostgreSQL version 14 as test database
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-app-api@0.5.0
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+
+## 0.2.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
## 0.2.0-next.1
### Minor Changes
diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md
index 1cb7a83a0e..d086b1d2ae 100644
--- a/packages/backend-test-utils/api-report.md
+++ b/packages/backend-test-utils/api-report.md
@@ -21,7 +21,6 @@ import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootLifecycleService } from '@backstage/backend-plugin-api';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
-import { ServiceRef } from '@backstage/backend-plugin-api';
import { TokenManagerService } from '@backstage/backend-plugin-api';
import { UrlReaderService } from '@backstage/backend-plugin-api';
@@ -127,11 +126,8 @@ export function setupRequestMockHandlers(worker: {
}): void;
// @public (undocumented)
-export function startTestBackend<
- TServices extends any[],
- TExtensionPoints extends any[],
->(
- options: TestBackendOptions,
+export function startTestBackend(
+ options: TestBackendOptions,
): Promise;
// @public (undocumented)
@@ -140,10 +136,7 @@ export interface TestBackend extends Backend {
}
// @public (undocumented)
-export interface TestBackendOptions<
- TServices extends any[],
- TExtensionPoints extends any[],
-> {
+export interface TestBackendOptions {
// (undocumented)
extensionPoints?: readonly [
...{
@@ -154,16 +147,7 @@ export interface TestBackendOptions<
},
];
// (undocumented)
- features?: BackendFeature[];
- // (undocumented)
- services?: readonly [
- ...{
- [index in keyof TServices]:
- | ServiceFactory
- | (() => ServiceFactory)
- | [ServiceRef, Partial];
- },
- ];
+ features?: Array BackendFeature)>;
}
// @public
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index beebf06624..02cb1d5cf9 100644
--- a/packages/backend-test-utils/package.json
+++ b/packages/backend-test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-test-utils",
"description": "Test helpers library for Backstage backends",
- "version": "0.2.0-next.1",
+ "version": "0.2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
index cc916d4309..b127712927 100644
--- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
+++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
@@ -31,7 +31,6 @@ import { startTestBackend } from './TestBackend';
let globalTestBackendHasBeenStopped = false;
beforeAll(async () => {
await startTestBackend({
- services: [],
features: [
createBackendModule({
moduleId: 'test.module',
@@ -46,7 +45,7 @@ beforeAll(async () => {
},
});
},
- })(),
+ }),
],
});
});
@@ -68,17 +67,37 @@ describe('TestBackend', () => {
const extensionPoint5 = createExtensionPoint({ id: 'b5' });
await expect(
startTestBackend({
- services: [
+ features: [
// @ts-expect-error
[extensionPoint1, { a: 'a' }],
- [serviceRef, { a: 'a' }],
- [serviceRef, { a: 'a', b: 'b' }],
- // @ts-expect-error
- [serviceRef, { c: 'c' }],
- // @ts-expect-error
- [serviceRef, { a: 'a', c: 'c' }],
- // @ts-expect-error
- [serviceRef, { a: 'a', b: 'b', c: 'c' }],
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ // @ts-expect-error
+ factory: async () => ({ a: 'a' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ factory: async () => ({ a: 'a', b: 'b' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ // @ts-expect-error
+ factory: async () => ({ c: 'c' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ // @ts-expect-error
+ factory: async () => ({ a: 'a', c: 'c' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ factory: async () => ({ a: 'a', b: 'b', c: 'c' }),
+ })),
],
extensionPoints: [
// @ts-expect-error
@@ -124,8 +143,7 @@ describe('TestBackend', () => {
});
await startTestBackend({
- services: [sf],
- features: [testModule()],
+ features: [testModule(), sf()],
});
expect(testFn).toHaveBeenCalledWith('winning');
@@ -150,7 +168,6 @@ describe('TestBackend', () => {
});
const backend = await startTestBackend({
- services: [],
features: [testModule()],
});
@@ -192,7 +209,6 @@ describe('TestBackend', () => {
});
await startTestBackend({
- services: [],
features: [testPlugin()],
});
});
@@ -220,4 +236,89 @@ describe('TestBackend', () => {
expect(res.status).toEqual(200);
expect(res.body).toEqual({ message: 'pong' });
});
+
+ it('should provide extension point implementations', async () => {
+ expect.assertions(3);
+
+ const extensionPointA = createExtensionPoint({ id: 'a' });
+ const extensionPointB = createExtensionPoint({ id: 'b' });
+ await expect(
+ startTestBackend({
+ extensionPoints: [
+ [extensionPointA, 'a'],
+ [extensionPointB, 'b'],
+ ],
+ features: [
+ createBackendModule({
+ pluginId: 'testA',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointA },
+ async init({ ext }) {
+ expect(ext).toBe('a');
+ },
+ });
+ },
+ }),
+ createBackendModule({
+ pluginId: 'testB',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointB },
+ async init({ ext }) {
+ expect(ext).toBe('b');
+ },
+ });
+ },
+ }),
+ ],
+ }),
+ ).resolves.not.toBeUndefined();
+ });
+
+ it('should reject extension point used by multiple plugins', async () => {
+ const extensionPointA = createExtensionPoint({ id: 'a' });
+ await expect(
+ startTestBackend({
+ extensionPoints: [[extensionPointA, 'a']],
+ features: [
+ createBackendModule({
+ pluginId: 'testA',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointA },
+ async init() {},
+ });
+ },
+ }),
+ createBackendModule({
+ pluginId: 'testB',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointA },
+ async init() {},
+ });
+ },
+ }),
+ ],
+ }),
+ ).rejects.toThrow(
+ "Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'",
+ );
+ });
+
+ it('should reject extension point not used by any plugin', async () => {
+ const extensionPointA = createExtensionPoint({ id: 'a' });
+ await expect(
+ startTestBackend({
+ extensionPoints: [[extensionPointA, 'a']],
+ }),
+ ).rejects.toThrow(
+ "Unable to determine the plugin ID of extension point(s) 'a'. Tested extension points must be depended on by one or more tested modules.",
+ );
+ });
});
diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts
index 8a4e1d8471..97da3543ae 100644
--- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts
+++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts
@@ -24,31 +24,21 @@ import {
} from '@backstage/backend-app-api';
import { HostDiscovery } from '@backstage/backend-common';
import {
- ServiceFactory,
- ServiceRef,
createServiceFactory,
BackendFeature,
ExtensionPoint,
coreServices,
- createBackendPlugin,
+ createBackendModule,
} from '@backstage/backend-plugin-api';
import { mockServices } from '../services';
import { ConfigReader } from '@backstage/config';
import express from 'express';
+// Direct internal import to avoid duplication
+// eslint-disable-next-line @backstage/no-forbidden-package-imports
+import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
/** @public */
-export interface TestBackendOptions<
- TServices extends any[],
- TExtensionPoints extends any[],
-> {
- services?: readonly [
- ...{
- [index in keyof TServices]:
- | ServiceFactory
- | (() => ServiceFactory)
- | [ServiceRef, Partial];
- },
- ];
+export interface TestBackendOptions {
extensionPoints?: readonly [
...{
[index in keyof TExtensionPoints]: [
@@ -57,7 +47,7 @@ export interface TestBackendOptions<
];
},
];
- features?: BackendFeature[];
+ features?: Array BackendFeature)>;
}
/** @public */
@@ -87,21 +77,108 @@ const defaultServiceFactories = [
mockServices.urlReader.factory(),
];
+/**
+ * Given a set of extension points and plugins, find
+ * @returns
+ */
+function createExtensionPointTestModules(
+ features: Array BackendFeature)>,
+ extensionPointTuples?: readonly [
+ ref: ExtensionPoint,
+ impl: unknown,
+ ][],
+): Array<() => BackendFeature> {
+ if (!extensionPointTuples) {
+ return [];
+ }
+
+ const registrations = features.flatMap(featureOrFunction => {
+ const feature =
+ typeof featureOrFunction === 'function'
+ ? featureOrFunction()
+ : featureOrFunction;
+
+ if (feature.$$type !== '@backstage/BackendFeature') {
+ throw new Error(
+ `Failed to add feature, invalid type '${feature.$$type}'`,
+ );
+ }
+
+ if (isInternalBackendFeature(feature)) {
+ if (feature.version !== 'v1') {
+ throw new Error(
+ `Failed to add feature, invalid version '${feature.version}'`,
+ );
+ }
+ return feature.getRegistrations();
+ }
+ return [];
+ });
+
+ const extensionPointMap = new Map(
+ extensionPointTuples.map(ep => [ep[0].id, ep]),
+ );
+ const extensionPointsToSort = new Set(extensionPointMap.keys());
+ const extensionPointsByPlugin = new Map();
+
+ for (const registration of registrations) {
+ if (registration.type === 'module') {
+ const testDep = Object.values(registration.init.deps).filter(dep =>
+ extensionPointsToSort.has(dep.id),
+ );
+ if (testDep.length > 0) {
+ let points = extensionPointsByPlugin.get(registration.pluginId);
+ if (!points) {
+ points = [];
+ extensionPointsByPlugin.set(registration.pluginId, points);
+ }
+ for (const { id } of testDep) {
+ points.push(id);
+ extensionPointsToSort.delete(id);
+ }
+ }
+ }
+ }
+
+ if (extensionPointsToSort.size > 0) {
+ const list = Array.from(extensionPointsToSort)
+ .map(id => `'${id}'`)
+ .join(', ');
+ throw new Error(
+ `Unable to determine the plugin ID of extension point(s) ${list}. ` +
+ 'Tested extension points must be depended on by one or more tested modules.',
+ );
+ }
+
+ const modules = [];
+
+ for (const [pluginId, pluginExtensionPointIds] of extensionPointsByPlugin) {
+ modules.push(
+ createBackendModule({
+ pluginId,
+ moduleId: 'testExtensionPointRegistration',
+ register(reg) {
+ for (const id of pluginExtensionPointIds) {
+ const tuple = extensionPointMap.get(id)!;
+ reg.registerExtensionPoint(...tuple);
+ }
+
+ reg.registerInit({ deps: {}, async init() {} });
+ },
+ }),
+ );
+ }
+
+ return modules;
+}
+
const backendInstancesToCleanUp = new Array();
/** @public */
-export async function startTestBackend<
- TServices extends any[],
- TExtensionPoints extends any[],
->(
- options: TestBackendOptions,
+export async function startTestBackend(
+ options: TestBackendOptions,
): Promise {
- const {
- services = [],
- extensionPoints = [],
- features = [],
- ...otherOptions
- } = options;
+ const { extensionPoints, features = [], ...otherOptions } = options;
let server: ExtendedHttpServer;
@@ -157,55 +234,20 @@ export async function startTestBackend<
},
});
- const factories = services.map(serviceDef => {
- if (Array.isArray(serviceDef)) {
- // if type is ExtensionPoint?
- // do something differently?
- const [ref, impl] = serviceDef;
- if (ref.scope === 'plugin') {
- return createServiceFactory({
- service: ref as ServiceRef,
- deps: {},
- factory: async () => impl,
- })();
- }
- return createServiceFactory({
- service: ref as ServiceRef,
- deps: {},
- factory: async () => impl,
- })();
- }
- if (typeof serviceDef === 'function') {
- return serviceDef();
- }
- return serviceDef as ServiceFactory;
- });
-
- for (const factory of defaultServiceFactories) {
- if (!factories.some(f => f.service.id === factory.service.id)) {
- factories.push(factory);
- }
- }
-
const backend = createSpecializedBackend({
...otherOptions,
- services: [...factories, rootHttpRouterFactory, discoveryFactory],
+ defaultServiceFactories: [
+ ...defaultServiceFactories,
+ rootHttpRouterFactory,
+ discoveryFactory,
+ ],
});
backendInstancesToCleanUp.push(backend);
- backend.add(
- createBackendPlugin({
- pluginId: `---test-extension-point-registrar`,
- register(reg) {
- for (const [ref, impl] of extensionPoints) {
- reg.registerExtensionPoint(ref, impl);
- }
-
- reg.registerInit({ deps: {}, async init() {} });
- },
- })(),
- );
+ for (const m of createExtensionPointTestModules(features, extensionPoints)) {
+ backend.add(m);
+ }
for (const feature of features) {
backend.add(feature);
@@ -248,3 +290,11 @@ function registerTestHooks() {
}
registerTestHooks();
+
+function isInternalBackendFeature(
+ feature: BackendFeature,
+): feature is InternalBackendFeature {
+ return (
+ typeof (feature as InternalBackendFeature).getRegistrations === 'function'
+ );
+}
diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md
index 84c802ae58..76ac221f69 100644
--- a/packages/backend/CHANGELOG.md
+++ b/packages/backend/CHANGELOG.md
@@ -1,5 +1,109 @@
# example-backend
+## 0.2.86
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.3
+ - @backstage/plugin-search-backend-module-pg@0.5.9
+ - @backstage/plugin-azure-devops-backend@0.3.27
+ - @backstage/plugin-kubernetes-backend@0.11.3
+ - @backstage/plugin-lighthouse-backend@0.2.4
+ - @backstage/plugin-permission-backend@0.5.23
+ - @backstage/plugin-scaffolder-backend@1.16.0
+ - @backstage/plugin-devtools-backend@0.1.3
+ - @backstage/plugin-techdocs-backend@1.6.5
+ - @backstage/backend-common@0.19.2
+ - @backstage/plugin-catalog-backend@1.12.0
+ - @backstage/plugin-badges-backend@0.2.3
+ - @backstage/plugin-events-backend@0.2.9
+ - @backstage/plugin-search-backend@1.4.0
+ - @backstage/plugin-kafka-backend@0.2.41
+ - @backstage/plugin-proxy-backend@0.3.0
+ - @backstage/plugin-todo-backend@0.2.0
+ - @backstage/plugin-app-backend@0.3.48
+ - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1
+ - @backstage/plugin-auth-backend@0.18.6
+ - @backstage/plugin-explore-backend@0.0.10
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.17
+ - @backstage/plugin-entity-feedback-backend@0.1.6
+ - @backstage/plugin-code-coverage-backend@0.2.14
+ - @backstage/plugin-search-backend-node@1.2.4
+ - @backstage/plugin-linguist-backend@0.4.0
+ - @backstage/plugin-playlist-backend@0.3.4
+ - @backstage/plugin-jenkins-backend@0.2.3
+ - @backstage/plugin-nomad-backend@0.1.2
+ - @backstage/plugin-catalog-node@1.4.1
+ - @backstage/plugin-events-node@0.2.9
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/integration@1.6.0
+ - @backstage/backend-tasks@0.5.5
+ - example-app@0.2.86
+ - @backstage/plugin-adr-backend@0.3.6
+ - @backstage/plugin-azure-sites-backend@0.1.10
+ - @backstage/plugin-graphql-backend@0.1.38
+ - @backstage/plugin-permission-node@0.7.11
+ - @backstage/plugin-rollbar-backend@0.1.45
+ - @backstage/plugin-tech-insights-backend@0.5.14
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32
+ - @backstage/plugin-tech-insights-node@0.4.6
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/plugin-permission-common@0.7.7
+ - @backstage/plugin-search-common@1.2.5
+
+## 0.2.86-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-auth-backend@0.18.6-next.2
+ - @backstage/plugin-scaffolder-backend@1.15.2-next.2
+ - @backstage/plugin-explore-backend@0.0.10-next.2
+ - @backstage/plugin-catalog-backend@1.12.0-next.2
+ - @backstage/plugin-proxy-backend@0.3.0-next.2
+ - @backstage/backend-tasks@0.5.5-next.2
+ - @backstage/plugin-app-backend@0.3.48-next.2
+ - @backstage/plugin-linguist-backend@0.4.0-next.2
+ - @backstage/plugin-techdocs-backend@1.6.5-next.2
+ - @backstage/backend-common@0.19.2-next.2
+ - example-app@0.2.86-next.2
+ - @backstage/plugin-adr-backend@0.3.6-next.2
+ - @backstage/plugin-azure-devops-backend@0.3.27-next.2
+ - @backstage/plugin-badges-backend@0.2.3-next.2
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2
+ - @backstage/plugin-catalog-node@1.4.1-next.2
+ - @backstage/plugin-devtools-backend@0.1.3-next.2
+ - @backstage/plugin-entity-feedback-backend@0.1.6-next.2
+ - @backstage/plugin-events-backend@0.2.9-next.2
+ - @backstage/plugin-events-node@0.2.9-next.2
+ - @backstage/plugin-kafka-backend@0.2.41-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.3-next.2
+ - @backstage/plugin-lighthouse-backend@0.2.4-next.2
+ - @backstage/plugin-permission-backend@0.5.23-next.2
+ - @backstage/plugin-permission-node@0.7.11-next.2
+ - @backstage/plugin-search-backend@1.4.0-next.2
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.3-next.2
+ - @backstage/plugin-search-backend-module-pg@0.5.9-next.2
+ - @backstage/plugin-search-backend-node@1.2.4-next.2
+ - @backstage/plugin-todo-backend@0.2.0-next.2
+ - @backstage/plugin-tech-insights-backend@0.5.14-next.2
+ - @backstage/plugin-tech-insights-node@0.4.6-next.2
+ - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1-next.2
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.17-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+ - @backstage/plugin-azure-sites-backend@0.1.10-next.2
+ - @backstage/plugin-code-coverage-backend@0.2.14-next.2
+ - @backstage/plugin-graphql-backend@0.1.38-next.2
+ - @backstage/plugin-jenkins-backend@0.2.3-next.2
+ - @backstage/plugin-nomad-backend@0.1.2-next.2
+ - @backstage/plugin-playlist-backend@0.3.4-next.2
+ - @backstage/plugin-rollbar-backend@0.1.45-next.2
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32-next.2
+
## 0.2.86-next.1
### Patch Changes
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 4f5b896d27..9e0896233d 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.2.86-next.1",
+ "version": "0.2.86",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md
index 2615c2ddd7..8a7af72d8d 100644
--- a/packages/cli-node/CHANGELOG.md
+++ b/packages/cli-node/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/cli-node
+## 0.1.3
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
## 0.1.3-next.0
### Patch Changes
diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json
index f87f70b135..589139a141 100644
--- a/packages/cli-node/package.json
+++ b/packages/cli-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-node",
"description": "Node.js library for Backstage CLIs",
- "version": "0.1.3-next.0",
+ "version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index 54660d1773..9294e55efd 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,25 @@
# @backstage/cli
+## 0.22.10
+
+### Patch Changes
+
+- 3f67cefb4780: Reload the frontend when app config changes
+- cebbf8a27f3c: Enable to print the config schema not merged with the `--no-merge` flag
+- 5c28ebc79fd6: Updated dependency `esbuild` to `^0.19.0`.
+- 971bdd6a4732: Bumped internal `nodemon` dependency.
+- Updated dependencies
+ - @backstage/config-loader@1.4.0
+ - @backstage/cli-node@0.1.3
+ - @backstage/integration@1.6.0
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/eslint-plugin@0.1.3
+ - @backstage/release-manifests@0.0.9
+ - @backstage/types@1.1.0
+
## 0.22.10-next.1
### Patch Changes
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 8fe6a579ea..95095fe78c 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.22.10-next.1",
+ "version": "0.22.10",
"publishConfig": {
"access": "public"
},
@@ -78,7 +78,7 @@
"cross-spawn": "^7.0.3",
"css-loader": "^6.5.1",
"diff": "^5.0.0",
- "esbuild": "^0.18.0",
+ "esbuild": "^0.19.0",
"esbuild-loader": "^2.18.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
@@ -167,7 +167,7 @@
"del": "^7.0.0",
"mock-fs": "^5.1.0",
"msw": "^1.0.0",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"ts-node": "^10.0.0",
"type-fest": "^2.19.0"
},
diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md
index b02d08495a..909bb71b37 100644
--- a/packages/config-loader/CHANGELOG.md
+++ b/packages/config-loader/CHANGELOG.md
@@ -1,5 +1,73 @@
# @backstage/config-loader
+## 1.4.0
+
+### Minor Changes
+
+- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored.
+
+ In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`.
+
+- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`.
+
+ Example usage:
+
+ ```ts
+ export interface Config {
+ /**
+ * Enforces a default of `secret` instead of `backend` for this object.
+ * @deepVisibility secret
+ */
+ mySecretProperty: {
+ type: 'object';
+ properties: {
+ secretValue: {
+ type: 'string';
+ };
+
+ verySecretProperty: {
+ type: 'string';
+ };
+ };
+ };
+ }
+ ```
+
+ Example of a schema that would not be allowed:
+
+ ```ts
+ export interface Config {
+ /**
+ * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object.
+ * @deepVisibility secret
+ */
+ mySecretProperty: {
+ type: 'object';
+ properties: {
+ frontendUrl: {
+ /**
+ * We can NOT override the visibility to reveal a property to the front end.
+ * @visibility frontend
+ */
+ type: 'string';
+ };
+
+ verySecretProperty: {
+ type: 'string';
+ };
+ };
+ };
+ }
+ ```
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
## 1.4.0-next.1
### Minor Changes
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index 5e82c0bb76..7fba2d8acb 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
- "version": "1.4.0-next.1",
+ "version": "1.4.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md
index 1de645fb9e..b09076f773 100644
--- a/packages/core-app-api/CHANGELOG.md
+++ b/packages/core-app-api/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/core-app-api
+## 1.9.1
+
+### Patch Changes
+
+- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension.
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+ - @backstage/version-bridge@1.0.4
+
## 1.9.1-next.0
### Patch Changes
diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json
index 15dca326e0..ff43d81329 100644
--- a/packages/core-app-api/package.json
+++ b/packages/core-app-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-app-api",
"description": "Core app API used by Backstage apps",
- "version": "1.9.1-next.0",
+ "version": "1.9.1",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md
index 4ebd92ceb6..af5e2b8d6a 100644
--- a/packages/core-components/CHANGELOG.md
+++ b/packages/core-components/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/core-components
+## 0.13.4
+
+### Patch Changes
+
+- 3d86be999fdf: Prefer simple `theme.spacing` without string interpolation
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/theme@0.4.1
+ - @backstage/version-bridge@1.0.4
+
## 0.13.4-next.0
### Patch Changes
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index c9b1584b85..641a4e7270 100644
--- a/packages/core-components/package.json
+++ b/packages/core-components/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
- "version": "0.13.4-next.0",
+ "version": "0.13.4",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md
index eb2f048d62..9ddd48393b 100644
--- a/packages/create-app/CHANGELOG.md
+++ b/packages/create-app/CHANGELOG.md
@@ -1,5 +1,24 @@
# @backstage/create-app
+## 0.5.4
+
+### Patch Changes
+
+- b441642fbe0d: Bumped create-app version.
+- 572abc7edf55: Bumped create-app version.
+- 74f77f151a96: Bumped create-app version.
+- 5cc0ac5ef3d1: Bump to a newer version of the `concurrently` library
+- 46c9a798e41d: Updated the `app-config.yaml` template to use `proxy.endpoints`.
+- 971bdd6a4732: Bumped internal `nodemon` dependency.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+
+## 0.5.4-next.2
+
+### Patch Changes
+
+- Bumped create-app version.
+
## 0.5.4-next.1
### Patch Changes
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index de1a9791ae..4f629d951e 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
- "version": "0.5.4-next.1",
+ "version": "0.5.4",
"publishConfig": {
"access": "public"
},
@@ -49,7 +49,7 @@
"@types/node": "^16.11.26",
"@types/recursive-readdir": "^2.2.0",
"mock-fs": "^5.1.1",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"ts-node": "^10.0.0"
},
"nodemonConfig": {
diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs
index 194549f079..08d2abff59 100644
--- a/packages/create-app/templates/default-app/app-config.yaml.hbs
+++ b/packages/create-app/templates/default-app/app-config.yaml.hbs
@@ -46,9 +46,10 @@ integrations:
proxy:
### Example for how to add a proxy endpoint for the frontend.
### A typical reason to do this is to handle HTTPS and CORS for internal services.
- # '/test':
- # target: 'https://example.com'
- # changeOrigin: true
+ # endpoints:
+ # '/test':
+ # target: 'https://example.com'
+ # changeOrigin: true
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
# Note: After experimenting with basic setup, use CI/CD to generate docs
diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs
index 0682114a35..c53ed6970f 100644
--- a/packages/create-app/templates/default-app/package.json.hbs
+++ b/packages/create-app/templates/default-app/package.json.hbs
@@ -31,7 +31,7 @@
"devDependencies": {
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@spotify/prettier-config": "^12.0.0",
- "concurrently": "^6.0.0",
+ "concurrently": "^8.0.0",
"lerna": "^4.0.0",
"node-gyp": "^9.0.0",
"prettier": "^2.3.2",
diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md
index 132f330d8f..99fb4f1372 100644
--- a/packages/dev-utils/CHANGELOG.md
+++ b/packages/dev-utils/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/dev-utils
+## 1.0.18
+
+### Patch Changes
+
+- 254ad469f053: Removed deprecated calls to `app.getProvider()` and `app.getRouter()` in `DevAppBuilder`
+- Updated dependencies
+ - @backstage/core-app-api@1.9.1
+ - @backstage/integration-react@1.1.16
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/app-defaults@1.4.2
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 1.0.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/integration-react@1.1.16-next.1
+
## 1.0.18-next.1
### Patch Changes
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index 730d8ffe4e..44b16b4a62 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
- "version": "1.0.18-next.1",
+ "version": "1.0.18",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx
index 36e87855ed..5f2b21b8d2 100644
--- a/packages/dev-utils/src/devApp/render.tsx
+++ b/packages/dev-utils/src/devApp/render.tsx
@@ -15,7 +15,7 @@
*/
import { createApp } from '@backstage/app-defaults';
-import { FlatRoutes } from '@backstage/core-app-api';
+import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
@@ -195,35 +195,30 @@ export class DevAppBuilder {
},
});
- const AppProvider = app.getProvider();
- const AppRouter = app.getRouter();
+ const DevApp = (
+ <>
+
+
+ {this.rootChildren}
+
+
+
+
+ {this.sidebarItems}
+
+
+
+
+
+ {this.routes}
+ } />
+
+
+
+ >
+ );
- const DevApp = () => {
- return (
-
-
-
- {this.rootChildren}
-
-
-
-
- {this.sidebarItems}
-
-
-
-
-
- {this.routes}
- } />
-
-
-
-
- );
- };
-
- return DevApp;
+ return app.createRoot(DevApp);
}
/**
diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md
index a334051aae..6bf5527c4b 100644
--- a/packages/e2e-test/CHANGELOG.md
+++ b/packages/e2e-test/CHANGELOG.md
@@ -1,5 +1,21 @@
# e2e-test
+## 0.2.6
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.4
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+
+## 0.2.6-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.4-next.2
+
## 0.2.6-next.1
### Patch Changes
diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json
index 3708f1bae4..48d4643394 100644
--- a/packages/e2e-test/package.json
+++ b/packages/e2e-test/package.json
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
- "version": "0.2.6-next.1",
+ "version": "0.2.6",
"private": true,
"backstage": {
"role": "cli"
@@ -35,7 +35,7 @@
"cross-fetch": "^3.1.5",
"fs-extra": "10.1.0",
"handlebars": "^4.7.3",
- "pgtools": "^0.3.0",
+ "pgtools": "^1.0.0",
"puppeteer": "^17.0.0",
"tree-kill": "^1.2.2"
},
@@ -44,7 +44,7 @@
"@types/fs-extra": "^9.0.1",
"@types/node": "^16.11.26",
"@types/puppeteer": "^5.4.4",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"ts-node": "^10.0.0"
},
"nodemonConfig": {
diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md
index dd43ed34a0..9d08c09bd9 100644
--- a/packages/integration-react/CHANGELOG.md
+++ b/packages/integration-react/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/integration-react
+## 1.1.16
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/integration@1.6.0
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+
## 1.1.16-next.1
### Patch Changes
diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json
index 71f7597635..ec68a20f36 100644
--- a/packages/integration-react/package.json
+++ b/packages/integration-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
- "version": "1.1.16-next.1",
+ "version": "1.1.16",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md
index d1f3f46306..b4b97f579e 100644
--- a/packages/integration/CHANGELOG.md
+++ b/packages/integration/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/integration
+## 1.6.0
+
+### Minor Changes
+
+- 443afcf7f567: Added `buildGerritGitilesArchiveUrl()` to construct a Gitiles URL to download an archive.
+ Gitiles URL that uses an authenticated prefix (`/a/`) can now be parsed by the integration.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+
## 1.5.1
### Patch Changes
diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md
index c2501b4ac2..7266950ac4 100644
--- a/packages/integration/api-report.md
+++ b/packages/integration/api-report.md
@@ -172,6 +172,14 @@ export type BitbucketServerIntegrationConfig = {
password?: string;
};
+// @public
+export function buildGerritGitilesArchiveUrl(
+ config: GerritIntegrationConfig,
+ project: string,
+ branch: string,
+ filePath: string,
+): string;
+
// @public
export class DefaultGithubCredentialsProvider
implements GithubCredentialsProvider
diff --git a/packages/integration/package.json b/packages/integration/package.json
index e02e070266..b0da382abd 100644
--- a/packages/integration/package.json
+++ b/packages/integration/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration",
"description": "Helpers for managing integrations towards external systems",
- "version": "1.5.1",
+ "version": "1.6.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts
index 5a829e88f9..c00bae384f 100644
--- a/packages/integration/src/gerrit/core.test.ts
+++ b/packages/integration/src/gerrit/core.test.ts
@@ -20,6 +20,7 @@ import fetch from 'cross-fetch';
import { setupRequestMockHandlers } from '../helpers';
import { GerritIntegrationConfig } from './config';
import {
+ buildGerritGitilesArchiveUrl,
buildGerritGitilesUrl,
getGerritBranchApiUrl,
getGerritCloneRepoUrl,
@@ -33,6 +34,41 @@ describe('gerrit core', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
+ describe('buildGerritGitilesArchiveUrl', () => {
+ const config: GerritIntegrationConfig = {
+ host: 'gerrit.com',
+ gitilesBaseUrl: 'https://gerrit.com/gitiles',
+ };
+ it('can create an archive url for a branch', () => {
+ expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual(
+ 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
+ );
+
+ expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '/')).toEqual(
+ 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
+ );
+ });
+ it('can create an archive url for a specific directory', () => {
+ expect(
+ buildGerritGitilesArchiveUrl(config, 'repo', 'dev', 'docs'),
+ ).toEqual(
+ 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
+ );
+ });
+ it('can create an authenticated url when auth is enabled', () => {
+ const authConfig = {
+ ...config,
+ username: 'username',
+ password: 'password',
+ };
+ expect(
+ buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
+ ).toEqual(
+ 'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
+ );
+ });
+ });
+
describe('buildGerritGitilesUrl', () => {
it('can create an url from arguments', () => {
const config: GerritIntegrationConfig = {
@@ -86,6 +122,25 @@ describe('gerrit core', () => {
);
expect(rootPath).toEqual('/');
});
+ it('can parse a valid authenticated gitiles url.', () => {
+ const config: GerritIntegrationConfig = {
+ host: 'gerrit.com',
+ gitilesBaseUrl: 'https://gerrit.com/gitiles',
+ };
+ const { branch, filePath, project } = parseGerritGitilesUrl(
+ config,
+ 'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md',
+ );
+ expect(project).toEqual('web/project');
+ expect(branch).toEqual('master');
+ expect(filePath).toEqual('README.md');
+
+ const { filePath: rootPath } = parseGerritGitilesUrl(
+ config,
+ 'https://gerrit.com/gitiles/web/project/+/refs/heads/master',
+ );
+ expect(rootPath).toEqual('/');
+ });
it('throws on incorrect gitiles urls.', () => {
const config: GerritIntegrationConfig = {
host: 'gerrit.com',
diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts
index 0403d8fc75..107d5a4469 100644
--- a/packages/integration/src/gerrit/core.ts
+++ b/packages/integration/src/gerrit/core.ts
@@ -37,6 +37,7 @@ const GERRIT_BODY_PREFIX = ")]}'";
*
* Gitiles url:
* https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
+ * https://g.com/a/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
*
*
* @param url - An URL pointing to a file stored in git.
@@ -47,7 +48,17 @@ export function parseGerritGitilesUrl(
config: GerritIntegrationConfig,
url: string,
): { branch: string; filePath: string; project: string } {
- const urlPath = url.replace(config.gitilesBaseUrl!, '');
+ const baseUrlParse = new URL(config.gitilesBaseUrl!);
+ const urlParse = new URL(url);
+
+ // Remove the gerrit authentication prefix '/a/' from the url
+ // In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles
+ // and the url provided is https://review.gerrit.com/a/plugins/gitiles/...
+ // remove the prefix only if the pathname start with '/a/'
+ const urlPath = urlParse.pathname
+ .substring(urlParse.pathname.startsWith('/a/') ? 2 : 0)
+ .replace(baseUrlParse.pathname, '');
+
const parts = urlPath.split('/').filter(p => !!p);
const projectEndIndex = parts.indexOf('+');
@@ -91,6 +102,28 @@ export function buildGerritGitilesUrl(
}/${project}/+/refs/heads/${branch}/${trimStart(filePath, '/')}`;
}
+/**
+ * Build a Gerrit Gitiles archive url that targets a specific branch and path
+ *
+ * @param config - A Gerrit provider config.
+ * @param project - The name of the git project
+ * @param branch - The branch we will target.
+ * @param filePath - The absolute file path.
+ * @public
+ */
+export function buildGerritGitilesArchiveUrl(
+ config: GerritIntegrationConfig,
+ project: string,
+ branch: string,
+ filePath: string,
+): string {
+ const archiveName =
+ filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`;
+ return `${getGitilesAuthenticationUrl(
+ config,
+ )}/${project}/+archive/refs/heads/${branch}${archiveName}`;
+}
+
/**
* Return the authentication prefix.
*
@@ -109,6 +142,26 @@ export function getAuthenticationPrefix(
return config.password ? '/a/' : '/';
}
+/**
+ * Return the authentication gitiles url.
+ *
+ * @remarks
+ *
+ * To authenticate with a password the API url must be prefixed with "/a/".
+ * If no password is set anonymous access (without the prefix) will
+ * be used.
+ *
+ * @param config - A Gerrit provider config.
+ */
+export function getGitilesAuthenticationUrl(
+ config: GerritIntegrationConfig,
+): string {
+ const parsedUrl = new URL(config.gitilesBaseUrl!);
+ return `${parsedUrl.protocol}//${parsedUrl.host}${getAuthenticationPrefix(
+ config,
+ )}${parsedUrl.pathname.substring(1)}`;
+}
+
/**
* Return the url to get branch info from the Gerrit API.
*
diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts
index 127c949c4e..6c6295f7a3 100644
--- a/packages/integration/src/gerrit/index.ts
+++ b/packages/integration/src/gerrit/index.ts
@@ -19,6 +19,7 @@ export {
readGerritIntegrationConfigs,
} from './config';
export {
+ buildGerritGitilesArchiveUrl,
getGerritBranchApiUrl,
getGerritCloneRepoUrl,
getGerritFileContentsApiUrl,
diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md
index 763c9f1cc1..ddebca0746 100644
--- a/packages/repo-tools/CHANGELOG.md
+++ b/packages/repo-tools/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/repo-tools
+## 0.3.3
+
+### Patch Changes
+
+- 75702e85862a: Bumped `@microsoft/api-extractor` dependency to `^7.36.4`, and `@microsoft/api-documenter` to `^7.22.33`.
+- 1f3337ebc707: Introducing a new, experimental command `backstage-repo-tools generate-catalog-info`, which can be used to create standardized `catalog-info.yaml` files for each Backstage package in a Backstage monorepo. It can also be used to automatically fix existing `catalog-info.yaml` files with the correct metadata (including `metadata.name`, `metadata.title`, and `metadata.description` introspected from the package's `package.json`, as well as `spec.owner` introspected from `CODEOWNERS`), e.g. in a post-commit hook.
+- ebeb77586975: Update `schema openapi generate` command to now create a default router that can be imported and used directly.
+- Updated dependencies
+ - @backstage/cli-node@0.1.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+
## 0.3.3-next.1
### Patch Changes
diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md
index a0df5e721c..9c87bd8dff 100644
--- a/packages/repo-tools/cli-report.md
+++ b/packages/repo-tools/cli-report.md
@@ -14,6 +14,7 @@ Options:
Commands:
api-reports [options] [paths...]
type-deps
+ generate-catalog-info [options]
schema [command]
help [command]
```
@@ -36,6 +37,16 @@ Options:
-h, --help
```
+### `backstage-repo-tools generate-catalog-info`
+
+```
+Usage: backstage-repo-tools generate-catalog-info [options]
+
+Options:
+ --dry-run
+ -h, --help
+```
+
### `backstage-repo-tools schema`
```
diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json
index c5d9e266c9..82099ea621 100644
--- a/packages/repo-tools/package.json
+++ b/packages/repo-tools/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/repo-tools",
"description": "CLI for Backstage repo tooling ",
- "version": "0.3.3-next.1",
+ "version": "0.3.3",
"publishConfig": {
"access": "public"
},
@@ -32,12 +32,13 @@
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@apisyouwonthate/style-guide": "^1.4.0",
+ "@backstage/catalog-model": "workspace:^",
"@backstage/cli-common": "workspace:^",
"@backstage/cli-node": "workspace:^",
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
- "@microsoft/api-documenter": "^7.19.27",
- "@microsoft/api-extractor": "^7.33.7",
+ "@microsoft/api-documenter": "^7.22.33",
+ "@microsoft/api-extractor": "^7.36.4",
"@stoplight/spectral-core": "^1.18.0",
"@stoplight/spectral-formatters": "^1.1.0",
"@stoplight/spectral-functions": "^1.7.2",
@@ -46,6 +47,7 @@
"@stoplight/spectral-runtime": "^1.1.2",
"@stoplight/types": "^13.14.0",
"chalk": "^4.0.0",
+ "codeowners-utils": "^1.0.2",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
"glob": "^8.0.3",
@@ -54,7 +56,8 @@
"lodash": "^4.17.21",
"minimatch": "^5.1.1",
"p-limit": "^3.0.2",
- "ts-node": "^10.0.0"
+ "ts-node": "^10.0.0",
+ "yaml-diff-patch": "^2.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
diff --git a/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts
new file mode 100644
index 0000000000..f485e872e9
--- /dev/null
+++ b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ CodeOwnersEntry,
+ CODEOWNERS_PATHS,
+ matchFile as matchCodeowner,
+ parse as parseCodeowners,
+} from 'codeowners-utils';
+import { relative as relativePath, resolve as resolvePath } from 'path';
+import { readFile } from './utils';
+
+export async function loadCodeowners(): Promise {
+ const maybeFiles = await Promise.allSettled(
+ CODEOWNERS_PATHS.map(async path =>
+ readFile(resolvePath('.', path), { encoding: 'utf-8' }),
+ ),
+ );
+ const file = maybeFiles.find(
+ maybeFile => maybeFile.status === 'fulfilled',
+ ) as PromiseFulfilledResult | undefined;
+
+ if (!file) {
+ throw new Error(
+ 'This utility expects a CODEOWNERS file, but no such file was found.',
+ );
+ }
+
+ return parseCodeowners(file.value);
+}
+
+export function getPossibleCodeowners(
+ codeowners: CodeOwnersEntry[],
+ relPath: string,
+): string[] {
+ const codeownerMaybe = matchCodeowner(relPath, codeowners);
+ return codeownerMaybe
+ ? codeownerMaybe.owners.map(
+ owner => (owner.match(/(?:\@[^\/]+\/)?([^\@\/]*)$/) || [])[1],
+ )
+ : [];
+}
+
+export function getOwnerFromCodeowners(
+ codeowners: CodeOwnersEntry[],
+ absPath: string,
+): string {
+ const relPath = relativePath('.', absPath);
+ const possibleOwners = getPossibleCodeowners(codeowners, relPath);
+ const owner = possibleOwners.slice(-1)[0];
+
+ if (!owner) {
+ throw new Error(`${relPath} isn't owned by anyone in CODEOWNERS`);
+ }
+
+ return owner;
+}
diff --git a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts
new file mode 100644
index 0000000000..08f73898c8
--- /dev/null
+++ b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2022 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 YAML from 'js-yaml';
+import pLimit from 'p-limit';
+import { relative as relativePath, resolve as resolvePath } from 'path';
+import { yamlOverwrite } from 'yaml-diff-patch';
+import chalk from 'chalk';
+import { PackageGraph, PackageRole } from '@backstage/cli-node';
+import { Entity } from '@backstage/catalog-model';
+import {
+ getOwnerFromCodeowners,
+ getPossibleCodeowners,
+ loadCodeowners,
+} from './codeowners';
+import {
+ BackstagePackageJson,
+ isBackstagePackage,
+ readFile,
+ writeFile,
+} from './utils';
+import { CodeOwnersEntry } from 'codeowners-utils';
+
+type CreateFixPackageInfoYamlsOptions = {
+ dryRun?: boolean;
+};
+
+export default async (opts: CreateFixPackageInfoYamlsOptions) => {
+ const { dryRun = false } = opts;
+ const packages = await PackageGraph.listTargetPackages();
+ const codeowners = await loadCodeowners();
+ const limit = pLimit(10);
+
+ const results = await Promise.allSettled(
+ packages.map(({ packageJson, dir }) =>
+ limit(async () => {
+ if (!isBackstagePackage(packageJson)) {
+ return;
+ }
+
+ // Check if there is already a corresponding catalog-info.yaml
+ const infoYamlPath = resolvePath(dir, 'catalog-info.yaml');
+ let yamlString = '';
+
+ try {
+ yamlString = await readFile(infoYamlPath, { encoding: 'utf-8' });
+ } catch (e) {
+ if (e.code === 'ENOENT') {
+ await createCatalogInfoYaml({
+ yamlPath: infoYamlPath,
+ packageJson,
+ codeowners,
+ dryRun,
+ });
+ return;
+ }
+
+ throw e;
+ }
+
+ await fixCatalogInfoYaml({
+ yamlPath: infoYamlPath,
+ packageJson,
+ codeowners,
+ yamlString,
+ dryRun,
+ });
+ }),
+ ),
+ );
+
+ const rejects = results.filter(
+ r => r.status === 'rejected',
+ ) as PromiseRejectedResult[];
+ if (rejects.length > 0) {
+ // Problems encountered. Print details here.
+ console.error(
+ chalk.red('Unable to create or fix catalog-info.yaml files\n'),
+ );
+ rejects.forEach(reject => console.error(` ${reject.reason}`));
+ console.error();
+ process.exit(1);
+ }
+};
+
+type CreateOptions = {
+ yamlPath: string;
+ packageJson: BackstagePackageJson;
+ codeowners: CodeOwnersEntry[];
+ dryRun: boolean;
+};
+
+type FixOptions = CreateOptions & {
+ yamlString: string;
+};
+
+type BackstagePackageEntity = Entity & {
+ spec: {
+ type: 'backstage-package';
+ backstageRole: PackageRole;
+ lifecycle: string;
+ owner: string;
+ [key: string]: any;
+ };
+};
+
+function createCatalogInfoYaml(options: CreateOptions) {
+ const { codeowners, dryRun, packageJson, yamlPath } = options;
+ const owner = getOwnerFromCodeowners(codeowners, yamlPath);
+ const entity = createOrMergeEntity(packageJson, owner);
+
+ return dryRun
+ ? Promise.resolve(console.error(`Create ${relativePath('.', yamlPath)}`))
+ : writeFile(yamlPath, YAML.dump(entity));
+}
+
+function fixCatalogInfoYaml(options: FixOptions) {
+ const { codeowners, dryRun, packageJson, yamlPath, yamlString } = options;
+ const possibleOwners = getPossibleCodeowners(
+ codeowners,
+ relativePath('.', yamlPath),
+ );
+ const safeName = packageJson.name
+ .replace(/[^a-z0-9_\-\.]+/g, '-')
+ .replace(/^[^a-z0-9]|[^a-z0-9]$/g, '');
+ let yamlJson: BackstagePackageEntity;
+
+ try {
+ yamlJson = YAML.load(yamlString) as BackstagePackageEntity;
+ } catch (e) {
+ throw new Error(`Unable to parse ${relativePath('.', yamlPath)}: ${e}`);
+ }
+
+ const badOwner = !possibleOwners.includes(yamlJson.spec?.owner);
+ const badTitle = yamlJson.metadata.title !== packageJson.name;
+ const badName = yamlJson.metadata.name !== safeName;
+ const badType =
+ yamlJson.spec?.type !== `backstage-${packageJson.backstage.role}`;
+ const badDesc = yamlJson.metadata.description !== packageJson.description;
+
+ if (badOwner || badTitle || badName || badType || badDesc) {
+ const owner = badOwner
+ ? getOwnerFromCodeowners(codeowners, yamlPath)
+ : yamlJson.spec?.owner;
+ const newJson = createOrMergeEntity(packageJson, owner, yamlJson);
+ return dryRun
+ ? Promise.resolve(console.error(`Update ${relativePath('.', yamlPath)}`))
+ : writeFile(yamlPath, yamlOverwrite(yamlString, newJson));
+ }
+
+ return Promise.resolve();
+}
+
+/**
+ * Canonical representation on how to create (or update) a backstage-package
+ * component.
+ */
+function createOrMergeEntity(
+ packageJson: BackstagePackageJson,
+ owner: string,
+ existingEntity: BackstagePackageEntity | Record = {},
+): BackstagePackageEntity {
+ const safeEntityName = packageJson.name
+ .replace(/[^a-z0-9_\-\.]+/g, '-')
+ .replace(/^[^a-z0-9]|[^a-z0-9]$/g, '');
+
+ return {
+ ...existingEntity,
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ ...existingEntity.metadata,
+ // Provide default name/title/description values.
+ name: safeEntityName,
+ title: packageJson.name,
+ ...(packageJson.description
+ ? { description: packageJson.description }
+ : undefined),
+ },
+ spec: {
+ lifecycle: 'experimental',
+ ...existingEntity.spec,
+ type: `backstage-${packageJson.backstage.role}`,
+ owner,
+ },
+ };
+}
diff --git a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts
new file mode 100644
index 0000000000..f446f4e867
--- /dev/null
+++ b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import fs from 'fs';
+import { Package } from '@manypkg/get-packages';
+import {
+ BackstagePackageJson as BackstagePackageJsonActual,
+ PackageRole,
+} from '@backstage/cli-node';
+import { promisify } from 'util';
+
+export const readFile = promisify(fs.readFile);
+export const writeFile = promisify(fs.writeFile);
+
+export type BackstagePackageJson = BackstagePackageJsonActual & {
+ description?: string;
+ backstage: {
+ role: PackageRole;
+ };
+};
+
+export function isBackstagePackage(
+ packageJson: Package['packageJson'],
+): packageJson is BackstagePackageJson {
+ return (
+ packageJson &&
+ packageJson.hasOwnProperty('backstage') &&
+ (packageJson as any)?.backstage?.role !== 'undefined'
+ );
+}
diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts
index e19ffde4b8..7aa574d291 100644
--- a/packages/repo-tools/src/commands/index.ts
+++ b/packages/repo-tools/src/commands/index.ts
@@ -96,6 +96,21 @@ export function registerCommands(program: Command) {
.description('Find inconsistencies in types of all packages and plugins')
.action(lazy(() => import('./type-deps/type-deps').then(m => m.default)));
+ program
+ .command('generate-catalog-info')
+ .option(
+ '--dry-run',
+ 'Shows what would happen without actually writing any yaml.',
+ )
+ .description('Create or fix info yaml files for all backstage packages')
+ .action(
+ lazy(() =>
+ import('./generate-catalog-info/generate-catalog-info').then(
+ m => m.default,
+ ),
+ ),
+ );
+
registerSchemaCommand(program);
}
diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md
index 915d54fec3..6728d9de41 100644
--- a/packages/techdocs-cli-embedded-app/CHANGELOG.md
+++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md
@@ -1,5 +1,34 @@
# techdocs-cli-embedded-app
+## 0.2.85
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-app-api@1.9.1
+ - @backstage/integration-react@1.1.16
+ - @backstage/cli@0.22.10
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog@1.12.1
+ - @backstage/app-defaults@1.4.2
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2
+ - @backstage/plugin-techdocs@1.6.6
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-techdocs-react@1.1.9
+
+## 0.2.85-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.12.1-next.2
+ - @backstage/plugin-techdocs@1.6.6-next.2
+ - @backstage/cli@0.22.10-next.1
+ - @backstage/integration-react@1.1.16-next.1
+
## 0.2.85-next.1
### Patch Changes
diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json
index d980d2ab6c..c8097fca3b 100644
--- a/packages/techdocs-cli-embedded-app/package.json
+++ b/packages/techdocs-cli-embedded-app/package.json
@@ -1,6 +1,6 @@
{
"name": "techdocs-cli-embedded-app",
- "version": "0.2.85-next.1",
+ "version": "0.2.85",
"private": true,
"backstage": {
"role": "frontend"
diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md
index ec0678b2d6..8334aa54b6 100644
--- a/packages/techdocs-cli/CHANGELOG.md
+++ b/packages/techdocs-cli/CHANGELOG.md
@@ -1,5 +1,25 @@
# @techdocs/cli
+## 1.4.5
+
+### Patch Changes
+
+- 971bdd6a4732: Bumped internal `nodemon` dependency.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/plugin-techdocs-node@1.7.4
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+
+## 1.4.5-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/plugin-techdocs-node@1.7.4-next.2
+
## 1.4.5-next.1
### Patch Changes
diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json
index 82ba61e0f6..3dc8eda540 100644
--- a/packages/techdocs-cli/package.json
+++ b/packages/techdocs-cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "Utility CLI for managing TechDocs sites in Backstage.",
- "version": "1.4.5-next.1",
+ "version": "1.4.5",
"publishConfig": {
"access": "public"
},
@@ -46,7 +46,7 @@
"@types/webpack-env": "^1.15.3",
"cypress": "^10.0.0",
"find-process": "^1.4.5",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app",
"ts-node": "^10.0.0"
},
diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md
index a3fb8ba382..d3e28b30d2 100644
--- a/packages/test-utils/CHANGELOG.md
+++ b/packages/test-utils/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/test-utils
+## 1.4.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-app-api@1.9.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+ - @backstage/types@1.1.0
+ - @backstage/plugin-permission-common@0.7.7
+ - @backstage/plugin-permission-react@0.4.14
+
## 1.4.2-next.0
### Patch Changes
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index ef074d13b2..73ef2c5268 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
- "version": "1.4.2-next.0",
+ "version": "1.4.2",
"publishConfig": {
"access": "public"
},
diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md
index 07b40ac349..e038706836 100644
--- a/plugins/adr-backend/CHANGELOG.md
+++ b/plugins/adr-backend/CHANGELOG.md
@@ -1,5 +1,28 @@
# @backstage/plugin-adr-backend
+## 0.3.6
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/integration@1.6.0
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/plugin-adr-common@0.2.12
+ - @backstage/plugin-search-common@1.2.5
+
+## 0.3.6-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+
## 0.3.6-next.1
### Patch Changes
diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json
index 0caa9563d8..c15eafd6cf 100644
--- a/plugins/adr-backend/package.json
+++ b/plugins/adr-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr-backend",
- "version": "0.3.6-next.1",
+ "version": "0.3.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md
index 9b8d5dbf74..dfe54806fe 100644
--- a/plugins/adr-common/CHANGELOG.md
+++ b/plugins/adr-common/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-adr-common
+## 0.2.12
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/integration@1.6.0
+ - @backstage/catalog-model@1.4.1
+ - @backstage/plugin-search-common@1.2.5
+
## 0.2.11
### Patch Changes
diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json
index 2def65cf55..25e02845ef 100644
--- a/plugins/adr-common/package.json
+++ b/plugins/adr-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-adr-common",
"description": "Common functionalities for the adr plugin",
- "version": "0.2.11",
+ "version": "0.2.12",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md
index 57f888eaa7..e25c80d4d2 100644
--- a/plugins/adr/CHANGELOG.md
+++ b/plugins/adr/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/plugin-adr
+## 0.6.4
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/integration-react@1.1.16
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/plugin-search-react@1.6.4
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-adr-common@0.2.12
+ - @backstage/plugin-search-common@1.2.5
+
+## 0.6.4-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/integration-react@1.1.16-next.1
+
## 0.6.4-next.1
### Patch Changes
diff --git a/plugins/adr/package.json b/plugins/adr/package.json
index 84d5080a4a..37bf13619a 100644
--- a/plugins/adr/package.json
+++ b/plugins/adr/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr",
- "version": "0.6.4-next.1",
+ "version": "0.6.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md
index 3892e5563b..7eea9d8287 100644
--- a/plugins/airbrake-backend/CHANGELOG.md
+++ b/plugins/airbrake-backend/CHANGELOG.md
@@ -1,5 +1,24 @@
# @backstage/plugin-airbrake-backend
+## 0.2.21
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/config@1.0.8
+
+## 0.2.21-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+
## 0.2.21-next.1
### Patch Changes
diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json
index f3ff319d65..530de9a77a 100644
--- a/plugins/airbrake-backend/package.json
+++ b/plugins/airbrake-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake-backend",
- "version": "0.2.21-next.1",
+ "version": "0.2.21",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md
index 5ef51bc443..b4d5afccdc 100644
--- a/plugins/airbrake/CHANGELOG.md
+++ b/plugins/airbrake/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/plugin-airbrake
+## 0.3.21
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/dev-utils@1.0.18
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 0.3.21-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/dev-utils@1.0.18-next.2
+
## 0.3.21-next.1
### Patch Changes
diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json
index 72f3bc6094..5ccd8bd076 100644
--- a/plugins/airbrake/package.json
+++ b/plugins/airbrake/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake",
- "version": "0.3.21-next.1",
+ "version": "0.3.21",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md
index 8bbf800b78..cd7c7973b5 100644
--- a/plugins/allure/CHANGELOG.md
+++ b/plugins/allure/CHANGELOG.md
@@ -1,5 +1,24 @@
# @backstage/plugin-allure
+## 0.1.37
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 0.1.37-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+
## 0.1.37-next.1
### Patch Changes
diff --git a/plugins/allure/package.json b/plugins/allure/package.json
index 730347cf8c..881d4d0c9d 100644
--- a/plugins/allure/package.json
+++ b/plugins/allure/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-allure",
"description": "A Backstage plugin that integrates with Allure",
- "version": "0.1.37-next.1",
+ "version": "0.1.37",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md
index d40cb001e2..b6ceb1c28d 100644
--- a/plugins/analytics-module-ga/CHANGELOG.md
+++ b/plugins/analytics-module-ga/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-analytics-module-ga
+## 0.1.32
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+
## 0.1.32-next.1
### Patch Changes
diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json
index ef77925719..434f231545 100644
--- a/plugins/analytics-module-ga/package.json
+++ b/plugins/analytics-module-ga/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga",
- "version": "0.1.32-next.1",
+ "version": "0.1.32",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md
index fd589fec4b..bc08d64809 100644
--- a/plugins/analytics-module-ga4/CHANGELOG.md
+++ b/plugins/analytics-module-ga4/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-analytics-module-ga4
+## 0.1.3
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+
## 0.1.3-next.1
### Patch Changes
diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json
index ef55377f0c..63f03e440b 100644
--- a/plugins/analytics-module-ga4/package.json
+++ b/plugins/analytics-module-ga4/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga4",
- "version": "0.1.3-next.1",
+ "version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-newrelic-browser/CHANGELOG.md b/plugins/analytics-module-newrelic-browser/CHANGELOG.md
index beea288700..3090cd3fc2 100644
--- a/plugins/analytics-module-newrelic-browser/CHANGELOG.md
+++ b/plugins/analytics-module-newrelic-browser/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-analytics-module-newrelic-browser
+## 0.0.1
+
+### Patch Changes
+
+- ec7357258853: Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-newrelic-browser) for more details!
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+
## 0.0.1-next.0
### Patch Changes
diff --git a/plugins/analytics-module-newrelic-browser/package.json b/plugins/analytics-module-newrelic-browser/package.json
index 677d3b8e16..ba72a1628b 100644
--- a/plugins/analytics-module-newrelic-browser/package.json
+++ b/plugins/analytics-module-newrelic-browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-newrelic-browser",
- "version": "0.0.1-next.0",
+ "version": "0.0.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md
index 42756b19df..e23c4a7c0c 100644
--- a/plugins/apache-airflow/CHANGELOG.md
+++ b/plugins/apache-airflow/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-apache-airflow
+## 0.2.14
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+
## 0.2.14-next.1
### Patch Changes
diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json
index a1e84dc6fb..281d49c781 100644
--- a/plugins/apache-airflow/package.json
+++ b/plugins/apache-airflow/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apache-airflow",
- "version": "0.2.14-next.1",
+ "version": "0.2.14",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md
index 7ed022f59e..f84d57542b 100644
--- a/plugins/api-docs/CHANGELOG.md
+++ b/plugins/api-docs/CHANGELOG.md
@@ -1,5 +1,25 @@
# @backstage/plugin-api-docs
+## 0.9.7
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog@1.12.1
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 0.9.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/plugin-catalog@1.12.1-next.2
+
## 0.9.7-next.1
### Patch Changes
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index 8d67619aa9..1fe4133515 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-api-docs",
"description": "A Backstage plugin that helps represent API entities in the frontend",
- "version": "0.9.7-next.1",
+ "version": "0.9.7",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md
index b733c807ba..0833267e83 100644
--- a/plugins/apollo-explorer/CHANGELOG.md
+++ b/plugins/apollo-explorer/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-apollo-explorer
+## 0.1.14
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+
## 0.1.14-next.1
### Patch Changes
diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json
index f989ba29ad..aae09fe19a 100644
--- a/plugins/apollo-explorer/package.json
+++ b/plugins/apollo-explorer/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apollo-explorer",
- "version": "0.1.14-next.1",
+ "version": "0.1.14",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md
index a92677ab41..2ac0068ccc 100644
--- a/plugins/app-backend/CHANGELOG.md
+++ b/plugins/app-backend/CHANGELOG.md
@@ -1,5 +1,30 @@
# @backstage/plugin-app-backend
+## 0.3.48
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/config-loader@1.4.0
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/plugin-app-node@0.1.0
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+
+## 0.3.48-next.2
+
+### Patch Changes
+
+- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options.
+- Updated dependencies
+ - @backstage/plugin-app-node@0.1.0-next.0
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/config-loader@1.4.0-next.1
+
## 0.3.48-next.1
### Patch Changes
diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json
index be1b6b3a80..05db6bb37d 100644
--- a/plugins/app-backend/package.json
+++ b/plugins/app-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-app-backend",
"description": "A Backstage backend plugin that serves the Backstage frontend app",
- "version": "0.3.48-next.1",
+ "version": "0.3.48",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts
index 37d38663ef..b6a972b4c7 100644
--- a/plugins/app-backend/src/service/appPlugin.test.ts
+++ b/plugins/app-backend/src/service/appPlugin.test.ts
@@ -39,8 +39,8 @@ describe('appPlugin', () => {
it('boots', async () => {
const { server } = await startTestBackend({
- features: [appPlugin()],
- services: [
+ features: [
+ appPlugin(),
mockServices.rootConfig.factory({
data: {
app: {
diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md
new file mode 100644
index 0000000000..1a2fd84689
--- /dev/null
+++ b/plugins/app-node/CHANGELOG.md
@@ -0,0 +1,23 @@
+# @backstage/plugin-app-node
+
+## 0.1.0
+
+### Minor Changes
+
+- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0-next.2
diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json
index c971bdb628..842086ecee 100644
--- a/plugins/app-node/package.json
+++ b/plugins/app-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-app-node",
"description": "Node.js library for the app plugin",
- "version": "0.0.0",
+ "version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/auth-backend-module-gcp-iap-provider/README.md b/plugins/auth-backend-module-gcp-iap-provider/README.md
new file mode 100644
index 0000000000..a5f3ce1d4c
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/README.md
@@ -0,0 +1,5 @@
+# Auth Backend Module - GCP IAP Provider
+
+## Links
+
+- [The Backstage homepage](https://backstage.io)
diff --git a/plugins/auth-backend-module-gcp-iap-provider/api-report.md b/plugins/auth-backend-module-gcp-iap-provider/api-report.md
new file mode 100644
index 0000000000..02b77cea36
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/api-report.md
@@ -0,0 +1,46 @@
+## API Report File for "@backstage/plugin-auth-backend-module-gcp-iap-provider"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { JsonPrimitive } from '@backstage/types';
+import { ProxyAuthenticator } from '@backstage/plugin-auth-node';
+import { SignInResolverFactory } from '@backstage/plugin-auth-node';
+
+// @public (undocumented)
+export const authModuleGcpIapProvider: () => BackendFeature;
+
+// @public (undocumented)
+export const gcpIapAuthenticator: ProxyAuthenticator<
+ {
+ jwtHeader: string;
+ tokenValidator: (token: string) => Promise;
+ },
+ {
+ iapToken: GcpIapTokenInfo;
+ }
+>;
+
+// @public
+export type GcpIapResult = {
+ iapToken: GcpIapTokenInfo;
+};
+
+// @public
+export namespace gcpIapSignInResolvers {
+ const emailMatchingUserEntityAnnotation: SignInResolverFactory<
+ GcpIapResult,
+ unknown
+ >;
+}
+
+// @public
+export type GcpIapTokenInfo = {
+ sub: string;
+ email: string;
+ [key: string]: JsonPrimitive;
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts
new file mode 100644
index 0000000000..945378ada6
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 interface Config {
+ auth?: {
+ providers?: {
+ /**
+ * Configuration for the Google Cloud Platform Identity-Aware Proxy (IAP) auth provider.
+ */
+ gcpIap?: {
+ [authEnv: string]: {
+ /**
+ * The audience to use when validating incoming JWT tokens.
+ * See https://backstage.io/docs/auth/google/gcp-iap-auth
+ */
+ audience: string;
+
+ /**
+ * The name of the header to read the JWT token from, defaults to `'x-goog-iap-jwt-assertion'`.
+ */
+ jwtHeader?: string;
+ };
+ };
+ };
+ };
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json
new file mode 100644
index 0000000000..9c073a6cb8
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider",
+ "description": "A GCP IAP auth provider module for the Backstage auth backend",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/auth-backend-module-gcp-iap-provider"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "@backstage/types": "workspace:^",
+ "google-auth-library": "^8.0.0"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "express": "^4.18.2",
+ "msw": "^1.0.0",
+ "supertest": "^6.1.3"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts
new file mode 100644
index 0000000000..51efbe1a04
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+import { mockServices } from '@backstage/backend-test-utils';
+import { Request } from 'express';
+import { gcpIapAuthenticator } from './authenticator';
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+jest.mock('./helpers', () => ({
+ createTokenValidator() {
+ return async () => ({ sub: 's', email: 'e' });
+ },
+}));
+
+describe('GcpIapProvider', () => {
+ it('should find default JWT header', async () => {
+ const ctx = await gcpIapAuthenticator.initialize({
+ config: mockServices.rootConfig({ data: { audience: 'my-audience' } }),
+ });
+ await expect(
+ gcpIapAuthenticator.authenticate(
+ {
+ req: {
+ header(name: string) {
+ return name === 'x-goog-iap-jwt-assertion'
+ ? 'my-token'
+ : undefined;
+ },
+ } as Request,
+ },
+ ctx,
+ ),
+ ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } });
+ });
+
+ it('should find custom JWT header', async () => {
+ const jwtHeader = 'x-custom-header';
+ const ctx = await gcpIapAuthenticator.initialize({
+ config: mockServices.rootConfig({
+ data: { audience: 'my-audience', jwtHeader },
+ }),
+ });
+ await expect(
+ gcpIapAuthenticator.authenticate(
+ {
+ req: {
+ header(name: string) {
+ return name === jwtHeader ? 'my-token' : undefined;
+ },
+ } as Request,
+ },
+ ctx,
+ ),
+ ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } });
+ });
+
+ it('should throw if header is missing', async () => {
+ const ctx = await gcpIapAuthenticator.initialize({
+ config: mockServices.rootConfig({
+ data: { audience: 'my-audience' },
+ }),
+ });
+ await expect(
+ gcpIapAuthenticator.authenticate(
+ {
+ req: {
+ header(_name: string) {
+ return undefined;
+ },
+ } as Request,
+ },
+ ctx,
+ ),
+ ).rejects.toThrow('Missing Google IAP header');
+ });
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts
new file mode 100644
index 0000000000..ca104ded86
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AuthenticationError } from '@backstage/errors';
+import { createProxyAuthenticator } from '@backstage/plugin-auth-node';
+import { createTokenValidator } from './helpers';
+import { GcpIapResult } from './types';
+
+/**
+ * The header name used by the IAP.
+ */
+const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion';
+
+/** @public */
+export const gcpIapAuthenticator = createProxyAuthenticator({
+ defaultProfileTransform: async (result: GcpIapResult) => {
+ return { profile: { email: result.iapToken.email } };
+ },
+ async initialize({ config }) {
+ const audience = config.getString('audience');
+ const jwtHeader =
+ config.getOptionalString('jwtHeader') ?? DEFAULT_IAP_JWT_HEADER;
+
+ const tokenValidator = createTokenValidator(audience);
+
+ return { jwtHeader, tokenValidator };
+ },
+ async authenticate({ req }, { jwtHeader, tokenValidator }) {
+ const token = req.header(jwtHeader);
+
+ if (!token || typeof token !== 'string') {
+ throw new AuthenticationError('Missing Google IAP header');
+ }
+
+ const iapToken = await tokenValidator(token);
+
+ return { result: { iapToken } };
+ },
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts
new file mode 100644
index 0000000000..4079f8f7d3
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts
@@ -0,0 +1,124 @@
+/*
+ * 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 { OAuth2Client } from 'google-auth-library';
+import { createTokenValidator } from './helpers';
+
+const mockJwt = 'a.b.c';
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('helpers', () => {
+ describe('createTokenValidator', () => {
+ it('runs the happy path', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => ({ sub: 's', email: 'e@mail.com' }),
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).resolves.toMatchObject({
+ sub: 's',
+ email: 'e@mail.com',
+ });
+ });
+
+ it('throws if listing keys fail', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => {
+ throw new Error('NOPE');
+ },
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Unable to list Google IAP token verification keys, Error: NOPE',
+ );
+ });
+
+ it('throws if the verifying signature fails', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => {
+ throw new Error('NOPE');
+ },
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token verification failed, Error: NOPE',
+ );
+ });
+
+ it('rejects empty payload', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => undefined,
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token verification failed, token had no payload',
+ );
+ });
+
+ it('rejects payload without subject', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => ({ email: 'e@mail.com' }),
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token payload is missing subject claim',
+ );
+ });
+
+ it('rejects payload without email', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => ({ sub: 's' }),
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token payload is missing email claim',
+ );
+ });
+ });
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts
new file mode 100644
index 0000000000..54db7117f9
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { AuthenticationError } from '@backstage/errors';
+import { OAuth2Client } from 'google-auth-library';
+import { GcpIapTokenInfo } from './types';
+
+export function createTokenValidator(
+ audience: string,
+ providedClient?: OAuth2Client,
+): (token: string) => Promise {
+ const client = providedClient ?? new OAuth2Client();
+
+ return async function tokenValidator(token) {
+ // TODO(freben): Rate limit the public key reads. It may be sensible to
+ // cache these for some reasonable time rather than asking for the public
+ // keys on every single sign-in. But since the rate of events here is so
+ // slow, I decided to keep it simple for now.
+ const response = await client.getIapPublicKeys().catch(error => {
+ throw new AuthenticationError(
+ `Unable to list Google IAP token verification keys, ${error}`,
+ );
+ });
+ const ticket = await client
+ .verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, [
+ 'https://cloud.google.com/iap',
+ ])
+ .catch(error => {
+ throw new AuthenticationError(
+ `Google IAP token verification failed, ${error}`,
+ );
+ });
+
+ const payload = ticket.getPayload();
+ if (!payload) {
+ throw new AuthenticationError(
+ 'Google IAP token verification failed, token had no payload',
+ );
+ }
+
+ if (!payload.sub) {
+ throw new AuthenticationError(
+ 'Google IAP token payload is missing subject claim',
+ );
+ }
+ if (!payload.email) {
+ throw new AuthenticationError(
+ 'Google IAP token payload is missing email claim',
+ );
+ }
+
+ return payload as unknown as GcpIapTokenInfo;
+ };
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/index.ts b/plugins/auth-backend-module-gcp-iap-provider/src/index.ts
new file mode 100644
index 0000000000..be6626a815
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { gcpIapAuthenticator } from './authenticator';
+export { authModuleGcpIapProvider } from './module';
+export { gcpIapSignInResolvers } from './resolvers';
+export { type GcpIapResult, type GcpIapTokenInfo } from './types';
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/module.ts b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts
new file mode 100644
index 0000000000..90771eecb9
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createBackendModule } from '@backstage/backend-plugin-api';
+import {
+ authProvidersExtensionPoint,
+ commonSignInResolvers,
+ createProxyAuthProviderFactory,
+} from '@backstage/plugin-auth-node';
+import { gcpIapAuthenticator } from './authenticator';
+import { gcpIapSignInResolvers } from './resolvers';
+
+/** @public */
+export const authModuleGcpIapProvider = createBackendModule({
+ pluginId: 'auth',
+ moduleId: 'gcpIapProvider',
+ register(reg) {
+ reg.registerInit({
+ deps: {
+ providers: authProvidersExtensionPoint,
+ },
+ async init({ providers }) {
+ providers.registerProvider({
+ providerId: 'gcpIap',
+ factory: createProxyAuthProviderFactory({
+ authenticator: gcpIapAuthenticator,
+ signInResolverFactories: {
+ ...gcpIapSignInResolvers,
+ ...commonSignInResolvers,
+ },
+ }),
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts
new file mode 100644
index 0000000000..32ab2b6cc7
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createSignInResolverFactory,
+ SignInInfo,
+} from '@backstage/plugin-auth-node';
+import { GcpIapResult } from './types';
+
+/**
+ * Available sign-in resolvers for the Google auth provider.
+ *
+ * @public
+ */
+export namespace gcpIapSignInResolvers {
+ /**
+ * Looks up the user by matching their email to the `google.com/email` annotation.
+ */
+ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
+ create() {
+ return async (info: SignInInfo, ctx) => {
+ const email = info.result.iapToken.email;
+
+ if (!email) {
+ throw new Error('Google IAP sign-in result is missing email');
+ }
+
+ return ctx.signInWithCatalogUser({
+ annotations: {
+ 'google.com/email': email,
+ },
+ });
+ };
+ },
+ });
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/types.ts b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts
new file mode 100644
index 0000000000..a967a4d579
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { JsonPrimitive } from '@backstage/types';
+
+/**
+ * The data extracted from an IAP token.
+ *
+ * @public
+ */
+export type GcpIapTokenInfo = {
+ /**
+ * The unique, stable identifier for the user.
+ */
+ sub: string;
+ /**
+ * User email address.
+ */
+ email: string;
+ /**
+ * Other fields.
+ */
+ [key: string]: JsonPrimitive;
+};
+
+/**
+ * The result of the initial auth challenge. This is the input to the auth
+ * callbacks.
+ *
+ * @public
+ */
+export type GcpIapResult = {
+ /**
+ * The data extracted from the IAP token header.
+ */
+ iapToken: GcpIapTokenInfo;
+};
diff --git a/plugins/auth-backend-module-google-provider/.eslintrc.js b/plugins/auth-backend-module-google-provider/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/auth-backend-module-google-provider/README.md b/plugins/auth-backend-module-google-provider/README.md
new file mode 100644
index 0000000000..e031482f64
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/README.md
@@ -0,0 +1,5 @@
+# Auth Backend Module - Google Provider
+
+## Links
+
+- [The Backstage homepage](https://backstage.io)
diff --git a/plugins/auth-backend-module-google-provider/api-report.md b/plugins/auth-backend-module-google-provider/api-report.md
new file mode 100644
index 0000000000..8040c44832
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/api-report.md
@@ -0,0 +1,31 @@
+## API Report File for "@backstage/plugin-auth-backend-module-google-provider"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { OAuthAuthenticator } from '@backstage/plugin-auth-node';
+import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node';
+import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node';
+import { PassportProfile } from '@backstage/plugin-auth-node';
+import { SignInResolverFactory } from '@backstage/plugin-auth-node';
+
+// @public (undocumented)
+export const authModuleGoogleProvider: () => BackendFeature;
+
+// @public (undocumented)
+export const googleAuthenticator: OAuthAuthenticator<
+ PassportOAuthAuthenticatorHelper,
+ PassportProfile
+>;
+
+// @public
+export namespace googleSignInResolvers {
+ const emailMatchingUserEntityAnnotation: SignInResolverFactory<
+ OAuthAuthenticatorResult,
+ unknown
+ >;
+}
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts
new file mode 100644
index 0000000000..6a1716c5bf
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/config.d.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 interface Config {
+ /** Configuration options for the auth plugin */
+ auth?: {
+ providers?: {
+ google?: {
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ callbackUrl?: string;
+ };
+ };
+ };
+ };
+}
diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json
new file mode 100644
index 0000000000..2104af5124
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@backstage/plugin-auth-backend-module-google-provider",
+ "description": "A Google auth provider module for the Backstage auth backend",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/auth-backend-module-google-provider"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "google-auth-library": "^8.0.0",
+ "passport-google-oauth20": "^2.0.0"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "@types/passport-google-oauth20": "^2.0.3",
+ "msw": "^1.0.0",
+ "supertest": "^6.1.3"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/auth-backend-module-google-provider/src/authenticator.ts b/plugins/auth-backend-module-google-provider/src/authenticator.ts
new file mode 100644
index 0000000000..a428519fee
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/authenticator.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ PassportOAuthAuthenticatorHelper,
+ PassportOAuthDoneCallback,
+ PassportProfile,
+ createOAuthAuthenticator,
+} from '@backstage/plugin-auth-node';
+import { OAuth2Client } from 'google-auth-library';
+import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
+
+/** @public */
+export const googleAuthenticator = createOAuthAuthenticator({
+ defaultProfileTransform:
+ PassportOAuthAuthenticatorHelper.defaultProfileTransform,
+ initialize({ callbackUrl, config }) {
+ const clientId = config.getString('clientId');
+ const clientSecret = config.getString('clientSecret');
+
+ return PassportOAuthAuthenticatorHelper.from(
+ new GoogleStrategy(
+ {
+ clientID: clientId,
+ clientSecret: clientSecret,
+ callbackURL: callbackUrl,
+ passReqToCallback: false,
+ },
+ (
+ accessToken: string,
+ refreshToken: string,
+ params: any,
+ fullProfile: PassportProfile,
+ done: PassportOAuthDoneCallback,
+ ) => {
+ done(
+ undefined,
+ {
+ fullProfile,
+ params,
+ accessToken,
+ },
+ {
+ refreshToken,
+ },
+ );
+ },
+ ),
+ );
+ },
+
+ async start(input, helper) {
+ return helper.start(input, {
+ accessType: 'offline',
+ prompt: 'consent',
+ });
+ },
+
+ async authenticate(input, helper) {
+ return helper.authenticate(input);
+ },
+
+ async refresh(input, helper) {
+ return helper.refresh(input);
+ },
+
+ async logout(input) {
+ if (input.refreshToken) {
+ const oauthClient = new OAuth2Client();
+ await oauthClient.revokeToken(input.refreshToken);
+ }
+ },
+});
diff --git a/plugins/auth-backend-module-google-provider/src/index.ts b/plugins/auth-backend-module-google-provider/src/index.ts
new file mode 100644
index 0000000000..9999dd2bc5
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { googleAuthenticator } from './authenticator';
+export { authModuleGoogleProvider } from './module';
+export { googleSignInResolvers } from './resolvers';
diff --git a/plugins/auth-backend-module-google-provider/src/module.ts b/plugins/auth-backend-module-google-provider/src/module.ts
new file mode 100644
index 0000000000..0354f99880
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/module.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createBackendModule } from '@backstage/backend-plugin-api';
+import {
+ authProvidersExtensionPoint,
+ commonSignInResolvers,
+ createOAuthProviderFactory,
+} from '@backstage/plugin-auth-node';
+import { googleAuthenticator } from './authenticator';
+import { googleSignInResolvers } from './resolvers';
+
+/** @public */
+export const authModuleGoogleProvider = createBackendModule({
+ pluginId: 'auth',
+ moduleId: 'googleProvider',
+ register(reg) {
+ reg.registerInit({
+ deps: {
+ providers: authProvidersExtensionPoint,
+ },
+ async init({ providers }) {
+ providers.registerProvider({
+ providerId: 'google',
+ factory: createOAuthProviderFactory({
+ authenticator: googleAuthenticator,
+ signInResolverFactories: {
+ ...googleSignInResolvers,
+ ...commonSignInResolvers,
+ },
+ }),
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts
new file mode 100644
index 0000000000..b19fc4d49f
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createSignInResolverFactory,
+ OAuthAuthenticatorResult,
+ PassportProfile,
+ SignInInfo,
+} from '@backstage/plugin-auth-node';
+
+/**
+ * Available sign-in resolvers for the Google auth provider.
+ *
+ * @public
+ */
+export namespace googleSignInResolvers {
+ /**
+ * Looks up the user by matching their email to the `google.com/email` annotation.
+ */
+ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
+ create() {
+ return async (
+ info: SignInInfo>,
+ ctx,
+ ) => {
+ const { profile } = info;
+
+ if (!profile.email) {
+ throw new Error('Google profile contained no email');
+ }
+
+ return ctx.signInWithCatalogUser({
+ annotations: {
+ 'google.com/email': profile.email,
+ },
+ });
+ };
+ },
+ });
+}
diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md
index 478570bb31..5504f162e6 100644
--- a/plugins/auth-backend/CHANGELOG.md
+++ b/plugins/auth-backend/CHANGELOG.md
@@ -1,5 +1,31 @@
# @backstage/plugin-auth-backend
+## 0.18.6
+
+### Patch Changes
+
+- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors
+- 9dad4b0e61bd: Updated config schema to match what was being used in code
+- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 0.18.6-next.2
+
+### Patch Changes
+
+- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors
+- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
## 0.18.6-next.1
### Patch Changes
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index 80db2e0b71..5eb038c436 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -5,98 +5,69 @@
```ts
///
-import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
+import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node';
+import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node';
+import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node';
+import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node';
+import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CacheService } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
+import { ClientAuthResponse } from '@backstage/plugin-auth-node';
import { Config } from '@backstage/config';
+import { CookieConfigurer as CookieConfigurer_2 } from '@backstage/plugin-auth-node';
+import { decodeOAuthState } from '@backstage/plugin-auth-node';
+import { encodeOAuthState } from '@backstage/plugin-auth-node';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
-import { GetEntitiesRequest } from '@backstage/catalog-client';
+import { GcpIapResult as GcpIapResult_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
+import { GcpIapTokenInfo as GcpIapTokenInfo_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { IncomingHttpHeaders } from 'http';
-import { JsonValue } from '@backstage/types';
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
+import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node';
+import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { prepareBackstageIdentityResponse as prepareBackstageIdentityResponse_2 } from '@backstage/plugin-auth-node';
import { Profile } from 'passport';
+import { ProfileInfo as ProfileInfo_2 } from '@backstage/plugin-auth-node';
+import { SignInInfo as SignInInfo_2 } from '@backstage/plugin-auth-node';
+import { SignInResolver as SignInResolver_2 } from '@backstage/plugin-auth-node';
import { TokenManager } from '@backstage/backend-common';
+import { TokenParams as TokenParams_2 } from '@backstage/plugin-auth-node';
import { TokenSet } from 'openid-client';
import { UserEntity } from '@backstage/catalog-model';
import { UserinfoResponse } from 'openid-client';
+import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-auth-node';
-// @public
+// @public @deprecated
export type AuthHandler = (
input: TAuthResult,
context: AuthResolverContext,
) => Promise;
-// @public
+// @public @deprecated
export type AuthHandlerResult = {
profile: ProfileInfo;
};
-// @public (undocumented)
-export type AuthProviderConfig = {
- baseUrl: string;
- appUrl: string;
- isOriginAllowed: (origin: string) => boolean;
- cookieConfigurer?: CookieConfigurer;
-};
+// @public @deprecated (undocumented)
+export type AuthProviderConfig = AuthProviderConfig_2;
-// @public (undocumented)
-export type AuthProviderFactory = (options: {
- providerId: string;
- globalConfig: AuthProviderConfig;
- config: Config;
- logger: Logger;
- resolverContext: AuthResolverContext;
-}) => AuthProviderRouteHandlers;
+// @public @deprecated (undocumented)
+export type AuthProviderFactory = AuthProviderFactory_2;
-// @public
-export interface AuthProviderRouteHandlers {
- frameHandler(req: express.Request, res: express.Response): Promise;
- logout?(req: express.Request, res: express.Response): Promise;
- refresh?(req: express.Request, res: express.Response): Promise;
- start(req: express.Request, res: express.Response): Promise;
-}
+// @public @deprecated (undocumented)
+export type AuthProviderRouteHandlers = AuthProviderRouteHandlers_2;
-// @public
-export type AuthResolverCatalogUserQuery =
- | {
- entityRef:
- | string
- | {
- kind?: string;
- namespace?: string;
- name: string;
- };
- }
- | {
- annotations: Record;
- }
- | {
- filter: Exclude;
- };
+// @public @deprecated (undocumented)
+export type AuthResolverCatalogUserQuery = AuthResolverCatalogUserQuery_2;
-// @public
-export type AuthResolverContext = {
- issueToken(params: TokenParams): Promise<{
- token: string;
- }>;
- findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{
- entity: Entity;
- }>;
- signInWithCatalogUser(
- query: AuthResolverCatalogUserQuery,
- ): Promise;
-};
+// @public @deprecated (undocumented)
+export type AuthResolverContext = AuthResolverContext_2;
-// @public (undocumented)
-export type AuthResponse = {
- providerInfo: ProviderInfo;
- profile: ProfileInfo;
- backstageIdentity?: BackstageIdentityResponse;
-};
+// @public @deprecated (undocumented)
+export type AuthResponse = ClientAuthResponse;
// @public (undocumented)
export type AwsAlbResult = {
@@ -151,7 +122,7 @@ export class CatalogIdentityClient {
findUser(query: { annotations: Record }): Promise;
resolveCatalogMembership(query: {
entityRefs: string[];
- logger?: Logger;
+ logger?: LoggerService;
}): Promise;
}
@@ -191,18 +162,8 @@ export type CloudflareAccessResult = {
token: string;
};
-// @public
-export type CookieConfigurer = (ctx: {
- providerId: string;
- baseUrl: string;
- callbackUrl: string;
- appOrigin: string;
-}) => {
- domain: string;
- path: string;
- secure: boolean;
- sameSite?: 'none' | 'lax' | 'strict';
-};
+// @public @deprecated (undocumented)
+export type CookieConfigurer = CookieConfigurer_2;
// @public
export function createAuthProviderIntegration<
@@ -235,23 +196,17 @@ export type EasyAuthResult = {
accessToken?: string;
};
-// @public (undocumented)
-export const encodeState: (state: OAuthState) => string;
+// @public @deprecated (undocumented)
+export const encodeState: typeof encodeOAuthState;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const ensuresXRequestedWith: (req: express.Request) => boolean;
-// @public
-export type GcpIapResult = {
- iapToken: GcpIapTokenInfo;
-};
+// @public @deprecated
+export type GcpIapResult = GcpIapResult_2;
-// @public
-export type GcpIapTokenInfo = {
- sub: string;
- email: string;
- [key: string]: JsonValue;
-};
+// @public @deprecated
+export type GcpIapTokenInfo = GcpIapTokenInfo_2;
// @public
export function getDefaultOwnershipEntityRefs(entity: Entity): string[];
@@ -276,7 +231,7 @@ export type OAuth2ProxyResult = {
getHeader(name: string): string | undefined;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export class OAuthAdapter implements AuthProviderRouteHandlers {
constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions);
// (undocumented)
@@ -298,7 +253,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
start(req: express.Request, res: express.Response): Promise;
}
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthAdapterOptions = {
providerId: string;
persistScopes?: boolean;
@@ -309,25 +264,10 @@ export type OAuthAdapterOptions = {
callbackUrl: string;
};
-// @public (undocumented)
-export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
- constructor(handlers: Map);
- // (undocumented)
- frameHandler(req: express.Request, res: express.Response): Promise;
- // (undocumented)
- logout(req: express.Request, res: express.Response): Promise;
- // (undocumented)
- static mapConfig(
- config: Config,
- factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers,
- ): OAuthEnvironmentHandler;
- // (undocumented)
- refresh(req: express.Request, res: express.Response): Promise;
- // (undocumented)
- start(req: express.Request, res: express.Response): Promise;
-}
+// @public @deprecated (undocumented)
+export const OAuthEnvironmentHandler: typeof OAuthEnvironmentHandler_2;
-// @public
+// @public @deprecated (undocumented)
export interface OAuthHandlers {
handler(req: express.Request): Promise<{
response: OAuthResponse;
@@ -341,12 +281,12 @@ export interface OAuthHandlers {
start(req: OAuthStartRequest): Promise;
}
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthLogoutRequest = express.Request<{}> & {
refreshToken: string;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthProviderInfo = {
accessToken: string;
idToken?: string;
@@ -354,59 +294,53 @@ export type OAuthProviderInfo = {
scope: string;
};
-// @public
+// @public @deprecated
export type OAuthProviderOptions = {
clientId: string;
clientSecret: string;
callbackUrl: string;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
};
-// @public
+// @public @deprecated (undocumented)
export type OAuthResponse = {
profile: ProfileInfo;
providerInfo: OAuthProviderInfo;
backstageIdentity?: BackstageSignInResult;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthResult = {
fullProfile: Profile;
params: {
id_token?: string;
scope: string;
+ token_type?: string;
expires_in: number;
};
accessToken: string;
refreshToken?: string;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthStartResponse = {
url: string;
status?: number;
};
-// @public (undocumented)
-export type OAuthState = {
- nonce: string;
- env: string;
- origin?: string;
- scope?: string;
- redirectUrl?: string;
- flow?: string;
-};
+// @public @deprecated (undocumented)
+export type OAuthState = OAuthState_2;
// @public
export type OidcAuthResult = {
@@ -414,24 +348,18 @@ export type OidcAuthResult = {
userinfo: UserinfoResponse;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const postMessageResponse: (
res: express.Response,
appOrigin: string,
response: WebMessageResponse,
) => void;
-// @public
-export function prepareBackstageIdentityResponse(
- result: BackstageSignInResult,
-): BackstageIdentityResponse;
+// @public @deprecated (undocumented)
+export const prepareBackstageIdentityResponse: typeof prepareBackstageIdentityResponse_2;
-// @public
-export type ProfileInfo = {
- email?: string;
- displayName?: string;
- picture?: string;
-};
+// @public @deprecated (undocumented)
+export type ProfileInfo = ProfileInfo_2;
// @public (undocumented)
export type ProviderFactories = {
@@ -452,7 +380,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
auth0: Readonly<{
@@ -467,7 +395,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
awsAlb: Readonly<{
@@ -480,7 +408,7 @@ export const providers: Readonly<{
};
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
bitbucket: Readonly<{
@@ -495,7 +423,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
usernameMatchingUserEntityAnnotation(): SignInResolver;
userIdMatchingUserEntityAnnotation(): SignInResolver;
@@ -513,7 +441,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver;
}>;
@@ -525,18 +453,18 @@ export const providers: Readonly<{
resolver: SignInResolver;
};
cache?: CacheService | undefined;
- }) => AuthProviderFactory;
+ }) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver;
}>;
}>;
gcpIap: Readonly<{
create: (options: {
- authHandler?: AuthHandler | undefined;
+ authHandler?: AuthHandler | undefined;
signIn: {
- resolver: SignInResolver;
+ resolver: SignInResolver;
};
- }) => AuthProviderFactory;
+ }) => AuthProviderFactory_2;
resolvers: never;
}>;
github: Readonly<{
@@ -552,7 +480,7 @@ export const providers: Readonly<{
stateEncoder?: StateEncoder | undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
usernameMatchingUserEntityName: () => SignInResolver;
}>;
@@ -569,7 +497,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
google: Readonly<{
@@ -584,11 +512,11 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
- emailLocalPartMatchingUserEntityName: () => SignInResolver;
- emailMatchingUserEntityProfileEmail: () => SignInResolver;
- emailMatchingUserEntityAnnotation(): SignInResolver;
+ emailMatchingUserEntityProfileEmail: () => SignInResolver_2;
+ emailLocalPartMatchingUserEntityName: () => SignInResolver_2;
+ emailMatchingUserEntityAnnotation: () => SignInResolver_2;
}>;
}>;
microsoft: Readonly<{
@@ -603,7 +531,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver;
emailMatchingUserEntityProfileEmail: () => SignInResolver;
@@ -622,7 +550,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
oauth2Proxy: Readonly<{
@@ -631,7 +559,7 @@ export const providers: Readonly<{
signIn: {
resolver: SignInResolver>;
};
- }) => AuthProviderFactory;
+ }) => AuthProviderFactory_2;
resolvers: never;
}>;
oidc: Readonly<{
@@ -646,7 +574,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver;
emailMatchingUserEntityProfileEmail: () => SignInResolver;
@@ -664,7 +592,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver;
emailMatchingUserEntityProfileEmail: () => SignInResolver;
@@ -683,7 +611,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
saml: Readonly<{
@@ -698,7 +626,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
nameIdMatchingUserEntityName(): SignInResolver;
}>;
@@ -713,13 +641,13 @@ export const providers: Readonly<{
};
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
}>;
-// @public (undocumented)
-export const readState: (stateString: string) => OAuthState;
+// @public @deprecated (undocumented)
+export const readState: typeof decodeOAuthState;
// @public (undocumented)
export interface RouterOptions {
@@ -732,7 +660,7 @@ export interface RouterOptions {
// (undocumented)
discovery: PluginEndpointDiscovery;
// (undocumented)
- logger: Logger;
+ logger: LoggerService;
// (undocumented)
providerFactories?: ProviderFactories;
// (undocumented)
@@ -746,42 +674,23 @@ export type SamlAuthResult = {
fullProfile: any;
};
-// @public
-export type SignInInfo = {
- profile: ProfileInfo;
- result: TAuthResult;
-};
+// @public @deprecated (undocumented)
+export type SignInInfo = SignInInfo_2;
-// @public
-export type SignInResolver = (
- info: SignInInfo,
- context: AuthResolverContext,
-) => Promise;
+// @public @deprecated (undocumented)
+export type SignInResolver = SignInResolver_2;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type StateEncoder = (req: OAuthStartRequest) => Promise<{
encodedState: string;
}>;
-// @public
-export type TokenParams = {
- claims: {
- sub: string;
- ent?: string[];
- } & Record;
-};
+// @public @deprecated (undocumented)
+export type TokenParams = TokenParams_2;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const verifyNonce: (req: express.Request, providerId: string) => void;
-// @public
-export type WebMessageResponse =
- | {
- type: 'authorization_response';
- response: AuthResponse;
- }
- | {
- type: 'authorization_response';
- error: Error;
- };
+// @public @deprecated (undocumented)
+export type WebMessageResponse = WebMessageResponse_2;
```
diff --git a/plugins/auth-backend/architecture.drawio.svg b/plugins/auth-backend/architecture.drawio.svg
new file mode 100644
index 0000000000..d709935e3e
--- /dev/null
+++ b/plugins/auth-backend/architecture.drawio.svg
@@ -0,0 +1,262 @@
+
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 2a3b80de6a..c8d1633a47 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"description": "A Backstage backend plugin that handles authentication",
- "version": "0.18.6-next.1",
+ "version": "0.18.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,10 +33,13 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^",
+ "@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/types": "workspace:^",
"@davidzemon/passport-okta-oauth": "^0.0.5",
diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts
index a5479a6470..b5f4f5dcfb 100644
--- a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts
+++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import {
DocumentData,
Firestore,
@@ -57,7 +57,7 @@ export class FirestoreKeyStore implements KeyStore {
static async verifyConnection(
keyStore: FirestoreKeyStore,
- logger?: Logger,
+ logger?: LoggerService,
): Promise {
try {
await keyStore.verify();
diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts
index 4cd8f91842..cc35c0d84e 100644
--- a/plugins/auth-backend/src/identity/KeyStores.ts
+++ b/plugins/auth-backend/src/identity/KeyStores.ts
@@ -15,7 +15,7 @@
*/
import { pickBy } from 'lodash';
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
@@ -26,7 +26,7 @@ import { MemoryKeyStore } from './MemoryKeyStore';
import { KeyStore } from './types';
type Options = {
- logger: Logger;
+ logger: LoggerService;
database: AuthDatabase;
};
diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts
index f80b79b6bc..ca16f8821f 100644
--- a/plugins/auth-backend/src/identity/TokenFactory.ts
+++ b/plugins/auth-backend/src/identity/TokenFactory.ts
@@ -18,14 +18,14 @@ import { AuthenticationError } from '@backstage/errors';
import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose';
import { DateTime } from 'luxon';
import { v4 as uuid } from 'uuid';
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import { AnyJWK, KeyStore, TokenIssuer, TokenParams } from './types';
const MS_IN_S = 1000;
type Options = {
- logger: Logger;
+ logger: LoggerService;
/** Value of the issuer claim in issued tokens */
issuer: string;
/** Key store used for storing signing keys */
@@ -57,7 +57,7 @@ type Options = {
*/
export class TokenFactory implements TokenIssuer {
private readonly issuer: string;
- private readonly logger: Logger;
+ private readonly logger: LoggerService;
private readonly keyStore: KeyStore;
private readonly keyDurationSeconds: number;
private readonly algorithm: string;
diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts
index e325f74c81..fcfc0345cc 100644
--- a/plugins/auth-backend/src/identity/types.ts
+++ b/plugins/auth-backend/src/identity/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { JsonValue } from '@backstage/types';
+import { TokenParams as _TokenParams } from '@backstage/plugin-auth-node';
/** Represents any form of serializable JWK */
export interface AnyJWK extends Record {
@@ -25,26 +25,10 @@ export interface AnyJWK extends Record {
}
/**
- * Parameters used to issue new ID Tokens
- *
* @public
+ * @deprecated import from `@backstage/plugin-auth-node` instead
*/
-export type TokenParams = {
- /**
- * The claims that will be embedded within the token. At a minimum, this should include
- * the subject claim, `sub`. It is common to also list entity ownership relations in the
- * `ent` list. Additional claims may also be added at the developer's discretion except
- * for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`,
- * `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future
- * without listing the change as a "breaking change".
- */
- claims: {
- /** The token subject, i.e. User ID */
- sub: string;
- /** A list of entity references that the user claims ownership through */
- ent?: string[];
- } & Record;
-};
+export type TokenParams = _TokenParams;
/**
* A TokenIssuer is able to issue verifiable ID Tokens on demand.
diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
index 494e8b4d38..ff983e27a5 100644
--- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
+++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import { ConflictError, NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import {
@@ -78,7 +78,7 @@ export class CatalogIdentityClient {
*/
async resolveCatalogMembership(query: {
entityRefs: string[];
- logger?: Logger;
+ logger?: LoggerService;
}): Promise {
const { entityRefs, logger } = query;
const resolvedEntityRefs = entityRefs
diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts
index 94ac4fba15..2f1770a3d4 100644
--- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts
+++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts
@@ -24,7 +24,10 @@ export const safelyEncodeURIComponent = (value: string) => {
return encodeURIComponent(value).replace(/'/g, '%27');
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead
+ */
export const postMessageResponse = (
res: express.Response,
appOrigin: string,
@@ -69,7 +72,10 @@ export const postMessageResponse = (
res.end(``);
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use inline logic to check that the `X-Requested-With` header is set to `'XMLHttpRequest'` instead.
+ */
export const ensuresXRequestedWith = (req: express.Request) => {
const requiredHeader = req.header('X-Requested-With');
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts
index f67198a1b0..36f3033b21 100644
--- a/plugins/auth-backend/src/lib/flow/types.ts
+++ b/plugins/auth-backend/src/lib/flow/types.ts
@@ -14,20 +14,10 @@
* limitations under the License.
*/
-import { AuthResponse } from '../../providers/types';
+import { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node';
/**
- * Payload sent as a post message after the auth request is complete.
- * If successful then has a valid payload with Auth information else contains an error.
- *
* @public
+ * @deprecated import from `@backstage/plugin-auth-node` instead
*/
-export type WebMessageResponse =
- | {
- type: 'authorization_response';
- response: AuthResponse;
- }
- | {
- type: 'authorization_response';
- error: Error;
- };
+export type WebMessageResponse = _WebMessageResponse;
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts
new file mode 100644
index 0000000000..5c5de6a890
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AuthResolverContext } from '@backstage/plugin-auth-node';
+import { AuthHandler } from '../../providers';
+import { OAuthResult } from '../oauth';
+import { PassportProfile } from '../passport/types';
+import { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler';
+
+describe('adaptLegacyOAuthHandler', () => {
+ it('should pass through undefined', () => {
+ expect(adaptLegacyOAuthHandler(undefined)).toBeUndefined();
+ });
+
+ it('should convert an old auth handler to a new profile transform', () => {
+ const authHandler: AuthHandler = jest.fn();
+ const profileTransform = adaptLegacyOAuthHandler(authHandler);
+
+ profileTransform?.(
+ {
+ fullProfile: { id: 'id' } as PassportProfile,
+ session: {
+ accessToken: 'token',
+ expiresInSeconds: 3,
+ scope: 'sco pe',
+ tokenType: 'bear',
+ idToken: 'id-token',
+ refreshToken: 'refresh-token',
+ },
+ },
+ { ctx: 'ctx' } as unknown as AuthResolverContext,
+ );
+
+ expect(authHandler).toHaveBeenCalledWith(
+ {
+ fullProfile: { id: 'id' },
+ accessToken: 'token',
+ params: {
+ scope: 'sco pe',
+ id_token: 'id-token',
+ expires_in: 3,
+ token_type: 'bear',
+ },
+ },
+ { ctx: 'ctx' },
+ );
+ });
+});
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts
new file mode 100644
index 0000000000..37f10ff184
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ OAuthAuthenticatorResult,
+ ProfileTransform,
+} from '@backstage/plugin-auth-node';
+import { AuthHandler } from '../../providers';
+import { OAuthResult } from '../oauth';
+import { PassportProfile } from '../passport/types';
+
+/** @internal */
+export function adaptLegacyOAuthHandler(
+ authHandler?: AuthHandler,
+): ProfileTransform> | undefined {
+ return (
+ authHandler &&
+ (async (result, ctx) =>
+ authHandler(
+ {
+ fullProfile: result.fullProfile,
+ accessToken: result.session.accessToken,
+ params: {
+ scope: result.session.scope,
+ id_token: result.session.idToken,
+ token_type: result.session.tokenType,
+ expires_in: result.session.expiresInSeconds,
+ },
+ },
+ ctx,
+ ))
+ );
+}
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts
new file mode 100644
index 0000000000..749cf96cbb
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ AuthResolverContext,
+ PassportProfile,
+} from '@backstage/plugin-auth-node';
+import { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver';
+
+describe('adaptLegacyOAuthSignInResolver', () => {
+ it('should pass through undefined', () => {
+ expect(adaptLegacyOAuthSignInResolver(undefined)).toBeUndefined();
+ });
+
+ it('should convert a legacy resolver to a new one', () => {
+ const legacyResolver = jest.fn();
+
+ const newResolver = adaptLegacyOAuthSignInResolver(legacyResolver);
+
+ newResolver?.(
+ {
+ profile: { email: 'em@i.l' },
+ result: {
+ fullProfile: { id: 'id' } as PassportProfile,
+ session: {
+ accessToken: 'token',
+ expiresInSeconds: 3,
+ scope: 'sco pe',
+ tokenType: 'bear',
+ idToken: 'id-token',
+ refreshToken: 'refresh-token',
+ },
+ },
+ },
+ { ctx: 'ctx' } as unknown as AuthResolverContext,
+ );
+
+ expect(legacyResolver).toHaveBeenCalledWith(
+ {
+ profile: { email: 'em@i.l' },
+ result: {
+ fullProfile: { id: 'id' },
+ accessToken: 'token',
+ refreshToken: 'refresh-token',
+ params: {
+ scope: 'sco pe',
+ id_token: 'id-token',
+ expires_in: 3,
+ token_type: 'bear',
+ },
+ },
+ },
+ { ctx: 'ctx' },
+ );
+ });
+});
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts
new file mode 100644
index 0000000000..e0318464ed
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ OAuthAuthenticatorResult,
+ PassportProfile,
+ SignInResolver,
+} from '@backstage/plugin-auth-node';
+import { OAuthResult } from '../oauth';
+
+/** @internal */
+export function adaptLegacyOAuthSignInResolver(
+ signInResolver?: SignInResolver,
+): SignInResolver> | undefined {
+ return (
+ signInResolver &&
+ (async (input, ctx) =>
+ signInResolver(
+ {
+ profile: input.profile,
+ result: {
+ fullProfile: input.result.fullProfile,
+ accessToken: input.result.session.accessToken,
+ refreshToken: input.result.session.refreshToken,
+ params: {
+ scope: input.result.session.scope,
+ id_token: input.result.session.idToken,
+ token_type: input.result.session.tokenType,
+ expires_in: input.result.session.expiresInSeconds,
+ },
+ },
+ },
+ ctx,
+ ))
+ );
+}
diff --git a/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts
new file mode 100644
index 0000000000..521dcf6395
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ AuthResolverContext,
+ PassportProfile,
+} from '@backstage/plugin-auth-node';
+import { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy';
+
+describe('adaptOAuthSignInResolverToLegacy', () => {
+ it('should pass through an empty object', () => {
+ const legacyResolvers = adaptOAuthSignInResolverToLegacy({});
+ expect(legacyResolvers).toEqual({});
+
+ // @ts-expect-error
+ legacyResolvers.missing?.();
+ });
+
+ it('should adapt a collection of sign-in resolvers', () => {
+ const resolverA = jest.fn();
+ const resolverB = jest.fn();
+
+ const legacyResolvers = adaptOAuthSignInResolverToLegacy({
+ resolverA,
+ resolverB,
+ });
+
+ const legacyResolverA = legacyResolvers.resolverA();
+ legacyResolverA(
+ {
+ profile: { email: 'em@i.l' },
+ result: {
+ fullProfile: { id: 'id' } as PassportProfile,
+ accessToken: 'token',
+ refreshToken: 'refresh-token',
+ params: {
+ scope: 'sco pe',
+ id_token: 'id-token',
+ expires_in: 3,
+ token_type: 'bear',
+ },
+ },
+ },
+ { ctx: 'ctx' } as unknown as AuthResolverContext,
+ );
+
+ expect(resolverA).toHaveBeenCalledWith(
+ {
+ profile: { email: 'em@i.l' },
+ result: {
+ fullProfile: { id: 'id' } as PassportProfile,
+ session: {
+ accessToken: 'token',
+ expiresInSeconds: 3,
+ scope: 'sco pe',
+ tokenType: 'bear',
+ idToken: 'id-token',
+ refreshToken: 'refresh-token',
+ },
+ },
+ },
+ { ctx: 'ctx' },
+ );
+
+ expect(resolverB).not.toHaveBeenCalled();
+ legacyResolvers.resolverB()(
+ { profile: {}, result: { params: {} } } as any,
+ {} as any,
+ );
+ expect(resolverB).toHaveBeenCalled();
+ });
+});
diff --git a/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts
new file mode 100644
index 0000000000..8be6049348
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ OAuthAuthenticatorResult,
+ PassportProfile,
+ SignInResolver,
+} from '@backstage/plugin-auth-node';
+import { OAuthResult } from '../oauth';
+
+/** @internal */
+export function adaptOAuthSignInResolverToLegacy<
+ TKeys extends string,
+>(resolvers: {
+ [key in TKeys]: SignInResolver>;
+}): { [key in TKeys]: () => SignInResolver } {
+ const legacyResolvers = {} as {
+ [key in TKeys]: () => SignInResolver;
+ };
+ for (const name of Object.keys(resolvers) as TKeys[]) {
+ const resolver = resolvers[name];
+ legacyResolvers[name] = () => async (input, ctx) =>
+ resolver(
+ {
+ profile: input.profile,
+ result: {
+ fullProfile: input.result.fullProfile,
+ session: {
+ accessToken: input.result.accessToken,
+ expiresInSeconds: input.result.params.expires_in,
+ scope: input.result.params.scope,
+ idToken: input.result.params.id_token,
+ tokenType: input.result.params.token_type ?? 'bearer',
+ refreshToken: input.result.refreshToken,
+ },
+ },
+ },
+ ctx,
+ );
+ }
+ return legacyResolvers;
+}
diff --git a/plugins/auth-backend/src/lib/legacy/index.ts b/plugins/auth-backend/src/lib/legacy/index.ts
new file mode 100644
index 0000000000..8bd8b5c62e
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler';
+export { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver';
+export { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy';
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
index fb0750f0cb..9b4afb4b2b 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
@@ -50,7 +50,10 @@ import { prepareBackstageIdentityResponse } from '../../providers/prepareBacksta
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
export const TEN_MINUTES_MS = 600 * 1000;
-/** @public */
+/**
+ * @public
+ * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
+ */
export type OAuthAdapterOptions = {
providerId: string;
persistScopes?: boolean;
@@ -61,7 +64,10 @@ export type OAuthAdapterOptions = {
callbackUrl: string;
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
+ */
export class OAuthAdapter implements AuthProviderRouteHandlers {
static fromConfig(
config: AuthProviderConfig,
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts
index 1c6d45d0a4..c9244eb29e 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts
@@ -14,84 +14,10 @@
* limitations under the License.
*/
-import express from 'express';
-import { Config } from '@backstage/config';
-import { InputError, NotFoundError } from '@backstage/errors';
-import { readState } from './helpers';
-import { AuthProviderRouteHandlers } from '../../providers/types';
+import { OAuthEnvironmentHandler as _OAuthEnvironmentHandler } from '@backstage/plugin-auth-node';
-/** @public */
-export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
- static mapConfig(
- config: Config,
- factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers,
- ) {
- const envs = config.keys();
- const handlers = new Map();
-
- for (const env of envs) {
- const envConfig = config.getConfig(env);
- const handler = factoryFunc(envConfig);
- handlers.set(env, handler);
- }
-
- return new OAuthEnvironmentHandler(handlers);
- }
-
- constructor(
- private readonly handlers: Map,
- ) {}
-
- async start(req: express.Request, res: express.Response): Promise {
- const provider = this.getProviderForEnv(req);
- await provider.start(req, res);
- }
-
- async frameHandler(
- req: express.Request,
- res: express.Response,
- ): Promise {
- const provider = this.getProviderForEnv(req);
- await provider.frameHandler(req, res);
- }
-
- async refresh(req: express.Request, res: express.Response): Promise {
- const provider = this.getProviderForEnv(req);
- await provider.refresh?.(req, res);
- }
-
- async logout(req: express.Request, res: express.Response): Promise {
- const provider = this.getProviderForEnv(req);
- await provider.logout?.(req, res);
- }
-
- private getRequestFromEnv(req: express.Request): string | undefined {
- const reqEnv = req.query.env?.toString();
- if (reqEnv) {
- return reqEnv;
- }
- const stateParams = req.query.state?.toString();
- if (!stateParams) {
- return undefined;
- }
- const env = readState(stateParams).env;
- return env;
- }
-
- private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers {
- const env: string | undefined = this.getRequestFromEnv(req);
-
- if (!env) {
- throw new InputError(`Must specify 'env' query to select environment`);
- }
-
- const handler = this.handlers.get(env);
- if (!handler) {
- throw new NotFoundError(
- `No configuration available for the '${env}' environment of this provider.`,
- );
- }
-
- return handler;
- }
-}
+/**
+ * @public
+ * @deprecated import from `@backstage/plugin-auth-node` instead
+ */
+export const OAuthEnvironmentHandler = _OAuthEnvironmentHandler;
diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts
index 58a3dcab05..c8db34e2cc 100644
--- a/plugins/auth-backend/src/lib/oauth/helpers.test.ts
+++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts
@@ -76,7 +76,7 @@ describe('OAuthProvider Utils', () => {
} as unknown as express.Request;
expect(() => {
verifyNonce(mockRequest, 'providera');
- }).toThrow('Invalid state passed via request');
+ }).toThrow('OAuth state is invalid, missing env');
});
it('should throw error if nonce mismatch', () => {
diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts
index 2b4e50b477..5e67b072e3 100644
--- a/plugins/auth-backend/src/lib/oauth/helpers.ts
+++ b/plugins/auth-backend/src/lib/oauth/helpers.ts
@@ -16,36 +16,28 @@
import express from 'express';
import { OAuthState } from './types';
-import pickBy from 'lodash/pickBy';
import { CookieConfigurer } from '../../providers/types';
+import {
+ decodeOAuthState,
+ encodeOAuthState,
+} from '@backstage/plugin-auth-node';
-/** @public */
-export const readState = (stateString: string): OAuthState => {
- const state = Object.fromEntries(
- new URLSearchParams(Buffer.from(stateString, 'hex').toString('utf-8')),
- );
- if (
- !state.nonce ||
- !state.env ||
- state.nonce?.length === 0 ||
- state.env?.length === 0
- ) {
- throw Error(`Invalid state passed via request`);
- }
+/**
+ * @public
+ * @deprecated Use `decodeOAuthState` from `@backstage/plugin-auth-node` instead
+ */
+export const readState = decodeOAuthState;
- return state as OAuthState;
-};
+/**
+ * @public
+ * @deprecated Use `encodeOAuthState` from `@backstage/plugin-auth-node` instead
+ */
+export const encodeState = encodeOAuthState;
-/** @public */
-export const encodeState = (state: OAuthState): string => {
- const stateString = new URLSearchParams(
- pickBy(state, value => value !== undefined),
- ).toString();
-
- return Buffer.from(stateString, 'utf-8').toString('hex');
-};
-
-/** @public */
+/**
+ * @public
+ * @deprecated Use inline logic to make sure the session and state nonce matches instead.
+ */
export const verifyNonce = (req: express.Request, providerId: string) => {
const cookieNonce = req.cookies[`${providerId}-nonce`];
const state: OAuthState = readState(req.query.state?.toString() ?? '');
diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts
index e960af2988..b7205c9b85 100644
--- a/plugins/auth-backend/src/lib/oauth/types.ts
+++ b/plugins/auth-backend/src/lib/oauth/types.ts
@@ -16,13 +16,17 @@
import express from 'express';
import { Profile as PassportProfile } from 'passport';
-import { BackstageSignInResult } from '@backstage/plugin-auth-node';
+import {
+ BackstageSignInResult,
+ OAuthState as _OAuthState,
+} from '@backstage/plugin-auth-node';
import { OAuthStartResponse, ProfileInfo } from '../../providers/types';
/**
* Common options for passport.js-based OAuth providers
*
* @public
+ * @deprecated No longer in use
*/
export type OAuthProviderOptions = {
/**
@@ -39,12 +43,16 @@ export type OAuthProviderOptions = {
callbackUrl: string;
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use `OAuthAuthenticatorResult` from `@backstage/plugin-auth-node` instead
+ */
export type OAuthResult = {
fullProfile: PassportProfile;
params: {
id_token?: string;
scope: string;
+ token_type?: string;
expires_in: number;
};
accessToken: string;
@@ -52,9 +60,8 @@ export type OAuthResult = {
};
/**
- * The expected response from an OAuth flow.
- *
* @public
+ * @deprecated Use `ClientAuthResponse` from `@backstage/plugin-auth-node` instead
*/
export type OAuthResponse = {
profile: ProfileInfo;
@@ -62,7 +69,10 @@ export type OAuthResponse = {
backstageIdentity?: BackstageSignInResult;
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
+ */
export type OAuthProviderInfo = {
/**
* An access token issued for the signed in user.
@@ -82,41 +92,41 @@ export type OAuthProviderInfo = {
scope: string;
};
-/** @public */
-export type OAuthState = {
- /* A type for the serialized value in the `state` parameter of the OAuth authorization flow
- */
- nonce: string;
- env: string;
- origin?: string;
- scope?: string;
- redirectUrl?: string;
- flow?: string;
-};
+/**
+ * @public
+ * @deprecated import from `@backstage/plugin-auth-node` instead
+ */
+export type OAuthState = _OAuthState;
-/** @public */
+/**
+ * @public
+ * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
+ */
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
+ */
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
+ */
export type OAuthLogoutRequest = express.Request<{}> & {
refreshToken: string;
};
/**
- * Any OAuth provider needs to implement this interface which has provider specific
- * handlers for different methods to perform authentication, get access tokens,
- * refresh tokens and perform sign out.
- *
* @public
+ * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export interface OAuthHandlers {
/**
diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts
index cbc2439563..7d22526193 100644
--- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts
+++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts
@@ -24,7 +24,7 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { ConflictError, InputError, NotFoundError } from '@backstage/errors';
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import { TokenIssuer, TokenParams } from '../../identity/types';
import { AuthResolverContext } from '../../providers';
import { AuthResolverCatalogUserQuery } from '../../providers/types';
@@ -54,7 +54,7 @@ export function getDefaultOwnershipEntityRefs(entity: Entity) {
*/
export class CatalogAuthResolverContext implements AuthResolverContext {
static create(options: {
- logger: Logger;
+ logger: LoggerService;
catalogApi: CatalogApi;
tokenIssuer: TokenIssuer;
tokenManager: TokenManager;
@@ -73,7 +73,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
}
private constructor(
- public readonly logger: Logger,
+ public readonly logger: LoggerService,
public readonly tokenIssuer: TokenIssuer,
public readonly catalogIdentityClient: CatalogIdentityClient,
private readonly catalogApi: CatalogApi,
diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts
deleted file mode 100644
index 0162366f61..0000000000
--- a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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 { ConflictError } from '@backstage/errors';
-import { OAuth2Client } from 'google-auth-library';
-import { createTokenValidator, parseRequestToken } from './helpers';
-
-const validJwt =
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-describe('helpers', () => {
- describe('createTokenValidator', () => {
- it('runs the happy path', async () => {
- const mockClient = {
- getIapPublicKeys: async () => ({ pubkeys: '' }),
- verifySignedJwtWithCertsAsync: async () => ({
- getPayload: () => ({ sub: 's', email: 'e@mail.com' }),
- }),
- };
- const validator = createTokenValidator(
- 'a',
- mockClient as unknown as OAuth2Client,
- );
- await expect(validator(validJwt)).resolves.toMatchObject({
- sub: 's',
- email: 'e@mail.com',
- });
- });
-
- it('throws if the client throws', async () => {
- const mockClient = {
- getIapPublicKeys: async () => {
- throw new TypeError('bam');
- },
- };
- const validator = createTokenValidator(
- 'a',
- mockClient as unknown as OAuth2Client,
- );
- await expect(validator(validJwt)).rejects.toThrow(TypeError);
- });
-
- it('rejects empty payload', async () => {
- const mockClient = {
- getIapPublicKeys: async () => ({ pubkeys: '' }),
- verifySignedJwtWithCertsAsync: async () => ({
- getPayload: () => undefined,
- }),
- };
- const validator = createTokenValidator(
- 'a',
- mockClient as unknown as OAuth2Client,
- );
- await expect(validator(validJwt)).rejects.toMatchObject({
- name: 'TypeError',
- message: 'Token had no payload',
- });
- });
- });
-
- describe('parseRequestToken', () => {
- it('runs the happy path', async () => {
- await expect(
- parseRequestToken(
- validJwt,
- async () => ({ sub: 's', email: 'e@mail.com' } as any),
- ),
- ).resolves.toMatchObject({
- iapToken: {
- sub: 's',
- email: 'e@mail.com',
- },
- });
- });
-
- it('rejects bad tokens', async () => {
- await expect(
- parseRequestToken(7, undefined as any),
- ).rejects.toMatchObject({
- name: 'AuthenticationError',
- message: 'Missing Google IAP header',
- });
- await expect(
- parseRequestToken(undefined, undefined as any),
- ).rejects.toMatchObject({
- name: 'AuthenticationError',
- message: 'Missing Google IAP header',
- });
- await expect(
- parseRequestToken('', undefined as any),
- ).rejects.toMatchObject({
- name: 'AuthenticationError',
- message: 'Missing Google IAP header',
- });
- });
-
- it('translates validator errors', async () => {
- await expect(
- parseRequestToken(validJwt, async () => {
- throw new ConflictError('Ouch');
- }),
- ).rejects.toMatchObject({
- name: 'AuthenticationError',
- message: 'Google IAP token verification failed, ConflictError: Ouch',
- });
- });
-
- it('rejects bad token payloads', async () => {
- await expect(
- parseRequestToken(validJwt, async () => ({ sub: 'a' } as any)),
- ).rejects.toMatchObject({
- name: 'AuthenticationError',
- message: 'Google IAP token payload is missing sub and/or email claim',
- });
- });
- });
-});
diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts
deleted file mode 100644
index 26d9a8c904..0000000000
--- a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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 { AuthenticationError } from '@backstage/errors';
-import { OAuth2Client, TokenPayload } from 'google-auth-library';
-import { AuthHandler } from '../types';
-import { GcpIapResult } from './types';
-
-export function createTokenValidator(
- audience: string,
- mockClient?: OAuth2Client,
-): (token: string) => Promise {
- const client = mockClient ?? new OAuth2Client();
-
- return async function tokenValidator(token) {
- // TODO(freben): Rate limit the public key reads. It may be sensible to
- // cache these for some reasonable time rather than asking for the public
- // keys on every single sign-in. But since the rate of events here is so
- // slow, I decided to keep it simple for now.
- const response = await client.getIapPublicKeys();
- const ticket = await client.verifySignedJwtWithCertsAsync(
- token,
- response.pubkeys,
- audience,
- ['https://cloud.google.com/iap'],
- );
-
- const payload = ticket.getPayload();
- if (!payload) {
- throw new TypeError('Token had no payload');
- }
-
- return payload;
- };
-}
-
-export async function parseRequestToken(
- jwtToken: unknown,
- tokenValidator: (token: string) => Promise,
-): Promise {
- if (typeof jwtToken !== 'string' || !jwtToken) {
- throw new AuthenticationError('Missing Google IAP header');
- }
-
- let payload: TokenPayload;
- try {
- payload = await tokenValidator(jwtToken);
- } catch (e) {
- throw new AuthenticationError(`Google IAP token verification failed, ${e}`);
- }
-
- if (!payload.sub || !payload.email) {
- throw new AuthenticationError(
- 'Google IAP token payload is missing sub and/or email claim',
- );
- }
-
- return {
- iapToken: {
- ...payload,
- sub: payload.sub,
- email: payload.email,
- },
- };
-}
-
-export const defaultAuthHandler: AuthHandler = async ({
- iapToken,
-}) => ({ profile: { email: iapToken.email } });
diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts
deleted file mode 100644
index c01db3a853..0000000000
--- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts
+++ /dev/null
@@ -1,75 +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.
- */
-
-import express from 'express';
-import request from 'supertest';
-import { AuthResolverContext } from '../types';
-import { GcpIapProvider } from './provider';
-import { DEFAULT_IAP_JWT_HEADER } from './types';
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-describe('GcpIapProvider', () => {
- const authHandler = jest.fn();
- const signInResolver = jest.fn();
- const tokenValidator = jest.fn();
-
- it.each([undefined, 'x-custom-header'])(
- 'runs the happy path',
- async jwtHeader => {
- const provider = new GcpIapProvider({
- authHandler,
- signInResolver,
- tokenValidator,
- resolverContext: {} as AuthResolverContext,
- jwtHeader: jwtHeader,
- });
-
- // { "sub": "user:default/me", "ent": ["group:default/home"] }
- const backstageToken =
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc';
- const iapToken = { sub: 's', email: 'e@mail.com' };
-
- authHandler.mockResolvedValueOnce({ email: 'e@mail.com' });
- signInResolver.mockResolvedValueOnce({ token: backstageToken });
- tokenValidator.mockResolvedValueOnce(iapToken);
-
- const app = express();
- app.use('/refresh', provider.refresh.bind(provider));
-
- const header = jwtHeader || DEFAULT_IAP_JWT_HEADER;
- const response = await request(app).get('/refresh').set(header, 'token');
-
- expect(response.status).toBe(200);
- expect(response.get('content-type')).toBe(
- 'application/json; charset=utf-8',
- );
- expect(response.body).toEqual({
- backstageIdentity: {
- token: backstageToken,
- identity: {
- type: 'user',
- userEntityRef: 'user:default/me',
- ownershipEntityRefs: ['group:default/home'],
- },
- },
- providerInfo: { iapToken },
- });
- },
- );
-});
diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts
index 7e77853737..db4d9195b2 100644
--- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts
+++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts
@@ -14,70 +14,11 @@
* limitations under the License.
*/
-import express from 'express';
-import { TokenPayload } from 'google-auth-library';
+import { gcpIapAuthenticator } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
+import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
-import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
-import {
- AuthHandler,
- AuthProviderRouteHandlers,
- AuthResolverContext,
- SignInResolver,
-} from '../types';
-import {
- createTokenValidator,
- defaultAuthHandler,
- parseRequestToken,
-} from './helpers';
-import { GcpIapResponse, GcpIapResult, DEFAULT_IAP_JWT_HEADER } from './types';
-
-export class GcpIapProvider implements AuthProviderRouteHandlers {
- private readonly authHandler: AuthHandler;
- private readonly signInResolver: SignInResolver;
- private readonly tokenValidator: (token: string) => Promise;
- private readonly resolverContext: AuthResolverContext;
- private readonly jwtHeader: string;
-
- constructor(options: {
- authHandler: AuthHandler;
- signInResolver: SignInResolver;
- tokenValidator: (token: string) => Promise;
- resolverContext: AuthResolverContext;
- jwtHeader?: string;
- }) {
- this.authHandler = options.authHandler;
- this.signInResolver = options.signInResolver;
- this.tokenValidator = options.tokenValidator;
- this.resolverContext = options.resolverContext;
- this.jwtHeader = options?.jwtHeader || DEFAULT_IAP_JWT_HEADER;
- }
-
- async start() {}
-
- async frameHandler() {}
-
- async refresh(req: express.Request, res: express.Response): Promise {
- const result = await parseRequestToken(
- req.header(this.jwtHeader),
- this.tokenValidator,
- );
-
- const { profile } = await this.authHandler(result, this.resolverContext);
-
- const backstageIdentity = await this.signInResolver(
- { profile, result },
- this.resolverContext,
- );
-
- const response: GcpIapResponse = {
- providerInfo: { iapToken: result.iapToken },
- profile,
- backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
- };
-
- res.json(response);
- }
-}
+import { AuthHandler, SignInResolver } from '../types';
+import { GcpIapResult } from './types';
/**
* Auth provider integration for Google Identity-Aware Proxy auth
@@ -104,21 +45,10 @@ export const gcpIap = createAuthProviderIntegration({
resolver: SignInResolver;
};
}) {
- return ({ config, resolverContext }) => {
- const audience = config.getString('audience');
- const jwtHeader = config.getOptionalString('jwtHeader');
-
- const authHandler = options.authHandler ?? defaultAuthHandler;
- const signInResolver = options.signIn.resolver;
- const tokenValidator = createTokenValidator(audience);
-
- return new GcpIapProvider({
- authHandler,
- signInResolver,
- tokenValidator,
- resolverContext,
- jwtHeader,
- });
- };
+ return createProxyAuthProviderFactory({
+ authenticator: gcpIapAuthenticator,
+ profileTransform: options?.authHandler,
+ signInResolver: options?.signIn?.resolver,
+ });
},
});
diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts
index 6519de64ac..b1f69318fc 100644
--- a/plugins/auth-backend/src/providers/gcp-iap/types.ts
+++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts
@@ -14,58 +14,24 @@
* limitations under the License.
*/
-import { JsonValue } from '@backstage/types';
-import { AuthResponse } from '../types';
-
-/**
- * The header name used by the IAP.
- */
-export const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion';
+import {
+ GcpIapTokenInfo as _GcpIapTokenInfo,
+ GcpIapResult as _GcpIapResult,
+} from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
/**
* The data extracted from an IAP token.
*
* @public
+ * @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead
*/
-export type GcpIapTokenInfo = {
- /**
- * The unique, stable identifier for the user.
- */
- sub: string;
- /**
- * User email address.
- */
- email: string;
- /**
- * Other fields.
- */
- [key: string]: JsonValue;
-};
+export type GcpIapTokenInfo = _GcpIapTokenInfo;
/**
* The result of the initial auth challenge. This is the input to the auth
* callbacks.
*
* @public
+ * @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead
*/
-export type GcpIapResult = {
- /**
- * The data extracted from the IAP token header.
- */
- iapToken: GcpIapTokenInfo;
-};
-
-/**
- * The provider info to return to the frontend.
- */
-export type GcpIapProviderInfo = {
- /**
- * The data extracted from the IAP token header.
- */
- iapToken: GcpIapTokenInfo;
-};
-
-/**
- * The shape of the response to return to callers.
- */
-export type GcpIapResponse = AuthResponse