'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var path = require('path'); var parseArgs = require('minimist'); var cliCommon = require('@backstage/cli-common'); var configLoader = require('@backstage/config-loader'); var config = require('@backstage/config'); var getPackages = require('@manypkg/get-packages'); var http = require('http'); var https = require('https'); var stoppableServer = require('stoppable'); var fs = require('fs-extra'); var forge = require('node-forge'); var cors = require('cors'); var helmet = require('helmet'); var morgan = require('morgan'); var compression = require('compression'); var minimatch = require('minimatch'); var errors = require('@backstage/errors'); var winston = require('winston'); var backendPluginApi = require('@backstage/backend-plugin-api'); var alpha = require('@backstage/backend-plugin-api/alpha'); var backendCommon = require('@backstage/backend-common'); var backendAppApi = require('@backstage/backend-app-api'); var PromiseRouter = require('express-promise-router'); var types = require('@backstage/types'); var pluginAuthNode = require('@backstage/plugin-auth-node'); var pluginPermissionNode = require('@backstage/plugin-permission-node'); var express = require('express'); var trimEnd = require('lodash/trimEnd'); var backendTasks = require('@backstage/backend-tasks'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var parseArgs__default = /*#__PURE__*/_interopDefaultLegacy(parseArgs); var http__namespace = /*#__PURE__*/_interopNamespace(http); var https__namespace = /*#__PURE__*/_interopNamespace(https); var stoppableServer__default = /*#__PURE__*/_interopDefaultLegacy(stoppableServer); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var forge__default = /*#__PURE__*/_interopDefaultLegacy(forge); var cors__default = /*#__PURE__*/_interopDefaultLegacy(cors); var helmet__default = /*#__PURE__*/_interopDefaultLegacy(helmet); var morgan__default = /*#__PURE__*/_interopDefaultLegacy(morgan); var compression__default = /*#__PURE__*/_interopDefaultLegacy(compression); var PromiseRouter__default = /*#__PURE__*/_interopDefaultLegacy(PromiseRouter); var express__default = /*#__PURE__*/_interopDefaultLegacy(express); var trimEnd__default = /*#__PURE__*/_interopDefaultLegacy(trimEnd); var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class ObservableConfigProxy { constructor(parent, parentKey) { this.parent = parent; this.parentKey = parentKey; __publicField(this, "config", new config.ConfigReader({})); __publicField(this, "subscribers", []); if (parent && !parentKey) { throw new Error("parentKey is required if parent is set"); } } setConfig(config) { if (this.parent) { throw new Error("immutable"); } this.config = config; for (const subscriber of this.subscribers) { try { subscriber(); } catch (error) { console.error(`Config subscriber threw error, ${error}`); } } } subscribe(onChange) { if (this.parent) { return this.parent.subscribe(onChange); } this.subscribers.push(onChange); return { unsubscribe: () => { const index = this.subscribers.indexOf(onChange); if (index >= 0) { this.subscribers.splice(index, 1); } } }; } select(required) { var _a; if (this.parent && this.parentKey) { if (required) { return this.parent.select(true).getConfig(this.parentKey); } return (_a = this.parent.select(false)) == null ? void 0 : _a.getOptionalConfig(this.parentKey); } return this.config; } has(key) { var _a, _b; return (_b = (_a = this.select(false)) == null ? void 0 : _a.has(key)) != null ? _b : false; } keys() { var _a, _b; return (_b = (_a = this.select(false)) == null ? void 0 : _a.keys()) != null ? _b : []; } get(key) { return this.select(true).get(key); } getOptional(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptional(key); } getConfig(key) { return new ObservableConfigProxy(this, key); } getOptionalConfig(key) { var _a; if ((_a = this.select(false)) == null ? void 0 : _a.has(key)) { return new ObservableConfigProxy(this, key); } return void 0; } getConfigArray(key) { return this.select(true).getConfigArray(key); } getOptionalConfigArray(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalConfigArray(key); } getNumber(key) { return this.select(true).getNumber(key); } getOptionalNumber(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalNumber(key); } getBoolean(key) { return this.select(true).getBoolean(key); } getOptionalBoolean(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalBoolean(key); } getString(key) { return this.select(true).getString(key); } getOptionalString(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalString(key); } getStringArray(key) { return this.select(true).getStringArray(key); } getOptionalStringArray(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalStringArray(key); } } function isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } async function createConfigSecretEnumerator(options) { const { logger, dir = process.cwd() } = options; const { packages } = await getPackages.getPackages(dir); const schema = await configLoader.loadConfigSchema({ dependencies: packages.map((p) => p.packageJson.name) }); return (config) => { var _a; const [secretsData] = schema.process( [{ data: (_a = config.getOptional()) != null ? _a : {}, context: "schema-enumerator" }], { visibility: ["secret"], ignoreSchemaErrors: true } ); const secrets = /* @__PURE__ */ new Set(); JSON.parse( JSON.stringify(secretsData), (_, v) => typeof v === "string" && secrets.add(v) ); logger.info( `Found ${secrets.size} new secrets in config that will be redacted` ); return secrets; }; } async function loadBackendConfig(options) { var _a, _b; const args = parseArgs__default["default"](options.argv); const configTargets = [(_a = args.config) != null ? _a : []].flat().map((arg) => isValidUrl(arg) ? { url: arg } : { path: path.resolve(arg) }); const paths = cliCommon.findPaths(__dirname); let currentCancelFunc = void 0; const config$1 = new ObservableConfigProxy(); const { appConfigs } = await configLoader.loadConfig({ configRoot: paths.targetRoot, configTargets, remote: options.remote, watch: ((_b = options.watch) != null ? _b : true) ? { onChange(newConfigs) { console.info( `Reloaded config from ${newConfigs.map((c) => c.context).join(", ")}` ); const configsToMerge = [...newConfigs]; if (options.additionalConfigs) { configsToMerge.push(...options.additionalConfigs); } config$1.setConfig(config.ConfigReader.fromConfigs(configsToMerge)); }, stopSignal: new Promise((resolve) => { if (currentCancelFunc) { currentCancelFunc(); } currentCancelFunc = resolve; if (module.hot) { module.hot.addDisposeHandler(resolve); } }) } : void 0 }); console.info( `Loaded config from ${appConfigs.map((c) => c.context).join(", ")}` ); const finalAppConfigs = [...appConfigs]; if (options.additionalConfigs) { finalAppConfigs.push(...options.additionalConfigs); } config$1.setConfig(config.ConfigReader.fromConfigs(finalAppConfigs)); return { config: config$1 }; } const DEFAULT_PORT = 7007; const DEFAULT_HOST = ""; function readHttpServerOptions(config) { return { listen: readHttpListenOptions(config), https: readHttpsOptions(config) }; } function readHttpListenOptions(config) { var _a, _b; const listen = config == null ? void 0 : config.getOptional("listen"); if (typeof listen === "string") { const parts = String(listen).split(":"); const port = parseInt(parts[parts.length - 1], 10); if (!isNaN(port)) { if (parts.length === 1) { return { port, host: DEFAULT_HOST }; } if (parts.length === 2) { return { host: parts[0], port }; } } throw new Error( `Unable to parse listen address ${listen}, expected or :` ); } const host = (_a = config == null ? void 0 : config.getOptional("listen.host")) != null ? _a : DEFAULT_HOST; if (typeof host !== "string") { config == null ? void 0 : config.getOptionalString("listen.host"); throw new Error("unreachable"); } return { port: (_b = config == null ? void 0 : config.getOptionalNumber("listen.port")) != null ? _b : DEFAULT_PORT, host }; } function readHttpsOptions(config) { const https = config == null ? void 0 : config.getOptional("https"); if (https === true) { const baseUrl = config.getString("baseUrl"); let hostname; try { hostname = new URL(baseUrl).hostname; } catch (error) { throw new Error(`Invalid baseUrl "${baseUrl}"`); } return { certificate: { type: "generated", hostname } }; } const cc = config == null ? void 0 : config.getOptionalConfig("https"); if (!cc) { return void 0; } return { certificate: { type: "pem", cert: cc.getString("certificate.cert"), key: cc.getString("certificate.key") } }; } const FIVE_DAYS_IN_MS = 5 * 24 * 60 * 60 * 1e3; const IP_HOSTNAME_REGEX = /:|^\d+\.\d+\.\d+\.\d+$/; async function getGeneratedCertificate(hostname, logger) { const hasModules = await fs__default["default"].pathExists("node_modules"); let certPath; if (hasModules) { certPath = path.resolve( "node_modules/.cache/backstage-backend/dev-cert.pem" ); await fs__default["default"].ensureDir(path.dirname(certPath)); } else { certPath = path.resolve(".dev-cert.pem"); } if (await fs__default["default"].pathExists(certPath)) { try { const cert = await fs__default["default"].readFile(certPath); const crt = forge__default["default"].pki.certificateFromPem(cert.toString()); const remainingMs = crt.validity.notAfter.getTime() - Date.now(); if (remainingMs > FIVE_DAYS_IN_MS) { logger.info("Using existing self-signed certificate"); return { key: cert, cert }; } } catch (error) { logger.warn(`Unable to use existing self-signed certificate, ${error}`); } } logger.info("Generating new self-signed certificate"); const newCert = await generateCertificate(hostname); await fs__default["default"].writeFile(certPath, newCert.cert + newCert.key, "utf8"); return newCert; } async function generateCertificate(hostname) { const attributes = [ { name: "commonName", value: "dev-cert" } ]; const sans = [ { type: 2, // DNS value: "localhost" }, { type: 2, value: "localhost.localdomain" }, { type: 2, value: "[::1]" }, { type: 7, // IP ip: "127.0.0.1" }, { type: 7, ip: "fe80::1" } ]; if (!sans.find(({ value, ip }) => value === hostname || ip === hostname)) { sans.push( IP_HOSTNAME_REGEX.test(hostname) ? { type: 7, ip: hostname } : { type: 2, value: hostname } ); } const params = { algorithm: "sha256", keySize: 2048, days: 30, extensions: [ { name: "keyUsage", keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true }, { name: "extKeyUsage", serverAuth: true, clientAuth: true, codeSigning: true, timeStamping: true }, { name: "subjectAltName", altNames: sans } ] }; return new Promise( (resolve, reject) => require("selfsigned").generate( attributes, params, (err, bundle) => { if (err) { reject(err); } else { resolve({ key: bundle.private, cert: bundle.cert }); } } ) ); } async function createHttpServer(listener, options, deps) { const server = await createServer(listener, options, deps); const stopper = stoppableServer__default["default"](server, 0); const stopServer = stopper.stop.bind(stopper); return Object.assign(server, { start() { return new Promise((resolve, reject) => { const handleStartupError = (error) => { server.close(); reject(error); }; server.on("error", handleStartupError); const { host, port } = options.listen; server.listen(port, host, () => { server.off("error", handleStartupError); deps.logger.info(`Listening on ${host}:${port}`); resolve(); }); }); }, stop() { return new Promise((resolve, reject) => { stopServer((error) => { if (error) { reject(error); } else { resolve(); } }); }); }, port() { const address = server.address(); if (typeof address === "string" || address === null) { throw new Error(`Unexpected server address '${address}'`); } return address.port; } }); } async function createServer(listener, options, deps) { if (options.https) { const { certificate } = options.https; if (certificate.type === "generated") { const credentials = await getGeneratedCertificate( certificate.hostname, deps.logger ); return https__namespace.createServer(credentials, listener); } return https__namespace.createServer(certificate, listener); } return http__namespace.createServer(listener); } function readHelmetOptions(config) { const cspOptions = readCspDirectives(config); return { contentSecurityPolicy: { useDefaults: false, directives: applyCspDirectives(cspOptions) }, // These are all disabled in order to maintain backwards compatibility // when bumping helmet v5. We can't enable these by default because // there is no way for users to configure them. // TODO(Rugvip): We should give control of this setup to consumers crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false }; } function readCspDirectives(config) { const cc = config == null ? void 0 : config.getOptionalConfig("csp"); if (!cc) { return void 0; } const result = {}; for (const key of cc.keys()) { if (cc.get(key) === false) { result[key] = false; } else { result[key] = cc.getStringArray(key); } } return result; } function applyCspDirectives(directives) { const result = helmet__default["default"].contentSecurityPolicy.getDefaultDirectives(); result["script-src"] = ["'self'", "'unsafe-eval'"]; delete result["form-action"]; if (directives) { for (const [key, value] of Object.entries(directives)) { if (value === false) { delete result[key]; } else { result[key] = value; } } } return result; } function readCorsOptions(config) { const cc = config == null ? void 0 : config.getOptionalConfig("cors"); if (!cc) { return { origin: false }; } return removeUnknown({ origin: createCorsOriginMatcher(readStringArray(cc, "origin")), methods: readStringArray(cc, "methods"), allowedHeaders: readStringArray(cc, "allowedHeaders"), exposedHeaders: readStringArray(cc, "exposedHeaders"), credentials: cc.getOptionalBoolean("credentials"), maxAge: cc.getOptionalNumber("maxAge"), preflightContinue: cc.getOptionalBoolean("preflightContinue"), optionsSuccessStatus: cc.getOptionalNumber("optionsSuccessStatus") }); } function removeUnknown(obj) { return Object.fromEntries( Object.entries(obj).filter(([, v]) => v !== void 0) ); } function readStringArray(config, key) { const value = config.getOptional(key); if (typeof value === "string") { return [value]; } else if (!value) { return void 0; } return config.getStringArray(key); } function createCorsOriginMatcher(allowedOriginPatterns) { if (!allowedOriginPatterns) { return void 0; } const allowedOriginMatchers = allowedOriginPatterns.map( (pattern) => new minimatch.Minimatch(pattern, { nocase: true, noglobstar: true }) ); return (origin, callback) => { return callback( null, allowedOriginMatchers.some((pattern) => pattern.match(origin != null ? origin : "")) ); }; } var __accessCheck$8 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$8 = (obj, member, getter) => { __accessCheck$8(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$8 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$8 = (obj, member, value, setter) => { __accessCheck$8(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var _config, _logger; const _MiddlewareFactory = class _MiddlewareFactory { constructor(options) { __privateAdd$8(this, _config, void 0); __privateAdd$8(this, _logger, void 0); __privateSet$8(this, _config, options.config); __privateSet$8(this, _logger, options.logger); } /** * Creates a new {@link MiddlewareFactory}. */ static create(options) { return new _MiddlewareFactory(options); } /** * Returns a middleware that unconditionally produces a 404 error response. * * @remarks * * Typically you want to place this middleware at the end of the chain, such * that it's the last one attempted after no other routes matched. * * @returns An Express request handler */ notFound() { return (_req, res) => { res.status(404).end(); }; } /** * Returns the compression middleware. * * @remarks * * The middleware will attempt to compress response bodies for all requests * that traverse through the middleware. */ compression() { return compression__default["default"](); } /** * Returns a request logging middleware. * * @remarks * * Typically you want to place this middleware at the start of the chain, such * that it always logs requests whether they are "caught" by handlers farther * down or not. * * @returns An Express request handler */ logging() { const logger = __privateGet$8(this, _logger).child({ type: "incomingRequest" }); return morgan__default["default"]("combined", { stream: { write(message) { logger.info(message.trimEnd()); } } }); } /** * Returns a middleware that implements the helmet library. * * @remarks * * This middleware applies security policies to incoming requests and outgoing * responses. It is configured using config keys such as `backend.csp`. * * @see {@link https://helmetjs.github.io/} * * @returns An Express request handler */ helmet() { return helmet__default["default"](readHelmetOptions(__privateGet$8(this, _config).getOptionalConfig("backend"))); } /** * Returns a middleware that implements the cors library. * * @remarks * * This middleware handles CORS. It is configured using the config key * `backend.cors`. * * @see {@link https://github.com/expressjs/cors} * * @returns An Express request handler */ cors() { return cors__default["default"](readCorsOptions(__privateGet$8(this, _config).getOptionalConfig("backend"))); } /** * Express middleware to handle errors during request processing. * * @remarks * * This is commonly the very last middleware in the chain. * * Its primary purpose is not to do translation of business logic exceptions, * but rather to be a global catch-all for uncaught "fatal" errors that are * expected to result in a 500 error. However, it also does handle some common * error types (such as http-error exceptions, and the well-known error types * in the `@backstage/errors` package) and returns the enclosed status code * accordingly. * * It will also produce a response body with a serialized form of the error, * unless a previous handler already did send a body. See * {@link @backstage/errors#ErrorResponseBody} for the response shape used. * * @returns An Express error request handler */ error(options = {}) { var _a; const showStackTraces = (_a = options.showStackTraces) != null ? _a : process.env.NODE_ENV === "development"; const logger = __privateGet$8(this, _logger).child({ type: "errorHandler" }); return (error, req, res, next) => { const statusCode = getStatusCode(error); if (options.logAllErrors || statusCode >= 500) { logger.error(`Request failed with status ${statusCode}`, error); } if (res.headersSent) { next(error); return; } const body = { error: errors.serializeError(error, { includeStack: showStackTraces }), request: { method: req.method, url: req.url }, response: { statusCode } }; res.status(statusCode).json(body); }; } }; _config = new WeakMap(); _logger = new WeakMap(); let MiddlewareFactory = _MiddlewareFactory; function getStatusCode(error) { const knownStatusCodeFields = ["statusCode", "status"]; for (const field of knownStatusCodeFields) { const statusCode = error[field]; if (typeof statusCode === "number" && (statusCode | 0) === statusCode && // is whole integer statusCode >= 100 && statusCode <= 599) { return statusCode; } } switch (error.name) { case errors.NotModifiedError.name: return 304; case errors.InputError.name: return 400; case errors.AuthenticationError.name: return 401; case errors.NotAllowedError.name: return 403; case errors.NotFoundError.name: return 404; case errors.ConflictError.name: return 409; case errors.NotImplementedError.name: return 501; case errors.ServiceUnavailableError.name: return 503; } return 500; } const escapeRegExp = (text) => { return text.replace(/[.*+?^${}(\)|[\]\\]/g, "\\$&"); }; var __accessCheck$7 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$7 = (obj, member, getter) => { __accessCheck$7(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$7 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$7 = (obj, member, value, setter) => { __accessCheck$7(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var _winston, _addRedactions; const _WinstonLogger = class _WinstonLogger { constructor(winston, addRedactions) { __privateAdd$7(this, _winston, void 0); __privateAdd$7(this, _addRedactions, void 0); __privateSet$7(this, _winston, winston); __privateSet$7(this, _addRedactions, addRedactions); } /** * Creates a {@link WinstonLogger} instance. */ static create(options) { var _a; const redacter = _WinstonLogger.redacter(); let logger = winston.createLogger({ level: options.level, format: winston.format.combine(redacter.format, options.format), transports: (_a = options.transports) != null ? _a : new winston.transports.Console() }); if (options.meta) { logger = logger.child(options.meta); } return new _WinstonLogger(logger, redacter.add); } /** * Creates a winston log formatter for redacting secrets. */ static redacter() { const redactionSet = /* @__PURE__ */ new Set(); let redactionPattern = void 0; return { format: winston.format((info) => { if (redactionPattern && typeof info.message === "string") { info.message = info.message.replace(redactionPattern, "[REDACTED]"); } if (redactionPattern && typeof info.stack === "string") { info.stack = info.stack.replace(redactionPattern, "[REDACTED]"); } return info; })(), add(newRedactions) { let added = 0; for (const redactionToTrim of newRedactions) { const redaction = redactionToTrim.trim(); if (redaction.length <= 1) { continue; } if (!redactionSet.has(redaction)) { redactionSet.add(redaction); added += 1; } } if (added > 0) { const redactions = Array.from(redactionSet).map((r) => escapeRegExp(r)).join("|"); redactionPattern = new RegExp(`(${redactions})`, "g"); } } }; } /** * Creates a pretty printed winston log formatter. */ static colorFormat() { const colorizer = winston.format.colorize(); return winston.format.combine( winston.format.timestamp(), winston.format.colorize({ colors: { timestamp: "dim", prefix: "blue", field: "cyan", debug: "grey" } }), winston.format.printf((info) => { const { timestamp, level, message, plugin, service, ...fields } = info; const prefix = plugin || service; const timestampColor = colorizer.colorize("timestamp", timestamp); const prefixColor = colorizer.colorize("prefix", prefix); const extraFields = Object.entries(fields).map( ([key, value]) => `${colorizer.colorize("field", `${key}`)}=${value}` ).join(" "); return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`; }) ); } error(message, meta) { __privateGet$7(this, _winston).error(message, meta); } warn(message, meta) { __privateGet$7(this, _winston).warn(message, meta); } info(message, meta) { __privateGet$7(this, _winston).info(message, meta); } debug(message, meta) { __privateGet$7(this, _winston).debug(message, meta); } child(meta) { return new _WinstonLogger(__privateGet$7(this, _winston).child(meta)); } addRedactions(redactions) { var _a; (_a = __privateGet$7(this, _addRedactions)) == null ? void 0 : _a.call(this, redactions); } }; _winston = new WeakMap(); _addRedactions = new WeakMap(); let WinstonLogger = _WinstonLogger; var __accessCheck$6 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$6 = (obj, member, getter) => { __accessCheck$6(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$6 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$6 = (obj, member, value, setter) => { __accessCheck$6(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var _hasStarted$1, _startupTasks$1, _hasShutdown, _shutdownTasks; class BackendLifecycleImpl { constructor(logger) { this.logger = logger; __privateAdd$6(this, _hasStarted$1, false); __privateAdd$6(this, _startupTasks$1, []); __privateAdd$6(this, _hasShutdown, false); __privateAdd$6(this, _shutdownTasks, []); } addStartupHook(hook, options) { if (__privateGet$6(this, _hasStarted$1)) { throw new Error("Attempted to add startup hook after startup"); } __privateGet$6(this, _startupTasks$1).push({ hook, options }); } async startup() { if (__privateGet$6(this, _hasStarted$1)) { return; } __privateSet$6(this, _hasStarted$1, true); this.logger.debug(`Running ${__privateGet$6(this, _startupTasks$1).length} startup tasks...`); await Promise.all( __privateGet$6(this, _startupTasks$1).map(async ({ hook, options }) => { var _a; const logger = (_a = options == null ? void 0 : options.logger) != null ? _a : this.logger; try { await hook(); logger.debug(`Startup hook succeeded`); } catch (error) { logger.error(`Startup hook failed, ${error}`); } }) ); } addShutdownHook(hook, options) { if (__privateGet$6(this, _hasShutdown)) { throw new Error("Attempted to add shutdown hook after shutdown"); } __privateGet$6(this, _shutdownTasks).push({ hook, options }); } async shutdown() { if (__privateGet$6(this, _hasShutdown)) { return; } __privateSet$6(this, _hasShutdown, true); this.logger.debug( `Running ${__privateGet$6(this, _shutdownTasks).length} shutdown tasks...` ); await Promise.all( __privateGet$6(this, _shutdownTasks).map(async ({ hook, options }) => { var _a; const logger = (_a = options == null ? void 0 : options.logger) != null ? _a : this.logger; try { await hook(); logger.debug(`Shutdown hook succeeded`); } catch (error) { logger.error(`Shutdown hook failed, ${error}`); } }) ); } } _hasStarted$1 = new WeakMap(); _startupTasks$1 = new WeakMap(); _hasShutdown = new WeakMap(); _shutdownTasks = new WeakMap(); const rootLifecycleServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.rootLifecycle, deps: { logger: backendPluginApi.coreServices.rootLogger }, async factory({ logger }) { return new BackendLifecycleImpl(logger); } }); var __accessCheck$5 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$5 = (obj, member, getter) => { __accessCheck$5(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$5 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$5 = (obj, member, value, setter) => { __accessCheck$5(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var _hasStarted, _startupTasks; class BackendPluginLifecycleImpl { constructor(logger, rootLifecycle, pluginMetadata) { this.logger = logger; this.rootLifecycle = rootLifecycle; this.pluginMetadata = pluginMetadata; __privateAdd$5(this, _hasStarted, false); __privateAdd$5(this, _startupTasks, []); } addStartupHook(hook, options) { if (__privateGet$5(this, _hasStarted)) { throw new Error("Attempted to add startup hook after startup"); } __privateGet$5(this, _startupTasks).push({ hook, options }); } async startup() { if (__privateGet$5(this, _hasStarted)) { return; } __privateSet$5(this, _hasStarted, true); this.logger.debug( `Running ${__privateGet$5(this, _startupTasks).length} plugin startup tasks...` ); await Promise.all( __privateGet$5(this, _startupTasks).map(async ({ hook, options }) => { var _a; const logger = (_a = options == null ? void 0 : options.logger) != null ? _a : this.logger; try { await hook(); logger.debug(`Plugin startup hook succeeded`); } catch (error) { logger.error(`Plugin startup hook failed, ${error}`); } }) ); } addShutdownHook(hook, options) { var _a, _b; const plugin = this.pluginMetadata.getId(); this.rootLifecycle.addShutdownHook(hook, { logger: (_b = (_a = options == null ? void 0 : options.logger) == null ? void 0 : _a.child({ plugin })) != null ? _b : this.logger }); } } _hasStarted = new WeakMap(); _startupTasks = new WeakMap(); const lifecycleServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.lifecycle, deps: { logger: backendPluginApi.coreServices.logger, rootLifecycle: backendPluginApi.coreServices.rootLifecycle, pluginMetadata: backendPluginApi.coreServices.pluginMetadata }, async factory({ rootLifecycle, logger, pluginMetadata }) { return new BackendPluginLifecycleImpl( logger, rootLifecycle, pluginMetadata ); } }); var __accessCheck$4 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$4 = (obj, member, getter) => { __accessCheck$4(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$4 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$4 = (obj, member, value, setter) => { __accessCheck$4(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod$3 = (obj, member, method) => { __accessCheck$4(obj, member, "access private method"); return method; }; var _nodeIds, _cycleKeys, _getCycleKey, getCycleKey_fn, _nodes, _allProvided; class Node { constructor(value, consumes, provides) { this.value = value; this.consumes = consumes; this.provides = provides; } static from(input) { return new Node( input.value, input.consumes ? new Set(input.consumes) : /* @__PURE__ */ new Set(), input.provides ? new Set(input.provides) : /* @__PURE__ */ new Set() ); } } const _CycleKeySet = class _CycleKeySet { constructor(nodes) { __privateAdd$4(this, _getCycleKey); __privateAdd$4(this, _nodeIds, void 0); __privateAdd$4(this, _cycleKeys, void 0); __privateSet$4(this, _nodeIds, new Map(nodes.map((n, i) => [n.value, i]))); __privateSet$4(this, _cycleKeys, /* @__PURE__ */ new Set()); } static from(nodes) { return new _CycleKeySet(nodes); } tryAdd(path) { const cycleKey = __privateMethod$3(this, _getCycleKey, getCycleKey_fn).call(this, path); if (__privateGet$4(this, _cycleKeys).has(cycleKey)) { return false; } __privateGet$4(this, _cycleKeys).add(cycleKey); return true; } }; _nodeIds = new WeakMap(); _cycleKeys = new WeakMap(); _getCycleKey = new WeakSet(); getCycleKey_fn = function(path) { return path.map((n) => __privateGet$4(this, _nodeIds).get(n)).sort().join(","); }; let CycleKeySet = _CycleKeySet; const _DependencyGraph = class _DependencyGraph { constructor(nodes) { __privateAdd$4(this, _nodes, void 0); __privateAdd$4(this, _allProvided, void 0); __privateSet$4(this, _nodes, nodes); __privateSet$4(this, _allProvided, /* @__PURE__ */ new Set()); for (const node of __privateGet$4(this, _nodes).values()) { for (const produced of node.provides) { __privateGet$4(this, _allProvided).add(produced); } } } static fromMap(nodes) { return this.fromIterable( Object.entries(nodes).map(([key, node]) => ({ value: String(key), ...node })) ); } static fromIterable(nodeInputs) { const nodes = new Array(); for (const nodeInput of nodeInputs) { nodes.push(Node.from(nodeInput)); } return new _DependencyGraph(nodes); } /** * Find all nodes that consume dependencies that are not provided by any other node. */ findUnsatisfiedDeps() { const unsatisfiedDependencies = []; for (const node of __privateGet$4(this, _nodes).values()) { const unsatisfied = Array.from(node.consumes).filter( (id) => !__privateGet$4(this, _allProvided).has(id) ); if (unsatisfied.length > 0) { unsatisfiedDependencies.push({ value: node.value, unsatisfied }); } } return unsatisfiedDependencies; } /** * Detect the first circular dependency 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() { return this.detectCircularDependencies().next().value; } /** * 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. */ *detectCircularDependencies() { const cycleKeys = CycleKeySet.from(__privateGet$4(this, _nodes)); for (const startNode of __privateGet$4(this, _nodes)) { const visited = /* @__PURE__ */ new Set(); const stack = new Array([ startNode, [startNode.value] ]); while (stack.length > 0) { const [node, path] = stack.pop(); if (visited.has(node)) { continue; } visited.add(node); for (const consumed of node.consumes) { const providerNodes = __privateGet$4(this, _nodes).filter( (other) => other.provides.has(consumed) ); for (const provider of providerNodes) { if (provider === startNode) { if (cycleKeys.tryAdd(path)) { yield [...path, startNode.value]; } break; } if (!visited.has(provider)) { stack.push([provider, [...path, provider.value]]); } } } } } return void 0; } /** * 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) { const allProvided = __privateGet$4(this, _allProvided); const producedSoFar = /* @__PURE__ */ new Set(); const waiting = new Set(__privateGet$4(this, _nodes).values()); const visited = /* @__PURE__ */ new Set(); const results = new Array(); let inFlight = 0; 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) { throw new Error("Circular dependency detected"); } await Promise.all(nodesToProcess.map(processNode)); } async function processNode(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; } }; _nodes = new WeakMap(); _allProvided = new WeakMap(); let DependencyGraph = _DependencyGraph; var __accessCheck$3 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$3 = (obj, member, getter) => { __accessCheck$3(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$3 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$3 = (obj, member, value, setter) => { __accessCheck$3(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod$2 = (obj, member, method) => { __accessCheck$3(obj, member, "access private method"); return method; }; var _providedFactories, _loadedDefaultFactories, _implementations, _rootServiceImplementations, _addedFactoryIds, _instantiatedFactories, _resolveFactory, resolveFactory_fn, _checkForMissingDeps, checkForMissingDeps_fn; function toInternalServiceFactory(factory) { const f = factory; if (f.$$type !== "@backstage/BackendFeature") { throw new Error(`Invalid service factory, bad type '${f.$$type}'`); } if (f.version !== "v1") { throw new Error(`Invalid service factory, bad version '${f.version}'`); } return f; } const pluginMetadataServiceFactory = backendPluginApi.createServiceFactory( (options) => ({ service: backendPluginApi.coreServices.pluginMetadata, deps: {}, factory: async () => ({ getId: () => options == null ? void 0 : options.pluginId }) }) ); const _ServiceRegistry = class _ServiceRegistry { constructor(factories) { __privateAdd$3(this, _resolveFactory); __privateAdd$3(this, _checkForMissingDeps); __privateAdd$3(this, _providedFactories, void 0); __privateAdd$3(this, _loadedDefaultFactories, void 0); __privateAdd$3(this, _implementations, void 0); __privateAdd$3(this, _rootServiceImplementations, /* @__PURE__ */ new Map()); __privateAdd$3(this, _addedFactoryIds, /* @__PURE__ */ new Set()); __privateAdd$3(this, _instantiatedFactories, /* @__PURE__ */ new Set()); __privateSet$3(this, _providedFactories, new Map( factories.map((sf) => [sf.service.id, toInternalServiceFactory(sf)]) )); __privateSet$3(this, _loadedDefaultFactories, /* @__PURE__ */ new Map()); __privateSet$3(this, _implementations, /* @__PURE__ */ new Map()); } static create(factories) { const registry = new _ServiceRegistry(factories); registry.checkForCircularDeps(); return registry; } checkForCircularDeps() { const graph = DependencyGraph.fromIterable( Array.from(__privateGet$3(this, _providedFactories)).map( ([serviceId, serviceFactory]) => ({ value: serviceId, provides: [serviceId], consumes: Object.values(serviceFactory.deps).map((d) => d.id) }) ) ); const circularDependencies = Array.from(graph.detectCircularDependencies()); if (circularDependencies.length) { const cycles = circularDependencies.map((c) => c.map((id) => `'${id}'`).join(" -> ")).join("\n "); throw new errors.ConflictError(`Circular dependencies detected: ${cycles}`); } } add(factory) { const factoryId = factory.service.id; if (factoryId === backendPluginApi.coreServices.pluginMetadata.id) { throw new Error( `The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden` ); } if (__privateGet$3(this, _addedFactoryIds).has(factoryId)) { throw new Error( `Duplicate service implementations provided for ${factoryId}` ); } if (__privateGet$3(this, _instantiatedFactories).has(factoryId)) { throw new Error( `Unable to set service factory with id ${factoryId}, service has already been instantiated` ); } __privateGet$3(this, _addedFactoryIds).add(factoryId); __privateGet$3(this, _providedFactories).set(factoryId, toInternalServiceFactory(factory)); } getServiceRefs() { return Array.from(__privateGet$3(this, _providedFactories).values()).map((f) => f.service); } get(ref, pluginId) { var _a; __privateGet$3(this, _instantiatedFactories).add(ref.id); return (_a = __privateMethod$2(this, _resolveFactory, resolveFactory_fn).call(this, ref, pluginId)) == null ? void 0 : _a.then((factory) => { if (factory.service.scope === "root") { let existing = __privateGet$3(this, _rootServiceImplementations).get(factory); if (!existing) { __privateMethod$2(this, _checkForMissingDeps, checkForMissingDeps_fn).call(this, factory, pluginId); const rootDeps = new Array(); for (const [name, serviceRef] of Object.entries(factory.deps)) { if (serviceRef.scope !== "root") { throw new Error( `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.` ); } const target = this.get(serviceRef, pluginId); rootDeps.push(target.then((impl) => [name, impl])); } existing = Promise.all(rootDeps).then( (entries) => factory.factory(Object.fromEntries(entries), void 0) ); __privateGet$3(this, _rootServiceImplementations).set(factory, existing); } return existing; } let implementation = __privateGet$3(this, _implementations).get(factory); if (!implementation) { __privateMethod$2(this, _checkForMissingDeps, checkForMissingDeps_fn).call(this, factory, pluginId); const rootDeps = new Array(); for (const [name, serviceRef] of Object.entries(factory.deps)) { if (serviceRef.scope === "root") { const target = this.get(serviceRef, pluginId); rootDeps.push(target.then((impl) => [name, impl])); } } implementation = { context: Promise.all(rootDeps).then( (entries) => { var _a2; return (_a2 = factory.createRootContext) == null ? void 0 : _a2.call(factory, Object.fromEntries(entries)); } ).catch((error) => { const cause = errors.stringifyError(error); throw new Error( `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}` ); }), byPlugin: /* @__PURE__ */ new Map() }; __privateGet$3(this, _implementations).set(factory, implementation); } let result = implementation.byPlugin.get(pluginId); if (!result) { const allDeps = new Array(); for (const [name, serviceRef] of Object.entries(factory.deps)) { const target = this.get(serviceRef, pluginId); allDeps.push(target.then((impl) => [name, impl])); } result = implementation.context.then( (context) => Promise.all(allDeps).then( (entries) => factory.factory(Object.fromEntries(entries), context) ) ).catch((error) => { const cause = errors.stringifyError(error); throw new Error( `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}` ); }); implementation.byPlugin.set(pluginId, result); } return result; }); } }; _providedFactories = new WeakMap(); _loadedDefaultFactories = new WeakMap(); _implementations = new WeakMap(); _rootServiceImplementations = new WeakMap(); _addedFactoryIds = new WeakMap(); _instantiatedFactories = new WeakMap(); _resolveFactory = new WeakSet(); resolveFactory_fn = function(ref, pluginId) { if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) { return Promise.resolve( toInternalServiceFactory(pluginMetadataServiceFactory({ pluginId })) ); } let resolvedFactory = __privateGet$3(this, _providedFactories).get(ref.id); const { __defaultFactory: defaultFactory } = ref; if (!resolvedFactory && !defaultFactory) { return void 0; } if (!resolvedFactory) { let loadedFactory = __privateGet$3(this, _loadedDefaultFactories).get(defaultFactory); if (!loadedFactory) { loadedFactory = Promise.resolve().then(() => defaultFactory(ref)).then( (f) => toInternalServiceFactory(typeof f === "function" ? f() : f) ); __privateGet$3(this, _loadedDefaultFactories).set(defaultFactory, loadedFactory); } resolvedFactory = loadedFactory.catch((error) => { throw new Error( `Failed to instantiate service '${ref.id}' because the default factory loader threw an error, ${errors.stringifyError( error )}` ); }); } return Promise.resolve(resolvedFactory); }; _checkForMissingDeps = new WeakSet(); checkForMissingDeps_fn = function(factory, pluginId) { const missingDeps = Object.values(factory.deps).filter((ref) => { if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) { return false; } if (__privateGet$3(this, _providedFactories).get(ref.id)) { return false; } return !ref.__defaultFactory; }); if (missingDeps.length) { const missing = missingDeps.map((r) => `'${r.id}'`).join(", "); throw new Error( `Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}` ); } }; let ServiceRegistry = _ServiceRegistry; var __accessCheck$2 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$2 = (obj, member, getter) => { __accessCheck$2(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$2 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$2 = (obj, member, value, setter) => { __accessCheck$2(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod$1 = (obj, member, method) => { __accessCheck$2(obj, member, "access private method"); return method; }; var _startPromise, _features, _extensionPoints, _serviceRegistry, _registeredFeatures, _getInitDeps, getInitDeps_fn, _addFeature, addFeature_fn, _doStart, doStart_fn, _getRootLifecycleImpl, getRootLifecycleImpl_fn, _getPluginLifecycleImpl, getPluginLifecycleImpl_fn; class BackendInitializer { constructor(defaultApiFactories) { __privateAdd$2(this, _getInitDeps); __privateAdd$2(this, _addFeature); __privateAdd$2(this, _doStart); // Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this __privateAdd$2(this, _getRootLifecycleImpl); __privateAdd$2(this, _getPluginLifecycleImpl); __privateAdd$2(this, _startPromise, void 0); __privateAdd$2(this, _features, new Array()); __privateAdd$2(this, _extensionPoints, /* @__PURE__ */ new Map()); __privateAdd$2(this, _serviceRegistry, void 0); __privateAdd$2(this, _registeredFeatures, new Array()); __privateSet$2(this, _serviceRegistry, ServiceRegistry.create([...defaultApiFactories])); } add(feature) { if (__privateGet$2(this, _startPromise)) { throw new Error("feature can not be added after the backend has started"); } __privateGet$2(this, _registeredFeatures).push(Promise.resolve(feature)); } async start() { if (__privateGet$2(this, _startPromise)) { throw new Error("Backend has already started"); } const exitHandler = async () => { process.removeListener("SIGTERM", exitHandler); process.removeListener("SIGINT", exitHandler); process.removeListener("beforeExit", exitHandler); try { await this.stop(); process.exit(0); } catch (error) { console.error(error); process.exit(1); } }; process.addListener("SIGTERM", exitHandler); process.addListener("SIGINT", exitHandler); process.addListener("beforeExit", exitHandler); __privateSet$2(this, _startPromise, __privateMethod$1(this, _doStart, doStart_fn).call(this)); await __privateGet$2(this, _startPromise); } async stop() { if (!__privateGet$2(this, _startPromise)) { return; } try { await __privateGet$2(this, _startPromise); } catch (error) { } const lifecycleService = await __privateMethod$1(this, _getRootLifecycleImpl, getRootLifecycleImpl_fn).call(this); await lifecycleService.shutdown(); } } _startPromise = new WeakMap(); _features = new WeakMap(); _extensionPoints = new WeakMap(); _serviceRegistry = new WeakMap(); _registeredFeatures = new WeakMap(); _getInitDeps = new WeakSet(); getInitDeps_fn = async function(deps, pluginId) { const result = /* @__PURE__ */ new Map(); const missingRefs = /* @__PURE__ */ new Set(); for (const [name, ref] of Object.entries(deps)) { const ep = __privateGet$2(this, _extensionPoints).get(ref.id); 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 __privateGet$2(this, _serviceRegistry).get( ref, pluginId ); if (impl) { result.set(name, impl); } else { missingRefs.add(ref); } } } if (missingRefs.size > 0) { const missing = Array.from(missingRefs).join(", "); throw new Error( `No extension point or service available for the following ref(s): ${missing}` ); } return Object.fromEntries(result); }; _addFeature = new WeakSet(); addFeature_fn = function(feature) { if (feature.$$type !== "@backstage/BackendFeature") { throw new Error( `Failed to add feature, invalid type '${feature.$$type}'` ); } if (isServiceFactory(feature)) { __privateGet$2(this, _serviceRegistry).add(feature); } else if (isInternalBackendFeature(feature)) { if (feature.version !== "v1") { throw new Error( `Failed to add feature, invalid version '${feature.version}'` ); } __privateGet$2(this, _features).push(feature); } else { throw new Error( `Failed to add feature, invalid feature ${JSON.stringify(feature)}` ); } }; _doStart = new WeakSet(); doStart_fn = async function() { __privateGet$2(this, _serviceRegistry).checkForCircularDeps(); for (const feature of __privateGet$2(this, _registeredFeatures)) { __privateMethod$1(this, _addFeature, addFeature_fn).call(this, await feature); } const featureDiscovery = await __privateGet$2(this, _serviceRegistry).get( alpha.featureDiscoveryServiceRef, "root" ); if (featureDiscovery) { const { features } = await featureDiscovery.getBackendFeatures(); for (const feature of features) { __privateMethod$1(this, _addFeature, addFeature_fn).call(this, feature); } __privateGet$2(this, _serviceRegistry).checkForCircularDeps(); } for (const ref of __privateGet$2(this, _serviceRegistry).getServiceRefs()) { if (ref.scope === "root") { await __privateGet$2(this, _serviceRegistry).get(ref, "root"); } } const pluginInits = /* @__PURE__ */ new Map(); const moduleInits = /* @__PURE__ */ new Map(); for (const feature of __privateGet$2(this, _features)) { for (const r of feature.getRegistrations()) { const provides = /* @__PURE__ */ new Set(); if (r.type === "plugin" || r.type === "module") { for (const [extRef, extImpl] of r.extensionPoints) { if (__privateGet$2(this, _extensionPoints).has(extRef.id)) { throw new Error( `ExtensionPoint with ID '${extRef.id}' is already registered` ); } __privateGet$2(this, _extensionPoints).set(extRef.id, { impl: extImpl, pluginId: r.pluginId }); provides.add(extRef); } } if (r.type === "plugin") { if (pluginInits.has(r.pluginId)) { throw new Error(`Plugin '${r.pluginId}' is already registered`); } pluginInits.set(r.pluginId, { provides, consumes: new Set(Object.values(r.init.deps)), init: r.init }); } else { let modules = moduleInits.get(r.pluginId); if (!modules) { modules = /* @__PURE__ */ new Map(); moduleInits.set(r.pluginId, modules); } if (modules.has(r.moduleId)) { throw new Error( `Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered` ); } modules.set(r.moduleId, { provides, consumes: new Set(Object.values(r.init.deps)), init: r.init }); } } } const allPluginIds = [ .../* @__PURE__ */ new Set([...pluginInits.keys(), ...moduleInits.keys()]) ]; await Promise.all( allPluginIds.map(async (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 errors.ConflictError( `Circular dependency detected for modules of plugin '${pluginId}', ${circular.map(({ moduleId }) => `'${moduleId}'`).join(" -> ")}` ); } await tree.parallelTopologicalTraversal( async ({ moduleId, moduleInit }) => { const moduleDeps = await __privateMethod$1(this, _getInitDeps, getInitDeps_fn).call(this, moduleInit.init.deps, pluginId); await moduleInit.init.func(moduleDeps).catch((error) => { throw new errors.ForwardedError( `Module '${moduleId}' for plugin '${pluginId}' startup failed`, error ); }); } ); } const pluginInit = pluginInits.get(pluginId); if (pluginInit) { const pluginDeps = await __privateMethod$1(this, _getInitDeps, getInitDeps_fn).call(this, pluginInit.init.deps, pluginId); await pluginInit.init.func(pluginDeps).catch((error) => { throw new errors.ForwardedError( `Plugin '${pluginId}' startup failed`, error ); }); } const lifecycleService2 = await __privateMethod$1(this, _getPluginLifecycleImpl, getPluginLifecycleImpl_fn).call(this, pluginId); await lifecycleService2.startup(); }) ); const lifecycleService = await __privateMethod$1(this, _getRootLifecycleImpl, getRootLifecycleImpl_fn).call(this); await lifecycleService.startup(); if (process.env.NODE_ENV !== "test") { const rootLogger = await __privateGet$2(this, _serviceRegistry).get( backendPluginApi.coreServices.rootLogger, "root" ); process.on("unhandledRejection", (reason) => { var _a; (_a = rootLogger == null ? void 0 : rootLogger.child({ type: "unhandledRejection" })) == null ? void 0 : _a.error("Unhandled rejection", reason); }); process.on("uncaughtException", (error) => { var _a; (_a = rootLogger == null ? void 0 : rootLogger.child({ type: "uncaughtException" })) == null ? void 0 : _a.error("Uncaught exception", error); }); } }; _getRootLifecycleImpl = new WeakSet(); getRootLifecycleImpl_fn = async function() { const lifecycleService = await __privateGet$2(this, _serviceRegistry).get( backendPluginApi.coreServices.rootLifecycle, "root" ); if (lifecycleService instanceof BackendLifecycleImpl) { return lifecycleService; } throw new Error("Unexpected root lifecycle service implementation"); }; _getPluginLifecycleImpl = new WeakSet(); getPluginLifecycleImpl_fn = async function(pluginId) { const lifecycleService = await __privateGet$2(this, _serviceRegistry).get( backendPluginApi.coreServices.lifecycle, pluginId ); if (lifecycleService instanceof BackendPluginLifecycleImpl) { return lifecycleService; } throw new Error("Unexpected plugin lifecycle service implementation"); }; function isServiceFactory(feature) { return !!feature.service; } function isInternalBackendFeature(feature) { return typeof feature.getRegistrations === "function"; } var __accessCheck$1 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$1 = (obj, member, getter) => { __accessCheck$1(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$1 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$1 = (obj, member, value, setter) => { __accessCheck$1(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var _initializer; class BackstageBackend { constructor(defaultServiceFactories) { __privateAdd$1(this, _initializer, void 0); __privateSet$1(this, _initializer, new BackendInitializer(defaultServiceFactories)); } add(feature) { if (isPromise(feature)) { __privateGet$1(this, _initializer).add(feature.then((f) => unwrapFeature(f.default))); } else { __privateGet$1(this, _initializer).add(unwrapFeature(feature)); } } async start() { await __privateGet$1(this, _initializer).start(); } async stop() { await __privateGet$1(this, _initializer).stop(); } } _initializer = new WeakMap(); function isPromise(value) { return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function"; } function unwrapFeature(feature) { if (typeof feature === "function") { return feature(); } if ("$$type" in feature) { return feature; } if ("default" in feature) { const defaultFeature = feature.default; return typeof defaultFeature === "function" ? defaultFeature() : defaultFeature; } return feature; } function createSpecializedBackend(options) { const services = options.defaultServiceFactories.map( (sf) => typeof sf === "function" ? sf() : sf ); const exists = /* @__PURE__ */ new Set(); const duplicates = /* @__PURE__ */ new Set(); for (const { service } of services) { if (exists.has(service.id)) { duplicates.add(service.id); } else { exists.add(service.id); } } if (duplicates.size > 0) { const ids = Array.from(duplicates).join(", "); throw new Error(`Duplicate service implementations provided for ${ids}`); } if (exists.has(backendPluginApi.coreServices.pluginMetadata.id)) { throw new Error( `The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden` ); } return new BackstageBackend(services); } const cacheServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.cache, deps: { config: backendPluginApi.coreServices.rootConfig, plugin: backendPluginApi.coreServices.pluginMetadata }, async createRootContext({ config }) { return backendCommon.CacheManager.fromConfig(config); }, async factory({ plugin }, manager) { return manager.forPlugin(plugin.getId()).getClient(); } }); const rootConfigServiceFactory = backendPluginApi.createServiceFactory( (options) => ({ service: backendPluginApi.coreServices.rootConfig, deps: {}, async factory() { const source = configLoader.ConfigSources.default({ argv: options == null ? void 0 : options.argv, remote: options == null ? void 0 : options.remote }); console.log(`Loading config from ${source}`); return await configLoader.ConfigSources.toConfig(source); } }) ); const databaseServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.database, deps: { config: backendPluginApi.coreServices.rootConfig, lifecycle: backendPluginApi.coreServices.lifecycle, pluginMetadata: backendPluginApi.coreServices.pluginMetadata }, async createRootContext({ config: config$1 }) { return config$1.getOptional("backend.database") ? backendCommon.DatabaseManager.fromConfig(config$1) : backendCommon.DatabaseManager.fromConfig( new config.ConfigReader({ backend: { database: { client: "better-sqlite3", connection: ":memory:" } } }) ); }, async factory({ pluginMetadata, lifecycle }, databaseManager) { return databaseManager.forPlugin(pluginMetadata.getId(), { pluginMetadata, lifecycle }); } }); class HostDiscovery { constructor(internalBaseUrl, externalBaseUrl, discoveryConfig) { this.internalBaseUrl = internalBaseUrl; this.externalBaseUrl = externalBaseUrl; this.discoveryConfig = discoveryConfig; } /** * Creates a new HostDiscovery discovery instance by reading * from the `backend` config section, specifically the `.baseUrl` for * discovering the external URL, and the `.listen` and `.https` config * for the internal one. * * Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`. * eg. * ```yaml * discovery: * endpoints: * - target: https://internal.example.com/internal-catalog * plugins: [catalog] * - target: https://internal.example.com/secure/api/{{pluginId}} * plugins: [auth, permission] * - target: * internal: https://internal.example.com/search * external: https://example.com/search * plugins: [search] * ``` * * The basePath defaults to `/api`, meaning the default full internal * path for the `catalog` plugin will be `http://localhost:7007/api/catalog`. */ static fromConfig(config, options) { var _a; const basePath = (_a = options == null ? void 0 : options.basePath) != null ? _a : "/api"; const externalBaseUrl = config.getString("backend.baseUrl").replace(/\/+$/, ""); const { listen: { host: listenHost = "::", port: listenPort } } = backendAppApi.readHttpServerOptions(config.getConfig("backend")); const protocol = config.has("backend.https") ? "https" : "http"; let host = listenHost; if (host === "::" || host === "") { host = "localhost"; } else if (host === "0.0.0.0") { host = "127.0.0.1"; } if (host.includes(":")) { host = `[${host}]`; } const internalBaseUrl = `${protocol}://${host}:${listenPort}`; return new HostDiscovery( internalBaseUrl + basePath, externalBaseUrl + basePath, config.getOptionalConfig("discovery") ); } getTargetFromConfig(pluginId, type) { var _a, _b; const endpoints = (_a = this.discoveryConfig) == null ? void 0 : _a.getOptionalConfigArray("endpoints"); const target = (_b = endpoints == null ? void 0 : endpoints.find((endpoint) => endpoint.getStringArray("plugins").includes(pluginId))) == null ? void 0 : _b.get("target"); if (!target) { const baseUrl = type === "external" ? this.externalBaseUrl : this.internalBaseUrl; return `${baseUrl}/${encodeURIComponent(pluginId)}`; } if (typeof target === "string") { return target.replace( /\{\{\s*pluginId\s*\}\}/g, encodeURIComponent(pluginId) ); } return target[type].replace( /\{\{\s*pluginId\s*\}\}/g, encodeURIComponent(pluginId) ); } async getBaseUrl(pluginId) { return this.getTargetFromConfig(pluginId, "internal"); } async getExternalBaseUrl(pluginId) { return this.getTargetFromConfig(pluginId, "external"); } } const discoveryServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.discovery, deps: { config: backendPluginApi.coreServices.rootConfig }, async factory({ config }) { return HostDiscovery.fromConfig(config); } }); const DEFAULT_TIMEOUT = { seconds: 5 }; function createLifecycleMiddleware(options) { const { lifecycle, startupRequestPauseTimeout = DEFAULT_TIMEOUT } = options; let state = "init"; const waiting = /* @__PURE__ */ new Set(); lifecycle.addStartupHook(async () => { if (state === "init") { state = "up"; for (const item of waiting) { clearTimeout(item.timeout); item.next(); } waiting.clear(); } }); lifecycle.addShutdownHook(async () => { state = "down"; for (const item of waiting) { clearTimeout(item.timeout); item.next(new errors.ServiceUnavailableError("Service is shutting down")); } waiting.clear(); }); const timeoutMs = types.durationToMilliseconds(startupRequestPauseTimeout); return (_req, _res, next) => { if (state === "up") { next(); return; } else if (state === "down") { next(new errors.ServiceUnavailableError("Service is shutting down")); return; } const item = { next, timeout: setTimeout(() => { if (waiting.delete(item)) { next(new errors.ServiceUnavailableError("Service has not started up yet")); } }, timeoutMs) }; waiting.add(item); }; } const httpRouterServiceFactory = backendPluginApi.createServiceFactory( (options) => ({ service: backendPluginApi.coreServices.httpRouter, deps: { plugin: backendPluginApi.coreServices.pluginMetadata, lifecycle: backendPluginApi.coreServices.lifecycle, rootHttpRouter: backendPluginApi.coreServices.rootHttpRouter }, async factory({ plugin, rootHttpRouter, lifecycle }) { var _a; const getPath = (_a = options == null ? void 0 : options.getPath) != null ? _a : (id) => `/api/${id}`; const path = getPath(plugin.getId()); const router = PromiseRouter__default["default"](); rootHttpRouter.use(path, router); router.use(createLifecycleMiddleware({ lifecycle })); return { use(handler) { router.use(handler); } }; } }) ); const identityServiceFactory = backendPluginApi.createServiceFactory( (options) => ({ service: backendPluginApi.coreServices.identity, deps: { discovery: backendPluginApi.coreServices.discovery }, async factory({ discovery }) { return pluginAuthNode.DefaultIdentityClient.create({ discovery, ...options }); } }) ); const loggerServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.logger, deps: { rootLogger: backendPluginApi.coreServices.rootLogger, plugin: backendPluginApi.coreServices.pluginMetadata }, factory({ rootLogger, plugin }) { return rootLogger.child({ plugin: plugin.getId() }); } }); const permissionsServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.permissions, deps: { config: backendPluginApi.coreServices.rootConfig, discovery: backendPluginApi.coreServices.discovery, tokenManager: backendPluginApi.coreServices.tokenManager }, async factory({ config, discovery, tokenManager }) { return pluginPermissionNode.ServerPermissionClient.fromConfig(config, { discovery, tokenManager }); } }); var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod = (obj, member, method) => { __accessCheck(obj, member, "access private method"); return method; }; var _indexPath, _router, _namedRoutes, _indexRouter, _existingPaths, _findConflictingPath, findConflictingPath_fn; function normalizePath(path) { return `${trimEnd__default["default"](path, "/")}/`; } const _DefaultRootHttpRouter = class _DefaultRootHttpRouter { constructor(indexPath) { __privateAdd(this, _findConflictingPath); __privateAdd(this, _indexPath, void 0); __privateAdd(this, _router, express.Router()); __privateAdd(this, _namedRoutes, express.Router()); __privateAdd(this, _indexRouter, express.Router()); __privateAdd(this, _existingPaths, new Array()); __privateSet(this, _indexPath, indexPath); __privateGet(this, _router).use(__privateGet(this, _namedRoutes)); if (__privateGet(this, _indexPath)) { __privateGet(this, _router).use(__privateGet(this, _indexRouter)); } } static create(options) { let indexPath; if ((options == null ? void 0 : options.indexPath) === false) { indexPath = void 0; } else if ((options == null ? void 0 : options.indexPath) === void 0) { indexPath = "/api/app"; } else if ((options == null ? void 0 : options.indexPath) === "") { throw new Error("indexPath option may not be an empty string"); } else { indexPath = options.indexPath; } return new _DefaultRootHttpRouter(indexPath); } use(path, handler) { if (path.match(/^[/\s]*$/)) { throw new Error(`Root router path may not be empty`); } const conflictingPath = __privateMethod(this, _findConflictingPath, findConflictingPath_fn).call(this, path); if (conflictingPath) { throw new Error( `Path ${path} conflicts with the existing path ${conflictingPath}` ); } __privateGet(this, _existingPaths).push(path); __privateGet(this, _namedRoutes).use(path, handler); if (__privateGet(this, _indexPath) === path) { __privateGet(this, _indexRouter).use(handler); } } handler() { return __privateGet(this, _router); } }; _indexPath = new WeakMap(); _router = new WeakMap(); _namedRoutes = new WeakMap(); _indexRouter = new WeakMap(); _existingPaths = new WeakMap(); _findConflictingPath = new WeakSet(); findConflictingPath_fn = function(newPath) { const normalizedNewPath = normalizePath(newPath); for (const path of __privateGet(this, _existingPaths)) { const normalizedPath = normalizePath(path); if (normalizedPath.startsWith(normalizedNewPath)) { return path; } if (normalizedNewPath.startsWith(normalizedPath)) { return path; } } return void 0; }; let DefaultRootHttpRouter = _DefaultRootHttpRouter; function defaultConfigure(context) { const { app, routes, middleware } = context; app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); app.use(middleware.logging()); app.use(routes); app.use(middleware.notFound()); app.use(middleware.error()); } const rootHttpRouterServiceFactory = backendPluginApi.createServiceFactory( (options) => ({ service: backendPluginApi.coreServices.rootHttpRouter, deps: { config: backendPluginApi.coreServices.rootConfig, rootLogger: backendPluginApi.coreServices.rootLogger, lifecycle: backendPluginApi.coreServices.rootLifecycle }, async factory({ config, rootLogger, lifecycle }) { const { indexPath, configure = defaultConfigure } = options != null ? options : {}; const logger = rootLogger.child({ service: "rootHttpRouter" }); const app = express__default["default"](); const router = DefaultRootHttpRouter.create({ indexPath }); const middleware = MiddlewareFactory.create({ config, logger }); configure({ app, routes: router.handler(), middleware, config, logger, lifecycle }); const server = await createHttpServer( app, readHttpServerOptions(config.getOptionalConfig("backend")), { logger } ); lifecycle.addShutdownHook(() => server.stop()); await server.start(); return router; } }) ); const rootLoggerServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.rootLogger, deps: { config: backendPluginApi.coreServices.rootConfig }, async factory({ config }) { var _a; const logger = WinstonLogger.create({ meta: { service: "backstage" }, level: process.env.LOG_LEVEL || "info", format: process.env.NODE_ENV === "production" ? winston.format.json() : WinstonLogger.colorFormat(), transports: [new winston.transports.Console()] }); const secretEnumerator = await createConfigSecretEnumerator({ logger }); logger.addRedactions(secretEnumerator(config)); (_a = config.subscribe) == null ? void 0 : _a.call(config, () => logger.addRedactions(secretEnumerator(config))); return logger; } }); const schedulerServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.scheduler, deps: { plugin: backendPluginApi.coreServices.pluginMetadata, databaseManager: backendPluginApi.coreServices.database, logger: backendPluginApi.coreServices.logger }, async factory({ plugin, databaseManager, logger }) { return backendTasks.TaskScheduler.forPlugin({ pluginId: plugin.getId(), databaseManager, logger: backendCommon.loggerToWinstonLogger(logger) }); } }); const tokenManagerServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.tokenManager, deps: { config: backendPluginApi.coreServices.rootConfig, logger: backendPluginApi.coreServices.rootLogger }, createRootContext({ config, logger }) { return backendCommon.ServerTokenManager.fromConfig(config, { logger }); }, async factory(_deps, tokenManager) { return tokenManager; } }); const urlReaderServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.urlReader, deps: { config: backendPluginApi.coreServices.rootConfig, logger: backendPluginApi.coreServices.logger }, async factory({ config, logger }) { return backendCommon.UrlReaders.default({ config, logger: backendCommon.loggerToWinstonLogger(logger) }); } }); exports.DefaultRootHttpRouter = DefaultRootHttpRouter; exports.HostDiscovery = HostDiscovery; exports.MiddlewareFactory = MiddlewareFactory; exports.WinstonLogger = WinstonLogger; exports.cacheServiceFactory = cacheServiceFactory; exports.createConfigSecretEnumerator = createConfigSecretEnumerator; exports.createHttpServer = createHttpServer; exports.createLifecycleMiddleware = createLifecycleMiddleware; exports.createSpecializedBackend = createSpecializedBackend; exports.databaseServiceFactory = databaseServiceFactory; exports.discoveryServiceFactory = discoveryServiceFactory; exports.httpRouterServiceFactory = httpRouterServiceFactory; exports.identityServiceFactory = identityServiceFactory; exports.lifecycleServiceFactory = lifecycleServiceFactory; exports.loadBackendConfig = loadBackendConfig; exports.loggerServiceFactory = loggerServiceFactory; exports.permissionsServiceFactory = permissionsServiceFactory; exports.readCorsOptions = readCorsOptions; exports.readHelmetOptions = readHelmetOptions; exports.readHttpServerOptions = readHttpServerOptions; exports.rootConfigServiceFactory = rootConfigServiceFactory; exports.rootHttpRouterServiceFactory = rootHttpRouterServiceFactory; exports.rootLifecycleServiceFactory = rootLifecycleServiceFactory; exports.rootLoggerServiceFactory = rootLoggerServiceFactory; exports.schedulerServiceFactory = schedulerServiceFactory; exports.tokenManagerServiceFactory = tokenManagerServiceFactory; exports.urlReaderServiceFactory = urlReaderServiceFactory; //# sourceMappingURL=index.cjs.js.map