@backstage/backend-app-api 0.8.1-next.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/alpha/package.json +1 -1
- package/dist/index.cjs.js +725 -3798
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -762
- package/package.json +9 -12
package/dist/index.cjs.js
CHANGED
|
@@ -1,3968 +1,895 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var configLoader = require('@backstage/config-loader');
|
|
4
|
-
var getPackages = require('@manypkg/get-packages');
|
|
5
|
-
var path = require('path');
|
|
6
|
-
var parseArgs = require('minimist');
|
|
7
|
-
var cliCommon = require('@backstage/cli-common');
|
|
8
|
-
var config = require('@backstage/config');
|
|
9
|
-
var http = require('http');
|
|
10
|
-
var https = require('https');
|
|
11
|
-
var stoppableServer = require('stoppable');
|
|
12
|
-
var fs = require('fs-extra');
|
|
13
|
-
var forge = require('node-forge');
|
|
14
|
-
var cors = require('cors');
|
|
15
|
-
var helmet = require('helmet');
|
|
16
|
-
var morgan = require('morgan');
|
|
17
|
-
var compression = require('compression');
|
|
18
|
-
var kebabCase = require('lodash/kebabCase');
|
|
19
|
-
var minimatch = require('minimatch');
|
|
20
|
-
var errors = require('@backstage/errors');
|
|
21
|
-
var crypto = require('crypto');
|
|
22
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
23
|
-
var
|
|
24
|
-
var tripleBeam = require('triple-beam');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
25
5
|
var alpha = require('@backstage/backend-plugin-api/alpha');
|
|
26
|
-
var backendCommon = require('@backstage/backend-common');
|
|
27
6
|
var pluginAuthNode = require('@backstage/plugin-auth-node');
|
|
28
|
-
var
|
|
29
|
-
var jose = require('jose');
|
|
30
|
-
var types = require('@backstage/types');
|
|
31
|
-
var uuid = require('uuid');
|
|
32
|
-
var luxon = require('luxon');
|
|
33
|
-
var fs$1 = require('fs');
|
|
34
|
-
var cookie = require('cookie');
|
|
35
|
-
var Router = require('express-promise-router');
|
|
36
|
-
var pathToRegexp = require('path-to-regexp');
|
|
37
|
-
var express = require('express');
|
|
38
|
-
var trimEnd = require('lodash/trimEnd');
|
|
39
|
-
var backendTasks = require('@backstage/backend-tasks');
|
|
40
|
-
var fetch$1 = require('node-fetch');
|
|
41
|
-
|
|
42
|
-
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
+
var backendCommon = require('@backstage/backend-common');
|
|
43
8
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (k !== 'default') {
|
|
50
|
-
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
51
|
-
Object.defineProperty(n, k, d.get ? d : {
|
|
52
|
-
enumerable: true,
|
|
53
|
-
get: function () { return e[k]; }
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
});
|
|
9
|
+
class Node {
|
|
10
|
+
constructor(value, consumes, provides) {
|
|
11
|
+
this.value = value;
|
|
12
|
+
this.consumes = consumes;
|
|
13
|
+
this.provides = provides;
|
|
57
14
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
var http__namespace = /*#__PURE__*/_interopNamespaceCompat(http);
|
|
64
|
-
var https__namespace = /*#__PURE__*/_interopNamespaceCompat(https);
|
|
65
|
-
var stoppableServer__default = /*#__PURE__*/_interopDefaultCompat(stoppableServer);
|
|
66
|
-
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
67
|
-
var forge__default = /*#__PURE__*/_interopDefaultCompat(forge);
|
|
68
|
-
var cors__default = /*#__PURE__*/_interopDefaultCompat(cors);
|
|
69
|
-
var helmet__default = /*#__PURE__*/_interopDefaultCompat(helmet);
|
|
70
|
-
var morgan__default = /*#__PURE__*/_interopDefaultCompat(morgan);
|
|
71
|
-
var compression__default = /*#__PURE__*/_interopDefaultCompat(compression);
|
|
72
|
-
var kebabCase__default = /*#__PURE__*/_interopDefaultCompat(kebabCase);
|
|
73
|
-
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
74
|
-
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
75
|
-
var trimEnd__default = /*#__PURE__*/_interopDefaultCompat(trimEnd);
|
|
76
|
-
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch$1);
|
|
77
|
-
|
|
78
|
-
async function createConfigSecretEnumerator$1(options) {
|
|
79
|
-
const { logger, dir = process.cwd() } = options;
|
|
80
|
-
const { packages } = await getPackages.getPackages(dir);
|
|
81
|
-
const schema = options.schema ?? await configLoader.loadConfigSchema({
|
|
82
|
-
dependencies: packages.map((p) => p.packageJson.name)
|
|
83
|
-
});
|
|
84
|
-
return (config) => {
|
|
85
|
-
const [secretsData] = schema.process(
|
|
86
|
-
[{ data: config.getOptional() ?? {}, context: "schema-enumerator" }],
|
|
87
|
-
{
|
|
88
|
-
visibility: ["secret"],
|
|
89
|
-
ignoreSchemaErrors: true
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
const secrets = /* @__PURE__ */ new Set();
|
|
93
|
-
JSON.parse(
|
|
94
|
-
JSON.stringify(secretsData.data),
|
|
95
|
-
(_, v) => typeof v === "string" && secrets.add(v)
|
|
96
|
-
);
|
|
97
|
-
logger.info(
|
|
98
|
-
`Found ${secrets.size} new secrets in config that will be redacted`
|
|
15
|
+
static from(input) {
|
|
16
|
+
return new Node(
|
|
17
|
+
input.value,
|
|
18
|
+
input.consumes ? new Set(input.consumes) : /* @__PURE__ */ new Set(),
|
|
19
|
+
input.provides ? new Set(input.provides) : /* @__PURE__ */ new Set()
|
|
99
20
|
);
|
|
100
|
-
return secrets;
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
class ObservableConfigProxy {
|
|
105
|
-
constructor(parent, parentKey) {
|
|
106
|
-
this.parent = parent;
|
|
107
|
-
this.parentKey = parentKey;
|
|
108
|
-
if (parent && !parentKey) {
|
|
109
|
-
throw new Error("parentKey is required if parent is set");
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
config = new config.ConfigReader({});
|
|
113
|
-
subscribers = [];
|
|
114
|
-
setConfig(config) {
|
|
115
|
-
if (this.parent) {
|
|
116
|
-
throw new Error("immutable");
|
|
117
|
-
}
|
|
118
|
-
this.config = config;
|
|
119
|
-
for (const subscriber of this.subscribers) {
|
|
120
|
-
try {
|
|
121
|
-
subscriber();
|
|
122
|
-
} catch (error) {
|
|
123
|
-
console.error(`Config subscriber threw error, ${error}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
subscribe(onChange) {
|
|
128
|
-
if (this.parent) {
|
|
129
|
-
return this.parent.subscribe(onChange);
|
|
130
|
-
}
|
|
131
|
-
this.subscribers.push(onChange);
|
|
132
|
-
return {
|
|
133
|
-
unsubscribe: () => {
|
|
134
|
-
const index = this.subscribers.indexOf(onChange);
|
|
135
|
-
if (index >= 0) {
|
|
136
|
-
this.subscribers.splice(index, 1);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
select(required) {
|
|
142
|
-
if (this.parent && this.parentKey) {
|
|
143
|
-
if (required) {
|
|
144
|
-
return this.parent.select(true).getConfig(this.parentKey);
|
|
145
|
-
}
|
|
146
|
-
return this.parent.select(false)?.getOptionalConfig(this.parentKey);
|
|
147
|
-
}
|
|
148
|
-
return this.config;
|
|
149
|
-
}
|
|
150
|
-
has(key) {
|
|
151
|
-
return this.select(false)?.has(key) ?? false;
|
|
152
|
-
}
|
|
153
|
-
keys() {
|
|
154
|
-
return this.select(false)?.keys() ?? [];
|
|
155
21
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return this.select(false)?.getOptional(key);
|
|
22
|
+
}
|
|
23
|
+
class CycleKeySet {
|
|
24
|
+
static from(nodes) {
|
|
25
|
+
return new CycleKeySet(nodes);
|
|
161
26
|
}
|
|
162
|
-
|
|
163
|
-
|
|
27
|
+
#nodeIds;
|
|
28
|
+
#cycleKeys;
|
|
29
|
+
constructor(nodes) {
|
|
30
|
+
this.#nodeIds = new Map(nodes.map((n, i) => [n.value, i]));
|
|
31
|
+
this.#cycleKeys = /* @__PURE__ */ new Set();
|
|
164
32
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
33
|
+
tryAdd(path) {
|
|
34
|
+
const cycleKey = this.#getCycleKey(path);
|
|
35
|
+
if (this.#cycleKeys.has(cycleKey)) {
|
|
36
|
+
return false;
|
|
168
37
|
}
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
getConfigArray(key) {
|
|
172
|
-
return this.select(true).getConfigArray(key);
|
|
173
|
-
}
|
|
174
|
-
getOptionalConfigArray(key) {
|
|
175
|
-
return this.select(false)?.getOptionalConfigArray(key);
|
|
176
|
-
}
|
|
177
|
-
getNumber(key) {
|
|
178
|
-
return this.select(true).getNumber(key);
|
|
179
|
-
}
|
|
180
|
-
getOptionalNumber(key) {
|
|
181
|
-
return this.select(false)?.getOptionalNumber(key);
|
|
182
|
-
}
|
|
183
|
-
getBoolean(key) {
|
|
184
|
-
return this.select(true).getBoolean(key);
|
|
185
|
-
}
|
|
186
|
-
getOptionalBoolean(key) {
|
|
187
|
-
return this.select(false)?.getOptionalBoolean(key);
|
|
188
|
-
}
|
|
189
|
-
getString(key) {
|
|
190
|
-
return this.select(true).getString(key);
|
|
191
|
-
}
|
|
192
|
-
getOptionalString(key) {
|
|
193
|
-
return this.select(false)?.getOptionalString(key);
|
|
194
|
-
}
|
|
195
|
-
getStringArray(key) {
|
|
196
|
-
return this.select(true).getStringArray(key);
|
|
197
|
-
}
|
|
198
|
-
getOptionalStringArray(key) {
|
|
199
|
-
return this.select(false)?.getOptionalStringArray(key);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function isValidUrl(url) {
|
|
204
|
-
try {
|
|
205
|
-
new URL(url);
|
|
38
|
+
this.#cycleKeys.add(cycleKey);
|
|
206
39
|
return true;
|
|
207
|
-
} catch {
|
|
208
|
-
return false;
|
|
209
40
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const createConfigSecretEnumerator = createConfigSecretEnumerator$1;
|
|
213
|
-
async function loadBackendConfig(options) {
|
|
214
|
-
const args = parseArgs__default.default(options.argv);
|
|
215
|
-
const configTargets = [args.config ?? []].flat().map((arg) => isValidUrl(arg) ? { url: arg } : { path: path.resolve(arg) });
|
|
216
|
-
const paths = cliCommon.findPaths(__dirname);
|
|
217
|
-
let currentCancelFunc = void 0;
|
|
218
|
-
const config$1 = new ObservableConfigProxy();
|
|
219
|
-
const { appConfigs } = await configLoader.loadConfig({
|
|
220
|
-
configRoot: paths.targetRoot,
|
|
221
|
-
configTargets,
|
|
222
|
-
remote: options.remote,
|
|
223
|
-
watch: options.watch ?? true ? {
|
|
224
|
-
onChange(newConfigs) {
|
|
225
|
-
console.info(
|
|
226
|
-
`Reloaded config from ${newConfigs.map((c) => c.context).join(", ")}`
|
|
227
|
-
);
|
|
228
|
-
const configsToMerge = [...newConfigs];
|
|
229
|
-
if (options.additionalConfigs) {
|
|
230
|
-
configsToMerge.push(...options.additionalConfigs);
|
|
231
|
-
}
|
|
232
|
-
config$1.setConfig(config.ConfigReader.fromConfigs(configsToMerge));
|
|
233
|
-
},
|
|
234
|
-
stopSignal: new Promise((resolve) => {
|
|
235
|
-
if (currentCancelFunc) {
|
|
236
|
-
currentCancelFunc();
|
|
237
|
-
}
|
|
238
|
-
currentCancelFunc = resolve;
|
|
239
|
-
if (module.hot) {
|
|
240
|
-
module.hot.addDisposeHandler(resolve);
|
|
241
|
-
}
|
|
242
|
-
})
|
|
243
|
-
} : void 0
|
|
244
|
-
});
|
|
245
|
-
console.info(
|
|
246
|
-
`Loaded config from ${appConfigs.map((c) => c.context).join(", ")}`
|
|
247
|
-
);
|
|
248
|
-
const finalAppConfigs = [...appConfigs];
|
|
249
|
-
if (options.additionalConfigs) {
|
|
250
|
-
finalAppConfigs.push(...options.additionalConfigs);
|
|
41
|
+
#getCycleKey(path) {
|
|
42
|
+
return path.map((n) => this.#nodeIds.get(n)).sort().join(",");
|
|
251
43
|
}
|
|
252
|
-
config$1.setConfig(config.ConfigReader.fromConfigs(finalAppConfigs));
|
|
253
|
-
return { config: config$1 };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const DEFAULT_PORT = 7007;
|
|
257
|
-
const DEFAULT_HOST = "";
|
|
258
|
-
function readHttpServerOptions$1(config) {
|
|
259
|
-
return {
|
|
260
|
-
listen: readHttpListenOptions(config),
|
|
261
|
-
https: readHttpsOptions(config)
|
|
262
|
-
};
|
|
263
44
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return { port, host: DEFAULT_HOST };
|
|
272
|
-
}
|
|
273
|
-
if (parts.length === 2) {
|
|
274
|
-
return { host: parts[0], port };
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
throw new Error(
|
|
278
|
-
`Unable to parse listen address ${listen}, expected <port> or <host>:<port>`
|
|
45
|
+
class DependencyGraph {
|
|
46
|
+
static fromMap(nodes) {
|
|
47
|
+
return this.fromIterable(
|
|
48
|
+
Object.entries(nodes).map(([key, node]) => ({
|
|
49
|
+
value: String(key),
|
|
50
|
+
...node
|
|
51
|
+
}))
|
|
279
52
|
);
|
|
280
53
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
return {
|
|
287
|
-
port: config?.getOptionalNumber("listen.port") ?? DEFAULT_PORT,
|
|
288
|
-
host
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
function readHttpsOptions(config) {
|
|
292
|
-
const https = config?.getOptional("https");
|
|
293
|
-
if (https === true) {
|
|
294
|
-
const baseUrl = config.getString("baseUrl");
|
|
295
|
-
let hostname;
|
|
296
|
-
try {
|
|
297
|
-
hostname = new URL(baseUrl).hostname;
|
|
298
|
-
} catch (error) {
|
|
299
|
-
throw new Error(`Invalid baseUrl "${baseUrl}"`);
|
|
300
|
-
}
|
|
301
|
-
return { certificate: { type: "generated", hostname } };
|
|
302
|
-
}
|
|
303
|
-
const cc = config?.getOptionalConfig("https");
|
|
304
|
-
if (!cc) {
|
|
305
|
-
return void 0;
|
|
306
|
-
}
|
|
307
|
-
return {
|
|
308
|
-
certificate: {
|
|
309
|
-
type: "pem",
|
|
310
|
-
cert: cc.getString("certificate.cert"),
|
|
311
|
-
key: cc.getString("certificate.key")
|
|
54
|
+
static fromIterable(nodeInputs) {
|
|
55
|
+
const nodes = new Array();
|
|
56
|
+
for (const nodeInput of nodeInputs) {
|
|
57
|
+
nodes.push(Node.from(nodeInput));
|
|
312
58
|
}
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const FIVE_DAYS_IN_MS = 5 * 24 * 60 * 60 * 1e3;
|
|
317
|
-
const IP_HOSTNAME_REGEX = /:|^\d+\.\d+\.\d+\.\d+$/;
|
|
318
|
-
async function getGeneratedCertificate(hostname, logger) {
|
|
319
|
-
const hasModules = await fs__default.default.pathExists("node_modules");
|
|
320
|
-
let certPath;
|
|
321
|
-
if (hasModules) {
|
|
322
|
-
certPath = path.resolve(
|
|
323
|
-
"node_modules/.cache/backstage-backend/dev-cert.pem"
|
|
324
|
-
);
|
|
325
|
-
await fs__default.default.ensureDir(path.dirname(certPath));
|
|
326
|
-
} else {
|
|
327
|
-
certPath = path.resolve(".dev-cert.pem");
|
|
59
|
+
return new DependencyGraph(nodes);
|
|
328
60
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
key: cert,
|
|
338
|
-
cert
|
|
339
|
-
};
|
|
61
|
+
#nodes;
|
|
62
|
+
#allProvided;
|
|
63
|
+
constructor(nodes) {
|
|
64
|
+
this.#nodes = nodes;
|
|
65
|
+
this.#allProvided = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const node of this.#nodes.values()) {
|
|
67
|
+
for (const produced of node.provides) {
|
|
68
|
+
this.#allProvided.add(produced);
|
|
340
69
|
}
|
|
341
|
-
} catch (error) {
|
|
342
|
-
logger.warn(`Unable to use existing self-signed certificate, ${error}`);
|
|
343
70
|
}
|
|
344
71
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
name: "commonName",
|
|
354
|
-
value: "dev-cert"
|
|
355
|
-
}
|
|
356
|
-
];
|
|
357
|
-
const sans = [
|
|
358
|
-
{
|
|
359
|
-
type: 2,
|
|
360
|
-
// DNS
|
|
361
|
-
value: "localhost"
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
type: 2,
|
|
365
|
-
value: "localhost.localdomain"
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
type: 2,
|
|
369
|
-
value: "[::1]"
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
type: 7,
|
|
373
|
-
// IP
|
|
374
|
-
ip: "127.0.0.1"
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
type: 7,
|
|
378
|
-
ip: "fe80::1"
|
|
379
|
-
}
|
|
380
|
-
];
|
|
381
|
-
if (!sans.find(({ value, ip }) => value === hostname || ip === hostname)) {
|
|
382
|
-
sans.push(
|
|
383
|
-
IP_HOSTNAME_REGEX.test(hostname) ? {
|
|
384
|
-
type: 7,
|
|
385
|
-
ip: hostname
|
|
386
|
-
} : {
|
|
387
|
-
type: 2,
|
|
388
|
-
value: hostname
|
|
389
|
-
}
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
const params = {
|
|
393
|
-
algorithm: "sha256",
|
|
394
|
-
keySize: 2048,
|
|
395
|
-
days: 30,
|
|
396
|
-
extensions: [
|
|
397
|
-
{
|
|
398
|
-
name: "keyUsage",
|
|
399
|
-
keyCertSign: true,
|
|
400
|
-
digitalSignature: true,
|
|
401
|
-
nonRepudiation: true,
|
|
402
|
-
keyEncipherment: true,
|
|
403
|
-
dataEncipherment: true
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: "extKeyUsage",
|
|
407
|
-
serverAuth: true,
|
|
408
|
-
clientAuth: true,
|
|
409
|
-
codeSigning: true,
|
|
410
|
-
timeStamping: true
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
name: "subjectAltName",
|
|
414
|
-
altNames: sans
|
|
415
|
-
}
|
|
416
|
-
]
|
|
417
|
-
};
|
|
418
|
-
return new Promise(
|
|
419
|
-
(resolve, reject) => require("selfsigned").generate(
|
|
420
|
-
attributes,
|
|
421
|
-
params,
|
|
422
|
-
(err, bundle) => {
|
|
423
|
-
if (err) {
|
|
424
|
-
reject(err);
|
|
425
|
-
} else {
|
|
426
|
-
resolve({ key: bundle.private, cert: bundle.cert });
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
)
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async function createHttpServer$1(listener, options, deps) {
|
|
434
|
-
const server = await createServer(listener, options, deps);
|
|
435
|
-
const stopper = stoppableServer__default.default(server, 0);
|
|
436
|
-
const stopServer = stopper.stop.bind(stopper);
|
|
437
|
-
return Object.assign(server, {
|
|
438
|
-
start() {
|
|
439
|
-
return new Promise((resolve, reject) => {
|
|
440
|
-
const handleStartupError = (error) => {
|
|
441
|
-
server.close();
|
|
442
|
-
reject(error);
|
|
443
|
-
};
|
|
444
|
-
server.on("error", handleStartupError);
|
|
445
|
-
const { host, port } = options.listen;
|
|
446
|
-
server.listen(port, host, () => {
|
|
447
|
-
server.off("error", handleStartupError);
|
|
448
|
-
deps.logger.info(`Listening on ${host}:${port}`);
|
|
449
|
-
resolve();
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
},
|
|
453
|
-
stop() {
|
|
454
|
-
return new Promise((resolve, reject) => {
|
|
455
|
-
stopServer((error) => {
|
|
456
|
-
if (error) {
|
|
457
|
-
reject(error);
|
|
458
|
-
} else {
|
|
459
|
-
resolve();
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
},
|
|
464
|
-
port() {
|
|
465
|
-
const address = server.address();
|
|
466
|
-
if (typeof address === "string" || address === null) {
|
|
467
|
-
throw new Error(`Unexpected server address '${address}'`);
|
|
468
|
-
}
|
|
469
|
-
return address.port;
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
async function createServer(listener, options, deps) {
|
|
474
|
-
if (options.https) {
|
|
475
|
-
const { certificate } = options.https;
|
|
476
|
-
if (certificate.type === "generated") {
|
|
477
|
-
const credentials = await getGeneratedCertificate(
|
|
478
|
-
certificate.hostname,
|
|
479
|
-
deps.logger
|
|
72
|
+
/**
|
|
73
|
+
* Find all nodes that consume dependencies that are not provided by any other node.
|
|
74
|
+
*/
|
|
75
|
+
findUnsatisfiedDeps() {
|
|
76
|
+
const unsatisfiedDependencies = [];
|
|
77
|
+
for (const node of this.#nodes.values()) {
|
|
78
|
+
const unsatisfied = Array.from(node.consumes).filter(
|
|
79
|
+
(id) => !this.#allProvided.has(id)
|
|
480
80
|
);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return https__namespace.createServer(certificate, listener);
|
|
484
|
-
}
|
|
485
|
-
return http__namespace.createServer(listener);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function readHelmetOptions$1(config) {
|
|
489
|
-
const cspOptions = readCspDirectives(config);
|
|
490
|
-
return {
|
|
491
|
-
contentSecurityPolicy: {
|
|
492
|
-
useDefaults: false,
|
|
493
|
-
directives: applyCspDirectives(cspOptions)
|
|
494
|
-
},
|
|
495
|
-
// These are all disabled in order to maintain backwards compatibility
|
|
496
|
-
// when bumping helmet v5. We can't enable these by default because
|
|
497
|
-
// there is no way for users to configure them.
|
|
498
|
-
// TODO(Rugvip): We should give control of this setup to consumers
|
|
499
|
-
crossOriginEmbedderPolicy: false,
|
|
500
|
-
crossOriginOpenerPolicy: false,
|
|
501
|
-
crossOriginResourcePolicy: false,
|
|
502
|
-
originAgentCluster: false
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
function readCspDirectives(config) {
|
|
506
|
-
const cc = config?.getOptionalConfig("csp");
|
|
507
|
-
if (!cc) {
|
|
508
|
-
return void 0;
|
|
509
|
-
}
|
|
510
|
-
const result = {};
|
|
511
|
-
for (const key of cc.keys()) {
|
|
512
|
-
if (cc.get(key) === false) {
|
|
513
|
-
result[key] = false;
|
|
514
|
-
} else {
|
|
515
|
-
result[key] = cc.getStringArray(key);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return result;
|
|
519
|
-
}
|
|
520
|
-
function applyCspDirectives(directives) {
|
|
521
|
-
const result = helmet__default.default.contentSecurityPolicy.getDefaultDirectives();
|
|
522
|
-
result["script-src"] = ["'self'", "'unsafe-eval'"];
|
|
523
|
-
delete result["form-action"];
|
|
524
|
-
if (directives) {
|
|
525
|
-
for (const [key, value] of Object.entries(directives)) {
|
|
526
|
-
const kebabCaseKey = kebabCase__default.default(key);
|
|
527
|
-
if (value === false) {
|
|
528
|
-
delete result[kebabCaseKey];
|
|
529
|
-
} else {
|
|
530
|
-
result[kebabCaseKey] = value;
|
|
81
|
+
if (unsatisfied.length > 0) {
|
|
82
|
+
unsatisfiedDependencies.push({ value: node.value, unsatisfied });
|
|
531
83
|
}
|
|
532
84
|
}
|
|
533
|
-
|
|
534
|
-
return result;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function readCorsOptions$1(config) {
|
|
538
|
-
const cc = config?.getOptionalConfig("cors");
|
|
539
|
-
if (!cc) {
|
|
540
|
-
return { origin: false };
|
|
541
|
-
}
|
|
542
|
-
return removeUnknown({
|
|
543
|
-
origin: createCorsOriginMatcher(readStringArray(cc, "origin")),
|
|
544
|
-
methods: readStringArray(cc, "methods"),
|
|
545
|
-
allowedHeaders: readStringArray(cc, "allowedHeaders"),
|
|
546
|
-
exposedHeaders: readStringArray(cc, "exposedHeaders"),
|
|
547
|
-
credentials: cc.getOptionalBoolean("credentials"),
|
|
548
|
-
maxAge: cc.getOptionalNumber("maxAge"),
|
|
549
|
-
preflightContinue: cc.getOptionalBoolean("preflightContinue"),
|
|
550
|
-
optionsSuccessStatus: cc.getOptionalNumber("optionsSuccessStatus")
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
function removeUnknown(obj) {
|
|
554
|
-
return Object.fromEntries(
|
|
555
|
-
Object.entries(obj).filter(([, v]) => v !== void 0)
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
function readStringArray(config, key) {
|
|
559
|
-
const value = config.getOptional(key);
|
|
560
|
-
if (typeof value === "string") {
|
|
561
|
-
return [value];
|
|
562
|
-
} else if (!value) {
|
|
563
|
-
return void 0;
|
|
564
|
-
}
|
|
565
|
-
return config.getStringArray(key);
|
|
566
|
-
}
|
|
567
|
-
function createCorsOriginMatcher(allowedOriginPatterns) {
|
|
568
|
-
if (!allowedOriginPatterns) {
|
|
569
|
-
return void 0;
|
|
570
|
-
}
|
|
571
|
-
const allowedOriginMatchers = allowedOriginPatterns.map(
|
|
572
|
-
(pattern) => new minimatch.Minimatch(pattern, { nocase: true, noglobstar: true })
|
|
573
|
-
);
|
|
574
|
-
return (origin, callback) => {
|
|
575
|
-
return callback(
|
|
576
|
-
null,
|
|
577
|
-
allowedOriginMatchers.some((pattern) => pattern.match(origin ?? ""))
|
|
578
|
-
);
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function handleBadError(error, logger) {
|
|
583
|
-
const logId = crypto.randomBytes(10).toString("hex");
|
|
584
|
-
logger.child({ logId }).error(`Filtered internal error with logId=${logId} from response`, error);
|
|
585
|
-
const newError = new Error(`An internal error occurred logId=${logId}`);
|
|
586
|
-
delete newError.stack;
|
|
587
|
-
return newError;
|
|
588
|
-
}
|
|
589
|
-
function applyInternalErrorFilter(error, logger) {
|
|
590
|
-
try {
|
|
591
|
-
errors.assertError(error);
|
|
592
|
-
} catch (assertionError) {
|
|
593
|
-
errors.assertError(assertionError);
|
|
594
|
-
return handleBadError(assertionError, logger);
|
|
595
|
-
}
|
|
596
|
-
const constructorName = error.constructor.name;
|
|
597
|
-
if (constructorName === "DatabaseError") {
|
|
598
|
-
return handleBadError(error, logger);
|
|
599
|
-
}
|
|
600
|
-
return error;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
let MiddlewareFactory$1 = class MiddlewareFactory {
|
|
604
|
-
#config;
|
|
605
|
-
#logger;
|
|
606
|
-
/**
|
|
607
|
-
* Creates a new {@link MiddlewareFactory}.
|
|
608
|
-
*/
|
|
609
|
-
static create(options) {
|
|
610
|
-
return new MiddlewareFactory(options);
|
|
611
|
-
}
|
|
612
|
-
constructor(options) {
|
|
613
|
-
this.#config = options.config;
|
|
614
|
-
this.#logger = options.logger;
|
|
85
|
+
return unsatisfiedDependencies;
|
|
615
86
|
}
|
|
616
87
|
/**
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
* @remarks
|
|
620
|
-
*
|
|
621
|
-
* Typically you want to place this middleware at the end of the chain, such
|
|
622
|
-
* that it's the last one attempted after no other routes matched.
|
|
623
|
-
*
|
|
624
|
-
* @returns An Express request handler
|
|
88
|
+
* Detect the first circular dependency within the graph, returning the path of nodes that
|
|
89
|
+
* form a cycle, with the same node as the first and last element of the array.
|
|
625
90
|
*/
|
|
626
|
-
|
|
627
|
-
return (
|
|
628
|
-
res.status(404).end();
|
|
629
|
-
};
|
|
91
|
+
detectCircularDependency() {
|
|
92
|
+
return this.detectCircularDependencies().next().value;
|
|
630
93
|
}
|
|
631
94
|
/**
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* @remarks
|
|
635
|
-
*
|
|
636
|
-
* The middleware will attempt to compress response bodies for all requests
|
|
637
|
-
* that traverse through the middleware.
|
|
95
|
+
* Detect circular dependencies within the graph, returning the path of nodes that
|
|
96
|
+
* form a cycle, with the same node as the first and last element of the array.
|
|
638
97
|
*/
|
|
639
|
-
|
|
640
|
-
|
|
98
|
+
*detectCircularDependencies() {
|
|
99
|
+
const cycleKeys = CycleKeySet.from(this.#nodes);
|
|
100
|
+
for (const startNode of this.#nodes) {
|
|
101
|
+
const visited = /* @__PURE__ */ new Set();
|
|
102
|
+
const stack = new Array([
|
|
103
|
+
startNode,
|
|
104
|
+
[startNode.value]
|
|
105
|
+
]);
|
|
106
|
+
while (stack.length > 0) {
|
|
107
|
+
const [node, path] = stack.pop();
|
|
108
|
+
if (visited.has(node)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
visited.add(node);
|
|
112
|
+
for (const consumed of node.consumes) {
|
|
113
|
+
const providerNodes = this.#nodes.filter(
|
|
114
|
+
(other) => other.provides.has(consumed)
|
|
115
|
+
);
|
|
116
|
+
for (const provider of providerNodes) {
|
|
117
|
+
if (provider === startNode) {
|
|
118
|
+
if (cycleKeys.tryAdd(path)) {
|
|
119
|
+
yield [...path, startNode.value];
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (!visited.has(provider)) {
|
|
124
|
+
stack.push([provider, [...path, provider.value]]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return void 0;
|
|
641
131
|
}
|
|
642
132
|
/**
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* @remarks
|
|
133
|
+
* Traverses the dependency graph in topological order, calling the provided
|
|
134
|
+
* function for each node and waiting for it to resolve.
|
|
646
135
|
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
* down or not.
|
|
136
|
+
* The nodes are traversed in parallel, but in such a way that no node is
|
|
137
|
+
* visited before all of its dependencies.
|
|
650
138
|
*
|
|
651
|
-
*
|
|
139
|
+
* Dependencies of nodes that are not produced by any other nodes will be ignored.
|
|
652
140
|
*/
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Returns a middleware that implements the helmet library.
|
|
667
|
-
*
|
|
668
|
-
* @remarks
|
|
669
|
-
*
|
|
670
|
-
* This middleware applies security policies to incoming requests and outgoing
|
|
671
|
-
* responses. It is configured using config keys such as `backend.csp`.
|
|
672
|
-
*
|
|
673
|
-
* @see {@link https://helmetjs.github.io/}
|
|
674
|
-
*
|
|
675
|
-
* @returns An Express request handler
|
|
676
|
-
*/
|
|
677
|
-
helmet() {
|
|
678
|
-
return helmet__default.default(readHelmetOptions$1(this.#config.getOptionalConfig("backend")));
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Returns a middleware that implements the cors library.
|
|
682
|
-
*
|
|
683
|
-
* @remarks
|
|
684
|
-
*
|
|
685
|
-
* This middleware handles CORS. It is configured using the config key
|
|
686
|
-
* `backend.cors`.
|
|
687
|
-
*
|
|
688
|
-
* @see {@link https://github.com/expressjs/cors}
|
|
689
|
-
*
|
|
690
|
-
* @returns An Express request handler
|
|
691
|
-
*/
|
|
692
|
-
cors() {
|
|
693
|
-
return cors__default.default(readCorsOptions$1(this.#config.getOptionalConfig("backend")));
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Express middleware to handle errors during request processing.
|
|
697
|
-
*
|
|
698
|
-
* @remarks
|
|
699
|
-
*
|
|
700
|
-
* This is commonly the very last middleware in the chain.
|
|
701
|
-
*
|
|
702
|
-
* Its primary purpose is not to do translation of business logic exceptions,
|
|
703
|
-
* but rather to be a global catch-all for uncaught "fatal" errors that are
|
|
704
|
-
* expected to result in a 500 error. However, it also does handle some common
|
|
705
|
-
* error types (such as http-error exceptions, and the well-known error types
|
|
706
|
-
* in the `@backstage/errors` package) and returns the enclosed status code
|
|
707
|
-
* accordingly.
|
|
708
|
-
*
|
|
709
|
-
* It will also produce a response body with a serialized form of the error,
|
|
710
|
-
* unless a previous handler already did send a body. See
|
|
711
|
-
* {@link @backstage/errors#ErrorResponseBody} for the response shape used.
|
|
712
|
-
*
|
|
713
|
-
* @returns An Express error request handler
|
|
714
|
-
*/
|
|
715
|
-
error(options = {}) {
|
|
716
|
-
const showStackTraces = options.showStackTraces ?? process.env.NODE_ENV === "development";
|
|
717
|
-
const logger = this.#logger.child({
|
|
718
|
-
type: "errorHandler"
|
|
719
|
-
});
|
|
720
|
-
return (rawError, req, res, next) => {
|
|
721
|
-
const error = applyInternalErrorFilter(rawError, logger);
|
|
722
|
-
const statusCode = getStatusCode(error);
|
|
723
|
-
if (options.logAllErrors || statusCode >= 500) {
|
|
724
|
-
logger.error(`Request failed with status ${statusCode}`, error);
|
|
725
|
-
}
|
|
726
|
-
if (res.headersSent) {
|
|
727
|
-
next(error);
|
|
141
|
+
async parallelTopologicalTraversal(fn) {
|
|
142
|
+
const allProvided = this.#allProvided;
|
|
143
|
+
const producedSoFar = /* @__PURE__ */ new Set();
|
|
144
|
+
const waiting = new Set(this.#nodes.values());
|
|
145
|
+
const visited = /* @__PURE__ */ new Set();
|
|
146
|
+
const results = new Array();
|
|
147
|
+
let inFlight = 0;
|
|
148
|
+
async function processMoreNodes() {
|
|
149
|
+
if (waiting.size === 0) {
|
|
728
150
|
return;
|
|
729
151
|
}
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
};
|
|
739
|
-
function getStatusCode(error) {
|
|
740
|
-
const knownStatusCodeFields = ["statusCode", "status"];
|
|
741
|
-
for (const field of knownStatusCodeFields) {
|
|
742
|
-
const statusCode = error[field];
|
|
743
|
-
if (typeof statusCode === "number" && (statusCode | 0) === statusCode && // is whole integer
|
|
744
|
-
statusCode >= 100 && statusCode <= 599) {
|
|
745
|
-
return statusCode;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
switch (error.name) {
|
|
749
|
-
case errors.NotModifiedError.name:
|
|
750
|
-
return 304;
|
|
751
|
-
case errors.InputError.name:
|
|
752
|
-
return 400;
|
|
753
|
-
case errors.AuthenticationError.name:
|
|
754
|
-
return 401;
|
|
755
|
-
case errors.NotAllowedError.name:
|
|
756
|
-
return 403;
|
|
757
|
-
case errors.NotFoundError.name:
|
|
758
|
-
return 404;
|
|
759
|
-
case errors.ConflictError.name:
|
|
760
|
-
return 409;
|
|
761
|
-
case errors.NotImplementedError.name:
|
|
762
|
-
return 501;
|
|
763
|
-
case errors.ServiceUnavailableError.name:
|
|
764
|
-
return 503;
|
|
765
|
-
}
|
|
766
|
-
return 500;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const readHttpServerOptions = readHttpServerOptions$1;
|
|
770
|
-
const createHttpServer = createHttpServer$1;
|
|
771
|
-
const readCorsOptions = readCorsOptions$1;
|
|
772
|
-
const readHelmetOptions = readHelmetOptions$1;
|
|
773
|
-
class MiddlewareFactory {
|
|
774
|
-
constructor(impl) {
|
|
775
|
-
this.impl = impl;
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Creates a new {@link MiddlewareFactory}.
|
|
779
|
-
*/
|
|
780
|
-
static create(options) {
|
|
781
|
-
return new MiddlewareFactory(MiddlewareFactory$1.create(options));
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Returns a middleware that unconditionally produces a 404 error response.
|
|
785
|
-
*
|
|
786
|
-
* @remarks
|
|
787
|
-
*
|
|
788
|
-
* Typically you want to place this middleware at the end of the chain, such
|
|
789
|
-
* that it's the last one attempted after no other routes matched.
|
|
790
|
-
*
|
|
791
|
-
* @returns An Express request handler
|
|
792
|
-
*/
|
|
793
|
-
notFound() {
|
|
794
|
-
return this.impl.notFound();
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Returns the compression middleware.
|
|
798
|
-
*
|
|
799
|
-
* @remarks
|
|
800
|
-
*
|
|
801
|
-
* The middleware will attempt to compress response bodies for all requests
|
|
802
|
-
* that traverse through the middleware.
|
|
803
|
-
*/
|
|
804
|
-
compression() {
|
|
805
|
-
return this.impl.compression();
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Returns a request logging middleware.
|
|
809
|
-
*
|
|
810
|
-
* @remarks
|
|
811
|
-
*
|
|
812
|
-
* Typically you want to place this middleware at the start of the chain, such
|
|
813
|
-
* that it always logs requests whether they are "caught" by handlers farther
|
|
814
|
-
* down or not.
|
|
815
|
-
*
|
|
816
|
-
* @returns An Express request handler
|
|
817
|
-
*/
|
|
818
|
-
logging() {
|
|
819
|
-
return this.impl.logging();
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* Returns a middleware that implements the helmet library.
|
|
823
|
-
*
|
|
824
|
-
* @remarks
|
|
825
|
-
*
|
|
826
|
-
* This middleware applies security policies to incoming requests and outgoing
|
|
827
|
-
* responses. It is configured using config keys such as `backend.csp`.
|
|
828
|
-
*
|
|
829
|
-
* @see {@link https://helmetjs.github.io/}
|
|
830
|
-
*
|
|
831
|
-
* @returns An Express request handler
|
|
832
|
-
*/
|
|
833
|
-
helmet() {
|
|
834
|
-
return this.impl.helmet();
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Returns a middleware that implements the cors library.
|
|
838
|
-
*
|
|
839
|
-
* @remarks
|
|
840
|
-
*
|
|
841
|
-
* This middleware handles CORS. It is configured using the config key
|
|
842
|
-
* `backend.cors`.
|
|
843
|
-
*
|
|
844
|
-
* @see {@link https://github.com/expressjs/cors}
|
|
845
|
-
*
|
|
846
|
-
* @returns An Express request handler
|
|
847
|
-
*/
|
|
848
|
-
cors() {
|
|
849
|
-
return this.impl.cors();
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Express middleware to handle errors during request processing.
|
|
853
|
-
*
|
|
854
|
-
* @remarks
|
|
855
|
-
*
|
|
856
|
-
* This is commonly the very last middleware in the chain.
|
|
857
|
-
*
|
|
858
|
-
* Its primary purpose is not to do translation of business logic exceptions,
|
|
859
|
-
* but rather to be a global catch-all for uncaught "fatal" errors that are
|
|
860
|
-
* expected to result in a 500 error. However, it also does handle some common
|
|
861
|
-
* error types (such as http-error exceptions, and the well-known error types
|
|
862
|
-
* in the `@backstage/errors` package) and returns the enclosed status code
|
|
863
|
-
* accordingly.
|
|
864
|
-
*
|
|
865
|
-
* It will also produce a response body with a serialized form of the error,
|
|
866
|
-
* unless a previous handler already did send a body. See
|
|
867
|
-
* {@link @backstage/errors#ErrorResponseBody} for the response shape used.
|
|
868
|
-
*
|
|
869
|
-
* @returns An Express error request handler
|
|
870
|
-
*/
|
|
871
|
-
error(options = {}) {
|
|
872
|
-
return this.impl.error(options);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const escapeRegExp = (text) => {
|
|
877
|
-
return text.replace(/[.*+?^${}(\)|[\]\\]/g, "\\$&");
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
let WinstonLogger$1 = class WinstonLogger {
|
|
881
|
-
#winston;
|
|
882
|
-
#addRedactions;
|
|
883
|
-
/**
|
|
884
|
-
* Creates a {@link WinstonLogger} instance.
|
|
885
|
-
*/
|
|
886
|
-
static create(options) {
|
|
887
|
-
const redacter = WinstonLogger.redacter();
|
|
888
|
-
const defaultFormatter = process.env.NODE_ENV === "production" ? winston.format.json() : WinstonLogger.colorFormat();
|
|
889
|
-
let logger = winston.createLogger({
|
|
890
|
-
level: process.env.LOG_LEVEL || options.level || "info",
|
|
891
|
-
format: winston.format.combine(
|
|
892
|
-
options.format ?? defaultFormatter,
|
|
893
|
-
redacter.format
|
|
894
|
-
),
|
|
895
|
-
transports: options.transports ?? new winston.transports.Console()
|
|
896
|
-
});
|
|
897
|
-
if (options.meta) {
|
|
898
|
-
logger = logger.child(options.meta);
|
|
899
|
-
}
|
|
900
|
-
return new WinstonLogger(logger, redacter.add);
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Creates a winston log formatter for redacting secrets.
|
|
904
|
-
*/
|
|
905
|
-
static redacter() {
|
|
906
|
-
const redactionSet = /* @__PURE__ */ new Set();
|
|
907
|
-
let redactionPattern = void 0;
|
|
908
|
-
return {
|
|
909
|
-
format: winston.format((obj) => {
|
|
910
|
-
if (!redactionPattern || !obj) {
|
|
911
|
-
return obj;
|
|
912
|
-
}
|
|
913
|
-
obj[tripleBeam.MESSAGE] = obj[tripleBeam.MESSAGE]?.replace?.(redactionPattern, "***");
|
|
914
|
-
return obj;
|
|
915
|
-
})(),
|
|
916
|
-
add(newRedactions) {
|
|
917
|
-
let added = 0;
|
|
918
|
-
for (const redactionToTrim of newRedactions) {
|
|
919
|
-
const redaction = redactionToTrim.trim();
|
|
920
|
-
if (redaction.length <= 1) {
|
|
921
|
-
continue;
|
|
922
|
-
}
|
|
923
|
-
if (!redactionSet.has(redaction)) {
|
|
924
|
-
redactionSet.add(redaction);
|
|
925
|
-
added += 1;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
if (added > 0) {
|
|
929
|
-
const redactions = Array.from(redactionSet).map((r) => escapeRegExp(r)).join("|");
|
|
930
|
-
redactionPattern = new RegExp(`(${redactions})`, "g");
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Creates a pretty printed winston log formatter.
|
|
937
|
-
*/
|
|
938
|
-
static colorFormat() {
|
|
939
|
-
const colorizer = winston.format.colorize();
|
|
940
|
-
return winston.format.combine(
|
|
941
|
-
winston.format.timestamp(),
|
|
942
|
-
winston.format.colorize({
|
|
943
|
-
colors: {
|
|
944
|
-
timestamp: "dim",
|
|
945
|
-
prefix: "blue",
|
|
946
|
-
field: "cyan",
|
|
947
|
-
debug: "grey"
|
|
948
|
-
}
|
|
949
|
-
}),
|
|
950
|
-
winston.format.printf((info) => {
|
|
951
|
-
const { timestamp, level, message, plugin, service, ...fields } = info;
|
|
952
|
-
const prefix = plugin || service;
|
|
953
|
-
const timestampColor = colorizer.colorize("timestamp", timestamp);
|
|
954
|
-
const prefixColor = colorizer.colorize("prefix", prefix);
|
|
955
|
-
const extraFields = Object.entries(fields).map(
|
|
956
|
-
([key, value]) => `${colorizer.colorize("field", `${key}`)}=${value}`
|
|
957
|
-
).join(" ");
|
|
958
|
-
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
|
|
959
|
-
})
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
constructor(winston, addRedactions) {
|
|
963
|
-
this.#winston = winston;
|
|
964
|
-
this.#addRedactions = addRedactions;
|
|
965
|
-
}
|
|
966
|
-
error(message, meta) {
|
|
967
|
-
this.#winston.error(message, meta);
|
|
968
|
-
}
|
|
969
|
-
warn(message, meta) {
|
|
970
|
-
this.#winston.warn(message, meta);
|
|
971
|
-
}
|
|
972
|
-
info(message, meta) {
|
|
973
|
-
this.#winston.info(message, meta);
|
|
974
|
-
}
|
|
975
|
-
debug(message, meta) {
|
|
976
|
-
this.#winston.debug(message, meta);
|
|
977
|
-
}
|
|
978
|
-
child(meta) {
|
|
979
|
-
return new WinstonLogger(this.#winston.child(meta));
|
|
980
|
-
}
|
|
981
|
-
addRedactions(redactions) {
|
|
982
|
-
this.#addRedactions?.(redactions);
|
|
983
|
-
}
|
|
984
|
-
};
|
|
985
|
-
|
|
986
|
-
const rootLoggerServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
987
|
-
service: backendPluginApi.coreServices.rootLogger,
|
|
988
|
-
deps: {
|
|
989
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
990
|
-
},
|
|
991
|
-
async factory({ config }) {
|
|
992
|
-
const logger = WinstonLogger$1.create({
|
|
993
|
-
meta: {
|
|
994
|
-
service: "backstage"
|
|
995
|
-
},
|
|
996
|
-
level: process.env.LOG_LEVEL || "info",
|
|
997
|
-
format: process.env.NODE_ENV === "production" ? winston.format.json() : WinstonLogger$1.colorFormat(),
|
|
998
|
-
transports: [new winston.transports.Console()]
|
|
999
|
-
});
|
|
1000
|
-
const secretEnumerator = await createConfigSecretEnumerator$1({ logger });
|
|
1001
|
-
logger.addRedactions(secretEnumerator(config));
|
|
1002
|
-
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
|
|
1003
|
-
return logger;
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
class WinstonLogger {
|
|
1008
|
-
constructor(impl) {
|
|
1009
|
-
this.impl = impl;
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Creates a {@link WinstonLogger} instance.
|
|
1013
|
-
*/
|
|
1014
|
-
static create(options) {
|
|
1015
|
-
return new WinstonLogger(WinstonLogger$1.create(options));
|
|
1016
|
-
}
|
|
1017
|
-
/**
|
|
1018
|
-
* Creates a winston log formatter for redacting secrets.
|
|
1019
|
-
*/
|
|
1020
|
-
static redacter() {
|
|
1021
|
-
return WinstonLogger$1.redacter();
|
|
1022
|
-
}
|
|
1023
|
-
/**
|
|
1024
|
-
* Creates a pretty printed winston log formatter.
|
|
1025
|
-
*/
|
|
1026
|
-
static colorFormat() {
|
|
1027
|
-
return WinstonLogger$1.colorFormat();
|
|
1028
|
-
}
|
|
1029
|
-
error(message, meta) {
|
|
1030
|
-
this.impl.error(message, meta);
|
|
1031
|
-
}
|
|
1032
|
-
warn(message, meta) {
|
|
1033
|
-
this.impl.warn(message, meta);
|
|
1034
|
-
}
|
|
1035
|
-
info(message, meta) {
|
|
1036
|
-
this.impl.info(message, meta);
|
|
1037
|
-
}
|
|
1038
|
-
debug(message, meta) {
|
|
1039
|
-
this.impl.debug(message, meta);
|
|
1040
|
-
}
|
|
1041
|
-
child(meta) {
|
|
1042
|
-
return this.impl.child(meta);
|
|
1043
|
-
}
|
|
1044
|
-
addRedactions(redactions) {
|
|
1045
|
-
this.impl.addRedactions(redactions);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
class Node {
|
|
1050
|
-
constructor(value, consumes, provides) {
|
|
1051
|
-
this.value = value;
|
|
1052
|
-
this.consumes = consumes;
|
|
1053
|
-
this.provides = provides;
|
|
1054
|
-
}
|
|
1055
|
-
static from(input) {
|
|
1056
|
-
return new Node(
|
|
1057
|
-
input.value,
|
|
1058
|
-
input.consumes ? new Set(input.consumes) : /* @__PURE__ */ new Set(),
|
|
1059
|
-
input.provides ? new Set(input.provides) : /* @__PURE__ */ new Set()
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
class CycleKeySet {
|
|
1064
|
-
static from(nodes) {
|
|
1065
|
-
return new CycleKeySet(nodes);
|
|
1066
|
-
}
|
|
1067
|
-
#nodeIds;
|
|
1068
|
-
#cycleKeys;
|
|
1069
|
-
constructor(nodes) {
|
|
1070
|
-
this.#nodeIds = new Map(nodes.map((n, i) => [n.value, i]));
|
|
1071
|
-
this.#cycleKeys = /* @__PURE__ */ new Set();
|
|
1072
|
-
}
|
|
1073
|
-
tryAdd(path) {
|
|
1074
|
-
const cycleKey = this.#getCycleKey(path);
|
|
1075
|
-
if (this.#cycleKeys.has(cycleKey)) {
|
|
1076
|
-
return false;
|
|
1077
|
-
}
|
|
1078
|
-
this.#cycleKeys.add(cycleKey);
|
|
1079
|
-
return true;
|
|
1080
|
-
}
|
|
1081
|
-
#getCycleKey(path) {
|
|
1082
|
-
return path.map((n) => this.#nodeIds.get(n)).sort().join(",");
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
class DependencyGraph {
|
|
1086
|
-
static fromMap(nodes) {
|
|
1087
|
-
return this.fromIterable(
|
|
1088
|
-
Object.entries(nodes).map(([key, node]) => ({
|
|
1089
|
-
value: String(key),
|
|
1090
|
-
...node
|
|
1091
|
-
}))
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
static fromIterable(nodeInputs) {
|
|
1095
|
-
const nodes = new Array();
|
|
1096
|
-
for (const nodeInput of nodeInputs) {
|
|
1097
|
-
nodes.push(Node.from(nodeInput));
|
|
1098
|
-
}
|
|
1099
|
-
return new DependencyGraph(nodes);
|
|
1100
|
-
}
|
|
1101
|
-
#nodes;
|
|
1102
|
-
#allProvided;
|
|
1103
|
-
constructor(nodes) {
|
|
1104
|
-
this.#nodes = nodes;
|
|
1105
|
-
this.#allProvided = /* @__PURE__ */ new Set();
|
|
1106
|
-
for (const node of this.#nodes.values()) {
|
|
1107
|
-
for (const produced of node.provides) {
|
|
1108
|
-
this.#allProvided.add(produced);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Find all nodes that consume dependencies that are not provided by any other node.
|
|
1114
|
-
*/
|
|
1115
|
-
findUnsatisfiedDeps() {
|
|
1116
|
-
const unsatisfiedDependencies = [];
|
|
1117
|
-
for (const node of this.#nodes.values()) {
|
|
1118
|
-
const unsatisfied = Array.from(node.consumes).filter(
|
|
1119
|
-
(id) => !this.#allProvided.has(id)
|
|
1120
|
-
);
|
|
1121
|
-
if (unsatisfied.length > 0) {
|
|
1122
|
-
unsatisfiedDependencies.push({ value: node.value, unsatisfied });
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
return unsatisfiedDependencies;
|
|
1126
|
-
}
|
|
1127
|
-
/**
|
|
1128
|
-
* Detect the first circular dependency within the graph, returning the path of nodes that
|
|
1129
|
-
* form a cycle, with the same node as the first and last element of the array.
|
|
1130
|
-
*/
|
|
1131
|
-
detectCircularDependency() {
|
|
1132
|
-
return this.detectCircularDependencies().next().value;
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Detect circular dependencies within the graph, returning the path of nodes that
|
|
1136
|
-
* form a cycle, with the same node as the first and last element of the array.
|
|
1137
|
-
*/
|
|
1138
|
-
*detectCircularDependencies() {
|
|
1139
|
-
const cycleKeys = CycleKeySet.from(this.#nodes);
|
|
1140
|
-
for (const startNode of this.#nodes) {
|
|
1141
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1142
|
-
const stack = new Array([
|
|
1143
|
-
startNode,
|
|
1144
|
-
[startNode.value]
|
|
1145
|
-
]);
|
|
1146
|
-
while (stack.length > 0) {
|
|
1147
|
-
const [node, path] = stack.pop();
|
|
1148
|
-
if (visited.has(node)) {
|
|
1149
|
-
continue;
|
|
1150
|
-
}
|
|
1151
|
-
visited.add(node);
|
|
1152
|
-
for (const consumed of node.consumes) {
|
|
1153
|
-
const providerNodes = this.#nodes.filter(
|
|
1154
|
-
(other) => other.provides.has(consumed)
|
|
1155
|
-
);
|
|
1156
|
-
for (const provider of providerNodes) {
|
|
1157
|
-
if (provider === startNode) {
|
|
1158
|
-
if (cycleKeys.tryAdd(path)) {
|
|
1159
|
-
yield [...path, startNode.value];
|
|
1160
|
-
}
|
|
1161
|
-
break;
|
|
1162
|
-
}
|
|
1163
|
-
if (!visited.has(provider)) {
|
|
1164
|
-
stack.push([provider, [...path, provider.value]]);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
return void 0;
|
|
1171
|
-
}
|
|
1172
|
-
/**
|
|
1173
|
-
* Traverses the dependency graph in topological order, calling the provided
|
|
1174
|
-
* function for each node and waiting for it to resolve.
|
|
1175
|
-
*
|
|
1176
|
-
* The nodes are traversed in parallel, but in such a way that no node is
|
|
1177
|
-
* visited before all of its dependencies.
|
|
1178
|
-
*
|
|
1179
|
-
* Dependencies of nodes that are not produced by any other nodes will be ignored.
|
|
1180
|
-
*/
|
|
1181
|
-
async parallelTopologicalTraversal(fn) {
|
|
1182
|
-
const allProvided = this.#allProvided;
|
|
1183
|
-
const producedSoFar = /* @__PURE__ */ new Set();
|
|
1184
|
-
const waiting = new Set(this.#nodes.values());
|
|
1185
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1186
|
-
const results = new Array();
|
|
1187
|
-
let inFlight = 0;
|
|
1188
|
-
async function processMoreNodes() {
|
|
1189
|
-
if (waiting.size === 0) {
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
const nodesToProcess = [];
|
|
1193
|
-
for (const node of waiting) {
|
|
1194
|
-
let ready = true;
|
|
1195
|
-
for (const consumed of node.consumes) {
|
|
1196
|
-
if (allProvided.has(consumed) && !producedSoFar.has(consumed)) {
|
|
1197
|
-
ready = false;
|
|
1198
|
-
continue;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
if (ready) {
|
|
1202
|
-
nodesToProcess.push(node);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
for (const node of nodesToProcess) {
|
|
1206
|
-
waiting.delete(node);
|
|
1207
|
-
}
|
|
1208
|
-
if (nodesToProcess.length === 0 && inFlight === 0) {
|
|
1209
|
-
throw new Error("Circular dependency detected");
|
|
1210
|
-
}
|
|
1211
|
-
await Promise.all(nodesToProcess.map(processNode));
|
|
1212
|
-
}
|
|
1213
|
-
async function processNode(node) {
|
|
1214
|
-
visited.add(node);
|
|
1215
|
-
inFlight += 1;
|
|
1216
|
-
const result = await fn(node.value);
|
|
1217
|
-
results.push(result);
|
|
1218
|
-
node.provides.forEach((produced) => producedSoFar.add(produced));
|
|
1219
|
-
inFlight -= 1;
|
|
1220
|
-
await processMoreNodes();
|
|
1221
|
-
}
|
|
1222
|
-
await processMoreNodes();
|
|
1223
|
-
return results;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
function toInternalServiceFactory(factory) {
|
|
1228
|
-
const f = factory;
|
|
1229
|
-
if (f.$$type !== "@backstage/BackendFeature") {
|
|
1230
|
-
throw new Error(`Invalid service factory, bad type '${f.$$type}'`);
|
|
1231
|
-
}
|
|
1232
|
-
if (f.version !== "v1") {
|
|
1233
|
-
throw new Error(`Invalid service factory, bad version '${f.version}'`);
|
|
1234
|
-
}
|
|
1235
|
-
return f;
|
|
1236
|
-
}
|
|
1237
|
-
function createPluginMetadataServiceFactory(pluginId) {
|
|
1238
|
-
return backendPluginApi.createServiceFactory({
|
|
1239
|
-
service: backendPluginApi.coreServices.pluginMetadata,
|
|
1240
|
-
deps: {},
|
|
1241
|
-
factory: async () => ({ getId: () => pluginId })
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
class ServiceRegistry {
|
|
1245
|
-
static create(factories) {
|
|
1246
|
-
const factoryMap = /* @__PURE__ */ new Map();
|
|
1247
|
-
for (const factory of factories) {
|
|
1248
|
-
if (factory.service.multiton) {
|
|
1249
|
-
const existing = factoryMap.get(factory.service.id) ?? [];
|
|
1250
|
-
factoryMap.set(
|
|
1251
|
-
factory.service.id,
|
|
1252
|
-
existing.concat(toInternalServiceFactory(factory))
|
|
1253
|
-
);
|
|
1254
|
-
} else {
|
|
1255
|
-
factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
const registry = new ServiceRegistry(factoryMap);
|
|
1259
|
-
registry.checkForCircularDeps();
|
|
1260
|
-
return registry;
|
|
1261
|
-
}
|
|
1262
|
-
#providedFactories;
|
|
1263
|
-
#loadedDefaultFactories;
|
|
1264
|
-
#implementations;
|
|
1265
|
-
#rootServiceImplementations = /* @__PURE__ */ new Map();
|
|
1266
|
-
#addedFactoryIds = /* @__PURE__ */ new Set();
|
|
1267
|
-
#instantiatedFactories = /* @__PURE__ */ new Set();
|
|
1268
|
-
constructor(factories) {
|
|
1269
|
-
this.#providedFactories = factories;
|
|
1270
|
-
this.#loadedDefaultFactories = /* @__PURE__ */ new Map();
|
|
1271
|
-
this.#implementations = /* @__PURE__ */ new Map();
|
|
1272
|
-
}
|
|
1273
|
-
#resolveFactory(ref, pluginId) {
|
|
1274
|
-
if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
|
|
1275
|
-
return Promise.resolve([
|
|
1276
|
-
toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId))
|
|
1277
|
-
]);
|
|
1278
|
-
}
|
|
1279
|
-
let resolvedFactory = this.#providedFactories.get(ref.id);
|
|
1280
|
-
const { __defaultFactory: defaultFactory } = ref;
|
|
1281
|
-
if (!resolvedFactory && !defaultFactory) {
|
|
1282
|
-
return void 0;
|
|
1283
|
-
}
|
|
1284
|
-
if (!resolvedFactory) {
|
|
1285
|
-
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory);
|
|
1286
|
-
if (!loadedFactory) {
|
|
1287
|
-
loadedFactory = Promise.resolve().then(() => defaultFactory(ref)).then(
|
|
1288
|
-
(f) => toInternalServiceFactory(typeof f === "function" ? f() : f)
|
|
1289
|
-
);
|
|
1290
|
-
this.#loadedDefaultFactories.set(defaultFactory, loadedFactory);
|
|
1291
|
-
}
|
|
1292
|
-
resolvedFactory = loadedFactory.then(
|
|
1293
|
-
(factory) => [factory],
|
|
1294
|
-
(error) => {
|
|
1295
|
-
throw new Error(
|
|
1296
|
-
`Failed to instantiate service '${ref.id}' because the default factory loader threw an error, ${errors.stringifyError(
|
|
1297
|
-
error
|
|
1298
|
-
)}`
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
return Promise.resolve(resolvedFactory);
|
|
1304
|
-
}
|
|
1305
|
-
#checkForMissingDeps(factory, pluginId) {
|
|
1306
|
-
const missingDeps = Object.values(factory.deps).filter((ref) => {
|
|
1307
|
-
if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
|
|
1308
|
-
return false;
|
|
1309
|
-
}
|
|
1310
|
-
if (this.#providedFactories.get(ref.id)) {
|
|
1311
|
-
return false;
|
|
1312
|
-
}
|
|
1313
|
-
if (ref.multiton) {
|
|
1314
|
-
return false;
|
|
1315
|
-
}
|
|
1316
|
-
return !ref.__defaultFactory;
|
|
1317
|
-
});
|
|
1318
|
-
if (missingDeps.length) {
|
|
1319
|
-
const missing = missingDeps.map((r) => `'${r.id}'`).join(", ");
|
|
1320
|
-
throw new Error(
|
|
1321
|
-
`Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`
|
|
1322
|
-
);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
checkForCircularDeps() {
|
|
1326
|
-
const graph = DependencyGraph.fromIterable(
|
|
1327
|
-
Array.from(this.#providedFactories).map(([serviceId, factories]) => ({
|
|
1328
|
-
value: serviceId,
|
|
1329
|
-
provides: [serviceId],
|
|
1330
|
-
consumes: factories.flatMap(
|
|
1331
|
-
(factory) => Object.values(factory.deps).map((d) => d.id)
|
|
1332
|
-
)
|
|
1333
|
-
}))
|
|
1334
|
-
);
|
|
1335
|
-
const circularDependencies = Array.from(graph.detectCircularDependencies());
|
|
1336
|
-
if (circularDependencies.length) {
|
|
1337
|
-
const cycles = circularDependencies.map((c) => c.map((id) => `'${id}'`).join(" -> ")).join("\n ");
|
|
1338
|
-
throw new errors.ConflictError(`Circular dependencies detected:
|
|
1339
|
-
${cycles}`);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
add(factory) {
|
|
1343
|
-
const factoryId = factory.service.id;
|
|
1344
|
-
if (factoryId === backendPluginApi.coreServices.pluginMetadata.id) {
|
|
1345
|
-
throw new Error(
|
|
1346
|
-
`The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
|
|
1347
|
-
);
|
|
1348
|
-
}
|
|
1349
|
-
if (this.#instantiatedFactories.has(factoryId)) {
|
|
1350
|
-
throw new Error(
|
|
1351
|
-
`Unable to set service factory with id ${factoryId}, service has already been instantiated`
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
if (factory.service.multiton) {
|
|
1355
|
-
const newFactories = (this.#providedFactories.get(factoryId) ?? []).concat(toInternalServiceFactory(factory));
|
|
1356
|
-
this.#providedFactories.set(factoryId, newFactories);
|
|
1357
|
-
} else {
|
|
1358
|
-
if (this.#addedFactoryIds.has(factoryId)) {
|
|
1359
|
-
throw new Error(
|
|
1360
|
-
`Duplicate service implementations provided for ${factoryId}`
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
this.#addedFactoryIds.add(factoryId);
|
|
1364
|
-
this.#providedFactories.set(factoryId, [
|
|
1365
|
-
toInternalServiceFactory(factory)
|
|
1366
|
-
]);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
async initializeEagerServicesWithScope(scope, pluginId = "root") {
|
|
1370
|
-
for (const [factory] of this.#providedFactories.values()) {
|
|
1371
|
-
if (factory.service.scope === scope) {
|
|
1372
|
-
if (scope === "root" && factory.initialization !== "lazy") {
|
|
1373
|
-
await this.get(factory.service, pluginId);
|
|
1374
|
-
} else if (scope === "plugin" && factory.initialization === "always") {
|
|
1375
|
-
await this.get(factory.service, pluginId);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
get(ref, pluginId) {
|
|
1381
|
-
this.#instantiatedFactories.add(ref.id);
|
|
1382
|
-
const resolvedFactory = this.#resolveFactory(ref, pluginId);
|
|
1383
|
-
if (!resolvedFactory) {
|
|
1384
|
-
return ref.multiton ? Promise.resolve([]) : void 0;
|
|
1385
|
-
}
|
|
1386
|
-
return resolvedFactory.then((factories) => {
|
|
1387
|
-
return Promise.all(
|
|
1388
|
-
factories.map((factory) => {
|
|
1389
|
-
if (factory.service.scope === "root") {
|
|
1390
|
-
let existing = this.#rootServiceImplementations.get(factory);
|
|
1391
|
-
if (!existing) {
|
|
1392
|
-
this.#checkForMissingDeps(factory, pluginId);
|
|
1393
|
-
const rootDeps = new Array();
|
|
1394
|
-
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
|
1395
|
-
if (serviceRef.scope !== "root") {
|
|
1396
|
-
throw new Error(
|
|
1397
|
-
`Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`
|
|
1398
|
-
);
|
|
1399
|
-
}
|
|
1400
|
-
const target = this.get(serviceRef, pluginId);
|
|
1401
|
-
rootDeps.push(target.then((impl) => [name, impl]));
|
|
1402
|
-
}
|
|
1403
|
-
existing = Promise.all(rootDeps).then(
|
|
1404
|
-
(entries) => factory.factory(Object.fromEntries(entries), void 0)
|
|
1405
|
-
);
|
|
1406
|
-
this.#rootServiceImplementations.set(factory, existing);
|
|
1407
|
-
}
|
|
1408
|
-
return existing;
|
|
1409
|
-
}
|
|
1410
|
-
let implementation = this.#implementations.get(factory);
|
|
1411
|
-
if (!implementation) {
|
|
1412
|
-
this.#checkForMissingDeps(factory, pluginId);
|
|
1413
|
-
const rootDeps = new Array();
|
|
1414
|
-
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
|
1415
|
-
if (serviceRef.scope === "root") {
|
|
1416
|
-
const target = this.get(serviceRef, pluginId);
|
|
1417
|
-
rootDeps.push(target.then((impl) => [name, impl]));
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
implementation = {
|
|
1421
|
-
context: Promise.all(rootDeps).then(
|
|
1422
|
-
(entries) => factory.createRootContext?.(Object.fromEntries(entries))
|
|
1423
|
-
).catch((error) => {
|
|
1424
|
-
const cause = errors.stringifyError(error);
|
|
1425
|
-
throw new Error(
|
|
1426
|
-
`Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`
|
|
1427
|
-
);
|
|
1428
|
-
}),
|
|
1429
|
-
byPlugin: /* @__PURE__ */ new Map()
|
|
1430
|
-
};
|
|
1431
|
-
this.#implementations.set(factory, implementation);
|
|
1432
|
-
}
|
|
1433
|
-
let result = implementation.byPlugin.get(pluginId);
|
|
1434
|
-
if (!result) {
|
|
1435
|
-
const allDeps = new Array();
|
|
1436
|
-
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
|
1437
|
-
const target = this.get(serviceRef, pluginId);
|
|
1438
|
-
allDeps.push(target.then((impl) => [name, impl]));
|
|
1439
|
-
}
|
|
1440
|
-
result = implementation.context.then(
|
|
1441
|
-
(context) => Promise.all(allDeps).then(
|
|
1442
|
-
(entries) => factory.factory(Object.fromEntries(entries), context)
|
|
1443
|
-
)
|
|
1444
|
-
).catch((error) => {
|
|
1445
|
-
const cause = errors.stringifyError(error);
|
|
1446
|
-
throw new Error(
|
|
1447
|
-
`Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`
|
|
1448
|
-
);
|
|
1449
|
-
});
|
|
1450
|
-
implementation.byPlugin.set(pluginId, result);
|
|
1451
|
-
}
|
|
1452
|
-
return result;
|
|
1453
|
-
})
|
|
1454
|
-
);
|
|
1455
|
-
}).then((results) => ref.multiton ? results : results[0]);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
const LOGGER_INTERVAL_MAX = 6e4;
|
|
1460
|
-
function joinIds(ids) {
|
|
1461
|
-
return [...ids].map((id) => `'${id}'`).join(", ");
|
|
1462
|
-
}
|
|
1463
|
-
function createInitializationLogger(pluginIds, rootLogger) {
|
|
1464
|
-
const logger = rootLogger?.child({ type: "initialization" });
|
|
1465
|
-
const starting = new Set(pluginIds);
|
|
1466
|
-
const started = /* @__PURE__ */ new Set();
|
|
1467
|
-
logger?.info(`Plugin initialization started: ${joinIds(pluginIds)}`);
|
|
1468
|
-
const getInitStatus = () => {
|
|
1469
|
-
let status = "";
|
|
1470
|
-
if (started.size > 0) {
|
|
1471
|
-
status = `, newly initialized: ${joinIds(started)}`;
|
|
1472
|
-
started.clear();
|
|
1473
|
-
}
|
|
1474
|
-
if (starting.size > 0) {
|
|
1475
|
-
status += `, still initializing: ${joinIds(starting)}`;
|
|
1476
|
-
}
|
|
1477
|
-
return status;
|
|
1478
|
-
};
|
|
1479
|
-
let interval = 1e3;
|
|
1480
|
-
let prevInterval = 0;
|
|
1481
|
-
let timeout;
|
|
1482
|
-
const onTimeout = () => {
|
|
1483
|
-
logger?.info(`Plugin initialization in progress${getInitStatus()}`);
|
|
1484
|
-
const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX);
|
|
1485
|
-
prevInterval = interval;
|
|
1486
|
-
interval = nextInterval;
|
|
1487
|
-
timeout = setTimeout(onTimeout, nextInterval);
|
|
1488
|
-
};
|
|
1489
|
-
timeout = setTimeout(onTimeout, interval);
|
|
1490
|
-
return {
|
|
1491
|
-
onPluginStarted(pluginId) {
|
|
1492
|
-
starting.delete(pluginId);
|
|
1493
|
-
started.add(pluginId);
|
|
1494
|
-
},
|
|
1495
|
-
onAllStarted() {
|
|
1496
|
-
logger?.info(`Plugin initialization complete${getInitStatus()}`);
|
|
1497
|
-
if (timeout) {
|
|
1498
|
-
clearTimeout(timeout);
|
|
1499
|
-
timeout = void 0;
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
class BackendInitializer {
|
|
1506
|
-
#startPromise;
|
|
1507
|
-
#registrations = new Array();
|
|
1508
|
-
#extensionPoints = /* @__PURE__ */ new Map();
|
|
1509
|
-
#serviceRegistry;
|
|
1510
|
-
#registeredFeatures = new Array();
|
|
1511
|
-
#registeredFeatureLoaders = new Array();
|
|
1512
|
-
constructor(defaultApiFactories) {
|
|
1513
|
-
this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]);
|
|
1514
|
-
}
|
|
1515
|
-
async #getInitDeps(deps, pluginId, moduleId) {
|
|
1516
|
-
const result = /* @__PURE__ */ new Map();
|
|
1517
|
-
const missingRefs = /* @__PURE__ */ new Set();
|
|
1518
|
-
for (const [name, ref] of Object.entries(deps)) {
|
|
1519
|
-
const ep = this.#extensionPoints.get(ref.id);
|
|
1520
|
-
if (ep) {
|
|
1521
|
-
if (ep.pluginId !== pluginId) {
|
|
1522
|
-
throw new Error(
|
|
1523
|
-
`Illegal dependency: Module '${moduleId}' for plugin '${pluginId}' attempted to depend on extension point '${ref.id}' for plugin '${ep.pluginId}'. Extension points can only be used within their plugin's scope.`
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
|
-
result.set(name, ep.impl);
|
|
1527
|
-
} else {
|
|
1528
|
-
const impl = await this.#serviceRegistry.get(
|
|
1529
|
-
ref,
|
|
1530
|
-
pluginId
|
|
1531
|
-
);
|
|
1532
|
-
if (impl) {
|
|
1533
|
-
result.set(name, impl);
|
|
1534
|
-
} else {
|
|
1535
|
-
missingRefs.add(ref);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
if (missingRefs.size > 0) {
|
|
1540
|
-
const missing = Array.from(missingRefs).join(", ");
|
|
1541
|
-
throw new Error(
|
|
1542
|
-
`No extension point or service available for the following ref(s): ${missing}`
|
|
1543
|
-
);
|
|
1544
|
-
}
|
|
1545
|
-
return Object.fromEntries(result);
|
|
1546
|
-
}
|
|
1547
|
-
add(feature) {
|
|
1548
|
-
if (this.#startPromise) {
|
|
1549
|
-
throw new Error("feature can not be added after the backend has started");
|
|
1550
|
-
}
|
|
1551
|
-
this.#registeredFeatures.push(Promise.resolve(feature));
|
|
1552
|
-
}
|
|
1553
|
-
#addFeature(feature) {
|
|
1554
|
-
if (isServiceFactory(feature)) {
|
|
1555
|
-
this.#serviceRegistry.add(feature);
|
|
1556
|
-
} else if (isBackendFeatureLoader(feature)) {
|
|
1557
|
-
this.#registeredFeatureLoaders.push(feature);
|
|
1558
|
-
} else if (isBackendRegistrations(feature)) {
|
|
1559
|
-
this.#registrations.push(feature);
|
|
1560
|
-
} else {
|
|
1561
|
-
throw new Error(
|
|
1562
|
-
`Failed to add feature, invalid feature ${JSON.stringify(feature)}`
|
|
1563
|
-
);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
async start() {
|
|
1567
|
-
if (this.#startPromise) {
|
|
1568
|
-
throw new Error("Backend has already started");
|
|
1569
|
-
}
|
|
1570
|
-
const exitHandler = async () => {
|
|
1571
|
-
process.removeListener("SIGTERM", exitHandler);
|
|
1572
|
-
process.removeListener("SIGINT", exitHandler);
|
|
1573
|
-
process.removeListener("beforeExit", exitHandler);
|
|
1574
|
-
try {
|
|
1575
|
-
await this.stop();
|
|
1576
|
-
process.exit(0);
|
|
1577
|
-
} catch (error) {
|
|
1578
|
-
console.error(error);
|
|
1579
|
-
process.exit(1);
|
|
1580
|
-
}
|
|
1581
|
-
};
|
|
1582
|
-
process.addListener("SIGTERM", exitHandler);
|
|
1583
|
-
process.addListener("SIGINT", exitHandler);
|
|
1584
|
-
process.addListener("beforeExit", exitHandler);
|
|
1585
|
-
this.#startPromise = this.#doStart();
|
|
1586
|
-
await this.#startPromise;
|
|
1587
|
-
}
|
|
1588
|
-
async #doStart() {
|
|
1589
|
-
this.#serviceRegistry.checkForCircularDeps();
|
|
1590
|
-
for (const feature of this.#registeredFeatures) {
|
|
1591
|
-
this.#addFeature(await feature);
|
|
1592
|
-
}
|
|
1593
|
-
const featureDiscovery = await this.#serviceRegistry.get(
|
|
1594
|
-
alpha.featureDiscoveryServiceRef,
|
|
1595
|
-
"root"
|
|
1596
|
-
);
|
|
1597
|
-
if (featureDiscovery) {
|
|
1598
|
-
const { features } = await featureDiscovery.getBackendFeatures();
|
|
1599
|
-
for (const feature of features) {
|
|
1600
|
-
this.#addFeature(feature);
|
|
1601
|
-
}
|
|
1602
|
-
this.#serviceRegistry.checkForCircularDeps();
|
|
1603
|
-
}
|
|
1604
|
-
await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders);
|
|
1605
|
-
await this.#serviceRegistry.initializeEagerServicesWithScope("root");
|
|
1606
|
-
const pluginInits = /* @__PURE__ */ new Map();
|
|
1607
|
-
const moduleInits = /* @__PURE__ */ new Map();
|
|
1608
|
-
for (const feature of this.#registrations) {
|
|
1609
|
-
for (const r of feature.getRegistrations()) {
|
|
1610
|
-
const provides = /* @__PURE__ */ new Set();
|
|
1611
|
-
if (r.type === "plugin" || r.type === "module") {
|
|
1612
|
-
for (const [extRef, extImpl] of r.extensionPoints) {
|
|
1613
|
-
if (this.#extensionPoints.has(extRef.id)) {
|
|
1614
|
-
throw new Error(
|
|
1615
|
-
`ExtensionPoint with ID '${extRef.id}' is already registered`
|
|
1616
|
-
);
|
|
1617
|
-
}
|
|
1618
|
-
this.#extensionPoints.set(extRef.id, {
|
|
1619
|
-
impl: extImpl,
|
|
1620
|
-
pluginId: r.pluginId
|
|
1621
|
-
});
|
|
1622
|
-
provides.add(extRef);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
if (r.type === "plugin") {
|
|
1626
|
-
if (pluginInits.has(r.pluginId)) {
|
|
1627
|
-
throw new Error(`Plugin '${r.pluginId}' is already registered`);
|
|
1628
|
-
}
|
|
1629
|
-
pluginInits.set(r.pluginId, {
|
|
1630
|
-
provides,
|
|
1631
|
-
consumes: new Set(Object.values(r.init.deps)),
|
|
1632
|
-
init: r.init
|
|
1633
|
-
});
|
|
1634
|
-
} else if (r.type === "module") {
|
|
1635
|
-
let modules = moduleInits.get(r.pluginId);
|
|
1636
|
-
if (!modules) {
|
|
1637
|
-
modules = /* @__PURE__ */ new Map();
|
|
1638
|
-
moduleInits.set(r.pluginId, modules);
|
|
1639
|
-
}
|
|
1640
|
-
if (modules.has(r.moduleId)) {
|
|
1641
|
-
throw new Error(
|
|
1642
|
-
`Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered`
|
|
1643
|
-
);
|
|
1644
|
-
}
|
|
1645
|
-
modules.set(r.moduleId, {
|
|
1646
|
-
provides,
|
|
1647
|
-
consumes: new Set(Object.values(r.init.deps)),
|
|
1648
|
-
init: r.init
|
|
1649
|
-
});
|
|
1650
|
-
} else {
|
|
1651
|
-
throw new Error(`Invalid registration type '${r.type}'`);
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
const allPluginIds = [...pluginInits.keys()];
|
|
1656
|
-
const initLogger = createInitializationLogger(
|
|
1657
|
-
allPluginIds,
|
|
1658
|
-
await this.#serviceRegistry.get(backendPluginApi.coreServices.rootLogger, "root")
|
|
1659
|
-
);
|
|
1660
|
-
await Promise.all(
|
|
1661
|
-
allPluginIds.map(async (pluginId) => {
|
|
1662
|
-
await this.#serviceRegistry.initializeEagerServicesWithScope(
|
|
1663
|
-
"plugin",
|
|
1664
|
-
pluginId
|
|
1665
|
-
);
|
|
1666
|
-
const modules = moduleInits.get(pluginId);
|
|
1667
|
-
if (modules) {
|
|
1668
|
-
const tree = DependencyGraph.fromIterable(
|
|
1669
|
-
Array.from(modules).map(([moduleId, moduleInit]) => ({
|
|
1670
|
-
value: { moduleId, moduleInit },
|
|
1671
|
-
// Relationships are reversed at this point since we're only interested in the extension points.
|
|
1672
|
-
// If a modules provides extension point A we want it to be initialized AFTER all modules
|
|
1673
|
-
// that depend on extension point A, so that they can provide their extensions.
|
|
1674
|
-
consumes: Array.from(moduleInit.provides).map((p) => p.id),
|
|
1675
|
-
provides: Array.from(moduleInit.consumes).map((c) => c.id)
|
|
1676
|
-
}))
|
|
1677
|
-
);
|
|
1678
|
-
const circular = tree.detectCircularDependency();
|
|
1679
|
-
if (circular) {
|
|
1680
|
-
throw new errors.ConflictError(
|
|
1681
|
-
`Circular dependency detected for modules of plugin '${pluginId}', ${circular.map(({ moduleId }) => `'${moduleId}'`).join(" -> ")}`
|
|
1682
|
-
);
|
|
1683
|
-
}
|
|
1684
|
-
await tree.parallelTopologicalTraversal(
|
|
1685
|
-
async ({ moduleId, moduleInit }) => {
|
|
1686
|
-
const moduleDeps = await this.#getInitDeps(
|
|
1687
|
-
moduleInit.init.deps,
|
|
1688
|
-
pluginId,
|
|
1689
|
-
moduleId
|
|
1690
|
-
);
|
|
1691
|
-
await moduleInit.init.func(moduleDeps).catch((error) => {
|
|
1692
|
-
throw new errors.ForwardedError(
|
|
1693
|
-
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
|
|
1694
|
-
error
|
|
1695
|
-
);
|
|
1696
|
-
});
|
|
1697
|
-
}
|
|
1698
|
-
);
|
|
1699
|
-
}
|
|
1700
|
-
const pluginInit = pluginInits.get(pluginId);
|
|
1701
|
-
if (pluginInit) {
|
|
1702
|
-
const pluginDeps = await this.#getInitDeps(
|
|
1703
|
-
pluginInit.init.deps,
|
|
1704
|
-
pluginId
|
|
1705
|
-
);
|
|
1706
|
-
await pluginInit.init.func(pluginDeps).catch((error) => {
|
|
1707
|
-
throw new errors.ForwardedError(
|
|
1708
|
-
`Plugin '${pluginId}' startup failed`,
|
|
1709
|
-
error
|
|
1710
|
-
);
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
initLogger.onPluginStarted(pluginId);
|
|
1714
|
-
const lifecycleService2 = await this.#getPluginLifecycleImpl(pluginId);
|
|
1715
|
-
await lifecycleService2.startup();
|
|
1716
|
-
})
|
|
1717
|
-
);
|
|
1718
|
-
const lifecycleService = await this.#getRootLifecycleImpl();
|
|
1719
|
-
await lifecycleService.startup();
|
|
1720
|
-
initLogger.onAllStarted();
|
|
1721
|
-
if (process.env.NODE_ENV !== "test") {
|
|
1722
|
-
const rootLogger = await this.#serviceRegistry.get(
|
|
1723
|
-
backendPluginApi.coreServices.rootLogger,
|
|
1724
|
-
"root"
|
|
1725
|
-
);
|
|
1726
|
-
process.on("unhandledRejection", (reason) => {
|
|
1727
|
-
rootLogger?.child({ type: "unhandledRejection" })?.error("Unhandled rejection", reason);
|
|
1728
|
-
});
|
|
1729
|
-
process.on("uncaughtException", (error) => {
|
|
1730
|
-
rootLogger?.child({ type: "uncaughtException" })?.error("Uncaught exception", error);
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
async stop() {
|
|
1735
|
-
if (!this.#startPromise) {
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
try {
|
|
1739
|
-
await this.#startPromise;
|
|
1740
|
-
} catch (error) {
|
|
1741
|
-
}
|
|
1742
|
-
const lifecycleService = await this.#getRootLifecycleImpl();
|
|
1743
|
-
await lifecycleService.shutdown();
|
|
1744
|
-
}
|
|
1745
|
-
// Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this
|
|
1746
|
-
async #getRootLifecycleImpl() {
|
|
1747
|
-
const lifecycleService = await this.#serviceRegistry.get(
|
|
1748
|
-
backendPluginApi.coreServices.rootLifecycle,
|
|
1749
|
-
"root"
|
|
1750
|
-
);
|
|
1751
|
-
const service = lifecycleService;
|
|
1752
|
-
if (service && typeof service.startup === "function" && typeof service.shutdown === "function") {
|
|
1753
|
-
return service;
|
|
1754
|
-
}
|
|
1755
|
-
throw new Error("Unexpected root lifecycle service implementation");
|
|
1756
|
-
}
|
|
1757
|
-
async #getPluginLifecycleImpl(pluginId) {
|
|
1758
|
-
const lifecycleService = await this.#serviceRegistry.get(
|
|
1759
|
-
backendPluginApi.coreServices.lifecycle,
|
|
1760
|
-
pluginId
|
|
1761
|
-
);
|
|
1762
|
-
const service = lifecycleService;
|
|
1763
|
-
if (service && typeof service.startup === "function") {
|
|
1764
|
-
return service;
|
|
1765
|
-
}
|
|
1766
|
-
throw new Error("Unexpected plugin lifecycle service implementation");
|
|
1767
|
-
}
|
|
1768
|
-
async #applyBackendFeatureLoaders(loaders) {
|
|
1769
|
-
for (const loader of loaders) {
|
|
1770
|
-
const deps = /* @__PURE__ */ new Map();
|
|
1771
|
-
const missingRefs = /* @__PURE__ */ new Set();
|
|
1772
|
-
for (const [name, ref] of Object.entries(loader.deps ?? {})) {
|
|
1773
|
-
if (ref.scope !== "root") {
|
|
1774
|
-
throw new Error(
|
|
1775
|
-
`Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`
|
|
1776
|
-
);
|
|
1777
|
-
}
|
|
1778
|
-
const impl = await this.#serviceRegistry.get(
|
|
1779
|
-
ref,
|
|
1780
|
-
"root"
|
|
1781
|
-
);
|
|
1782
|
-
if (impl) {
|
|
1783
|
-
deps.set(name, impl);
|
|
1784
|
-
} else {
|
|
1785
|
-
missingRefs.add(ref);
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
if (missingRefs.size > 0) {
|
|
1789
|
-
const missing = Array.from(missingRefs).join(", ");
|
|
1790
|
-
throw new Error(
|
|
1791
|
-
`No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`
|
|
1792
|
-
);
|
|
1793
|
-
}
|
|
1794
|
-
const result = await loader.loader(Object.fromEntries(deps)).catch((error) => {
|
|
1795
|
-
throw new errors.ForwardedError(
|
|
1796
|
-
`Feature loader ${loader.description} failed`,
|
|
1797
|
-
error
|
|
1798
|
-
);
|
|
1799
|
-
});
|
|
1800
|
-
let didAddServiceFactory = false;
|
|
1801
|
-
const newLoaders = new Array();
|
|
1802
|
-
for await (const feature of result) {
|
|
1803
|
-
if (isBackendFeatureLoader(feature)) {
|
|
1804
|
-
newLoaders.push(feature);
|
|
1805
|
-
} else {
|
|
1806
|
-
didAddServiceFactory ||= isServiceFactory(feature);
|
|
1807
|
-
this.#addFeature(feature);
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
if (didAddServiceFactory) {
|
|
1811
|
-
this.#serviceRegistry.checkForCircularDeps();
|
|
1812
|
-
}
|
|
1813
|
-
if (newLoaders.length > 0) {
|
|
1814
|
-
await this.#applyBackendFeatureLoaders(newLoaders);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
function toInternalBackendFeature(feature) {
|
|
1820
|
-
if (feature.$$type !== "@backstage/BackendFeature") {
|
|
1821
|
-
throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
|
|
1822
|
-
}
|
|
1823
|
-
const internal = feature;
|
|
1824
|
-
if (internal.version !== "v1") {
|
|
1825
|
-
throw new Error(
|
|
1826
|
-
`Invalid BackendFeature, bad version '${internal.version}'`
|
|
1827
|
-
);
|
|
1828
|
-
}
|
|
1829
|
-
return internal;
|
|
1830
|
-
}
|
|
1831
|
-
function isServiceFactory(feature) {
|
|
1832
|
-
const internal = toInternalBackendFeature(feature);
|
|
1833
|
-
if (internal.featureType === "service") {
|
|
1834
|
-
return true;
|
|
1835
|
-
}
|
|
1836
|
-
return "service" in internal;
|
|
1837
|
-
}
|
|
1838
|
-
function isBackendRegistrations(feature) {
|
|
1839
|
-
const internal = toInternalBackendFeature(feature);
|
|
1840
|
-
if (internal.featureType === "registrations") {
|
|
1841
|
-
return true;
|
|
1842
|
-
}
|
|
1843
|
-
return "getRegistrations" in internal;
|
|
1844
|
-
}
|
|
1845
|
-
function isBackendFeatureLoader(feature) {
|
|
1846
|
-
return toInternalBackendFeature(feature).featureType === "loader";
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
class BackstageBackend {
|
|
1850
|
-
#initializer;
|
|
1851
|
-
constructor(defaultServiceFactories) {
|
|
1852
|
-
this.#initializer = new BackendInitializer(defaultServiceFactories);
|
|
1853
|
-
}
|
|
1854
|
-
add(feature) {
|
|
1855
|
-
if (isPromise(feature)) {
|
|
1856
|
-
this.#initializer.add(feature.then((f) => unwrapFeature(f.default)));
|
|
1857
|
-
} else {
|
|
1858
|
-
this.#initializer.add(unwrapFeature(feature));
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
async start() {
|
|
1862
|
-
await this.#initializer.start();
|
|
1863
|
-
}
|
|
1864
|
-
async stop() {
|
|
1865
|
-
await this.#initializer.stop();
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
function isPromise(value) {
|
|
1869
|
-
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
1870
|
-
}
|
|
1871
|
-
function unwrapFeature(feature) {
|
|
1872
|
-
if (typeof feature === "function") {
|
|
1873
|
-
return feature();
|
|
1874
|
-
}
|
|
1875
|
-
if ("$$type" in feature) {
|
|
1876
|
-
return feature;
|
|
1877
|
-
}
|
|
1878
|
-
if ("default" in feature) {
|
|
1879
|
-
const defaultFeature = feature.default;
|
|
1880
|
-
return typeof defaultFeature === "function" ? defaultFeature() : defaultFeature;
|
|
1881
|
-
}
|
|
1882
|
-
return feature;
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
function createSpecializedBackend(options) {
|
|
1886
|
-
const exists = /* @__PURE__ */ new Set();
|
|
1887
|
-
const duplicates = /* @__PURE__ */ new Set();
|
|
1888
|
-
for (const { service } of options.defaultServiceFactories) {
|
|
1889
|
-
if (exists.has(service.id)) {
|
|
1890
|
-
duplicates.add(service.id);
|
|
1891
|
-
} else {
|
|
1892
|
-
exists.add(service.id);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
if (duplicates.size > 0) {
|
|
1896
|
-
const ids = Array.from(duplicates).join(", ");
|
|
1897
|
-
throw new Error(`Duplicate service implementations provided for ${ids}`);
|
|
1898
|
-
}
|
|
1899
|
-
if (exists.has(backendPluginApi.coreServices.pluginMetadata.id)) {
|
|
1900
|
-
throw new Error(
|
|
1901
|
-
`The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
1904
|
-
return new BackstageBackend(options.defaultServiceFactories);
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
const cacheServiceFactory = backendPluginApi.createServiceFactory({
|
|
1908
|
-
service: backendPluginApi.coreServices.cache,
|
|
1909
|
-
deps: {
|
|
1910
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
1911
|
-
logger: backendPluginApi.coreServices.rootLogger,
|
|
1912
|
-
plugin: backendPluginApi.coreServices.pluginMetadata
|
|
1913
|
-
},
|
|
1914
|
-
async createRootContext({ config, logger }) {
|
|
1915
|
-
return backendCommon.CacheManager.fromConfig(config, { logger });
|
|
1916
|
-
},
|
|
1917
|
-
async factory({ plugin }, manager) {
|
|
1918
|
-
return manager.forPlugin(plugin.getId()).getClient();
|
|
1919
|
-
}
|
|
1920
|
-
});
|
|
1921
|
-
|
|
1922
|
-
const rootConfigServiceFactory = backendPluginApi.createServiceFactory(
|
|
1923
|
-
(options) => ({
|
|
1924
|
-
service: backendPluginApi.coreServices.rootConfig,
|
|
1925
|
-
deps: {},
|
|
1926
|
-
async factory() {
|
|
1927
|
-
const source = configLoader.ConfigSources.default({
|
|
1928
|
-
argv: options?.argv,
|
|
1929
|
-
remote: options?.remote,
|
|
1930
|
-
watch: options?.watch
|
|
1931
|
-
});
|
|
1932
|
-
console.log(`Loading config from ${source}`);
|
|
1933
|
-
return await configLoader.ConfigSources.toConfig(source);
|
|
1934
|
-
}
|
|
1935
|
-
})
|
|
1936
|
-
);
|
|
1937
|
-
|
|
1938
|
-
const databaseServiceFactory = backendPluginApi.createServiceFactory({
|
|
1939
|
-
service: backendPluginApi.coreServices.database,
|
|
1940
|
-
deps: {
|
|
1941
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
1942
|
-
lifecycle: backendPluginApi.coreServices.lifecycle,
|
|
1943
|
-
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
1944
|
-
},
|
|
1945
|
-
async createRootContext({ config: config$1 }) {
|
|
1946
|
-
return config$1.getOptional("backend.database") ? backendCommon.DatabaseManager.fromConfig(config$1) : backendCommon.DatabaseManager.fromConfig(
|
|
1947
|
-
new config.ConfigReader({
|
|
1948
|
-
backend: {
|
|
1949
|
-
database: { client: "better-sqlite3", connection: ":memory:" }
|
|
1950
|
-
}
|
|
1951
|
-
})
|
|
1952
|
-
);
|
|
1953
|
-
},
|
|
1954
|
-
async factory({ pluginMetadata, lifecycle }, databaseManager) {
|
|
1955
|
-
return databaseManager.forPlugin(pluginMetadata.getId(), {
|
|
1956
|
-
pluginMetadata,
|
|
1957
|
-
lifecycle
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
});
|
|
1961
|
-
|
|
1962
|
-
let HostDiscovery$1 = class HostDiscovery {
|
|
1963
|
-
constructor(internalBaseUrl, externalBaseUrl, discoveryConfig) {
|
|
1964
|
-
this.internalBaseUrl = internalBaseUrl;
|
|
1965
|
-
this.externalBaseUrl = externalBaseUrl;
|
|
1966
|
-
this.discoveryConfig = discoveryConfig;
|
|
1967
|
-
}
|
|
1968
|
-
/**
|
|
1969
|
-
* Creates a new HostDiscovery discovery instance by reading
|
|
1970
|
-
* from the `backend` config section, specifically the `.baseUrl` for
|
|
1971
|
-
* discovering the external URL, and the `.listen` and `.https` config
|
|
1972
|
-
* for the internal one.
|
|
1973
|
-
*
|
|
1974
|
-
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
|
1975
|
-
* eg.
|
|
1976
|
-
* ```yaml
|
|
1977
|
-
* discovery:
|
|
1978
|
-
* endpoints:
|
|
1979
|
-
* - target: https://internal.example.com/internal-catalog
|
|
1980
|
-
* plugins: [catalog]
|
|
1981
|
-
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
|
1982
|
-
* plugins: [auth, permission]
|
|
1983
|
-
* - target:
|
|
1984
|
-
* internal: https://internal.example.com/search
|
|
1985
|
-
* external: https://example.com/search
|
|
1986
|
-
* plugins: [search]
|
|
1987
|
-
* ```
|
|
1988
|
-
*
|
|
1989
|
-
* The basePath defaults to `/api`, meaning the default full internal
|
|
1990
|
-
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
|
1991
|
-
*/
|
|
1992
|
-
static fromConfig(config, options) {
|
|
1993
|
-
const basePath = options?.basePath ?? "/api";
|
|
1994
|
-
const externalBaseUrl = config.getString("backend.baseUrl").replace(/\/+$/, "");
|
|
1995
|
-
const {
|
|
1996
|
-
listen: { host: listenHost = "::", port: listenPort }
|
|
1997
|
-
} = readHttpServerOptions$1(config.getConfig("backend"));
|
|
1998
|
-
const protocol = config.has("backend.https") ? "https" : "http";
|
|
1999
|
-
let host = listenHost;
|
|
2000
|
-
if (host === "::" || host === "") {
|
|
2001
|
-
host = "localhost";
|
|
2002
|
-
} else if (host === "0.0.0.0") {
|
|
2003
|
-
host = "127.0.0.1";
|
|
2004
|
-
}
|
|
2005
|
-
if (host.includes(":")) {
|
|
2006
|
-
host = `[${host}]`;
|
|
2007
|
-
}
|
|
2008
|
-
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
|
|
2009
|
-
return new HostDiscovery(
|
|
2010
|
-
internalBaseUrl + basePath,
|
|
2011
|
-
externalBaseUrl + basePath,
|
|
2012
|
-
config.getOptionalConfig("discovery")
|
|
2013
|
-
);
|
|
2014
|
-
}
|
|
2015
|
-
getTargetFromConfig(pluginId, type) {
|
|
2016
|
-
const endpoints = this.discoveryConfig?.getOptionalConfigArray("endpoints");
|
|
2017
|
-
const target = endpoints?.find((endpoint) => endpoint.getStringArray("plugins").includes(pluginId))?.get("target");
|
|
2018
|
-
if (!target) {
|
|
2019
|
-
const baseUrl = type === "external" ? this.externalBaseUrl : this.internalBaseUrl;
|
|
2020
|
-
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
|
|
2021
|
-
}
|
|
2022
|
-
if (typeof target === "string") {
|
|
2023
|
-
return target.replace(
|
|
2024
|
-
/\{\{\s*pluginId\s*\}\}/g,
|
|
2025
|
-
encodeURIComponent(pluginId)
|
|
2026
|
-
);
|
|
2027
|
-
}
|
|
2028
|
-
return target[type].replace(
|
|
2029
|
-
/\{\{\s*pluginId\s*\}\}/g,
|
|
2030
|
-
encodeURIComponent(pluginId)
|
|
2031
|
-
);
|
|
2032
|
-
}
|
|
2033
|
-
async getBaseUrl(pluginId) {
|
|
2034
|
-
return this.getTargetFromConfig(pluginId, "internal");
|
|
2035
|
-
}
|
|
2036
|
-
async getExternalBaseUrl(pluginId) {
|
|
2037
|
-
return this.getTargetFromConfig(pluginId, "external");
|
|
2038
|
-
}
|
|
2039
|
-
};
|
|
2040
|
-
|
|
2041
|
-
backendPluginApi.createServiceFactory({
|
|
2042
|
-
service: backendPluginApi.coreServices.discovery,
|
|
2043
|
-
deps: {
|
|
2044
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
2045
|
-
},
|
|
2046
|
-
async factory({ config }) {
|
|
2047
|
-
return HostDiscovery$1.fromConfig(config);
|
|
2048
|
-
}
|
|
2049
|
-
});
|
|
2050
|
-
|
|
2051
|
-
class HostDiscovery {
|
|
2052
|
-
constructor(impl) {
|
|
2053
|
-
this.impl = impl;
|
|
2054
|
-
}
|
|
2055
|
-
/**
|
|
2056
|
-
* Creates a new HostDiscovery discovery instance by reading
|
|
2057
|
-
* from the `backend` config section, specifically the `.baseUrl` for
|
|
2058
|
-
* discovering the external URL, and the `.listen` and `.https` config
|
|
2059
|
-
* for the internal one.
|
|
2060
|
-
*
|
|
2061
|
-
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
|
2062
|
-
* eg.
|
|
2063
|
-
* ```yaml
|
|
2064
|
-
* discovery:
|
|
2065
|
-
* endpoints:
|
|
2066
|
-
* - target: https://internal.example.com/internal-catalog
|
|
2067
|
-
* plugins: [catalog]
|
|
2068
|
-
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
|
2069
|
-
* plugins: [auth, permission]
|
|
2070
|
-
* - target:
|
|
2071
|
-
* internal: https://internal.example.com/search
|
|
2072
|
-
* external: https://example.com/search
|
|
2073
|
-
* plugins: [search]
|
|
2074
|
-
* ```
|
|
2075
|
-
*
|
|
2076
|
-
* The basePath defaults to `/api`, meaning the default full internal
|
|
2077
|
-
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
|
2078
|
-
*/
|
|
2079
|
-
static fromConfig(config, options) {
|
|
2080
|
-
return new HostDiscovery(HostDiscovery$1.fromConfig(config, options));
|
|
2081
|
-
}
|
|
2082
|
-
async getBaseUrl(pluginId) {
|
|
2083
|
-
return this.impl.getBaseUrl(pluginId);
|
|
2084
|
-
}
|
|
2085
|
-
async getExternalBaseUrl(pluginId) {
|
|
2086
|
-
return this.impl.getExternalBaseUrl(pluginId);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
const discoveryServiceFactory = backendPluginApi.createServiceFactory({
|
|
2091
|
-
service: backendPluginApi.coreServices.discovery,
|
|
2092
|
-
deps: {
|
|
2093
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
2094
|
-
},
|
|
2095
|
-
async factory({ config }) {
|
|
2096
|
-
return HostDiscovery.fromConfig(config);
|
|
2097
|
-
}
|
|
2098
|
-
});
|
|
2099
|
-
|
|
2100
|
-
const identityServiceFactory = backendPluginApi.createServiceFactory(
|
|
2101
|
-
(options) => ({
|
|
2102
|
-
service: backendPluginApi.coreServices.identity,
|
|
2103
|
-
deps: {
|
|
2104
|
-
discovery: backendPluginApi.coreServices.discovery
|
|
2105
|
-
},
|
|
2106
|
-
async factory({ discovery }) {
|
|
2107
|
-
return pluginAuthNode.DefaultIdentityClient.create({ discovery, ...options });
|
|
2108
|
-
}
|
|
2109
|
-
})
|
|
2110
|
-
);
|
|
2111
|
-
|
|
2112
|
-
class BackendPluginLifecycleImpl {
|
|
2113
|
-
constructor(logger, rootLifecycle, pluginMetadata) {
|
|
2114
|
-
this.logger = logger;
|
|
2115
|
-
this.rootLifecycle = rootLifecycle;
|
|
2116
|
-
this.pluginMetadata = pluginMetadata;
|
|
2117
|
-
}
|
|
2118
|
-
#hasStarted = false;
|
|
2119
|
-
#startupTasks = [];
|
|
2120
|
-
addStartupHook(hook, options) {
|
|
2121
|
-
if (this.#hasStarted) {
|
|
2122
|
-
throw new Error("Attempted to add startup hook after startup");
|
|
2123
|
-
}
|
|
2124
|
-
this.#startupTasks.push({ hook, options });
|
|
2125
|
-
}
|
|
2126
|
-
async startup() {
|
|
2127
|
-
if (this.#hasStarted) {
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
this.#hasStarted = true;
|
|
2131
|
-
this.logger.debug(
|
|
2132
|
-
`Running ${this.#startupTasks.length} plugin startup tasks...`
|
|
2133
|
-
);
|
|
2134
|
-
await Promise.all(
|
|
2135
|
-
this.#startupTasks.map(async ({ hook, options }) => {
|
|
2136
|
-
const logger = options?.logger ?? this.logger;
|
|
2137
|
-
try {
|
|
2138
|
-
await hook();
|
|
2139
|
-
logger.debug(`Plugin startup hook succeeded`);
|
|
2140
|
-
} catch (error) {
|
|
2141
|
-
logger.error(`Plugin startup hook failed, ${error}`);
|
|
2142
|
-
}
|
|
2143
|
-
})
|
|
2144
|
-
);
|
|
2145
|
-
}
|
|
2146
|
-
addShutdownHook(hook, options) {
|
|
2147
|
-
const plugin = this.pluginMetadata.getId();
|
|
2148
|
-
this.rootLifecycle.addShutdownHook(hook, {
|
|
2149
|
-
logger: options?.logger?.child({ plugin }) ?? this.logger
|
|
2150
|
-
});
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
const lifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
2154
|
-
service: backendPluginApi.coreServices.lifecycle,
|
|
2155
|
-
deps: {
|
|
2156
|
-
logger: backendPluginApi.coreServices.logger,
|
|
2157
|
-
rootLifecycle: backendPluginApi.coreServices.rootLifecycle,
|
|
2158
|
-
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
2159
|
-
},
|
|
2160
|
-
async factory({ rootLifecycle, logger, pluginMetadata }) {
|
|
2161
|
-
return new BackendPluginLifecycleImpl(
|
|
2162
|
-
logger,
|
|
2163
|
-
rootLifecycle,
|
|
2164
|
-
pluginMetadata
|
|
2165
|
-
);
|
|
2166
|
-
}
|
|
2167
|
-
});
|
|
2168
|
-
|
|
2169
|
-
const permissionsServiceFactory = backendPluginApi.createServiceFactory({
|
|
2170
|
-
service: backendPluginApi.coreServices.permissions,
|
|
2171
|
-
deps: {
|
|
2172
|
-
auth: backendPluginApi.coreServices.auth,
|
|
2173
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
2174
|
-
discovery: backendPluginApi.coreServices.discovery,
|
|
2175
|
-
tokenManager: backendPluginApi.coreServices.tokenManager
|
|
2176
|
-
},
|
|
2177
|
-
async factory({ auth, config, discovery, tokenManager }) {
|
|
2178
|
-
return pluginPermissionNode.ServerPermissionClient.fromConfig(config, {
|
|
2179
|
-
auth,
|
|
2180
|
-
discovery,
|
|
2181
|
-
tokenManager
|
|
2182
|
-
});
|
|
2183
|
-
}
|
|
2184
|
-
});
|
|
2185
|
-
|
|
2186
|
-
class BackendLifecycleImpl {
|
|
2187
|
-
constructor(logger) {
|
|
2188
|
-
this.logger = logger;
|
|
2189
|
-
}
|
|
2190
|
-
#hasStarted = false;
|
|
2191
|
-
#startupTasks = [];
|
|
2192
|
-
addStartupHook(hook, options) {
|
|
2193
|
-
if (this.#hasStarted) {
|
|
2194
|
-
throw new Error("Attempted to add startup hook after startup");
|
|
2195
|
-
}
|
|
2196
|
-
this.#startupTasks.push({ hook, options });
|
|
2197
|
-
}
|
|
2198
|
-
async startup() {
|
|
2199
|
-
if (this.#hasStarted) {
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
this.#hasStarted = true;
|
|
2203
|
-
this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`);
|
|
2204
|
-
await Promise.all(
|
|
2205
|
-
this.#startupTasks.map(async ({ hook, options }) => {
|
|
2206
|
-
const logger = options?.logger ?? this.logger;
|
|
2207
|
-
try {
|
|
2208
|
-
await hook();
|
|
2209
|
-
logger.debug(`Startup hook succeeded`);
|
|
2210
|
-
} catch (error) {
|
|
2211
|
-
logger.error(`Startup hook failed, ${error}`);
|
|
152
|
+
const nodesToProcess = [];
|
|
153
|
+
for (const node of waiting) {
|
|
154
|
+
let ready = true;
|
|
155
|
+
for (const consumed of node.consumes) {
|
|
156
|
+
if (allProvided.has(consumed) && !producedSoFar.has(consumed)) {
|
|
157
|
+
ready = false;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
2212
160
|
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
161
|
+
if (ready) {
|
|
162
|
+
nodesToProcess.push(node);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const node of nodesToProcess) {
|
|
166
|
+
waiting.delete(node);
|
|
167
|
+
}
|
|
168
|
+
if (nodesToProcess.length === 0 && inFlight === 0) {
|
|
169
|
+
throw new Error("Circular dependency detected");
|
|
170
|
+
}
|
|
171
|
+
await Promise.all(nodesToProcess.map(processNode));
|
|
2221
172
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
173
|
+
async function processNode(node) {
|
|
174
|
+
visited.add(node);
|
|
175
|
+
inFlight += 1;
|
|
176
|
+
const result = await fn(node.value);
|
|
177
|
+
results.push(result);
|
|
178
|
+
node.provides.forEach((produced) => producedSoFar.add(produced));
|
|
179
|
+
inFlight -= 1;
|
|
180
|
+
await processMoreNodes();
|
|
2227
181
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
`Running ${this.#shutdownTasks.length} shutdown tasks...`
|
|
2231
|
-
);
|
|
2232
|
-
await Promise.all(
|
|
2233
|
-
this.#shutdownTasks.map(async ({ hook, options }) => {
|
|
2234
|
-
const logger = options?.logger ?? this.logger;
|
|
2235
|
-
try {
|
|
2236
|
-
await hook();
|
|
2237
|
-
logger.debug(`Shutdown hook succeeded`);
|
|
2238
|
-
} catch (error) {
|
|
2239
|
-
logger.error(`Shutdown hook failed, ${error}`);
|
|
2240
|
-
}
|
|
2241
|
-
})
|
|
2242
|
-
);
|
|
182
|
+
await processMoreNodes();
|
|
183
|
+
return results;
|
|
2243
184
|
}
|
|
2244
185
|
}
|
|
2245
|
-
const rootLifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
2246
|
-
service: backendPluginApi.coreServices.rootLifecycle,
|
|
2247
|
-
deps: {
|
|
2248
|
-
logger: backendPluginApi.coreServices.rootLogger
|
|
2249
|
-
},
|
|
2250
|
-
async factory({ logger }) {
|
|
2251
|
-
return new BackendLifecycleImpl(logger);
|
|
2252
|
-
}
|
|
2253
|
-
});
|
|
2254
186
|
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
logger: backendPluginApi.coreServices.rootLogger
|
|
2260
|
-
},
|
|
2261
|
-
createRootContext({ config, logger }) {
|
|
2262
|
-
return backendCommon.ServerTokenManager.fromConfig(config, {
|
|
2263
|
-
logger,
|
|
2264
|
-
allowDisabledTokenManager: true
|
|
2265
|
-
});
|
|
2266
|
-
},
|
|
2267
|
-
async factory(_deps, tokenManager) {
|
|
2268
|
-
return tokenManager;
|
|
187
|
+
function toInternalServiceFactory(factory) {
|
|
188
|
+
const f = factory;
|
|
189
|
+
if (f.$$type !== "@backstage/BackendFeature") {
|
|
190
|
+
throw new Error(`Invalid service factory, bad type '${f.$$type}'`);
|
|
2269
191
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
const urlReaderServiceFactory = backendPluginApi.createServiceFactory({
|
|
2273
|
-
service: backendPluginApi.coreServices.urlReader,
|
|
2274
|
-
deps: {
|
|
2275
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
2276
|
-
logger: backendPluginApi.coreServices.logger
|
|
2277
|
-
},
|
|
2278
|
-
async factory({ config, logger }) {
|
|
2279
|
-
return backendCommon.UrlReaders.default({
|
|
2280
|
-
config,
|
|
2281
|
-
logger
|
|
2282
|
-
});
|
|
192
|
+
if (f.version !== "v1") {
|
|
193
|
+
throw new Error(`Invalid service factory, bad version '${f.version}'`);
|
|
2283
194
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
function createCredentialsWithServicePrincipal(sub, token, accessRestrictions) {
|
|
2287
|
-
return {
|
|
2288
|
-
$$type: "@backstage/BackstageCredentials",
|
|
2289
|
-
version: "v1",
|
|
2290
|
-
token,
|
|
2291
|
-
principal: {
|
|
2292
|
-
type: "service",
|
|
2293
|
-
subject: sub,
|
|
2294
|
-
accessRestrictions
|
|
2295
|
-
}
|
|
2296
|
-
};
|
|
2297
|
-
}
|
|
2298
|
-
function createCredentialsWithUserPrincipal(sub, token, expiresAt) {
|
|
2299
|
-
return {
|
|
2300
|
-
$$type: "@backstage/BackstageCredentials",
|
|
2301
|
-
version: "v1",
|
|
2302
|
-
token,
|
|
2303
|
-
expiresAt,
|
|
2304
|
-
principal: {
|
|
2305
|
-
type: "user",
|
|
2306
|
-
userEntityRef: sub
|
|
2307
|
-
}
|
|
2308
|
-
};
|
|
2309
|
-
}
|
|
2310
|
-
function createCredentialsWithNonePrincipal() {
|
|
2311
|
-
return {
|
|
2312
|
-
$$type: "@backstage/BackstageCredentials",
|
|
2313
|
-
version: "v1",
|
|
2314
|
-
principal: {
|
|
2315
|
-
type: "none"
|
|
2316
|
-
}
|
|
2317
|
-
};
|
|
195
|
+
return f;
|
|
2318
196
|
}
|
|
2319
|
-
function
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
throw new Error(
|
|
2326
|
-
`Invalid credential version ${internalCredentials.version}`
|
|
2327
|
-
);
|
|
2328
|
-
}
|
|
2329
|
-
return internalCredentials;
|
|
197
|
+
function createPluginMetadataServiceFactory(pluginId) {
|
|
198
|
+
return backendPluginApi.createServiceFactory({
|
|
199
|
+
service: backendPluginApi.coreServices.pluginMetadata,
|
|
200
|
+
deps: {},
|
|
201
|
+
factory: async () => ({ getId: () => pluginId })
|
|
202
|
+
});
|
|
2330
203
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
this.pluginKeySource = pluginKeySource;
|
|
2341
|
-
}
|
|
2342
|
-
async authenticate(token, options) {
|
|
2343
|
-
const pluginResult = await this.pluginTokenHandler.verifyToken(token);
|
|
2344
|
-
if (pluginResult) {
|
|
2345
|
-
if (pluginResult.limitedUserToken) {
|
|
2346
|
-
const userResult2 = await this.userTokenHandler.verifyToken(
|
|
2347
|
-
pluginResult.limitedUserToken
|
|
2348
|
-
);
|
|
2349
|
-
if (!userResult2) {
|
|
2350
|
-
throw new errors.AuthenticationError(
|
|
2351
|
-
"Invalid user token in plugin token obo claim"
|
|
2352
|
-
);
|
|
2353
|
-
}
|
|
2354
|
-
return createCredentialsWithUserPrincipal(
|
|
2355
|
-
userResult2.userEntityRef,
|
|
2356
|
-
pluginResult.limitedUserToken,
|
|
2357
|
-
this.#getJwtExpiration(pluginResult.limitedUserToken)
|
|
204
|
+
class ServiceRegistry {
|
|
205
|
+
static create(factories) {
|
|
206
|
+
const factoryMap = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const factory of factories) {
|
|
208
|
+
if (factory.service.multiton) {
|
|
209
|
+
const existing = factoryMap.get(factory.service.id) ?? [];
|
|
210
|
+
factoryMap.set(
|
|
211
|
+
factory.service.id,
|
|
212
|
+
existing.concat(toInternalServiceFactory(factory))
|
|
2358
213
|
);
|
|
214
|
+
} else {
|
|
215
|
+
factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]);
|
|
2359
216
|
}
|
|
2360
|
-
return createCredentialsWithServicePrincipal(pluginResult.subject);
|
|
2361
|
-
}
|
|
2362
|
-
const userResult = await this.userTokenHandler.verifyToken(token);
|
|
2363
|
-
if (userResult) {
|
|
2364
|
-
if (!options?.allowLimitedAccess && this.userTokenHandler.isLimitedUserToken(token)) {
|
|
2365
|
-
throw new errors.AuthenticationError("Illegal limited user token");
|
|
2366
|
-
}
|
|
2367
|
-
return createCredentialsWithUserPrincipal(
|
|
2368
|
-
userResult.userEntityRef,
|
|
2369
|
-
token,
|
|
2370
|
-
this.#getJwtExpiration(token)
|
|
2371
|
-
);
|
|
2372
|
-
}
|
|
2373
|
-
const externalResult = await this.externalTokenHandler.verifyToken(token);
|
|
2374
|
-
if (externalResult) {
|
|
2375
|
-
return createCredentialsWithServicePrincipal(
|
|
2376
|
-
externalResult.subject,
|
|
2377
|
-
void 0,
|
|
2378
|
-
externalResult.accessRestrictions
|
|
2379
|
-
);
|
|
2380
|
-
}
|
|
2381
|
-
throw new errors.AuthenticationError("Illegal token");
|
|
2382
|
-
}
|
|
2383
|
-
isPrincipal(credentials, type) {
|
|
2384
|
-
const principal = credentials.principal;
|
|
2385
|
-
if (type === "unknown") {
|
|
2386
|
-
return true;
|
|
2387
|
-
}
|
|
2388
|
-
if (principal.type !== type) {
|
|
2389
|
-
return false;
|
|
2390
217
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
return createCredentialsWithNonePrincipal();
|
|
2395
|
-
}
|
|
2396
|
-
async getOwnServiceCredentials() {
|
|
2397
|
-
return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);
|
|
218
|
+
const registry = new ServiceRegistry(factoryMap);
|
|
219
|
+
registry.checkForCircularDeps();
|
|
220
|
+
return registry;
|
|
2398
221
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
if (targetSupportsNewAuth) {
|
|
2410
|
-
return this.pluginTokenHandler.issueToken({
|
|
2411
|
-
pluginId: this.pluginId,
|
|
2412
|
-
targetPluginId
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
return this.tokenManager.getToken().catch((error) => {
|
|
2416
|
-
throw new errors.ForwardedError(
|
|
2417
|
-
`Unable to generate legacy token for communication with the '${targetPluginId}' plugin. You will typically encounter this error when attempting to call a plugin that does not exist, or is deployed with an old version of Backstage`,
|
|
2418
|
-
error
|
|
2419
|
-
);
|
|
2420
|
-
});
|
|
2421
|
-
case "user": {
|
|
2422
|
-
const { token } = internalForward;
|
|
2423
|
-
if (!token) {
|
|
2424
|
-
throw new Error("User credentials is unexpectedly missing token");
|
|
2425
|
-
}
|
|
2426
|
-
if (targetSupportsNewAuth) {
|
|
2427
|
-
const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(
|
|
2428
|
-
token
|
|
2429
|
-
);
|
|
2430
|
-
return this.pluginTokenHandler.issueToken({
|
|
2431
|
-
pluginId: this.pluginId,
|
|
2432
|
-
targetPluginId,
|
|
2433
|
-
onBehalfOf
|
|
2434
|
-
});
|
|
2435
|
-
}
|
|
2436
|
-
if (this.userTokenHandler.isLimitedUserToken(token)) {
|
|
2437
|
-
throw new errors.AuthenticationError(
|
|
2438
|
-
`Unable to call '${targetPluginId}' plugin on behalf of user, because the target plugin does not support on-behalf-of tokens or the plugin doesn't exist`
|
|
2439
|
-
);
|
|
2440
|
-
}
|
|
2441
|
-
return { token };
|
|
2442
|
-
}
|
|
2443
|
-
default:
|
|
2444
|
-
throw new errors.AuthenticationError(
|
|
2445
|
-
`Refused to issue service token for credential type '${type}'`
|
|
2446
|
-
);
|
|
2447
|
-
}
|
|
222
|
+
#providedFactories;
|
|
223
|
+
#loadedDefaultFactories;
|
|
224
|
+
#implementations;
|
|
225
|
+
#rootServiceImplementations = /* @__PURE__ */ new Map();
|
|
226
|
+
#addedFactoryIds = /* @__PURE__ */ new Set();
|
|
227
|
+
#instantiatedFactories = /* @__PURE__ */ new Set();
|
|
228
|
+
constructor(factories) {
|
|
229
|
+
this.#providedFactories = factories;
|
|
230
|
+
this.#loadedDefaultFactories = /* @__PURE__ */ new Map();
|
|
231
|
+
this.#implementations = /* @__PURE__ */ new Map();
|
|
2448
232
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
);
|
|
233
|
+
#resolveFactory(ref, pluginId) {
|
|
234
|
+
if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
|
|
235
|
+
return Promise.resolve([
|
|
236
|
+
toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId))
|
|
237
|
+
]);
|
|
2455
238
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
return { keys: keys.map(({ key }) => key) };
|
|
2461
|
-
}
|
|
2462
|
-
#getJwtExpiration(token) {
|
|
2463
|
-
const { exp } = jose.decodeJwt(token);
|
|
2464
|
-
if (!exp) {
|
|
2465
|
-
throw new errors.AuthenticationError("User token is missing expiration");
|
|
239
|
+
let resolvedFactory = this.#providedFactories.get(ref.id);
|
|
240
|
+
const { __defaultFactory: defaultFactory } = ref;
|
|
241
|
+
if (!resolvedFactory && !defaultFactory) {
|
|
242
|
+
return void 0;
|
|
2466
243
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
function
|
|
2472
|
-
const configs = externalAccessEntryConfig.getOptionalConfigArray("accessRestrictions") ?? [];
|
|
2473
|
-
const result = /* @__PURE__ */ new Map();
|
|
2474
|
-
for (const config of configs) {
|
|
2475
|
-
const validKeys = ["plugin", "permission", "permissionAttribute"];
|
|
2476
|
-
for (const key of config.keys()) {
|
|
2477
|
-
if (!validKeys.includes(key)) {
|
|
2478
|
-
const valid = validKeys.map((k) => `'${k}'`).join(", ");
|
|
2479
|
-
throw new Error(
|
|
2480
|
-
`Invalid key '${key}' in 'accessRestrictions' config, expected one of ${valid}`
|
|
244
|
+
if (!resolvedFactory) {
|
|
245
|
+
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory);
|
|
246
|
+
if (!loadedFactory) {
|
|
247
|
+
loadedFactory = Promise.resolve().then(() => defaultFactory(ref)).then(
|
|
248
|
+
(f) => toInternalServiceFactory(typeof f === "function" ? f() : f)
|
|
2481
249
|
);
|
|
250
|
+
this.#loadedDefaultFactories.set(defaultFactory, loadedFactory);
|
|
2482
251
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
252
|
+
resolvedFactory = loadedFactory.then(
|
|
253
|
+
(factory) => [factory],
|
|
254
|
+
(error) => {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Failed to instantiate service '${ref.id}' because the default factory loader threw an error, ${errors.stringifyError(
|
|
257
|
+
error
|
|
258
|
+
)}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
2490
261
|
);
|
|
2491
262
|
}
|
|
2492
|
-
|
|
2493
|
-
...permissionNames ? { permissionNames } : {},
|
|
2494
|
-
...permissionAttributes ? { permissionAttributes } : {}
|
|
2495
|
-
});
|
|
2496
|
-
}
|
|
2497
|
-
return result.size ? result : void 0;
|
|
2498
|
-
}
|
|
2499
|
-
function readStringOrStringArrayFromConfig(root, key, validValues) {
|
|
2500
|
-
if (!root.has(key)) {
|
|
2501
|
-
return void 0;
|
|
2502
|
-
}
|
|
2503
|
-
const rawValues = Array.isArray(root.get(key)) ? root.getStringArray(key) : [root.getString(key)];
|
|
2504
|
-
const values = [
|
|
2505
|
-
...new Set(
|
|
2506
|
-
rawValues.map((v) => v.split(/[ ,]/)).flat().filter(Boolean)
|
|
2507
|
-
)
|
|
2508
|
-
];
|
|
2509
|
-
if (!values.length) {
|
|
2510
|
-
return void 0;
|
|
263
|
+
return Promise.resolve(resolvedFactory);
|
|
2511
264
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
if (
|
|
2515
|
-
|
|
2516
|
-
throw new Error(
|
|
2517
|
-
`Invalid value '${value}' at '${key}' in 'permissionAttributes' config, valid values are ${valid}`
|
|
2518
|
-
);
|
|
265
|
+
#checkForMissingDeps(factory, pluginId) {
|
|
266
|
+
const missingDeps = Object.values(factory.deps).filter((ref) => {
|
|
267
|
+
if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
|
|
268
|
+
return false;
|
|
2519
269
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
}
|
|
2530
|
-
function readPermissionAttributes(externalAccessEntryConfig) {
|
|
2531
|
-
const config = externalAccessEntryConfig.getOptionalConfig(
|
|
2532
|
-
"permissionAttribute"
|
|
2533
|
-
);
|
|
2534
|
-
if (!config) {
|
|
2535
|
-
return void 0;
|
|
2536
|
-
}
|
|
2537
|
-
const validKeys = ["action"];
|
|
2538
|
-
for (const key of config.keys()) {
|
|
2539
|
-
if (!validKeys.includes(key)) {
|
|
2540
|
-
const valid = validKeys.map((k) => `'${k}'`).join(", ");
|
|
270
|
+
if (this.#providedFactories.get(ref.id)) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (ref.multiton) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return !ref.__defaultFactory;
|
|
277
|
+
});
|
|
278
|
+
if (missingDeps.length) {
|
|
279
|
+
const missing = missingDeps.map((r) => `'${r.id}'`).join(", ");
|
|
2541
280
|
throw new Error(
|
|
2542
|
-
`
|
|
281
|
+
`Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`
|
|
2543
282
|
);
|
|
2544
283
|
}
|
|
2545
284
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
return Object.keys(result).length ? result : void 0;
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
class LegacyTokenHandler {
|
|
2559
|
-
#entries = new Array();
|
|
2560
|
-
add(config) {
|
|
2561
|
-
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2562
|
-
this.#doAdd(
|
|
2563
|
-
config.getString("options.secret"),
|
|
2564
|
-
config.getString("options.subject"),
|
|
2565
|
-
allAccessRestrictions
|
|
285
|
+
checkForCircularDeps() {
|
|
286
|
+
const graph = DependencyGraph.fromIterable(
|
|
287
|
+
Array.from(this.#providedFactories).map(([serviceId, factories]) => ({
|
|
288
|
+
value: serviceId,
|
|
289
|
+
provides: [serviceId],
|
|
290
|
+
consumes: factories.flatMap(
|
|
291
|
+
(factory) => Object.values(factory.deps).map((d) => d.id)
|
|
292
|
+
)
|
|
293
|
+
}))
|
|
2566
294
|
);
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
}
|
|
2572
|
-
#doAdd(secret, subject, allAccessRestrictions) {
|
|
2573
|
-
if (!secret.match(/^\S+$/)) {
|
|
2574
|
-
throw new Error("Illegal secret, must be a valid base64 string");
|
|
2575
|
-
} else if (!subject.match(/^\S+$/)) {
|
|
2576
|
-
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2577
|
-
}
|
|
2578
|
-
let key;
|
|
2579
|
-
try {
|
|
2580
|
-
key = jose.base64url.decode(secret);
|
|
2581
|
-
} catch {
|
|
2582
|
-
throw new Error("Illegal secret, must be a valid base64 string");
|
|
2583
|
-
}
|
|
2584
|
-
if (this.#entries.some((e) => e.key === key)) {
|
|
2585
|
-
throw new Error(
|
|
2586
|
-
"Legacy externalAccess token was declared more than once"
|
|
2587
|
-
);
|
|
2588
|
-
}
|
|
2589
|
-
this.#entries.push({
|
|
2590
|
-
key,
|
|
2591
|
-
result: {
|
|
2592
|
-
subject,
|
|
2593
|
-
allAccessRestrictions
|
|
2594
|
-
}
|
|
2595
|
-
});
|
|
2596
|
-
}
|
|
2597
|
-
async verifyToken(token) {
|
|
2598
|
-
try {
|
|
2599
|
-
const { alg } = jose.decodeProtectedHeader(token);
|
|
2600
|
-
if (alg !== "HS256") {
|
|
2601
|
-
return void 0;
|
|
2602
|
-
}
|
|
2603
|
-
const { sub, aud } = jose.decodeJwt(token);
|
|
2604
|
-
if (sub !== "backstage-server" || aud) {
|
|
2605
|
-
return void 0;
|
|
2606
|
-
}
|
|
2607
|
-
} catch (e) {
|
|
2608
|
-
return void 0;
|
|
2609
|
-
}
|
|
2610
|
-
for (const { key, result } of this.#entries) {
|
|
2611
|
-
try {
|
|
2612
|
-
await jose.jwtVerify(token, key);
|
|
2613
|
-
return result;
|
|
2614
|
-
} catch (e) {
|
|
2615
|
-
if (e.code !== "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") {
|
|
2616
|
-
throw e;
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
295
|
+
const circularDependencies = Array.from(graph.detectCircularDependencies());
|
|
296
|
+
if (circularDependencies.length) {
|
|
297
|
+
const cycles = circularDependencies.map((c) => c.map((id) => `'${id}'`).join(" -> ")).join("\n ");
|
|
298
|
+
throw new errors.ConflictError(`Circular dependencies detected:
|
|
299
|
+
${cycles}`);
|
|
2619
300
|
}
|
|
2620
|
-
return void 0;
|
|
2621
301
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
class StaticTokenHandler {
|
|
2626
|
-
#entries = /* @__PURE__ */ new Map();
|
|
2627
|
-
add(config) {
|
|
2628
|
-
const token = config.getString("options.token");
|
|
2629
|
-
const subject = config.getString("options.subject");
|
|
2630
|
-
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2631
|
-
if (!token.match(/^\S+$/)) {
|
|
2632
|
-
throw new Error("Illegal token, must be a set of non-space characters");
|
|
2633
|
-
} else if (token.length < MIN_TOKEN_LENGTH) {
|
|
2634
|
-
throw new Error(
|
|
2635
|
-
`Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`
|
|
2636
|
-
);
|
|
2637
|
-
} else if (!subject.match(/^\S+$/)) {
|
|
2638
|
-
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2639
|
-
} else if (this.#entries.has(token)) {
|
|
302
|
+
add(factory) {
|
|
303
|
+
const factoryId = factory.service.id;
|
|
304
|
+
if (factoryId === backendPluginApi.coreServices.pluginMetadata.id) {
|
|
2640
305
|
throw new Error(
|
|
2641
|
-
|
|
306
|
+
`The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
|
|
2642
307
|
);
|
|
2643
308
|
}
|
|
2644
|
-
this.#
|
|
2645
|
-
}
|
|
2646
|
-
async verifyToken(token) {
|
|
2647
|
-
return this.#entries.get(token);
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
class JWKSHandler {
|
|
2652
|
-
#entries = [];
|
|
2653
|
-
add(config) {
|
|
2654
|
-
if (!config.getString("options.url").match(/^\S+$/)) {
|
|
309
|
+
if (this.#instantiatedFactories.has(factoryId)) {
|
|
2655
310
|
throw new Error(
|
|
2656
|
-
|
|
311
|
+
`Unable to set service factory with id ${factoryId}, service has already been instantiated`
|
|
2657
312
|
);
|
|
2658
313
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
const audiences = readStringOrStringArrayFromConfig(
|
|
2665
|
-
config,
|
|
2666
|
-
"options.audience"
|
|
2667
|
-
);
|
|
2668
|
-
const subjectPrefix = config.getOptionalString("options.subjectPrefix");
|
|
2669
|
-
const url = new URL(config.getString("options.url"));
|
|
2670
|
-
const jwks = jose.createRemoteJWKSet(url);
|
|
2671
|
-
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2672
|
-
this.#entries.push({
|
|
2673
|
-
algorithms,
|
|
2674
|
-
audiences,
|
|
2675
|
-
issuers,
|
|
2676
|
-
jwks,
|
|
2677
|
-
subjectPrefix,
|
|
2678
|
-
url,
|
|
2679
|
-
allAccessRestrictions
|
|
2680
|
-
});
|
|
2681
|
-
}
|
|
2682
|
-
async verifyToken(token) {
|
|
2683
|
-
for (const entry of this.#entries) {
|
|
2684
|
-
try {
|
|
2685
|
-
const {
|
|
2686
|
-
payload: { sub }
|
|
2687
|
-
} = await jose.jwtVerify(token, entry.jwks, {
|
|
2688
|
-
algorithms: entry.algorithms,
|
|
2689
|
-
issuer: entry.issuers,
|
|
2690
|
-
audience: entry.audiences
|
|
2691
|
-
});
|
|
2692
|
-
if (sub) {
|
|
2693
|
-
const prefix = entry.subjectPrefix ? `external:${entry.subjectPrefix}:` : "external:";
|
|
2694
|
-
return {
|
|
2695
|
-
subject: `${prefix}${sub}`,
|
|
2696
|
-
allAccessRestrictions: entry.allAccessRestrictions
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
} catch {
|
|
2700
|
-
continue;
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
return void 0;
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
const NEW_CONFIG_KEY = "backend.auth.externalAccess";
|
|
2708
|
-
const OLD_CONFIG_KEY = "backend.auth.keys";
|
|
2709
|
-
let loggedDeprecationWarning = false;
|
|
2710
|
-
class ExternalTokenHandler {
|
|
2711
|
-
constructor(ownPluginId, handlers) {
|
|
2712
|
-
this.ownPluginId = ownPluginId;
|
|
2713
|
-
this.handlers = handlers;
|
|
2714
|
-
}
|
|
2715
|
-
static create(options) {
|
|
2716
|
-
const { ownPluginId, config, logger } = options;
|
|
2717
|
-
const staticHandler = new StaticTokenHandler();
|
|
2718
|
-
const legacyHandler = new LegacyTokenHandler();
|
|
2719
|
-
const jwksHandler = new JWKSHandler();
|
|
2720
|
-
const handlers = {
|
|
2721
|
-
static: staticHandler,
|
|
2722
|
-
legacy: legacyHandler,
|
|
2723
|
-
jwks: jwksHandler
|
|
2724
|
-
};
|
|
2725
|
-
const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? [];
|
|
2726
|
-
for (const handlerConfig of handlerConfigs) {
|
|
2727
|
-
const type = handlerConfig.getString("type");
|
|
2728
|
-
const handler = handlers[type];
|
|
2729
|
-
if (!handler) {
|
|
2730
|
-
const valid = Object.keys(handlers).map((k) => `'${k}'`).join(", ");
|
|
314
|
+
if (factory.service.multiton) {
|
|
315
|
+
const newFactories = (this.#providedFactories.get(factoryId) ?? []).concat(toInternalServiceFactory(factory));
|
|
316
|
+
this.#providedFactories.set(factoryId, newFactories);
|
|
317
|
+
} else {
|
|
318
|
+
if (this.#addedFactoryIds.has(factoryId)) {
|
|
2731
319
|
throw new Error(
|
|
2732
|
-
`
|
|
320
|
+
`Duplicate service implementations provided for ${factoryId}`
|
|
2733
321
|
);
|
|
2734
322
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
loggedDeprecationWarning = true;
|
|
2740
|
-
logger.warn(
|
|
2741
|
-
`DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`
|
|
2742
|
-
);
|
|
2743
|
-
}
|
|
2744
|
-
for (const handlerConfig of legacyConfigs) {
|
|
2745
|
-
legacyHandler.addOld(handlerConfig);
|
|
323
|
+
this.#addedFactoryIds.add(factoryId);
|
|
324
|
+
this.#providedFactories.set(factoryId, [
|
|
325
|
+
toInternalServiceFactory(factory)
|
|
326
|
+
]);
|
|
2746
327
|
}
|
|
2747
|
-
return new ExternalTokenHandler(ownPluginId, Object.values(handlers));
|
|
2748
328
|
}
|
|
2749
|
-
async
|
|
2750
|
-
for (const
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
if (
|
|
2755
|
-
|
|
2756
|
-
this.ownPluginId
|
|
2757
|
-
);
|
|
2758
|
-
if (!accessRestrictions) {
|
|
2759
|
-
const valid = [...allAccessRestrictions.keys()].map((k) => `'${k}'`).join(", ");
|
|
2760
|
-
throw new errors.NotAllowedError(
|
|
2761
|
-
`This token's access is restricted to plugin(s) ${valid}`
|
|
2762
|
-
);
|
|
2763
|
-
}
|
|
2764
|
-
return {
|
|
2765
|
-
...rest,
|
|
2766
|
-
accessRestrictions
|
|
2767
|
-
};
|
|
329
|
+
async initializeEagerServicesWithScope(scope, pluginId = "root") {
|
|
330
|
+
for (const [factory] of this.#providedFactories.values()) {
|
|
331
|
+
if (factory.service.scope === scope) {
|
|
332
|
+
if (scope === "root" && factory.initialization !== "lazy") {
|
|
333
|
+
await this.get(factory.service, pluginId);
|
|
334
|
+
} else if (scope === "plugin" && factory.initialization === "always") {
|
|
335
|
+
await this.get(factory.service, pluginId);
|
|
2768
336
|
}
|
|
2769
|
-
return rest;
|
|
2770
337
|
}
|
|
2771
338
|
}
|
|
2772
|
-
return void 0;
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
const CLOCK_MARGIN_S = 10;
|
|
2777
|
-
class JwksClient {
|
|
2778
|
-
constructor(getEndpoint) {
|
|
2779
|
-
this.getEndpoint = getEndpoint;
|
|
2780
|
-
}
|
|
2781
|
-
#keyStore;
|
|
2782
|
-
#keyStoreUpdated = 0;
|
|
2783
|
-
get getKey() {
|
|
2784
|
-
if (!this.#keyStore) {
|
|
2785
|
-
throw new errors.AuthenticationError(
|
|
2786
|
-
"refreshKeyStore must be called before jwksClient.getKey"
|
|
2787
|
-
);
|
|
2788
|
-
}
|
|
2789
|
-
return this.#keyStore;
|
|
2790
339
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const header = await jose.decodeProtectedHeader(rawJwtToken);
|
|
2797
|
-
let keyStoreHasKey;
|
|
2798
|
-
try {
|
|
2799
|
-
if (this.#keyStore) {
|
|
2800
|
-
const [_, rawPayload, rawSignature] = rawJwtToken.split(".");
|
|
2801
|
-
keyStoreHasKey = await this.#keyStore(header, {
|
|
2802
|
-
payload: rawPayload,
|
|
2803
|
-
signature: rawSignature
|
|
2804
|
-
});
|
|
2805
|
-
}
|
|
2806
|
-
} catch (error) {
|
|
2807
|
-
keyStoreHasKey = false;
|
|
2808
|
-
}
|
|
2809
|
-
const issuedAfterLastRefresh = payload?.iat && payload.iat > this.#keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2810
|
-
if (!this.#keyStore || !keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2811
|
-
const endpoint = await this.getEndpoint();
|
|
2812
|
-
this.#keyStore = jose.createRemoteJWKSet(endpoint);
|
|
2813
|
-
this.#keyStoreUpdated = Date.now() / 1e3;
|
|
340
|
+
get(ref, pluginId) {
|
|
341
|
+
this.#instantiatedFactories.add(ref.id);
|
|
342
|
+
const resolvedFactory = this.#resolveFactory(ref, pluginId);
|
|
343
|
+
if (!resolvedFactory) {
|
|
344
|
+
return ref.multiton ? Promise.resolve([]) : void 0;
|
|
2814
345
|
}
|
|
346
|
+
return resolvedFactory.then((factories) => {
|
|
347
|
+
return Promise.all(
|
|
348
|
+
factories.map((factory) => {
|
|
349
|
+
if (factory.service.scope === "root") {
|
|
350
|
+
let existing = this.#rootServiceImplementations.get(factory);
|
|
351
|
+
if (!existing) {
|
|
352
|
+
this.#checkForMissingDeps(factory, pluginId);
|
|
353
|
+
const rootDeps = new Array();
|
|
354
|
+
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
|
355
|
+
if (serviceRef.scope !== "root") {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const target = this.get(serviceRef, pluginId);
|
|
361
|
+
rootDeps.push(target.then((impl) => [name, impl]));
|
|
362
|
+
}
|
|
363
|
+
existing = Promise.all(rootDeps).then(
|
|
364
|
+
(entries) => factory.factory(Object.fromEntries(entries), void 0)
|
|
365
|
+
);
|
|
366
|
+
this.#rootServiceImplementations.set(factory, existing);
|
|
367
|
+
}
|
|
368
|
+
return existing;
|
|
369
|
+
}
|
|
370
|
+
let implementation = this.#implementations.get(factory);
|
|
371
|
+
if (!implementation) {
|
|
372
|
+
this.#checkForMissingDeps(factory, pluginId);
|
|
373
|
+
const rootDeps = new Array();
|
|
374
|
+
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
|
375
|
+
if (serviceRef.scope === "root") {
|
|
376
|
+
const target = this.get(serviceRef, pluginId);
|
|
377
|
+
rootDeps.push(target.then((impl) => [name, impl]));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
implementation = {
|
|
381
|
+
context: Promise.all(rootDeps).then(
|
|
382
|
+
(entries) => factory.createRootContext?.(Object.fromEntries(entries))
|
|
383
|
+
).catch((error) => {
|
|
384
|
+
const cause = errors.stringifyError(error);
|
|
385
|
+
throw new Error(
|
|
386
|
+
`Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`
|
|
387
|
+
);
|
|
388
|
+
}),
|
|
389
|
+
byPlugin: /* @__PURE__ */ new Map()
|
|
390
|
+
};
|
|
391
|
+
this.#implementations.set(factory, implementation);
|
|
392
|
+
}
|
|
393
|
+
let result = implementation.byPlugin.get(pluginId);
|
|
394
|
+
if (!result) {
|
|
395
|
+
const allDeps = new Array();
|
|
396
|
+
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
|
397
|
+
const target = this.get(serviceRef, pluginId);
|
|
398
|
+
allDeps.push(target.then((impl) => [name, impl]));
|
|
399
|
+
}
|
|
400
|
+
result = implementation.context.then(
|
|
401
|
+
(context) => Promise.all(allDeps).then(
|
|
402
|
+
(entries) => factory.factory(Object.fromEntries(entries), context)
|
|
403
|
+
)
|
|
404
|
+
).catch((error) => {
|
|
405
|
+
const cause = errors.stringifyError(error);
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
implementation.byPlugin.set(pluginId, result);
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
}).then((results) => ref.multiton ? results : results[0]);
|
|
2815
416
|
}
|
|
2816
417
|
}
|
|
2817
418
|
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
targetPluginInflightChecks = /* @__PURE__ */ new Map();
|
|
2833
|
-
static create(options) {
|
|
2834
|
-
return new PluginTokenHandler(
|
|
2835
|
-
options.logger,
|
|
2836
|
-
options.ownPluginId,
|
|
2837
|
-
options.keySource,
|
|
2838
|
-
options.algorithm ?? "ES256",
|
|
2839
|
-
Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3),
|
|
2840
|
-
options.discovery
|
|
2841
|
-
);
|
|
2842
|
-
}
|
|
2843
|
-
async verifyToken(token) {
|
|
2844
|
-
try {
|
|
2845
|
-
const { typ } = jose.decodeProtectedHeader(token);
|
|
2846
|
-
if (typ !== pluginAuthNode.tokenTypes.plugin.typParam) {
|
|
2847
|
-
return void 0;
|
|
2848
|
-
}
|
|
2849
|
-
} catch {
|
|
2850
|
-
return void 0;
|
|
2851
|
-
}
|
|
2852
|
-
const pluginId = String(jose.decodeJwt(token).sub);
|
|
2853
|
-
if (!pluginId) {
|
|
2854
|
-
throw new errors.AuthenticationError("Invalid plugin token: missing subject");
|
|
2855
|
-
}
|
|
2856
|
-
if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
2857
|
-
throw new errors.AuthenticationError(
|
|
2858
|
-
"Invalid plugin token: forbidden subject format"
|
|
2859
|
-
);
|
|
2860
|
-
}
|
|
2861
|
-
const jwksClient = await this.getJwksClient(pluginId);
|
|
2862
|
-
await jwksClient.refreshKeyStore(token);
|
|
2863
|
-
const { payload } = await jose.jwtVerify(
|
|
2864
|
-
token,
|
|
2865
|
-
jwksClient.getKey,
|
|
2866
|
-
{
|
|
2867
|
-
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
2868
|
-
audience: this.ownPluginId,
|
|
2869
|
-
requiredClaims: ["iat", "exp", "sub", "aud"]
|
|
2870
|
-
}
|
|
2871
|
-
).catch((e) => {
|
|
2872
|
-
throw new errors.AuthenticationError("Invalid plugin token", e);
|
|
2873
|
-
});
|
|
2874
|
-
return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo };
|
|
2875
|
-
}
|
|
2876
|
-
async issueToken(options) {
|
|
2877
|
-
const { pluginId, targetPluginId, onBehalfOf } = options;
|
|
2878
|
-
const key = await this.keySource.getPrivateSigningKey();
|
|
2879
|
-
const sub = pluginId;
|
|
2880
|
-
const aud = targetPluginId;
|
|
2881
|
-
const iat = Math.floor(Date.now() / SECONDS_IN_MS$2);
|
|
2882
|
-
const ourExp = iat + this.keyDurationSeconds;
|
|
2883
|
-
const exp = onBehalfOf ? Math.min(
|
|
2884
|
-
ourExp,
|
|
2885
|
-
Math.floor(onBehalfOf.expiresAt.getTime() / SECONDS_IN_MS$2)
|
|
2886
|
-
) : ourExp;
|
|
2887
|
-
const claims = { sub, aud, iat, exp, obo: onBehalfOf?.token };
|
|
2888
|
-
const token = await new jose.SignJWT(claims).setProtectedHeader({
|
|
2889
|
-
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
2890
|
-
alg: this.algorithm,
|
|
2891
|
-
kid: key.kid
|
|
2892
|
-
}).setAudience(aud).setSubject(sub).setIssuedAt(iat).setExpirationTime(exp).sign(await jose.importJWK(key));
|
|
2893
|
-
return { token };
|
|
2894
|
-
}
|
|
2895
|
-
async isTargetPluginSupported(targetPluginId) {
|
|
2896
|
-
if (this.supportedTargetPlugins.has(targetPluginId)) {
|
|
2897
|
-
return true;
|
|
419
|
+
const LOGGER_INTERVAL_MAX = 6e4;
|
|
420
|
+
function joinIds(ids) {
|
|
421
|
+
return [...ids].map((id) => `'${id}'`).join(", ");
|
|
422
|
+
}
|
|
423
|
+
function createInitializationLogger(pluginIds, rootLogger) {
|
|
424
|
+
const logger = rootLogger?.child({ type: "initialization" });
|
|
425
|
+
const starting = new Set(pluginIds);
|
|
426
|
+
const started = /* @__PURE__ */ new Set();
|
|
427
|
+
logger?.info(`Plugin initialization started: ${joinIds(pluginIds)}`);
|
|
428
|
+
const getInitStatus = () => {
|
|
429
|
+
let status = "";
|
|
430
|
+
if (started.size > 0) {
|
|
431
|
+
status = `, newly initialized: ${joinIds(started)}`;
|
|
432
|
+
started.clear();
|
|
2898
433
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
return inFlight;
|
|
434
|
+
if (starting.size > 0) {
|
|
435
|
+
status += `, still initializing: ${joinIds(starting)}`;
|
|
2902
436
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
this.targetPluginInflightChecks.delete(targetPluginId);
|
|
437
|
+
return status;
|
|
438
|
+
};
|
|
439
|
+
let interval = 1e3;
|
|
440
|
+
let prevInterval = 0;
|
|
441
|
+
let timeout;
|
|
442
|
+
const onTimeout = () => {
|
|
443
|
+
logger?.info(`Plugin initialization in progress${getInitStatus()}`);
|
|
444
|
+
const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX);
|
|
445
|
+
prevInterval = interval;
|
|
446
|
+
interval = nextInterval;
|
|
447
|
+
timeout = setTimeout(onTimeout, nextInterval);
|
|
448
|
+
};
|
|
449
|
+
timeout = setTimeout(onTimeout, interval);
|
|
450
|
+
return {
|
|
451
|
+
onPluginStarted(pluginId) {
|
|
452
|
+
starting.delete(pluginId);
|
|
453
|
+
started.add(pluginId);
|
|
454
|
+
},
|
|
455
|
+
onAllStarted() {
|
|
456
|
+
logger?.info(`Plugin initialization complete${getInitStatus()}`);
|
|
457
|
+
if (timeout) {
|
|
458
|
+
clearTimeout(timeout);
|
|
459
|
+
timeout = void 0;
|
|
2927
460
|
}
|
|
2928
|
-
};
|
|
2929
|
-
const check = doCheck();
|
|
2930
|
-
this.targetPluginInflightChecks.set(targetPluginId, check);
|
|
2931
|
-
return check;
|
|
2932
|
-
}
|
|
2933
|
-
async getJwksClient(pluginId) {
|
|
2934
|
-
const client = this.jwksMap.get(pluginId);
|
|
2935
|
-
if (client) {
|
|
2936
|
-
return client;
|
|
2937
|
-
}
|
|
2938
|
-
if (!await this.isTargetPluginSupported(pluginId)) {
|
|
2939
|
-
throw new errors.AuthenticationError(
|
|
2940
|
-
`Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint`
|
|
2941
|
-
);
|
|
2942
461
|
}
|
|
2943
|
-
|
|
2944
|
-
return new URL(
|
|
2945
|
-
`${await this.discovery.getBaseUrl(
|
|
2946
|
-
pluginId
|
|
2947
|
-
)}/.backstage/auth/v1/jwks.json`
|
|
2948
|
-
);
|
|
2949
|
-
});
|
|
2950
|
-
this.jwksMap.set(pluginId, newClient);
|
|
2951
|
-
return newClient;
|
|
2952
|
-
}
|
|
462
|
+
};
|
|
2953
463
|
}
|
|
2954
464
|
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
);
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
tableName: MIGRATIONS_TABLE
|
|
2965
|
-
});
|
|
2966
|
-
}
|
|
2967
|
-
class DatabaseKeyStore {
|
|
2968
|
-
constructor(client, logger) {
|
|
2969
|
-
this.client = client;
|
|
2970
|
-
this.logger = logger;
|
|
2971
|
-
}
|
|
2972
|
-
static async create(options) {
|
|
2973
|
-
const { database, logger } = options;
|
|
2974
|
-
const client = await database.getClient();
|
|
2975
|
-
if (!database.migrations?.skip) {
|
|
2976
|
-
await applyDatabaseMigrations(client);
|
|
2977
|
-
}
|
|
2978
|
-
return new DatabaseKeyStore(client, logger);
|
|
2979
|
-
}
|
|
2980
|
-
async addKey(options) {
|
|
2981
|
-
await this.client(TABLE).insert({
|
|
2982
|
-
id: options.key.kid,
|
|
2983
|
-
key: JSON.stringify(options.key),
|
|
2984
|
-
expires_at: options.expiresAt.toISOString()
|
|
2985
|
-
});
|
|
465
|
+
class BackendInitializer {
|
|
466
|
+
#startPromise;
|
|
467
|
+
#registrations = new Array();
|
|
468
|
+
#extensionPoints = /* @__PURE__ */ new Map();
|
|
469
|
+
#serviceRegistry;
|
|
470
|
+
#registeredFeatures = new Array();
|
|
471
|
+
#registeredFeatureLoaders = new Array();
|
|
472
|
+
constructor(defaultApiFactories) {
|
|
473
|
+
this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]);
|
|
2986
474
|
}
|
|
2987
|
-
async
|
|
2988
|
-
const
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
475
|
+
async #getInitDeps(deps, pluginId, moduleId) {
|
|
476
|
+
const result = /* @__PURE__ */ new Map();
|
|
477
|
+
const missingRefs = /* @__PURE__ */ new Set();
|
|
478
|
+
for (const [name, ref] of Object.entries(deps)) {
|
|
479
|
+
const ep = this.#extensionPoints.get(ref.id);
|
|
480
|
+
if (ep) {
|
|
481
|
+
if (ep.pluginId !== pluginId) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`Illegal dependency: Module '${moduleId}' for plugin '${pluginId}' attempted to depend on extension point '${ref.id}' for plugin '${ep.pluginId}'. Extension points can only be used within their plugin's scope.`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
result.set(name, ep.impl);
|
|
2999
487
|
} else {
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
if (expiredKeys.length > 0) {
|
|
3004
|
-
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
3005
|
-
this.logger.info(
|
|
3006
|
-
`Removing expired plugin service keys, '${kids.join("', '")}'`
|
|
3007
|
-
);
|
|
3008
|
-
this.client(TABLE).delete().whereIn("id", kids).catch((error) => {
|
|
3009
|
-
this.logger.error(
|
|
3010
|
-
"Failed to remove expired plugin service keys",
|
|
3011
|
-
error
|
|
488
|
+
const impl = await this.#serviceRegistry.get(
|
|
489
|
+
ref,
|
|
490
|
+
pluginId
|
|
3012
491
|
);
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
const SECONDS_IN_MS$1 = 1e3;
|
|
3020
|
-
const KEY_EXPIRATION_MARGIN_FACTOR = 3;
|
|
3021
|
-
class DatabasePluginKeySource {
|
|
3022
|
-
constructor(keyStore, logger, keyDurationSeconds, algorithm) {
|
|
3023
|
-
this.keyStore = keyStore;
|
|
3024
|
-
this.logger = logger;
|
|
3025
|
-
this.keyDurationSeconds = keyDurationSeconds;
|
|
3026
|
-
this.algorithm = algorithm;
|
|
3027
|
-
}
|
|
3028
|
-
privateKeyPromise;
|
|
3029
|
-
keyExpiry;
|
|
3030
|
-
static async create(options) {
|
|
3031
|
-
const keyStore = await DatabaseKeyStore.create({
|
|
3032
|
-
database: options.database,
|
|
3033
|
-
logger: options.logger
|
|
3034
|
-
});
|
|
3035
|
-
return new DatabasePluginKeySource(
|
|
3036
|
-
keyStore,
|
|
3037
|
-
options.logger,
|
|
3038
|
-
Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3),
|
|
3039
|
-
options.algorithm ?? "ES256"
|
|
3040
|
-
);
|
|
3041
|
-
}
|
|
3042
|
-
async getPrivateSigningKey() {
|
|
3043
|
-
if (this.privateKeyPromise) {
|
|
3044
|
-
if (this.keyExpiry && this.keyExpiry.getTime() > Date.now()) {
|
|
3045
|
-
return this.privateKeyPromise;
|
|
492
|
+
if (impl) {
|
|
493
|
+
result.set(name, impl);
|
|
494
|
+
} else {
|
|
495
|
+
missingRefs.add(ref);
|
|
496
|
+
}
|
|
3046
497
|
}
|
|
3047
|
-
this.logger.info(`Signing key has expired, generating new key`);
|
|
3048
|
-
delete this.privateKeyPromise;
|
|
3049
498
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
const key = await jose.generateKeyPair(this.algorithm);
|
|
3056
|
-
const publicKey = await jose.exportJWK(key.publicKey);
|
|
3057
|
-
const privateKey = await jose.exportJWK(key.privateKey);
|
|
3058
|
-
publicKey.kid = privateKey.kid = kid;
|
|
3059
|
-
publicKey.alg = privateKey.alg = this.algorithm;
|
|
3060
|
-
this.logger.info(`Created new signing key ${kid}`);
|
|
3061
|
-
await this.keyStore.addKey({
|
|
3062
|
-
id: kid,
|
|
3063
|
-
key: publicKey,
|
|
3064
|
-
expiresAt: new Date(
|
|
3065
|
-
Date.now() + this.keyDurationSeconds * SECONDS_IN_MS$1 * KEY_EXPIRATION_MARGIN_FACTOR
|
|
3066
|
-
)
|
|
3067
|
-
});
|
|
3068
|
-
return privateKey;
|
|
3069
|
-
})();
|
|
3070
|
-
this.privateKeyPromise = promise;
|
|
3071
|
-
try {
|
|
3072
|
-
await promise;
|
|
3073
|
-
} catch (error) {
|
|
3074
|
-
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
3075
|
-
delete this.keyExpiry;
|
|
3076
|
-
delete this.privateKeyPromise;
|
|
499
|
+
if (missingRefs.size > 0) {
|
|
500
|
+
const missing = Array.from(missingRefs).join(", ");
|
|
501
|
+
throw new Error(
|
|
502
|
+
`No extension point or service available for the following ref(s): ${missing}`
|
|
503
|
+
);
|
|
3077
504
|
}
|
|
3078
|
-
return
|
|
3079
|
-
}
|
|
3080
|
-
listKeys() {
|
|
3081
|
-
return this.keyStore.listKeys();
|
|
505
|
+
return Object.fromEntries(result);
|
|
3082
506
|
}
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
constructor(keyPairs, keyDurationSeconds) {
|
|
3089
|
-
this.keyPairs = keyPairs;
|
|
3090
|
-
this.keyDurationSeconds = keyDurationSeconds;
|
|
507
|
+
add(feature) {
|
|
508
|
+
if (this.#startPromise) {
|
|
509
|
+
throw new Error("feature can not be added after the backend has started");
|
|
510
|
+
}
|
|
511
|
+
this.#registeredFeatures.push(Promise.resolve(feature));
|
|
3091
512
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
return staticKeyConfig;
|
|
3101
|
-
});
|
|
3102
|
-
const keyPairs = await Promise.all(
|
|
3103
|
-
keyConfigs.map(async (k) => await this.loadKeyPair(k))
|
|
3104
|
-
);
|
|
3105
|
-
if (keyPairs.length < 1) {
|
|
3106
|
-
throw new Error(
|
|
3107
|
-
"At least one key pair must be provided in static.keys, when the static key store type is used"
|
|
3108
|
-
);
|
|
3109
|
-
} else if (!keyPairs[0].privateKey) {
|
|
513
|
+
#addFeature(feature) {
|
|
514
|
+
if (isServiceFactory(feature)) {
|
|
515
|
+
this.#serviceRegistry.add(feature);
|
|
516
|
+
} else if (isBackendFeatureLoader(feature)) {
|
|
517
|
+
this.#registeredFeatureLoaders.push(feature);
|
|
518
|
+
} else if (isBackendRegistrations(feature)) {
|
|
519
|
+
this.#registrations.push(feature);
|
|
520
|
+
} else {
|
|
3110
521
|
throw new Error(
|
|
3111
|
-
|
|
522
|
+
`Failed to add feature, invalid feature ${JSON.stringify(feature)}`
|
|
3112
523
|
);
|
|
3113
524
|
}
|
|
3114
|
-
return new StaticConfigPluginKeySource(
|
|
3115
|
-
keyPairs,
|
|
3116
|
-
types.durationToMilliseconds(options.keyDuration) / SECONDS_IN_MS
|
|
3117
|
-
);
|
|
3118
|
-
}
|
|
3119
|
-
async getPrivateSigningKey() {
|
|
3120
|
-
return this.keyPairs[0].privateKey;
|
|
3121
|
-
}
|
|
3122
|
-
async listKeys() {
|
|
3123
|
-
const keys = this.keyPairs.map((k) => this.keyPairToStoredKey(k));
|
|
3124
|
-
return { keys };
|
|
3125
|
-
}
|
|
3126
|
-
static async loadKeyPair(options) {
|
|
3127
|
-
const algorithm = options.algorithm;
|
|
3128
|
-
const keyId = options.keyId;
|
|
3129
|
-
const publicKey = await this.loadPublicKeyFromFile(
|
|
3130
|
-
options.publicKeyFile,
|
|
3131
|
-
keyId,
|
|
3132
|
-
algorithm
|
|
3133
|
-
);
|
|
3134
|
-
const privateKey = options.privateKeyFile ? await this.loadPrivateKeyFromFile(
|
|
3135
|
-
options.privateKeyFile,
|
|
3136
|
-
keyId,
|
|
3137
|
-
algorithm
|
|
3138
|
-
) : void 0;
|
|
3139
|
-
return { publicKey, privateKey, keyId };
|
|
3140
|
-
}
|
|
3141
|
-
static async loadPublicKeyFromFile(path, keyId, algorithm) {
|
|
3142
|
-
return this.loadKeyFromFile(path, keyId, algorithm, jose.importSPKI);
|
|
3143
|
-
}
|
|
3144
|
-
static async loadPrivateKeyFromFile(path, keyId, algorithm) {
|
|
3145
|
-
return this.loadKeyFromFile(path, keyId, algorithm, jose.importPKCS8);
|
|
3146
|
-
}
|
|
3147
|
-
static async loadKeyFromFile(path, keyId, algorithm, importer) {
|
|
3148
|
-
const content = await fs$1.promises.readFile(path, { encoding: "utf8", flag: "r" });
|
|
3149
|
-
const key = await importer(content, algorithm);
|
|
3150
|
-
const jwk = await jose.exportJWK(key);
|
|
3151
|
-
jwk.kid = keyId;
|
|
3152
|
-
jwk.alg = algorithm;
|
|
3153
|
-
return jwk;
|
|
3154
525
|
}
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
526
|
+
async start() {
|
|
527
|
+
if (this.#startPromise) {
|
|
528
|
+
throw new Error("Backend has already started");
|
|
529
|
+
}
|
|
530
|
+
const exitHandler = async () => {
|
|
531
|
+
process.removeListener("SIGTERM", exitHandler);
|
|
532
|
+
process.removeListener("SIGINT", exitHandler);
|
|
533
|
+
process.removeListener("beforeExit", exitHandler);
|
|
534
|
+
try {
|
|
535
|
+
await this.stop();
|
|
536
|
+
process.exit(0);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error(error);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
3164
541
|
};
|
|
542
|
+
process.addListener("SIGTERM", exitHandler);
|
|
543
|
+
process.addListener("SIGINT", exitHandler);
|
|
544
|
+
process.addListener("beforeExit", exitHandler);
|
|
545
|
+
this.#startPromise = this.#doStart();
|
|
546
|
+
await this.#startPromise;
|
|
3165
547
|
}
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
const
|
|
3169
|
-
|
|
3170
|
-
const keyStoreConfig = options.config.getOptionalConfig(CONFIG_ROOT_KEY);
|
|
3171
|
-
const type = keyStoreConfig?.getOptionalString("type") ?? "database";
|
|
3172
|
-
if (!keyStoreConfig || type === "database") {
|
|
3173
|
-
return DatabasePluginKeySource.create({
|
|
3174
|
-
database: options.database,
|
|
3175
|
-
logger: options.logger,
|
|
3176
|
-
keyDuration: options.keyDuration,
|
|
3177
|
-
algorithm: options.algorithm
|
|
3178
|
-
});
|
|
3179
|
-
} else if (type === "static") {
|
|
3180
|
-
return StaticConfigPluginKeySource.create({
|
|
3181
|
-
sourceConfig: keyStoreConfig,
|
|
3182
|
-
keyDuration: options.keyDuration
|
|
3183
|
-
});
|
|
3184
|
-
}
|
|
3185
|
-
throw new Error(
|
|
3186
|
-
`Unsupported config value ${CONFIG_ROOT_KEY}.type '${type}'; expected one of 'database', 'static'`
|
|
3187
|
-
);
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
class UserTokenHandler {
|
|
3191
|
-
constructor(jwksClient) {
|
|
3192
|
-
this.jwksClient = jwksClient;
|
|
3193
|
-
}
|
|
3194
|
-
static create(options) {
|
|
3195
|
-
const jwksClient = new JwksClient(async () => {
|
|
3196
|
-
const url = await options.discovery.getBaseUrl("auth");
|
|
3197
|
-
return new URL(`${url}/.well-known/jwks.json`);
|
|
3198
|
-
});
|
|
3199
|
-
return new UserTokenHandler(jwksClient);
|
|
3200
|
-
}
|
|
3201
|
-
async verifyToken(token) {
|
|
3202
|
-
const verifyOpts = this.#getTokenVerificationOptions(token);
|
|
3203
|
-
if (!verifyOpts) {
|
|
3204
|
-
return void 0;
|
|
3205
|
-
}
|
|
3206
|
-
await this.jwksClient.refreshKeyStore(token);
|
|
3207
|
-
const { payload } = await jose.jwtVerify(
|
|
3208
|
-
token,
|
|
3209
|
-
this.jwksClient.getKey,
|
|
3210
|
-
verifyOpts
|
|
3211
|
-
).catch((e) => {
|
|
3212
|
-
throw new errors.AuthenticationError("Invalid token", e);
|
|
3213
|
-
});
|
|
3214
|
-
const userEntityRef = payload.sub;
|
|
3215
|
-
if (!userEntityRef) {
|
|
3216
|
-
throw new errors.AuthenticationError("No user sub found in token");
|
|
548
|
+
async #doStart() {
|
|
549
|
+
this.#serviceRegistry.checkForCircularDeps();
|
|
550
|
+
for (const feature of this.#registeredFeatures) {
|
|
551
|
+
this.#addFeature(await feature);
|
|
3217
552
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
typ: pluginAuthNode.tokenTypes.user.typParam
|
|
3227
|
-
};
|
|
3228
|
-
}
|
|
3229
|
-
if (typ === pluginAuthNode.tokenTypes.limitedUser.typParam) {
|
|
3230
|
-
return {
|
|
3231
|
-
requiredClaims: ["iat", "exp", "sub"],
|
|
3232
|
-
typ: pluginAuthNode.tokenTypes.limitedUser.typParam
|
|
3233
|
-
};
|
|
553
|
+
const featureDiscovery = await this.#serviceRegistry.get(
|
|
554
|
+
alpha.featureDiscoveryServiceRef,
|
|
555
|
+
"root"
|
|
556
|
+
);
|
|
557
|
+
if (featureDiscovery) {
|
|
558
|
+
const { features } = await featureDiscovery.getBackendFeatures();
|
|
559
|
+
for (const feature of features) {
|
|
560
|
+
this.#addFeature(feature);
|
|
3234
561
|
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
562
|
+
this.#serviceRegistry.checkForCircularDeps();
|
|
563
|
+
}
|
|
564
|
+
await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders);
|
|
565
|
+
await this.#serviceRegistry.initializeEagerServicesWithScope("root");
|
|
566
|
+
const pluginInits = /* @__PURE__ */ new Map();
|
|
567
|
+
const moduleInits = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const feature of this.#registrations) {
|
|
569
|
+
for (const r of feature.getRegistrations()) {
|
|
570
|
+
const provides = /* @__PURE__ */ new Set();
|
|
571
|
+
if (r.type === "plugin" || r.type === "module") {
|
|
572
|
+
for (const [extRef, extImpl] of r.extensionPoints) {
|
|
573
|
+
if (this.#extensionPoints.has(extRef.id)) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`ExtensionPoint with ID '${extRef.id}' is already registered`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
this.#extensionPoints.set(extRef.id, {
|
|
579
|
+
impl: extImpl,
|
|
580
|
+
pluginId: r.pluginId
|
|
581
|
+
});
|
|
582
|
+
provides.add(extRef);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (r.type === "plugin") {
|
|
586
|
+
if (pluginInits.has(r.pluginId)) {
|
|
587
|
+
throw new Error(`Plugin '${r.pluginId}' is already registered`);
|
|
588
|
+
}
|
|
589
|
+
pluginInits.set(r.pluginId, {
|
|
590
|
+
provides,
|
|
591
|
+
consumes: new Set(Object.values(r.init.deps)),
|
|
592
|
+
init: r.init
|
|
593
|
+
});
|
|
594
|
+
} else if (r.type === "module") {
|
|
595
|
+
let modules = moduleInits.get(r.pluginId);
|
|
596
|
+
if (!modules) {
|
|
597
|
+
modules = /* @__PURE__ */ new Map();
|
|
598
|
+
moduleInits.set(r.pluginId, modules);
|
|
599
|
+
}
|
|
600
|
+
if (modules.has(r.moduleId)) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
`Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
modules.set(r.moduleId, {
|
|
606
|
+
provides,
|
|
607
|
+
consumes: new Set(Object.values(r.init.deps)),
|
|
608
|
+
init: r.init
|
|
609
|
+
});
|
|
610
|
+
} else {
|
|
611
|
+
throw new Error(`Invalid registration type '${r.type}'`);
|
|
612
|
+
}
|
|
3240
613
|
}
|
|
3241
|
-
} catch {
|
|
3242
614
|
}
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
const header = JSON.parse(
|
|
3248
|
-
new TextDecoder().decode(jose.base64url.decode(headerRaw))
|
|
615
|
+
const allPluginIds = [...pluginInits.keys()];
|
|
616
|
+
const initLogger = createInitializationLogger(
|
|
617
|
+
allPluginIds,
|
|
618
|
+
await this.#serviceRegistry.get(backendPluginApi.coreServices.rootLogger, "root")
|
|
3249
619
|
);
|
|
3250
|
-
|
|
3251
|
-
|
|
620
|
+
await Promise.all(
|
|
621
|
+
allPluginIds.map(async (pluginId) => {
|
|
622
|
+
await this.#serviceRegistry.initializeEagerServicesWithScope(
|
|
623
|
+
"plugin",
|
|
624
|
+
pluginId
|
|
625
|
+
);
|
|
626
|
+
const modules = moduleInits.get(pluginId);
|
|
627
|
+
if (modules) {
|
|
628
|
+
const tree = DependencyGraph.fromIterable(
|
|
629
|
+
Array.from(modules).map(([moduleId, moduleInit]) => ({
|
|
630
|
+
value: { moduleId, moduleInit },
|
|
631
|
+
// Relationships are reversed at this point since we're only interested in the extension points.
|
|
632
|
+
// If a modules provides extension point A we want it to be initialized AFTER all modules
|
|
633
|
+
// that depend on extension point A, so that they can provide their extensions.
|
|
634
|
+
consumes: Array.from(moduleInit.provides).map((p) => p.id),
|
|
635
|
+
provides: Array.from(moduleInit.consumes).map((c) => c.id)
|
|
636
|
+
}))
|
|
637
|
+
);
|
|
638
|
+
const circular = tree.detectCircularDependency();
|
|
639
|
+
if (circular) {
|
|
640
|
+
throw new errors.ConflictError(
|
|
641
|
+
`Circular dependency detected for modules of plugin '${pluginId}', ${circular.map(({ moduleId }) => `'${moduleId}'`).join(" -> ")}`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
await tree.parallelTopologicalTraversal(
|
|
645
|
+
async ({ moduleId, moduleInit }) => {
|
|
646
|
+
const moduleDeps = await this.#getInitDeps(
|
|
647
|
+
moduleInit.init.deps,
|
|
648
|
+
pluginId,
|
|
649
|
+
moduleId
|
|
650
|
+
);
|
|
651
|
+
await moduleInit.init.func(moduleDeps).catch((error) => {
|
|
652
|
+
throw new errors.ForwardedError(
|
|
653
|
+
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
|
|
654
|
+
error
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const pluginInit = pluginInits.get(pluginId);
|
|
661
|
+
if (pluginInit) {
|
|
662
|
+
const pluginDeps = await this.#getInitDeps(
|
|
663
|
+
pluginInit.init.deps,
|
|
664
|
+
pluginId
|
|
665
|
+
);
|
|
666
|
+
await pluginInit.init.func(pluginDeps).catch((error) => {
|
|
667
|
+
throw new errors.ForwardedError(
|
|
668
|
+
`Plugin '${pluginId}' startup failed`,
|
|
669
|
+
error
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
initLogger.onPluginStarted(pluginId);
|
|
674
|
+
const lifecycleService2 = await this.#getPluginLifecycleImpl(pluginId);
|
|
675
|
+
await lifecycleService2.startup();
|
|
676
|
+
})
|
|
3252
677
|
);
|
|
3253
|
-
const
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
"
|
|
678
|
+
const lifecycleService = await this.#getRootLifecycleImpl();
|
|
679
|
+
await lifecycleService.startup();
|
|
680
|
+
initLogger.onAllStarted();
|
|
681
|
+
if (process.env.NODE_ENV !== "test") {
|
|
682
|
+
const rootLogger = await this.#serviceRegistry.get(
|
|
683
|
+
backendPluginApi.coreServices.rootLogger,
|
|
684
|
+
"root"
|
|
3260
685
|
);
|
|
686
|
+
process.on("unhandledRejection", (reason) => {
|
|
687
|
+
rootLogger?.child({ type: "unhandledRejection" })?.error("Unhandled rejection", reason);
|
|
688
|
+
});
|
|
689
|
+
process.on("uncaughtException", (error) => {
|
|
690
|
+
rootLogger?.child({ type: "uncaughtException" })?.error("Uncaught exception", error);
|
|
691
|
+
});
|
|
3261
692
|
}
|
|
3262
|
-
const limitedUserToken = [
|
|
3263
|
-
jose.base64url.encode(
|
|
3264
|
-
JSON.stringify({
|
|
3265
|
-
typ: pluginAuthNode.tokenTypes.limitedUser.typParam,
|
|
3266
|
-
alg: header.alg,
|
|
3267
|
-
kid: header.kid
|
|
3268
|
-
})
|
|
3269
|
-
),
|
|
3270
|
-
jose.base64url.encode(
|
|
3271
|
-
JSON.stringify({
|
|
3272
|
-
sub: payload.sub,
|
|
3273
|
-
iat: payload.iat,
|
|
3274
|
-
exp: payload.exp
|
|
3275
|
-
})
|
|
3276
|
-
),
|
|
3277
|
-
payload.uip
|
|
3278
|
-
].join(".");
|
|
3279
|
-
return { token: limitedUserToken, expiresAt: new Date(payload.exp * 1e3) };
|
|
3280
693
|
}
|
|
3281
|
-
|
|
694
|
+
async stop() {
|
|
695
|
+
if (!this.#startPromise) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
3282
698
|
try {
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
} catch {
|
|
3286
|
-
return false;
|
|
699
|
+
await this.#startPromise;
|
|
700
|
+
} catch (error) {
|
|
3287
701
|
}
|
|
702
|
+
const lifecycleService = await this.#getRootLifecycleImpl();
|
|
703
|
+
await lifecycleService.shutdown();
|
|
3288
704
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
const
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
3295
|
-
logger: backendPluginApi.coreServices.rootLogger,
|
|
3296
|
-
discovery: backendPluginApi.coreServices.discovery,
|
|
3297
|
-
plugin: backendPluginApi.coreServices.pluginMetadata,
|
|
3298
|
-
database: backendPluginApi.coreServices.database,
|
|
3299
|
-
// Re-using the token manager makes sure that we use the same generated keys for
|
|
3300
|
-
// development as plugins that have not yet been migrated. It's important that this
|
|
3301
|
-
// keeps working as long as there are plugins that have not been migrated to the
|
|
3302
|
-
// new auth services in the new backend system.
|
|
3303
|
-
tokenManager: backendPluginApi.coreServices.tokenManager
|
|
3304
|
-
},
|
|
3305
|
-
async factory({ config, discovery, plugin, tokenManager, logger, database }) {
|
|
3306
|
-
const disableDefaultAuthPolicy = config.getOptionalBoolean(
|
|
3307
|
-
"backend.auth.dangerouslyDisableDefaultAuthPolicy"
|
|
3308
|
-
) ?? false;
|
|
3309
|
-
const keyDuration = { hours: 1 };
|
|
3310
|
-
const keySource = await createPluginKeySource({
|
|
3311
|
-
config,
|
|
3312
|
-
database,
|
|
3313
|
-
logger,
|
|
3314
|
-
keyDuration
|
|
3315
|
-
});
|
|
3316
|
-
const userTokens = UserTokenHandler.create({
|
|
3317
|
-
discovery
|
|
3318
|
-
});
|
|
3319
|
-
const pluginTokens = PluginTokenHandler.create({
|
|
3320
|
-
ownPluginId: plugin.getId(),
|
|
3321
|
-
logger,
|
|
3322
|
-
keySource,
|
|
3323
|
-
keyDuration,
|
|
3324
|
-
discovery
|
|
3325
|
-
});
|
|
3326
|
-
const externalTokens = ExternalTokenHandler.create({
|
|
3327
|
-
ownPluginId: plugin.getId(),
|
|
3328
|
-
config,
|
|
3329
|
-
logger
|
|
3330
|
-
});
|
|
3331
|
-
return new DefaultAuthService(
|
|
3332
|
-
userTokens,
|
|
3333
|
-
pluginTokens,
|
|
3334
|
-
externalTokens,
|
|
3335
|
-
tokenManager,
|
|
3336
|
-
plugin.getId(),
|
|
3337
|
-
disableDefaultAuthPolicy,
|
|
3338
|
-
keySource
|
|
705
|
+
// Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this
|
|
706
|
+
async #getRootLifecycleImpl() {
|
|
707
|
+
const lifecycleService = await this.#serviceRegistry.get(
|
|
708
|
+
backendPluginApi.coreServices.rootLifecycle,
|
|
709
|
+
"root"
|
|
3339
710
|
);
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
const authServiceFactory = authServiceFactory$1;
|
|
3344
|
-
|
|
3345
|
-
const FIVE_MINUTES_MS = 5 * 60 * 1e3;
|
|
3346
|
-
const BACKSTAGE_AUTH_COOKIE = "backstage-auth";
|
|
3347
|
-
function getTokenFromRequest(req) {
|
|
3348
|
-
const authHeader = req.headers.authorization;
|
|
3349
|
-
if (typeof authHeader === "string") {
|
|
3350
|
-
const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i);
|
|
3351
|
-
const token = matches?.[1];
|
|
3352
|
-
if (token) {
|
|
3353
|
-
return token;
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
return void 0;
|
|
3357
|
-
}
|
|
3358
|
-
function getCookieFromRequest(req) {
|
|
3359
|
-
const cookieHeader = req.headers.cookie;
|
|
3360
|
-
if (cookieHeader) {
|
|
3361
|
-
const cookies = cookie.parse(cookieHeader);
|
|
3362
|
-
const token = cookies[BACKSTAGE_AUTH_COOKIE];
|
|
3363
|
-
if (token) {
|
|
3364
|
-
return token;
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
return void 0;
|
|
3368
|
-
}
|
|
3369
|
-
function willExpireSoon(expiresAt) {
|
|
3370
|
-
return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();
|
|
3371
|
-
}
|
|
3372
|
-
const credentialsSymbol = Symbol("backstage-credentials");
|
|
3373
|
-
const limitedCredentialsSymbol = Symbol("backstage-limited-credentials");
|
|
3374
|
-
class DefaultHttpAuthService {
|
|
3375
|
-
#auth;
|
|
3376
|
-
#discovery;
|
|
3377
|
-
#pluginId;
|
|
3378
|
-
constructor(auth, discovery, pluginId) {
|
|
3379
|
-
this.#auth = auth;
|
|
3380
|
-
this.#discovery = discovery;
|
|
3381
|
-
this.#pluginId = pluginId;
|
|
3382
|
-
}
|
|
3383
|
-
async #extractCredentialsFromRequest(req) {
|
|
3384
|
-
const token = getTokenFromRequest(req);
|
|
3385
|
-
if (!token) {
|
|
3386
|
-
return await this.#auth.getNoneCredentials();
|
|
3387
|
-
}
|
|
3388
|
-
return await this.#auth.authenticate(token);
|
|
3389
|
-
}
|
|
3390
|
-
async #extractLimitedCredentialsFromRequest(req) {
|
|
3391
|
-
const token = getTokenFromRequest(req);
|
|
3392
|
-
if (token) {
|
|
3393
|
-
return await this.#auth.authenticate(token, {
|
|
3394
|
-
allowLimitedAccess: true
|
|
3395
|
-
});
|
|
3396
|
-
}
|
|
3397
|
-
const cookie = getCookieFromRequest(req);
|
|
3398
|
-
if (cookie) {
|
|
3399
|
-
return await this.#auth.authenticate(cookie, {
|
|
3400
|
-
allowLimitedAccess: true
|
|
3401
|
-
});
|
|
711
|
+
const service = lifecycleService;
|
|
712
|
+
if (service && typeof service.startup === "function" && typeof service.shutdown === "function") {
|
|
713
|
+
return service;
|
|
3402
714
|
}
|
|
3403
|
-
|
|
3404
|
-
}
|
|
3405
|
-
async #getCredentials(req) {
|
|
3406
|
-
return req[credentialsSymbol] ??= this.#extractCredentialsFromRequest(req);
|
|
3407
|
-
}
|
|
3408
|
-
async #getLimitedCredentials(req) {
|
|
3409
|
-
return req[limitedCredentialsSymbol] ??= this.#extractLimitedCredentialsFromRequest(req);
|
|
715
|
+
throw new Error("Unexpected root lifecycle service implementation");
|
|
3410
716
|
}
|
|
3411
|
-
async
|
|
3412
|
-
const
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
return credentials;
|
|
3416
|
-
}
|
|
3417
|
-
if (this.#auth.isPrincipal(credentials, "none")) {
|
|
3418
|
-
if (allowed.includes("none")) {
|
|
3419
|
-
return credentials;
|
|
3420
|
-
}
|
|
3421
|
-
throw new errors.AuthenticationError("Missing credentials");
|
|
3422
|
-
} else if (this.#auth.isPrincipal(credentials, "user")) {
|
|
3423
|
-
if (allowed.includes("user")) {
|
|
3424
|
-
return credentials;
|
|
3425
|
-
}
|
|
3426
|
-
throw new errors.NotAllowedError(
|
|
3427
|
-
`This endpoint does not allow 'user' credentials`
|
|
3428
|
-
);
|
|
3429
|
-
} else if (this.#auth.isPrincipal(credentials, "service")) {
|
|
3430
|
-
if (allowed.includes("service")) {
|
|
3431
|
-
return credentials;
|
|
3432
|
-
}
|
|
3433
|
-
throw new errors.NotAllowedError(
|
|
3434
|
-
`This endpoint does not allow 'service' credentials`
|
|
3435
|
-
);
|
|
3436
|
-
}
|
|
3437
|
-
throw new errors.NotAllowedError(
|
|
3438
|
-
"Unknown principal type, this should never happen"
|
|
717
|
+
async #getPluginLifecycleImpl(pluginId) {
|
|
718
|
+
const lifecycleService = await this.#serviceRegistry.get(
|
|
719
|
+
backendPluginApi.coreServices.lifecycle,
|
|
720
|
+
pluginId
|
|
3439
721
|
);
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
throw new Error("Failed to issue user cookie, headers were already sent");
|
|
722
|
+
const service = lifecycleService;
|
|
723
|
+
if (service && typeof service.startup === "function") {
|
|
724
|
+
return service;
|
|
3444
725
|
}
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
726
|
+
throw new Error("Unexpected plugin lifecycle service implementation");
|
|
727
|
+
}
|
|
728
|
+
async #applyBackendFeatureLoaders(loaders) {
|
|
729
|
+
for (const loader of loaders) {
|
|
730
|
+
const deps = /* @__PURE__ */ new Map();
|
|
731
|
+
const missingRefs = /* @__PURE__ */ new Set();
|
|
732
|
+
for (const [name, ref] of Object.entries(loader.deps ?? {})) {
|
|
733
|
+
if (ref.scope !== "root") {
|
|
734
|
+
throw new Error(
|
|
735
|
+
`Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
const impl = await this.#serviceRegistry.get(
|
|
739
|
+
ref,
|
|
740
|
+
"root"
|
|
3451
741
|
);
|
|
3452
|
-
|
|
742
|
+
if (impl) {
|
|
743
|
+
deps.set(name, impl);
|
|
744
|
+
} else {
|
|
745
|
+
missingRefs.add(ref);
|
|
746
|
+
}
|
|
3453
747
|
}
|
|
3454
|
-
if (
|
|
3455
|
-
|
|
3456
|
-
|
|
748
|
+
if (missingRefs.size > 0) {
|
|
749
|
+
const missing = Array.from(missingRefs).join(", ");
|
|
750
|
+
throw new Error(
|
|
751
|
+
`No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`
|
|
3457
752
|
);
|
|
3458
753
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
|
|
3474
|
-
...await this.#getCookieOptions(res.req),
|
|
3475
|
-
expires: expiresAt
|
|
3476
|
-
});
|
|
3477
|
-
return { expiresAt };
|
|
3478
|
-
}
|
|
3479
|
-
async #getCookieOptions(_req) {
|
|
3480
|
-
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
|
|
3481
|
-
this.#pluginId
|
|
3482
|
-
);
|
|
3483
|
-
const externalBaseUrl = new URL(externalBaseUrlStr);
|
|
3484
|
-
const secure = externalBaseUrl.protocol === "https:" || externalBaseUrl.hostname === "localhost";
|
|
3485
|
-
return {
|
|
3486
|
-
domain: externalBaseUrl.hostname,
|
|
3487
|
-
httpOnly: true,
|
|
3488
|
-
secure,
|
|
3489
|
-
priority: "high",
|
|
3490
|
-
sameSite: secure ? "none" : "lax"
|
|
3491
|
-
};
|
|
3492
|
-
}
|
|
3493
|
-
async #existingCookieExpiration(req) {
|
|
3494
|
-
const existingCookie = getCookieFromRequest(req);
|
|
3495
|
-
if (!existingCookie) {
|
|
3496
|
-
return void 0;
|
|
3497
|
-
}
|
|
3498
|
-
try {
|
|
3499
|
-
const existingCredentials = await this.#auth.authenticate(
|
|
3500
|
-
existingCookie,
|
|
3501
|
-
{
|
|
3502
|
-
allowLimitedAccess: true
|
|
754
|
+
const result = await loader.loader(Object.fromEntries(deps)).catch((error) => {
|
|
755
|
+
throw new errors.ForwardedError(
|
|
756
|
+
`Feature loader ${loader.description} failed`,
|
|
757
|
+
error
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
let didAddServiceFactory = false;
|
|
761
|
+
const newLoaders = new Array();
|
|
762
|
+
for await (const feature of result) {
|
|
763
|
+
if (isBackendFeatureLoader(feature)) {
|
|
764
|
+
newLoaders.push(feature);
|
|
765
|
+
} else {
|
|
766
|
+
didAddServiceFactory ||= isServiceFactory(feature);
|
|
767
|
+
this.#addFeature(feature);
|
|
3503
768
|
}
|
|
3504
|
-
);
|
|
3505
|
-
if (!this.#auth.isPrincipal(existingCredentials, "user")) {
|
|
3506
|
-
return void 0;
|
|
3507
769
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
770
|
+
if (didAddServiceFactory) {
|
|
771
|
+
this.#serviceRegistry.checkForCircularDeps();
|
|
772
|
+
}
|
|
773
|
+
if (newLoaders.length > 0) {
|
|
774
|
+
await this.#applyBackendFeatureLoaders(newLoaders);
|
|
3512
775
|
}
|
|
3513
|
-
throw error;
|
|
3514
776
|
}
|
|
3515
777
|
}
|
|
3516
778
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
auth: backendPluginApi.coreServices.auth,
|
|
3521
|
-
discovery: backendPluginApi.coreServices.discovery,
|
|
3522
|
-
plugin: backendPluginApi.coreServices.pluginMetadata
|
|
3523
|
-
},
|
|
3524
|
-
async factory({ auth, discovery, plugin }) {
|
|
3525
|
-
return new DefaultHttpAuthService(auth, discovery, plugin.getId());
|
|
779
|
+
function toInternalBackendFeature(feature) {
|
|
780
|
+
if (feature.$$type !== "@backstage/BackendFeature") {
|
|
781
|
+
throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
|
|
3526
782
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
function createLifecycleMiddleware$1(options) {
|
|
3533
|
-
const { lifecycle, startupRequestPauseTimeout = DEFAULT_TIMEOUT } = options;
|
|
3534
|
-
let state = "init";
|
|
3535
|
-
const waiting = /* @__PURE__ */ new Set();
|
|
3536
|
-
lifecycle.addStartupHook(async () => {
|
|
3537
|
-
if (state === "init") {
|
|
3538
|
-
state = "up";
|
|
3539
|
-
for (const item of waiting) {
|
|
3540
|
-
clearTimeout(item.timeout);
|
|
3541
|
-
item.next();
|
|
3542
|
-
}
|
|
3543
|
-
waiting.clear();
|
|
3544
|
-
}
|
|
3545
|
-
});
|
|
3546
|
-
lifecycle.addShutdownHook(async () => {
|
|
3547
|
-
state = "down";
|
|
3548
|
-
for (const item of waiting) {
|
|
3549
|
-
clearTimeout(item.timeout);
|
|
3550
|
-
item.next(new errors.ServiceUnavailableError("Service is shutting down"));
|
|
3551
|
-
}
|
|
3552
|
-
waiting.clear();
|
|
3553
|
-
});
|
|
3554
|
-
const timeoutMs = types.durationToMilliseconds(startupRequestPauseTimeout);
|
|
3555
|
-
return (_req, _res, next) => {
|
|
3556
|
-
if (state === "up") {
|
|
3557
|
-
next();
|
|
3558
|
-
return;
|
|
3559
|
-
} else if (state === "down") {
|
|
3560
|
-
next(new errors.ServiceUnavailableError("Service is shutting down"));
|
|
3561
|
-
return;
|
|
3562
|
-
}
|
|
3563
|
-
const item = {
|
|
3564
|
-
next,
|
|
3565
|
-
timeout: setTimeout(() => {
|
|
3566
|
-
if (waiting.delete(item)) {
|
|
3567
|
-
next(new errors.ServiceUnavailableError("Service has not started up yet"));
|
|
3568
|
-
}
|
|
3569
|
-
}, timeoutMs)
|
|
3570
|
-
};
|
|
3571
|
-
waiting.add(item);
|
|
3572
|
-
};
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
function createPathPolicyPredicate(policyPath) {
|
|
3576
|
-
if (policyPath === "/" || policyPath === "*") {
|
|
3577
|
-
return () => true;
|
|
783
|
+
const internal = feature;
|
|
784
|
+
if (internal.version !== "v1") {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`Invalid BackendFeature, bad version '${internal.version}'`
|
|
787
|
+
);
|
|
3578
788
|
}
|
|
3579
|
-
|
|
3580
|
-
end: false
|
|
3581
|
-
});
|
|
3582
|
-
return (path) => {
|
|
3583
|
-
return pathRegex.test(path);
|
|
3584
|
-
};
|
|
789
|
+
return internal;
|
|
3585
790
|
}
|
|
3586
|
-
function
|
|
3587
|
-
const
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
);
|
|
3591
|
-
if (disableDefaultAuthPolicy) {
|
|
3592
|
-
return {
|
|
3593
|
-
middleware: (_req, _res, next) => next(),
|
|
3594
|
-
addAuthPolicy: () => {
|
|
3595
|
-
}
|
|
3596
|
-
};
|
|
791
|
+
function isServiceFactory(feature) {
|
|
792
|
+
const internal = toInternalBackendFeature(feature);
|
|
793
|
+
if (internal.featureType === "service") {
|
|
794
|
+
return true;
|
|
3597
795
|
}
|
|
3598
|
-
|
|
3599
|
-
const cookiePredicates = new Array();
|
|
3600
|
-
const middleware = (req, _, next) => {
|
|
3601
|
-
const allowsUnauthenticated = unauthenticatedPredicates.some(
|
|
3602
|
-
(predicate) => predicate(req.path)
|
|
3603
|
-
);
|
|
3604
|
-
if (allowsUnauthenticated) {
|
|
3605
|
-
next();
|
|
3606
|
-
return;
|
|
3607
|
-
}
|
|
3608
|
-
const allowsCookie = cookiePredicates.some(
|
|
3609
|
-
(predicate) => predicate(req.path)
|
|
3610
|
-
);
|
|
3611
|
-
httpAuth.credentials(req, {
|
|
3612
|
-
allow: ["user", "service"],
|
|
3613
|
-
allowLimitedAccess: allowsCookie
|
|
3614
|
-
}).then(
|
|
3615
|
-
() => next(),
|
|
3616
|
-
(err) => next(err)
|
|
3617
|
-
);
|
|
3618
|
-
};
|
|
3619
|
-
const addAuthPolicy = (policy) => {
|
|
3620
|
-
if (policy.allow === "unauthenticated") {
|
|
3621
|
-
unauthenticatedPredicates.push(createPathPolicyPredicate(policy.path));
|
|
3622
|
-
} else if (policy.allow === "user-cookie") {
|
|
3623
|
-
cookiePredicates.push(createPathPolicyPredicate(policy.path));
|
|
3624
|
-
} else {
|
|
3625
|
-
throw new Error("Invalid auth policy");
|
|
3626
|
-
}
|
|
3627
|
-
};
|
|
3628
|
-
return { middleware, addAuthPolicy };
|
|
796
|
+
return "service" in internal;
|
|
3629
797
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
});
|
|
3637
|
-
return router;
|
|
798
|
+
function isBackendRegistrations(feature) {
|
|
799
|
+
const internal = toInternalBackendFeature(feature);
|
|
800
|
+
if (internal.featureType === "registrations") {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
return "getRegistrations" in internal;
|
|
3638
804
|
}
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
function createCookieAuthRefreshMiddleware(options) {
|
|
3642
|
-
const { auth, httpAuth } = options;
|
|
3643
|
-
const router = Router__default.default();
|
|
3644
|
-
router.get(WELL_KNOWN_COOKIE_PATH_V1, async (_, res) => {
|
|
3645
|
-
const { expiresAt } = await httpAuth.issueUserCookie(res);
|
|
3646
|
-
res.json({ expiresAt: expiresAt.toISOString() });
|
|
3647
|
-
});
|
|
3648
|
-
router.delete(WELL_KNOWN_COOKIE_PATH_V1, async (_, res) => {
|
|
3649
|
-
const credentials = await auth.getNoneCredentials();
|
|
3650
|
-
await httpAuth.issueUserCookie(res, { credentials });
|
|
3651
|
-
res.status(204).end();
|
|
3652
|
-
});
|
|
3653
|
-
return router;
|
|
805
|
+
function isBackendFeatureLoader(feature) {
|
|
806
|
+
return toInternalBackendFeature(feature).featureType === "loader";
|
|
3654
807
|
}
|
|
3655
808
|
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
plugin: backendPluginApi.coreServices.pluginMetadata,
|
|
3661
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
3662
|
-
lifecycle: backendPluginApi.coreServices.lifecycle,
|
|
3663
|
-
rootHttpRouter: backendPluginApi.coreServices.rootHttpRouter,
|
|
3664
|
-
auth: backendPluginApi.coreServices.auth,
|
|
3665
|
-
httpAuth: backendPluginApi.coreServices.httpAuth
|
|
3666
|
-
},
|
|
3667
|
-
async factory({ auth, httpAuth, config, plugin, rootHttpRouter, lifecycle }) {
|
|
3668
|
-
const router = Router__default.default();
|
|
3669
|
-
rootHttpRouter.use(`/api/${plugin.getId()}`, router);
|
|
3670
|
-
const credentialsBarrier = createCredentialsBarrier({
|
|
3671
|
-
httpAuth,
|
|
3672
|
-
config
|
|
3673
|
-
});
|
|
3674
|
-
router.use(createAuthIntegrationRouter({ auth }));
|
|
3675
|
-
router.use(createLifecycleMiddleware$1({ lifecycle }));
|
|
3676
|
-
router.use(credentialsBarrier.middleware);
|
|
3677
|
-
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
|
|
3678
|
-
return {
|
|
3679
|
-
use(handler) {
|
|
3680
|
-
router.use(handler);
|
|
3681
|
-
},
|
|
3682
|
-
addAuthPolicy(policy) {
|
|
3683
|
-
credentialsBarrier.addAuthPolicy(policy);
|
|
3684
|
-
}
|
|
3685
|
-
};
|
|
3686
|
-
}
|
|
3687
|
-
});
|
|
3688
|
-
|
|
3689
|
-
const httpRouterServiceFactory = httpRouterServiceFactory$1;
|
|
3690
|
-
|
|
3691
|
-
const createLifecycleMiddleware = createLifecycleMiddleware$1;
|
|
3692
|
-
|
|
3693
|
-
const loggerServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
3694
|
-
service: backendPluginApi.coreServices.logger,
|
|
3695
|
-
deps: {
|
|
3696
|
-
rootLogger: backendPluginApi.coreServices.rootLogger,
|
|
3697
|
-
plugin: backendPluginApi.coreServices.pluginMetadata
|
|
3698
|
-
},
|
|
3699
|
-
factory({ rootLogger, plugin }) {
|
|
3700
|
-
return rootLogger.child({ plugin: plugin.getId() });
|
|
809
|
+
class BackstageBackend {
|
|
810
|
+
#initializer;
|
|
811
|
+
constructor(defaultServiceFactories) {
|
|
812
|
+
this.#initializer = new BackendInitializer(defaultServiceFactories);
|
|
3701
813
|
}
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
function normalizePath(path) {
|
|
3707
|
-
return `${trimEnd__default.default(path, "/")}/`;
|
|
3708
|
-
}
|
|
3709
|
-
let DefaultRootHttpRouter$1 = class DefaultRootHttpRouter {
|
|
3710
|
-
#indexPath;
|
|
3711
|
-
#router = express.Router();
|
|
3712
|
-
#namedRoutes = express.Router();
|
|
3713
|
-
#indexRouter = express.Router();
|
|
3714
|
-
#existingPaths = new Array();
|
|
3715
|
-
static create(options) {
|
|
3716
|
-
let indexPath;
|
|
3717
|
-
if (options?.indexPath === false) {
|
|
3718
|
-
indexPath = void 0;
|
|
3719
|
-
} else if (options?.indexPath === void 0) {
|
|
3720
|
-
indexPath = "/api/app";
|
|
3721
|
-
} else if (options?.indexPath === "") {
|
|
3722
|
-
throw new Error("indexPath option may not be an empty string");
|
|
814
|
+
add(feature) {
|
|
815
|
+
if (isPromise(feature)) {
|
|
816
|
+
this.#initializer.add(feature.then((f) => unwrapFeature(f.default)));
|
|
3723
817
|
} else {
|
|
3724
|
-
|
|
3725
|
-
}
|
|
3726
|
-
return new DefaultRootHttpRouter(indexPath);
|
|
3727
|
-
}
|
|
3728
|
-
constructor(indexPath) {
|
|
3729
|
-
this.#indexPath = indexPath;
|
|
3730
|
-
this.#router.use(this.#namedRoutes);
|
|
3731
|
-
this.#router.use("/api/", (_req, _res, next) => {
|
|
3732
|
-
next("router");
|
|
3733
|
-
});
|
|
3734
|
-
if (this.#indexPath) {
|
|
3735
|
-
this.#router.use(this.#indexRouter);
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
use(path, handler) {
|
|
3739
|
-
if (path.match(/^[/\s]*$/)) {
|
|
3740
|
-
throw new Error(`Root router path may not be empty`);
|
|
3741
|
-
}
|
|
3742
|
-
const conflictingPath = this.#findConflictingPath(path);
|
|
3743
|
-
if (conflictingPath) {
|
|
3744
|
-
throw new Error(
|
|
3745
|
-
`Path ${path} conflicts with the existing path ${conflictingPath}`
|
|
3746
|
-
);
|
|
3747
|
-
}
|
|
3748
|
-
this.#existingPaths.push(path);
|
|
3749
|
-
this.#namedRoutes.use(path, handler);
|
|
3750
|
-
if (this.#indexPath === path) {
|
|
3751
|
-
this.#indexRouter.use(handler);
|
|
818
|
+
this.#initializer.add(unwrapFeature(feature));
|
|
3752
819
|
}
|
|
3753
820
|
}
|
|
3754
|
-
|
|
3755
|
-
|
|
821
|
+
async start() {
|
|
822
|
+
await this.#initializer.start();
|
|
3756
823
|
}
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
for (const path of this.#existingPaths) {
|
|
3760
|
-
const normalizedPath = normalizePath(path);
|
|
3761
|
-
if (normalizedPath.startsWith(normalizedNewPath)) {
|
|
3762
|
-
return path;
|
|
3763
|
-
}
|
|
3764
|
-
if (normalizedNewPath.startsWith(normalizedPath)) {
|
|
3765
|
-
return path;
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
|
-
return void 0;
|
|
824
|
+
async stop() {
|
|
825
|
+
await this.#initializer.stop();
|
|
3769
826
|
}
|
|
3770
|
-
};
|
|
3771
|
-
|
|
3772
|
-
function createHealthRouter(options) {
|
|
3773
|
-
const router = Router__default.default();
|
|
3774
|
-
router.get(
|
|
3775
|
-
"/.backstage/health/v1/readiness",
|
|
3776
|
-
async (_request, response) => {
|
|
3777
|
-
const { status, payload } = await options.health.getReadiness();
|
|
3778
|
-
response.status(status).json(payload);
|
|
3779
|
-
}
|
|
3780
|
-
);
|
|
3781
|
-
router.get(
|
|
3782
|
-
"/.backstage/health/v1/liveness",
|
|
3783
|
-
async (_request, response) => {
|
|
3784
|
-
const { status, payload } = await options.health.getLiveness();
|
|
3785
|
-
response.status(status).json(payload);
|
|
3786
|
-
}
|
|
3787
|
-
);
|
|
3788
|
-
return router;
|
|
3789
827
|
}
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
applyDefaults();
|
|
828
|
+
function isPromise(value) {
|
|
829
|
+
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
3793
830
|
}
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
3798
|
-
rootLogger: backendPluginApi.coreServices.rootLogger,
|
|
3799
|
-
lifecycle: backendPluginApi.coreServices.rootLifecycle,
|
|
3800
|
-
health: backendPluginApi.coreServices.rootHealth
|
|
3801
|
-
},
|
|
3802
|
-
async factory({ config, rootLogger, lifecycle, health }) {
|
|
3803
|
-
const { indexPath, configure = defaultConfigure } = options ?? {};
|
|
3804
|
-
const logger = rootLogger.child({ service: "rootHttpRouter" });
|
|
3805
|
-
const app = express__default.default();
|
|
3806
|
-
const router = DefaultRootHttpRouter$1.create({ indexPath });
|
|
3807
|
-
const middleware = MiddlewareFactory$1.create({ config, logger });
|
|
3808
|
-
const routes = router.handler();
|
|
3809
|
-
const healthRouter = createHealthRouter({ health });
|
|
3810
|
-
const server = await createHttpServer$1(
|
|
3811
|
-
app,
|
|
3812
|
-
readHttpServerOptions$1(config.getOptionalConfig("backend")),
|
|
3813
|
-
{ logger }
|
|
3814
|
-
);
|
|
3815
|
-
configure({
|
|
3816
|
-
app,
|
|
3817
|
-
server,
|
|
3818
|
-
routes,
|
|
3819
|
-
middleware,
|
|
3820
|
-
config,
|
|
3821
|
-
logger,
|
|
3822
|
-
lifecycle,
|
|
3823
|
-
healthRouter,
|
|
3824
|
-
applyDefaults() {
|
|
3825
|
-
app.use(middleware.helmet());
|
|
3826
|
-
app.use(middleware.cors());
|
|
3827
|
-
app.use(middleware.compression());
|
|
3828
|
-
app.use(middleware.logging());
|
|
3829
|
-
app.use(healthRouter);
|
|
3830
|
-
app.use(routes);
|
|
3831
|
-
app.use(middleware.notFound());
|
|
3832
|
-
app.use(middleware.error());
|
|
3833
|
-
}
|
|
3834
|
-
});
|
|
3835
|
-
lifecycle.addShutdownHook(() => server.stop());
|
|
3836
|
-
await server.start();
|
|
3837
|
-
return router;
|
|
3838
|
-
}
|
|
3839
|
-
})();
|
|
3840
|
-
const rootHttpRouterServiceFactory$1 = Object.assign(
|
|
3841
|
-
rootHttpRouterServiceFactoryWithOptions,
|
|
3842
|
-
rootHttpRouterServiceFactoryWithOptions()
|
|
3843
|
-
);
|
|
3844
|
-
|
|
3845
|
-
const rootHttpRouterServiceFactory = rootHttpRouterServiceFactory$1;
|
|
3846
|
-
|
|
3847
|
-
class DefaultRootHttpRouter {
|
|
3848
|
-
constructor(impl) {
|
|
3849
|
-
this.impl = impl;
|
|
3850
|
-
}
|
|
3851
|
-
static create(options) {
|
|
3852
|
-
return new DefaultRootHttpRouter(DefaultRootHttpRouter$1.create(options));
|
|
3853
|
-
}
|
|
3854
|
-
use(path, handler) {
|
|
3855
|
-
this.impl.use(path, handler);
|
|
831
|
+
function unwrapFeature(feature) {
|
|
832
|
+
if ("$$type" in feature) {
|
|
833
|
+
return feature;
|
|
3856
834
|
}
|
|
3857
|
-
|
|
3858
|
-
return
|
|
835
|
+
if ("default" in feature) {
|
|
836
|
+
return feature.default;
|
|
3859
837
|
}
|
|
838
|
+
return feature;
|
|
3860
839
|
}
|
|
3861
840
|
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
const
|
|
3865
|
-
service
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
async factory({ plugin, databaseManager, logger }) {
|
|
3872
|
-
return backendTasks.TaskScheduler.forPlugin({
|
|
3873
|
-
pluginId: plugin.getId(),
|
|
3874
|
-
databaseManager,
|
|
3875
|
-
logger
|
|
3876
|
-
});
|
|
841
|
+
function createSpecializedBackend(options) {
|
|
842
|
+
const exists = /* @__PURE__ */ new Set();
|
|
843
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
844
|
+
for (const { service } of options.defaultServiceFactories) {
|
|
845
|
+
if (exists.has(service.id)) {
|
|
846
|
+
duplicates.add(service.id);
|
|
847
|
+
} else {
|
|
848
|
+
exists.add(service.id);
|
|
849
|
+
}
|
|
3877
850
|
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
discovery;
|
|
3882
|
-
constructor(options) {
|
|
3883
|
-
this.discovery = options.discovery;
|
|
851
|
+
if (duplicates.size > 0) {
|
|
852
|
+
const ids = Array.from(duplicates).join(", ");
|
|
853
|
+
throw new Error(`Duplicate service implementations provided for ${ids}`);
|
|
3884
854
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
throw new Error("Only user credentials are supported");
|
|
3889
|
-
}
|
|
3890
|
-
if (!internalCredentials.token) {
|
|
3891
|
-
throw new Error("User credentials is unexpectedly missing token");
|
|
3892
|
-
}
|
|
3893
|
-
const { sub: userEntityRef, ent: tokenEnt } = jose.decodeJwt(
|
|
3894
|
-
internalCredentials.token
|
|
855
|
+
if (exists.has(backendPluginApi.coreServices.pluginMetadata.id)) {
|
|
856
|
+
throw new Error(
|
|
857
|
+
`The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
|
|
3895
858
|
);
|
|
3896
|
-
if (typeof userEntityRef !== "string") {
|
|
3897
|
-
throw new Error("User entity ref must be a string");
|
|
3898
|
-
}
|
|
3899
|
-
let ownershipEntityRefs = tokenEnt;
|
|
3900
|
-
if (!ownershipEntityRefs) {
|
|
3901
|
-
const userInfoResp = await fetch__default.default(
|
|
3902
|
-
`${await this.discovery.getBaseUrl("auth")}/v1/userinfo`,
|
|
3903
|
-
{
|
|
3904
|
-
headers: {
|
|
3905
|
-
Authorization: `Bearer ${internalCredentials.token}`
|
|
3906
|
-
}
|
|
3907
|
-
}
|
|
3908
|
-
);
|
|
3909
|
-
if (!userInfoResp.ok) {
|
|
3910
|
-
throw await errors.ResponseError.fromResponse(userInfoResp);
|
|
3911
|
-
}
|
|
3912
|
-
const {
|
|
3913
|
-
claims: { ent }
|
|
3914
|
-
} = await userInfoResp.json();
|
|
3915
|
-
ownershipEntityRefs = ent;
|
|
3916
|
-
}
|
|
3917
|
-
if (!ownershipEntityRefs) {
|
|
3918
|
-
throw new Error("Ownership entity refs can not be determined");
|
|
3919
|
-
} else if (!Array.isArray(ownershipEntityRefs) || ownershipEntityRefs.some((ref) => typeof ref !== "string")) {
|
|
3920
|
-
throw new Error("Ownership entity refs must be an array of strings");
|
|
3921
|
-
}
|
|
3922
|
-
return { userEntityRef, ownershipEntityRefs };
|
|
3923
859
|
}
|
|
860
|
+
return new BackstageBackend(options.defaultServiceFactories);
|
|
3924
861
|
}
|
|
3925
862
|
|
|
3926
|
-
const
|
|
3927
|
-
|
|
863
|
+
const identityServiceFactory = backendPluginApi.createServiceFactory(
|
|
864
|
+
(options) => ({
|
|
865
|
+
service: backendPluginApi.coreServices.identity,
|
|
866
|
+
deps: {
|
|
867
|
+
discovery: backendPluginApi.coreServices.discovery
|
|
868
|
+
},
|
|
869
|
+
async factory({ discovery }) {
|
|
870
|
+
return pluginAuthNode.DefaultIdentityClient.create({ discovery, ...options });
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
const tokenManagerServiceFactory = backendPluginApi.createServiceFactory({
|
|
876
|
+
service: backendPluginApi.coreServices.tokenManager,
|
|
3928
877
|
deps: {
|
|
3929
|
-
|
|
878
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
879
|
+
logger: backendPluginApi.coreServices.rootLogger
|
|
880
|
+
},
|
|
881
|
+
createRootContext({ config, logger }) {
|
|
882
|
+
return backendCommon.ServerTokenManager.fromConfig(config, {
|
|
883
|
+
logger,
|
|
884
|
+
allowDisabledTokenManager: true
|
|
885
|
+
});
|
|
3930
886
|
},
|
|
3931
|
-
async factory(
|
|
3932
|
-
return
|
|
887
|
+
async factory(_deps, tokenManager) {
|
|
888
|
+
return tokenManager;
|
|
3933
889
|
}
|
|
3934
890
|
});
|
|
3935
891
|
|
|
3936
|
-
const userInfoServiceFactory = userInfoServiceFactory$1;
|
|
3937
|
-
|
|
3938
|
-
exports.DefaultRootHttpRouter = DefaultRootHttpRouter;
|
|
3939
|
-
exports.HostDiscovery = HostDiscovery;
|
|
3940
|
-
exports.MiddlewareFactory = MiddlewareFactory;
|
|
3941
|
-
exports.WinstonLogger = WinstonLogger;
|
|
3942
|
-
exports.authServiceFactory = authServiceFactory;
|
|
3943
|
-
exports.cacheServiceFactory = cacheServiceFactory;
|
|
3944
|
-
exports.createConfigSecretEnumerator = createConfigSecretEnumerator;
|
|
3945
|
-
exports.createHttpServer = createHttpServer;
|
|
3946
|
-
exports.createLifecycleMiddleware = createLifecycleMiddleware;
|
|
3947
892
|
exports.createSpecializedBackend = createSpecializedBackend;
|
|
3948
|
-
exports.databaseServiceFactory = databaseServiceFactory;
|
|
3949
|
-
exports.discoveryServiceFactory = discoveryServiceFactory;
|
|
3950
|
-
exports.httpAuthServiceFactory = httpAuthServiceFactory;
|
|
3951
|
-
exports.httpRouterServiceFactory = httpRouterServiceFactory;
|
|
3952
893
|
exports.identityServiceFactory = identityServiceFactory;
|
|
3953
|
-
exports.lifecycleServiceFactory = lifecycleServiceFactory;
|
|
3954
|
-
exports.loadBackendConfig = loadBackendConfig;
|
|
3955
|
-
exports.loggerServiceFactory = loggerServiceFactory;
|
|
3956
|
-
exports.permissionsServiceFactory = permissionsServiceFactory;
|
|
3957
|
-
exports.readCorsOptions = readCorsOptions;
|
|
3958
|
-
exports.readHelmetOptions = readHelmetOptions;
|
|
3959
|
-
exports.readHttpServerOptions = readHttpServerOptions;
|
|
3960
|
-
exports.rootConfigServiceFactory = rootConfigServiceFactory;
|
|
3961
|
-
exports.rootHttpRouterServiceFactory = rootHttpRouterServiceFactory;
|
|
3962
|
-
exports.rootLifecycleServiceFactory = rootLifecycleServiceFactory;
|
|
3963
|
-
exports.rootLoggerServiceFactory = rootLoggerServiceFactory;
|
|
3964
|
-
exports.schedulerServiceFactory = schedulerServiceFactory;
|
|
3965
894
|
exports.tokenManagerServiceFactory = tokenManagerServiceFactory;
|
|
3966
|
-
exports.urlReaderServiceFactory = urlReaderServiceFactory;
|
|
3967
|
-
exports.userInfoServiceFactory = userInfoServiceFactory;
|
|
3968
895
|
//# sourceMappingURL=index.cjs.js.map
|