@backstage/cli 0.36.0-next.1 → 0.36.0-next.2
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 +15 -0
- package/dist/index.cjs.js +1 -0
- package/dist/modules/auth/commands/list.cjs.js +23 -0
- package/dist/modules/auth/commands/login.cjs.js +316 -0
- package/dist/modules/auth/commands/logout.cjs.js +55 -0
- package/dist/modules/auth/commands/printToken.cjs.js +41 -0
- package/dist/modules/auth/commands/select.cjs.js +32 -0
- package/dist/modules/auth/commands/show.cjs.js +59 -0
- package/dist/modules/auth/index.cjs.js +44 -0
- package/dist/modules/auth/lib/auth.cjs.js +60 -0
- package/dist/modules/auth/lib/http.cjs.js +26 -0
- package/dist/modules/auth/lib/localServer.cjs.js +80 -0
- package/dist/modules/auth/lib/pkce.cjs.js +23 -0
- package/dist/modules/auth/lib/prompt.cjs.js +44 -0
- package/dist/modules/auth/lib/secretStore.cjs.js +81 -0
- package/dist/modules/auth/lib/storage.cjs.js +152 -0
- package/dist/modules/build/commands/buildWorkspace.cjs.js +31 -2
- package/dist/modules/build/commands/package/build/command.cjs.js +62 -15
- package/dist/modules/build/commands/package/build/index.cjs.js +3 -1
- package/dist/modules/build/commands/package/clean.cjs.js +1 -1
- package/dist/modules/build/commands/package/postpack.cjs.js +1 -1
- package/dist/modules/build/commands/package/prepack.cjs.js +1 -1
- package/dist/modules/build/commands/package/start/command.cjs.js +68 -11
- package/dist/modules/build/commands/package/start/index.cjs.js +3 -1
- package/dist/modules/build/commands/repo/build.cjs.js +45 -10
- package/dist/modules/build/commands/repo/clean.cjs.js +1 -1
- package/dist/modules/build/commands/repo/start.cjs.js +54 -5
- package/dist/modules/build/index.cjs.js +5 -129
- package/dist/modules/build/lib/optionsParser.cjs.js +9 -24
- package/dist/modules/build/lib/role.cjs.js +1 -1
- package/dist/modules/config/commands/docs.cjs.js +1 -0
- package/dist/modules/config/commands/print.cjs.js +3 -2
- package/dist/modules/config/commands/schema.cjs.js +3 -2
- package/dist/modules/config/commands/validate.cjs.js +1 -0
- package/dist/modules/create-github-app/commands/create-github-app/index.cjs.js +14 -3
- package/dist/modules/create-github-app/index.cjs.js +1 -9
- package/dist/modules/info/commands/info.cjs.js +1 -0
- package/dist/modules/lint/commands/package/lint.cjs.js +42 -10
- package/dist/modules/lint/commands/repo/lint.cjs.js +95 -25
- package/dist/modules/lint/index.cjs.js +2 -60
- package/dist/modules/lint/lib/optionsParser.cjs.js +9 -24
- package/dist/modules/maintenance/commands/repo/fix.cjs.js +31 -5
- package/dist/modules/maintenance/commands/repo/list-deprecations.cjs.js +20 -4
- package/dist/modules/maintenance/index.cjs.js +2 -20
- package/dist/modules/migrate/commands/packageExports.cjs.js +1 -1
- package/dist/modules/migrate/commands/packageLintConfigs.cjs.js +1 -1
- package/dist/modules/migrate/commands/packageRole.cjs.js +1 -1
- package/dist/modules/migrate/commands/packageScripts.cjs.js +1 -1
- package/dist/modules/migrate/commands/reactRouterDeps.cjs.js +1 -1
- package/dist/modules/migrate/commands/versions/bump.cjs.js +41 -11
- package/dist/modules/migrate/commands/versions/migrate.cjs.js +24 -3
- package/dist/modules/migrate/index.cjs.js +2 -25
- package/dist/modules/new/commands/new.cjs.js +70 -15
- package/dist/modules/new/index.cjs.js +1 -29
- package/dist/modules/new/lib/preparation/loadPortableTemplate.cjs.js +2 -2
- package/dist/modules/new/lib/preparation/loadPortableTemplateConfig.cjs.js +2 -2
- package/dist/modules/test/commands/package/test.cjs.js +1 -7
- package/dist/modules/test/commands/repo/test.cjs.js +45 -34
- package/dist/modules/test/index.cjs.js +2 -32
- package/dist/modules/translations/commands/export.cjs.js +1 -0
- package/dist/modules/translations/commands/import.cjs.js +1 -0
- package/dist/packages/backend-defaults/package.json.cjs.js +1 -1
- package/dist/packages/backend-plugin-api/package.json.cjs.js +1 -1
- package/dist/packages/backend-test-utils/package.json.cjs.js +1 -1
- package/dist/packages/catalog-client/package.json.cjs.js +1 -1
- package/dist/packages/cli/package.json.cjs.js +7 -4
- package/dist/packages/core-app-api/package.json.cjs.js +1 -1
- package/dist/packages/core-components/package.json.cjs.js +1 -1
- package/dist/packages/core-plugin-api/package.json.cjs.js +1 -1
- package/dist/packages/frontend-defaults/package.json.cjs.js +1 -1
- package/dist/packages/frontend-plugin-api/package.json.cjs.js +1 -1
- package/dist/packages/frontend-test-utils/package.json.cjs.js +1 -1
- package/dist/plugins/auth-backend/package.json.cjs.js +1 -1
- package/dist/plugins/auth-backend-module-guest-provider/package.json.cjs.js +1 -1
- package/dist/plugins/catalog-node/package.json.cjs.js +1 -1
- package/dist/plugins/scaffolder-node/package.json.cjs.js +1 -1
- package/dist/plugins/scaffolder-node-test-utils/package.json.cjs.js +1 -1
- package/dist/wiring/CliInitializer.cjs.js +12 -2
- package/package.json +23 -17
- package/dist/wiring/lazy.cjs.js +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @backstage/cli
|
|
2
2
|
|
|
3
|
+
## 0.36.0-next.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d0f4cd2: Added new `auth` command group for authenticating the CLI with Backstage instances using OAuth 2.0 with a pre-registered client metadata document. Commands include `login`, `logout`, `list`, `show`, `print-token`, and `select` for managing multiple authenticated instances.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- a4e5902: Internal refactor of the CLI command registration
|
|
12
|
+
- ff4a45a: Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. Several camelCase CLI flags have been deprecated in favor of their kebab-case equivalents (e.g. `--successCache` → `--success-cache`). The old camelCase forms still work but will now log a deprecation warning. Please update any scripts or CI configurations to use the kebab-case versions.
|
|
13
|
+
- 4a75544: Updated dependency `react-refresh` to `^0.18.0`.
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @backstage/cli-common@0.2.0-next.2
|
|
16
|
+
- @backstage/integration@2.0.0-next.2
|
|
17
|
+
|
|
3
18
|
## 0.36.0-next.1
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -14,6 +14,7 @@ var CliInitializer = require('./wiring/CliInitializer.cjs.js');
|
|
|
14
14
|
initializer.add(import('./modules/new/index.cjs.js'));
|
|
15
15
|
initializer.add(import('./modules/test/index.cjs.js'));
|
|
16
16
|
initializer.add(import('./modules/translations/index.cjs.js'));
|
|
17
|
+
initializer.add(import('./modules/auth/index.cjs.js'));
|
|
17
18
|
await initializer.run();
|
|
18
19
|
})();
|
|
19
20
|
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cleye = require('cleye');
|
|
6
|
+
var storage = require('../lib/storage.cjs.js');
|
|
7
|
+
|
|
8
|
+
var list = async ({ args, info }) => {
|
|
9
|
+
cleye.cli({ help: info }, void 0, args);
|
|
10
|
+
const { instances, selected } = await storage.getAllInstances();
|
|
11
|
+
if (!instances.length) {
|
|
12
|
+
process.stderr.write("No instances found\n");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const inst of instances) {
|
|
16
|
+
const mark = inst.name === selected?.name ? "* " : " ";
|
|
17
|
+
process.stdout.write(`${mark}${inst.name} - ${inst.baseUrl}
|
|
18
|
+
`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
exports.default = list;
|
|
23
|
+
//# sourceMappingURL=list.cjs.js.map
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cleye = require('cleye');
|
|
6
|
+
var localServer = require('../lib/localServer.cjs.js');
|
|
7
|
+
var node_child_process = require('node:child_process');
|
|
8
|
+
var pkce = require('../lib/pkce.cjs.js');
|
|
9
|
+
var http = require('../lib/http.cjs.js');
|
|
10
|
+
var storage = require('../lib/storage.cjs.js');
|
|
11
|
+
var secretStore = require('../lib/secretStore.cjs.js');
|
|
12
|
+
var crypto = require('node:crypto');
|
|
13
|
+
var fs = require('fs-extra');
|
|
14
|
+
var path = require('node:path');
|
|
15
|
+
var glob = require('glob');
|
|
16
|
+
var YAML = require('yaml');
|
|
17
|
+
var inquirer = require('inquirer');
|
|
18
|
+
|
|
19
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
20
|
+
|
|
21
|
+
var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
|
|
22
|
+
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
23
|
+
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
24
|
+
var glob__default = /*#__PURE__*/_interopDefaultCompat(glob);
|
|
25
|
+
var YAML__default = /*#__PURE__*/_interopDefaultCompat(YAML);
|
|
26
|
+
var inquirer__default = /*#__PURE__*/_interopDefaultCompat(inquirer);
|
|
27
|
+
|
|
28
|
+
const TOKEN_EXCHANGE_TIMEOUT_MS = 3e4;
|
|
29
|
+
var login = async ({ args, info }) => {
|
|
30
|
+
const {
|
|
31
|
+
flags: { backendUrl, noBrowser, instance: instanceFlag }
|
|
32
|
+
} = cleye.cli(
|
|
33
|
+
{
|
|
34
|
+
help: info,
|
|
35
|
+
flags: {
|
|
36
|
+
backendUrl: { type: String, description: "Backend base URL" },
|
|
37
|
+
noBrowser: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
description: "Do not open browser automatically"
|
|
40
|
+
},
|
|
41
|
+
instance: {
|
|
42
|
+
type: String,
|
|
43
|
+
description: "Name for this instance (used by other auth commands)"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
void 0,
|
|
48
|
+
args
|
|
49
|
+
);
|
|
50
|
+
const { instances, selected } = await storage.getAllInstances();
|
|
51
|
+
let backendBaseUrl;
|
|
52
|
+
let instanceName;
|
|
53
|
+
if (instanceFlag) {
|
|
54
|
+
instanceName = instanceFlag;
|
|
55
|
+
const targetInstance = instances.find((i) => i.name === instanceFlag);
|
|
56
|
+
if (targetInstance) {
|
|
57
|
+
backendBaseUrl = normalizeUrl(backendUrl) ?? targetInstance.baseUrl;
|
|
58
|
+
} else {
|
|
59
|
+
backendBaseUrl = normalizeUrl(backendUrl) ?? await pickBaseUrl();
|
|
60
|
+
}
|
|
61
|
+
} else if (backendUrl) {
|
|
62
|
+
backendBaseUrl = normalizeUrl(backendUrl);
|
|
63
|
+
instanceName = deriveInstanceName(backendBaseUrl);
|
|
64
|
+
} else if (instances.length > 0) {
|
|
65
|
+
const choice = await promptForInstance(instances, selected);
|
|
66
|
+
if (choice === "__new__") {
|
|
67
|
+
backendBaseUrl = await pickBaseUrl();
|
|
68
|
+
instanceName = deriveInstanceName(backendBaseUrl);
|
|
69
|
+
} else {
|
|
70
|
+
const targetInstance = instances.find((i) => i.name === choice);
|
|
71
|
+
if (!targetInstance) {
|
|
72
|
+
throw new Error("Instance not found");
|
|
73
|
+
}
|
|
74
|
+
backendBaseUrl = targetInstance.baseUrl;
|
|
75
|
+
instanceName = targetInstance.name;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
backendBaseUrl = await pickBaseUrl();
|
|
79
|
+
instanceName = deriveInstanceName(backendBaseUrl);
|
|
80
|
+
}
|
|
81
|
+
const authBaseUrl = `${backendBaseUrl}/api/auth`;
|
|
82
|
+
const clientId = `${authBaseUrl}/.well-known/oauth-client/cli.json`;
|
|
83
|
+
const metadataResponse = await fetch(clientId, {
|
|
84
|
+
signal: AbortSignal.timeout(3e4)
|
|
85
|
+
});
|
|
86
|
+
if (!metadataResponse.ok) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Server does not support CLI authentication. Ensure CIMD is enabled on the backend.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const { verifier, challenge, state } = createPkceState();
|
|
92
|
+
const callback = await localServer.startCallbackServer({ state });
|
|
93
|
+
try {
|
|
94
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
95
|
+
authBaseUrl,
|
|
96
|
+
clientId,
|
|
97
|
+
redirectUri: callback.url,
|
|
98
|
+
state,
|
|
99
|
+
challenge
|
|
100
|
+
});
|
|
101
|
+
await openBrowserOrPrint(authorizeUrl, noBrowser);
|
|
102
|
+
const code = await waitForAuthorizationCode(callback, state);
|
|
103
|
+
const token = await exchangeAuthorizationCode({
|
|
104
|
+
authBaseUrl,
|
|
105
|
+
code,
|
|
106
|
+
redirectUri: callback.url,
|
|
107
|
+
verifier
|
|
108
|
+
});
|
|
109
|
+
await persistInstance({
|
|
110
|
+
instanceName,
|
|
111
|
+
backendBaseUrl,
|
|
112
|
+
clientId,
|
|
113
|
+
token
|
|
114
|
+
});
|
|
115
|
+
process.stdout.write("Login successful\n");
|
|
116
|
+
} finally {
|
|
117
|
+
await callback.close();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
async function promptForInstance(instances, selected) {
|
|
121
|
+
const choices = instances.map((i) => ({
|
|
122
|
+
name: `${i.name === selected?.name ? "* " : " "}${i.name} (${i.baseUrl})`,
|
|
123
|
+
value: i.name
|
|
124
|
+
}));
|
|
125
|
+
choices.push({
|
|
126
|
+
name: "Add new instance...",
|
|
127
|
+
value: "__new__"
|
|
128
|
+
});
|
|
129
|
+
const { choice } = await inquirer__default.default.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: "list",
|
|
132
|
+
name: "choice",
|
|
133
|
+
message: "Select instance to authenticate:",
|
|
134
|
+
choices,
|
|
135
|
+
default: selected?.name ?? "__new__"
|
|
136
|
+
}
|
|
137
|
+
]);
|
|
138
|
+
return choice;
|
|
139
|
+
}
|
|
140
|
+
async function pickBaseUrl() {
|
|
141
|
+
const cwd = process.cwd();
|
|
142
|
+
const candidates = [];
|
|
143
|
+
const patterns = [
|
|
144
|
+
"app-config.yaml",
|
|
145
|
+
"app-config.*.yaml",
|
|
146
|
+
"packages/*/app-config.yaml",
|
|
147
|
+
"packages/*/app-config.*.yaml"
|
|
148
|
+
];
|
|
149
|
+
const files = patterns.flatMap((p) => glob__default.default.sync(p, { cwd, nodir: true }));
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
try {
|
|
152
|
+
const content = await fs__default.default.readFile(path__default.default.resolve(cwd, file), "utf8");
|
|
153
|
+
const doc = YAML__default.default.parse(content);
|
|
154
|
+
const url = doc?.backend?.baseUrl;
|
|
155
|
+
if (url) {
|
|
156
|
+
candidates.push({ url: normalizeUrl(url), file });
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const list = [...new Map(candidates.map((c) => [c.url, c])).values()];
|
|
162
|
+
if (list.length === 0) {
|
|
163
|
+
const { manual } = await inquirer__default.default.prompt([
|
|
164
|
+
{ type: "input", name: "manual", message: "Enter backend base URL" }
|
|
165
|
+
]);
|
|
166
|
+
return normalizeUrl(manual);
|
|
167
|
+
}
|
|
168
|
+
if (list.length === 1) {
|
|
169
|
+
return list[0].url;
|
|
170
|
+
}
|
|
171
|
+
const { picked } = await inquirer__default.default.prompt([
|
|
172
|
+
{
|
|
173
|
+
type: "list",
|
|
174
|
+
name: "picked",
|
|
175
|
+
message: "Select backend base URL",
|
|
176
|
+
choices: [
|
|
177
|
+
...list.map((e) => ({ name: `${e.url} (${e.file})`, value: e.url })),
|
|
178
|
+
{ name: "Enter manually", value: "__manual__" }
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
]);
|
|
182
|
+
if (picked === "__manual__") {
|
|
183
|
+
const { manual } = await inquirer__default.default.prompt([
|
|
184
|
+
{ type: "input", name: "manual", message: "Enter backend base URL" }
|
|
185
|
+
]);
|
|
186
|
+
return normalizeUrl(manual);
|
|
187
|
+
}
|
|
188
|
+
return picked;
|
|
189
|
+
}
|
|
190
|
+
function normalizeUrl(u) {
|
|
191
|
+
if (u === void 0) {
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const url = new URL(u);
|
|
196
|
+
return url.toString().replace(/\/$/, "");
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(`'${u}' is not a valid URL`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function deriveInstanceName(url) {
|
|
202
|
+
return new URL(url).host;
|
|
203
|
+
}
|
|
204
|
+
function createPkceState() {
|
|
205
|
+
const verifier = pkce.generateVerifier();
|
|
206
|
+
const challenge = pkce.challengeFromVerifier(verifier);
|
|
207
|
+
const state = cryptoRandom();
|
|
208
|
+
return { verifier, challenge, state };
|
|
209
|
+
}
|
|
210
|
+
function buildAuthorizeUrl(options) {
|
|
211
|
+
const { authBaseUrl, clientId, redirectUri, state, challenge } = options;
|
|
212
|
+
const authorize = new URL(`${authBaseUrl}/v1/authorize`);
|
|
213
|
+
authorize.searchParams.set("client_id", clientId);
|
|
214
|
+
authorize.searchParams.set("redirect_uri", redirectUri);
|
|
215
|
+
authorize.searchParams.set("response_type", "code");
|
|
216
|
+
authorize.searchParams.set("scope", "openid offline_access");
|
|
217
|
+
authorize.searchParams.set("state", state);
|
|
218
|
+
authorize.searchParams.set("code_challenge", challenge);
|
|
219
|
+
authorize.searchParams.set("code_challenge_method", "S256");
|
|
220
|
+
return authorize.toString();
|
|
221
|
+
}
|
|
222
|
+
async function openBrowserOrPrint(url, noBrowser) {
|
|
223
|
+
if (noBrowser) {
|
|
224
|
+
process.stdout.write(`Open this URL to continue: ${url}
|
|
225
|
+
`);
|
|
226
|
+
} else {
|
|
227
|
+
process.stdout.write(`Opening the following URL: ${url}
|
|
228
|
+
`);
|
|
229
|
+
openInBrowser(url);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function waitForAuthorizationCode(callback, expectedState) {
|
|
233
|
+
const { code, state } = await callback.waitForCode();
|
|
234
|
+
if (state !== expectedState) {
|
|
235
|
+
throw new Error("State mismatch");
|
|
236
|
+
}
|
|
237
|
+
return code;
|
|
238
|
+
}
|
|
239
|
+
async function exchangeAuthorizationCode(options) {
|
|
240
|
+
const { authBaseUrl, code, redirectUri, verifier } = options;
|
|
241
|
+
return await http.httpJson(`${authBaseUrl}/v1/token`, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
body: {
|
|
244
|
+
grant_type: "authorization_code",
|
|
245
|
+
code,
|
|
246
|
+
redirect_uri: redirectUri,
|
|
247
|
+
code_verifier: verifier
|
|
248
|
+
},
|
|
249
|
+
signal: AbortSignal.timeout(TOKEN_EXCHANGE_TIMEOUT_MS)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async function persistInstance(options) {
|
|
253
|
+
const { instanceName, backendBaseUrl, clientId, token } = options;
|
|
254
|
+
const secretStore$1 = await secretStore.getSecretStore();
|
|
255
|
+
await storage.withMetadataLock(async () => {
|
|
256
|
+
const service = `backstage-cli:auth-instance:${instanceName}`;
|
|
257
|
+
await secretStore$1.set(service, "accessToken", token.access_token);
|
|
258
|
+
if (token.refresh_token) {
|
|
259
|
+
await secretStore$1.set(service, "refreshToken", token.refresh_token);
|
|
260
|
+
} else {
|
|
261
|
+
process.stderr.write(
|
|
262
|
+
"Warning: No refresh token received. You will need to re-authenticate when the access token expires.\n"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
let existing;
|
|
266
|
+
try {
|
|
267
|
+
existing = await storage.getInstanceByName(instanceName);
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
await storage.upsertInstance({
|
|
271
|
+
name: instanceName,
|
|
272
|
+
baseUrl: backendBaseUrl,
|
|
273
|
+
clientId,
|
|
274
|
+
issuedAt: Date.now(),
|
|
275
|
+
accessTokenExpiresAt: Date.now() + token.expires_in * 1e3,
|
|
276
|
+
selected: existing?.selected
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function cryptoRandom() {
|
|
281
|
+
return crypto__default.default.randomBytes(32).toString("hex");
|
|
282
|
+
}
|
|
283
|
+
function openInBrowser(url) {
|
|
284
|
+
const handleError = (error) => {
|
|
285
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
286
|
+
process.stderr.write(
|
|
287
|
+
`Warning: Failed to open browser automatically: ${message}
|
|
288
|
+
`
|
|
289
|
+
);
|
|
290
|
+
process.stderr.write(`Please open this URL manually: ${url}
|
|
291
|
+
`);
|
|
292
|
+
};
|
|
293
|
+
const spawnOpts = { detached: true, stdio: "ignore" };
|
|
294
|
+
let child;
|
|
295
|
+
try {
|
|
296
|
+
if (process.platform === "darwin") {
|
|
297
|
+
child = node_child_process.spawn("open", [url], spawnOpts);
|
|
298
|
+
} else if (process.platform === "win32") {
|
|
299
|
+
child = node_child_process.spawn(
|
|
300
|
+
"powershell",
|
|
301
|
+
["-Command", `Start-Process '${url.replace(/'/g, "''")}'`],
|
|
302
|
+
spawnOpts
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
child = node_child_process.spawn("xdg-open", [url], spawnOpts);
|
|
306
|
+
}
|
|
307
|
+
child.unref();
|
|
308
|
+
child.on("error", handleError);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
handleError(error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
exports.default = login;
|
|
315
|
+
exports.openInBrowser = openInBrowser;
|
|
316
|
+
//# sourceMappingURL=login.cjs.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cleye = require('cleye');
|
|
6
|
+
var secretStore = require('../lib/secretStore.cjs.js');
|
|
7
|
+
var storage = require('../lib/storage.cjs.js');
|
|
8
|
+
var http = require('../lib/http.cjs.js');
|
|
9
|
+
var prompt = require('../lib/prompt.cjs.js');
|
|
10
|
+
|
|
11
|
+
var logout = async ({ args, info }) => {
|
|
12
|
+
const {
|
|
13
|
+
flags: { instance: instanceFlag }
|
|
14
|
+
} = cleye.cli(
|
|
15
|
+
{
|
|
16
|
+
help: info,
|
|
17
|
+
flags: {
|
|
18
|
+
instance: {
|
|
19
|
+
type: String,
|
|
20
|
+
description: "Name of the instance to log out"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
void 0,
|
|
25
|
+
args
|
|
26
|
+
);
|
|
27
|
+
const { name: instanceName } = await prompt.pickInstance(instanceFlag);
|
|
28
|
+
await storage.withMetadataLock(async () => {
|
|
29
|
+
const instance = await storage.getInstanceByName(instanceName);
|
|
30
|
+
const secretStore$1 = await secretStore.getSecretStore();
|
|
31
|
+
const service = `backstage-cli:auth-instance:${instanceName}`;
|
|
32
|
+
const refreshToken = await secretStore$1.get(service, "refreshToken") ?? "";
|
|
33
|
+
if (refreshToken) {
|
|
34
|
+
try {
|
|
35
|
+
const authBaseUrl = new URL("/api/auth", instance.baseUrl).toString().replace(/\/$/, "");
|
|
36
|
+
await http.httpJson(`${authBaseUrl}/v1/revoke`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
body: {
|
|
39
|
+
token: refreshToken,
|
|
40
|
+
token_type_hint: "refresh_token"
|
|
41
|
+
},
|
|
42
|
+
signal: AbortSignal.timeout(3e4)
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await secretStore$1.delete(service, "accessToken");
|
|
48
|
+
await secretStore$1.delete(service, "refreshToken");
|
|
49
|
+
await storage.removeInstance(instance.name);
|
|
50
|
+
});
|
|
51
|
+
process.stdout.write("Logged out\n");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
exports.default = logout;
|
|
55
|
+
//# sourceMappingURL=logout.cjs.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cleye = require('cleye');
|
|
6
|
+
var auth = require('../lib/auth.cjs.js');
|
|
7
|
+
var storage = require('../lib/storage.cjs.js');
|
|
8
|
+
var secretStore = require('../lib/secretStore.cjs.js');
|
|
9
|
+
|
|
10
|
+
var printToken = async ({ args, info }) => {
|
|
11
|
+
const {
|
|
12
|
+
flags: { instance: instanceFlag }
|
|
13
|
+
} = cleye.cli(
|
|
14
|
+
{
|
|
15
|
+
help: info,
|
|
16
|
+
flags: {
|
|
17
|
+
instance: {
|
|
18
|
+
type: String,
|
|
19
|
+
description: "Name of the instance to use"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
void 0,
|
|
24
|
+
args
|
|
25
|
+
);
|
|
26
|
+
let instance = await storage.getSelectedInstance(instanceFlag);
|
|
27
|
+
if (auth.accessTokenNeedsRefresh(instance)) {
|
|
28
|
+
instance = await auth.refreshAccessToken(instance.name);
|
|
29
|
+
}
|
|
30
|
+
const secretStore$1 = await secretStore.getSecretStore();
|
|
31
|
+
const service = `backstage-cli:auth-instance:${instance.name}`;
|
|
32
|
+
const accessToken = await secretStore$1.get(service, "accessToken");
|
|
33
|
+
if (!accessToken) {
|
|
34
|
+
throw new Error('No access token found. Run "auth login" to authenticate.');
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write(`${accessToken}
|
|
37
|
+
`);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
exports.default = printToken;
|
|
41
|
+
//# sourceMappingURL=printToken.cjs.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cleye = require('cleye');
|
|
6
|
+
var storage = require('../lib/storage.cjs.js');
|
|
7
|
+
var prompt = require('../lib/prompt.cjs.js');
|
|
8
|
+
|
|
9
|
+
var select = async ({ args, info }) => {
|
|
10
|
+
const {
|
|
11
|
+
flags: { instance: instanceFlag }
|
|
12
|
+
} = cleye.cli(
|
|
13
|
+
{
|
|
14
|
+
help: info,
|
|
15
|
+
flags: {
|
|
16
|
+
instance: {
|
|
17
|
+
type: String,
|
|
18
|
+
description: "Name of the instance to select"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
void 0,
|
|
23
|
+
args
|
|
24
|
+
);
|
|
25
|
+
const instance = await prompt.pickInstance(instanceFlag);
|
|
26
|
+
await storage.setSelectedInstance(instance.name);
|
|
27
|
+
process.stderr.write(`Selected instance '${instance.name}'
|
|
28
|
+
`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports.default = select;
|
|
32
|
+
//# sourceMappingURL=select.cjs.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cleye = require('cleye');
|
|
6
|
+
var http = require('../lib/http.cjs.js');
|
|
7
|
+
var storage = require('../lib/storage.cjs.js');
|
|
8
|
+
var auth = require('../lib/auth.cjs.js');
|
|
9
|
+
var secretStore = require('../lib/secretStore.cjs.js');
|
|
10
|
+
|
|
11
|
+
var show = async ({ args, info }) => {
|
|
12
|
+
const {
|
|
13
|
+
flags: { instance: instanceFlag }
|
|
14
|
+
} = cleye.cli(
|
|
15
|
+
{
|
|
16
|
+
help: info,
|
|
17
|
+
flags: {
|
|
18
|
+
instance: {
|
|
19
|
+
type: String,
|
|
20
|
+
description: "Name of the instance to show"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
void 0,
|
|
25
|
+
args
|
|
26
|
+
);
|
|
27
|
+
let instance = await storage.getSelectedInstance(instanceFlag);
|
|
28
|
+
if (auth.accessTokenNeedsRefresh(instance)) {
|
|
29
|
+
process.stdout.write("Refreshing access token...\n");
|
|
30
|
+
instance = await auth.refreshAccessToken(instance.name);
|
|
31
|
+
}
|
|
32
|
+
const authBase = new URL("/api/auth", instance.baseUrl).toString().replace(/\/$/, "");
|
|
33
|
+
const secretStore$1 = await secretStore.getSecretStore();
|
|
34
|
+
const service = `backstage-cli:auth-instance:${instance.name}`;
|
|
35
|
+
const accessToken = await secretStore$1.get(service, "accessToken");
|
|
36
|
+
if (!accessToken) {
|
|
37
|
+
throw new Error('No access token found. Run "auth login" to authenticate.');
|
|
38
|
+
}
|
|
39
|
+
const userinfo = await http.httpJson(
|
|
40
|
+
`${authBase}/v1/userinfo`,
|
|
41
|
+
{
|
|
42
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
43
|
+
signal: AbortSignal.timeout(3e4)
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
process.stdout.write(`User: ${userinfo.claims.sub}
|
|
47
|
+
`);
|
|
48
|
+
process.stdout.write(`
|
|
49
|
+
`);
|
|
50
|
+
process.stdout.write(`Ownership:
|
|
51
|
+
`);
|
|
52
|
+
for (const ent of userinfo.claims.ent ?? []) {
|
|
53
|
+
process.stdout.write(` - ${ent}
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
exports.default = show;
|
|
59
|
+
//# sourceMappingURL=show.cjs.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var factory = require('../../wiring/factory.cjs.js');
|
|
6
|
+
|
|
7
|
+
var index = factory.createCliPlugin({
|
|
8
|
+
pluginId: "auth",
|
|
9
|
+
init: async (reg) => {
|
|
10
|
+
reg.addCommand({
|
|
11
|
+
path: ["auth", "login"],
|
|
12
|
+
description: "Log in the CLI to a Backstage instance",
|
|
13
|
+
execute: { loader: () => import('./commands/login.cjs.js') }
|
|
14
|
+
});
|
|
15
|
+
reg.addCommand({
|
|
16
|
+
path: ["auth", "logout"],
|
|
17
|
+
description: "Log out the CLI and clear stored credentials",
|
|
18
|
+
execute: { loader: () => import('./commands/logout.cjs.js') }
|
|
19
|
+
});
|
|
20
|
+
reg.addCommand({
|
|
21
|
+
path: ["auth", "show"],
|
|
22
|
+
description: "Show details of an authenticated instance",
|
|
23
|
+
execute: { loader: () => import('./commands/show.cjs.js') }
|
|
24
|
+
});
|
|
25
|
+
reg.addCommand({
|
|
26
|
+
path: ["auth", "list"],
|
|
27
|
+
description: "List authenticated instances",
|
|
28
|
+
execute: { loader: () => import('./commands/list.cjs.js') }
|
|
29
|
+
});
|
|
30
|
+
reg.addCommand({
|
|
31
|
+
path: ["auth", "print-token"],
|
|
32
|
+
description: "Print an access token to stdout (auto-refresh if needed)",
|
|
33
|
+
execute: { loader: () => import('./commands/printToken.cjs.js') }
|
|
34
|
+
});
|
|
35
|
+
reg.addCommand({
|
|
36
|
+
path: ["auth", "select"],
|
|
37
|
+
description: "Select the default instance",
|
|
38
|
+
execute: { loader: () => import('./commands/select.cjs.js') }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
exports.default = index;
|
|
44
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var storage = require('./storage.cjs.js');
|
|
5
|
+
var secretStore = require('./secretStore.cjs.js');
|
|
6
|
+
var http = require('./http.cjs.js');
|
|
7
|
+
|
|
8
|
+
const TokenResponseSchema = zod.z.object({
|
|
9
|
+
access_token: zod.z.string().min(1),
|
|
10
|
+
token_type: zod.z.string().min(1),
|
|
11
|
+
expires_in: zod.z.number().positive().finite(),
|
|
12
|
+
refresh_token: zod.z.string().min(1).optional()
|
|
13
|
+
});
|
|
14
|
+
function accessTokenNeedsRefresh(instance) {
|
|
15
|
+
return instance.accessTokenExpiresAt <= Date.now() + 2 * 6e4;
|
|
16
|
+
}
|
|
17
|
+
async function refreshAccessToken(instanceName) {
|
|
18
|
+
const secretStore$1 = await secretStore.getSecretStore();
|
|
19
|
+
return storage.withMetadataLock(async () => {
|
|
20
|
+
const instance = await storage.getInstanceByName(instanceName);
|
|
21
|
+
const service = `backstage-cli:auth-instance:${instanceName}`;
|
|
22
|
+
const refreshToken = await secretStore$1.get(service, "refreshToken") ?? "";
|
|
23
|
+
if (!refreshToken) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"Access token is expired and no refresh token is available"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const response = await http.httpJson(
|
|
29
|
+
`${instance.baseUrl}/api/auth/v1/token`,
|
|
30
|
+
{
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: {
|
|
33
|
+
grant_type: "refresh_token",
|
|
34
|
+
refresh_token: refreshToken
|
|
35
|
+
},
|
|
36
|
+
signal: AbortSignal.timeout(3e4)
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
const parsed = TokenResponseSchema.safeParse(response);
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
throw new Error(`Invalid token response: ${parsed.error.message}`);
|
|
42
|
+
}
|
|
43
|
+
const token = parsed.data;
|
|
44
|
+
await secretStore$1.set(service, "accessToken", token.access_token);
|
|
45
|
+
if (token.refresh_token) {
|
|
46
|
+
await secretStore$1.set(service, "refreshToken", token.refresh_token);
|
|
47
|
+
}
|
|
48
|
+
const newInstance = {
|
|
49
|
+
...instance,
|
|
50
|
+
issuedAt: Date.now(),
|
|
51
|
+
accessTokenExpiresAt: Date.now() + token.expires_in * 1e3
|
|
52
|
+
};
|
|
53
|
+
await storage.upsertInstance(newInstance);
|
|
54
|
+
return newInstance;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exports.accessTokenNeedsRefresh = accessTokenNeedsRefresh;
|
|
59
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
60
|
+
//# sourceMappingURL=auth.cjs.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fetch = require('cross-fetch');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
5
|
+
|
|
6
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
9
|
+
|
|
10
|
+
async function httpJson(url, init) {
|
|
11
|
+
const res = await fetch__default.default(url, {
|
|
12
|
+
...init,
|
|
13
|
+
body: init?.body ? JSON.stringify(init.body) : void 0,
|
|
14
|
+
headers: {
|
|
15
|
+
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
16
|
+
...init?.headers
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw await errors.ResponseError.fromResponse(res);
|
|
21
|
+
}
|
|
22
|
+
return await res.json();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
exports.httpJson = httpJson;
|
|
26
|
+
//# sourceMappingURL=http.cjs.js.map
|