@backstage/cli-node 0.2.19-next.1 → 0.3.1-next.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/config/nodeTransform.cjs +87 -0
  3. package/config/nodeTransformHooks.mjs +294 -0
  4. package/dist/auth/CliAuth.cjs.js +108 -0
  5. package/dist/auth/CliAuth.cjs.js.map +1 -0
  6. package/dist/auth/authIdentifiers.cjs.js +8 -0
  7. package/dist/auth/authIdentifiers.cjs.js.map +1 -0
  8. package/dist/auth/httpJson.cjs.js +21 -0
  9. package/dist/auth/httpJson.cjs.js.map +1 -0
  10. package/dist/auth/secretStore.cjs.js +96 -0
  11. package/dist/auth/secretStore.cjs.js.map +1 -0
  12. package/dist/auth/storage.cjs.js +161 -0
  13. package/dist/auth/storage.cjs.js.map +1 -0
  14. package/dist/cli-internal/src/InternalCliModule.cjs.js +11 -0
  15. package/dist/cli-internal/src/InternalCliModule.cjs.js.map +1 -0
  16. package/dist/cli-internal/src/InternalCommandNode.cjs.js +25 -0
  17. package/dist/cli-internal/src/InternalCommandNode.cjs.js.map +1 -0
  18. package/dist/cli-internal/src/knownPluginPackages.cjs.js +40 -0
  19. package/dist/cli-internal/src/knownPluginPackages.cjs.js.map +1 -0
  20. package/dist/cli-module/createCliModule.cjs.js +25 -0
  21. package/dist/cli-module/createCliModule.cjs.js.map +1 -0
  22. package/dist/cli-module/runCliModule.cjs.js +138 -0
  23. package/dist/cli-module/runCliModule.cjs.js.map +1 -0
  24. package/dist/index.cjs.js +6 -0
  25. package/dist/index.cjs.js.map +1 -1
  26. package/dist/index.d.ts +226 -3
  27. package/dist/opaque-internal/src/OpaqueType.cjs.js +105 -0
  28. package/dist/opaque-internal/src/OpaqueType.cjs.js.map +1 -0
  29. package/dist/roles/PackageRoles.cjs.js +22 -17
  30. package/dist/roles/PackageRoles.cjs.js.map +1 -1
  31. package/dist/yarn/yarnPlugin.cjs.js +8 -9
  32. package/dist/yarn/yarnPlugin.cjs.js.map +1 -1
  33. package/package.json +24 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @backstage/cli-node
2
2
 
3
+ ## 0.3.1-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @backstage/cli-common@0.2.1-next.0
9
+ - @backstage/errors@1.2.7
10
+ - @backstage/types@1.2.2
11
+
12
+ ## 0.3.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 7d055ef: Added `createCliModule` API and related types for building Backstage CLI plugins.
17
+
18
+ ### Patch Changes
19
+
20
+ - 94a885a: Added a new `cli-module` package role for packages that provide CLI plugin extensions.
21
+ - 12fa965: Added `CliAuth` class for managing CLI authentication state. This provides a class-based API with a static `create` method that resolves the currently selected (or explicitly named) auth instance, transparently refreshes expired access tokens, and exposes helpers for other CLI modules to authenticate with a Backstage backend.
22
+ - 61cb976: Added `toString()` method to `Lockfile` for serializing lockfiles back to string format.
23
+ - 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code.
24
+ - 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
25
+ - 3c811bf: Added `hasBackstageYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`.
26
+ - a49a40d: Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports.
27
+ - a9d23c4: Properly support `package.json` `workspaces` field
28
+ - Updated dependencies
29
+ - @backstage/cli-common@0.2.0
30
+
3
31
  ## 0.2.19-next.1
4
32
 
5
33
  ### Patch Changes
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Copyright 2024 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ const { pathToFileURL } = require('node:url');
18
+ const { transformSync } = require('@swc/core');
19
+ const { addHook } = require('pirates');
20
+ const { Module } = require('node:module');
21
+
22
+ // This hooks into module resolution and overrides imports of packages that
23
+ // exist in the linked workspace to instead be resolved from the linked workspace.
24
+ if (process.env.BACKSTAGE_CLI_LINKED_WORKSPACE) {
25
+ const { join: joinPath } = require('node:path');
26
+ const { getPackagesSync } = require('@manypkg/get-packages');
27
+ const { packages: linkedPackages, root: linkedRoot } = getPackagesSync(
28
+ process.env.BACKSTAGE_CLI_LINKED_WORKSPACE,
29
+ );
30
+
31
+ // Matches all packages in the linked workspaces, as well as sub-path exports from them
32
+ const replacementRegex = new RegExp(
33
+ `^(?:${linkedPackages
34
+ .map(pkg => pkg.packageJson.name)
35
+ .join('|')})(?:/.*)?$`,
36
+ );
37
+
38
+ const origLoad = Module._load;
39
+ Module._load = function requireHook(request, parent) {
40
+ if (!replacementRegex.test(request)) {
41
+ return origLoad.call(this, request, parent);
42
+ }
43
+
44
+ // The package import that we're overriding will always existing in the root
45
+ // node_modules of the linked workspace, so it's enough to override the
46
+ // parent paths with that single entry
47
+ return origLoad.call(this, request, {
48
+ ...parent,
49
+ paths: [joinPath(linkedRoot.dir, 'node_modules')],
50
+ });
51
+ };
52
+ }
53
+
54
+ addHook(
55
+ (code, filename) => {
56
+ const transformed = transformSync(code, {
57
+ filename,
58
+ sourceMaps: 'inline',
59
+ module: {
60
+ type: 'commonjs',
61
+ ignoreDynamic: true,
62
+ },
63
+ jsc: {
64
+ target: 'es2023',
65
+ parser: {
66
+ syntax: 'typescript',
67
+ },
68
+ },
69
+ });
70
+ process.send?.({ type: 'watch', path: filename });
71
+ return transformed.code;
72
+ },
73
+ { extensions: ['.ts', '.cts'], ignoreNodeModules: true },
74
+ );
75
+
76
+ addHook(
77
+ (code, filename) => {
78
+ process.send?.({ type: 'watch', path: filename });
79
+ return code;
80
+ },
81
+ { extensions: ['.js', '.cjs'], ignoreNodeModules: true },
82
+ );
83
+
84
+ // Register module hooks, used by "type": "module" in package.json, .mjs and
85
+ // .mts files, as well as dynamic import(...)s, although dynamic imports will be
86
+ // handled be the CommonJS hooks in this file if what it points to is CommonJS.
87
+ Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename));
@@ -0,0 +1,294 @@
1
+ /*
2
+ * Copyright 2024 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { dirname, extname, resolve as resolvePath } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { transformFile } from '@swc/core';
20
+ import { isBuiltin } from 'node:module';
21
+ import { readFile } from 'node:fs/promises';
22
+ import { existsSync } from 'node:fs';
23
+
24
+ // @ts-check
25
+
26
+ // No explicit file extension, no type in package.json
27
+ const DEFAULT_MODULE_FORMAT = 'commonjs';
28
+
29
+ // Source file extensions to look for when using bundle resolution strategy
30
+ const SRC_EXTS = ['.ts', '.js'];
31
+ const TS_EXTS = ['.ts', '.mts', '.cts'];
32
+ const moduleTypeTable = {
33
+ '.mjs': 'module',
34
+ '.mts': 'module',
35
+ '.cjs': 'commonjs',
36
+ '.cts': 'commonjs',
37
+ '.ts': undefined,
38
+ '.js': undefined,
39
+ };
40
+
41
+ /** @type {import('module').ResolveHook} */
42
+ export async function resolve(specifier, context, nextResolve) {
43
+ // Built-in modules are handled by the default resolver
44
+ if (isBuiltin(specifier)) {
45
+ return nextResolve(specifier, context);
46
+ }
47
+
48
+ const ext = extname(specifier);
49
+
50
+ // Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below.
51
+ if (ext === '.json' && !context.importAttributes?.type) {
52
+ const jsonResult = await nextResolve(specifier, context);
53
+ return {
54
+ ...jsonResult,
55
+ format: 'commonjs',
56
+ importAttributes: { type: 'json' },
57
+ };
58
+ }
59
+
60
+ // Anything else with an explicit extension is handled by the default
61
+ // resolver, except that we help determine the module type where needed.
62
+ if (ext !== '') {
63
+ return withDetectedModuleType(await nextResolve(specifier, context));
64
+ }
65
+
66
+ // Other external modules are handled by the default resolver, but again we
67
+ // help determine the module type where needed.
68
+ if (!specifier.startsWith('.')) {
69
+ return withDetectedModuleType(await nextResolve(specifier, context));
70
+ }
71
+
72
+ // The rest of this function handles the case of resolving imports that do not
73
+ // specify any extension and might point to a directory with an `index.*`
74
+ // file. We resolve those using the same logic as most JS bundlers would, with
75
+ // the addition of checking if there's an explicit module format listed in the
76
+ // closest `package.json` file.
77
+ //
78
+ // We use a bundle resolution strategy in order to keep code consistent across
79
+ // Backstage codebases that contains code both for Web and Node.js, and to
80
+ // support packages with common code that can be used in both environments.
81
+ try {
82
+ // This is expected to throw, but in the event that this module specifier is
83
+ // supported we prefer to use the default resolver.
84
+ return await nextResolve(specifier, context);
85
+ } catch (error) {
86
+ if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
87
+ const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`;
88
+ const resolved = await resolveWithoutExt(spec, context, nextResolve);
89
+ if (resolved) {
90
+ return withDetectedModuleType(resolved);
91
+ }
92
+ } else if (error.code === 'ERR_MODULE_NOT_FOUND') {
93
+ const resolved = await resolveWithoutExt(specifier, context, nextResolve);
94
+ if (resolved) {
95
+ return withDetectedModuleType(resolved);
96
+ }
97
+ }
98
+
99
+ // Unexpected error or no resolution found
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Populates the `format` field in the resolved object based on the closest `package.json` file.
106
+ *
107
+ * @param {import('module').ResolveFnOutput} resolved
108
+ * @returns {Promise<import('module').ResolveFnOutput>}
109
+ */
110
+ async function withDetectedModuleType(resolved) {
111
+ // Already has an explicit format
112
+ if (resolved.format) {
113
+ return resolved;
114
+ }
115
+ // Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default.
116
+ if (resolved.format === null) {
117
+ return { ...resolved, format: DEFAULT_MODULE_FORMAT };
118
+ }
119
+
120
+ const ext = extname(resolved.url);
121
+
122
+ const explicitFormat = moduleTypeTable[ext];
123
+ if (explicitFormat) {
124
+ return {
125
+ ...resolved,
126
+ format: explicitFormat,
127
+ };
128
+ }
129
+
130
+ // Under normal circumstances .js files should reliably have a format and so
131
+ // we should only reach this point for .ts files. However, if additional
132
+ // custom loaders are being used the format may not be detected for .js files
133
+ // either. As such we don't restrict the file format at this point.
134
+
135
+ // TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring
136
+ const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url));
137
+ if (!packageJsonPath) {
138
+ return resolved;
139
+ }
140
+
141
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
142
+ return {
143
+ ...resolved,
144
+ format: packageJson.type ?? DEFAULT_MODULE_FORMAT,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Find the closest package.json file from the given path.
150
+ *
151
+ * TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable.
152
+ * @param {string} startPath
153
+ * @returns {Promise<string | undefined>}
154
+ */
155
+ async function findPackageJSON(startPath) {
156
+ let path = startPath;
157
+
158
+ // Some confidence check to avoid infinite loop
159
+ for (let i = 0; i < 1000; i++) {
160
+ const packagePath = resolvePath(path, 'package.json');
161
+ if (existsSync(packagePath)) {
162
+ return packagePath;
163
+ }
164
+
165
+ const newPath = dirname(path);
166
+ if (newPath === path) {
167
+ return undefined;
168
+ }
169
+ path = newPath;
170
+ }
171
+
172
+ throw new Error(
173
+ `Iteration limit reached when searching for package.json at ${startPath}`,
174
+ );
175
+ }
176
+
177
+ /** @type {import('module').ResolveHook} */
178
+ async function resolveWithoutExt(specifier, context, nextResolve) {
179
+ for (const tryExt of SRC_EXTS) {
180
+ try {
181
+ const resolved = await nextResolve(specifier + tryExt, {
182
+ ...context,
183
+ format: 'commonjs',
184
+ });
185
+ return {
186
+ ...resolved,
187
+ format: moduleTypeTable[tryExt] ?? resolved.format,
188
+ };
189
+ } catch {
190
+ /* ignore */
191
+ }
192
+ }
193
+ return undefined;
194
+ }
195
+
196
+ /** @type {import('module').LoadHook} */
197
+ export async function load(url, context, nextLoad) {
198
+ // Non-file URLs are handled by the default loader
199
+ if (!url.startsWith('file://')) {
200
+ return nextLoad(url, context);
201
+ }
202
+
203
+ // JSON files loaded as CommonJS are handled by this custom loader, because
204
+ // the default one doesn't work. For JSON loading to work we'd need the
205
+ // synchronous hooks that aren't supported yet, or avoid using the CommonJS
206
+ // compatibility.
207
+ if (
208
+ context.format === 'commonjs' &&
209
+ context.importAttributes?.type === 'json'
210
+ ) {
211
+ try {
212
+ // TODO(Rugvip): Make sure this is valid JSON
213
+ const content = await readFile(fileURLToPath(url), 'utf8');
214
+ return {
215
+ source: `module.exports = (${content})`,
216
+ format: 'commonjs',
217
+ shortCircuit: true,
218
+ };
219
+ } catch {
220
+ // Let the default loader generate the error
221
+ return nextLoad(url, context);
222
+ }
223
+ }
224
+
225
+ const ext = extname(url);
226
+
227
+ // Non-TS files are handled by the default loader
228
+ if (!TS_EXTS.includes(ext)) {
229
+ return nextLoad(url, context);
230
+ }
231
+
232
+ const format = context.format ?? DEFAULT_MODULE_FORMAT;
233
+
234
+ // We have two choices at this point, we can either transform CommonJS files
235
+ // and return the transformed source code, or let the default loader handle
236
+ // them. If we transform them ourselves we will enter CommonJS compatibility
237
+ // mode in the new module system in Node.js, this effectively means all
238
+ // CommonJS loaded via `require` calls from this point will all be treated as
239
+ // if it was loaded via `import` calls from modules.
240
+ //
241
+ // The CommonJS compatibility layer will try to identify named exports and
242
+ // make them available directly, which is convenient as it avoids things like
243
+ // `import(...).then(m => m.default.foo)`, allowing you to instead write
244
+ // `import(...).then(m => m.foo)`. The compatibility layer doesn't always work
245
+ // all that well though, and can lead to module loading issues in many cases,
246
+ // especially for older code.
247
+
248
+ // This `if` block opts-out of using CommonJS compatibility mode by default,
249
+ // and instead leaves it to our existing loader to transform CommonJS. We do
250
+ // however use compatibility mode for the more explicit .cts file extension,
251
+ // allows for a way to opt-in to the new behavior.
252
+ //
253
+ // TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead
254
+ if (format === 'commonjs' && ext !== '.cts') {
255
+ return nextLoad(url, { ...context, format });
256
+ }
257
+
258
+ // If the Node.js version we're running supports TypeScript, i.e. type
259
+ // stripping, we hand over to the default loader. This is done for all cases
260
+ // except if we're loading a .ts file that's been resolved to CommonJS format.
261
+ // This is because these files aren't actually CommonJS in the Backstage build
262
+ // system, and need to be transformed to CommonJS.
263
+ if (
264
+ format === 'module-typescript' ||
265
+ (format === 'module-commonjs' && ext !== '.ts')
266
+ ) {
267
+ return nextLoad(url, { ...context, format });
268
+ }
269
+
270
+ const transformed = await transformFile(fileURLToPath(url), {
271
+ sourceMaps: 'inline',
272
+ module: {
273
+ type: format === 'module' ? 'es6' : 'commonjs',
274
+ ignoreDynamic: true,
275
+
276
+ // This helps the Node.js CommonJS compat layer identify named exports.
277
+ exportInteropAnnotation: true,
278
+ },
279
+ jsc: {
280
+ target: 'es2023',
281
+ parser: {
282
+ syntax: 'typescript',
283
+ },
284
+ },
285
+ });
286
+
287
+ return {
288
+ ...context,
289
+ shortCircuit: true,
290
+ source: transformed.code,
291
+ format,
292
+ responseURL: url,
293
+ };
294
+ }
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ var storage = require('./storage.cjs.js');
4
+ var secretStore = require('./secretStore.cjs.js');
5
+ var authIdentifiers = require('./authIdentifiers.cjs.js');
6
+ var httpJson = require('./httpJson.cjs.js');
7
+ var v3 = require('zod/v3');
8
+
9
+ const TokenResponseSchema = v3.z.object({
10
+ access_token: v3.z.string().min(1),
11
+ token_type: v3.z.string().min(1),
12
+ expires_in: v3.z.number().positive().finite(),
13
+ refresh_token: v3.z.string().min(1).optional()
14
+ });
15
+ class CliAuth {
16
+ #secretStore;
17
+ #instance;
18
+ /**
19
+ * Resolve the current auth instance and return a ready-to-use
20
+ * {@link CliAuth} object. Throws when no instance can be found.
21
+ */
22
+ static async create(options) {
23
+ const instance = await storage.getSelectedInstance(options?.instanceName);
24
+ const secretStore$1 = await secretStore.getSecretStore();
25
+ return new CliAuth(instance, secretStore$1);
26
+ }
27
+ constructor(instance, secretStore) {
28
+ this.#instance = instance;
29
+ this.#secretStore = secretStore;
30
+ }
31
+ /** Returns the name of the resolved auth instance. */
32
+ getInstanceName() {
33
+ return this.#instance.name;
34
+ }
35
+ /** Returns the base URL of the resolved auth instance. */
36
+ getBaseUrl() {
37
+ return this.#instance.baseUrl;
38
+ }
39
+ /**
40
+ * Returns a valid access token, refreshing it first if the current
41
+ * token is expired or about to expire.
42
+ */
43
+ async getAccessToken() {
44
+ if (storage.accessTokenNeedsRefresh(this.#instance)) {
45
+ await this.#refreshAccessToken();
46
+ }
47
+ const service = authIdentifiers.getAuthInstanceService(this.#instance.name);
48
+ const token = await this.#secretStore.get(service, "accessToken");
49
+ if (!token) {
50
+ throw new Error(
51
+ 'No access token found. Run "auth login" to authenticate.'
52
+ );
53
+ }
54
+ return token;
55
+ }
56
+ /**
57
+ * Reads a per-instance metadata value previously stored by the
58
+ * auth module (e.g. `pluginSources`).
59
+ */
60
+ async getMetadata(key) {
61
+ return storage.getInstanceMetadata(this.#instance.name, key);
62
+ }
63
+ /**
64
+ * Writes a per-instance metadata value to the on-disk instance store.
65
+ */
66
+ async setMetadata(key, value) {
67
+ return storage.updateInstanceMetadata(this.#instance.name, key, value);
68
+ }
69
+ async #refreshAccessToken() {
70
+ const service = authIdentifiers.getAuthInstanceService(this.#instance.name);
71
+ const refreshToken = await this.#secretStore.get(service, "refreshToken") ?? "";
72
+ if (!refreshToken) {
73
+ throw new Error(
74
+ "Access token is expired and no refresh token is available"
75
+ );
76
+ }
77
+ const response = await httpJson.httpJson(
78
+ `${this.#instance.baseUrl}/api/auth/v1/token`,
79
+ {
80
+ method: "POST",
81
+ body: {
82
+ grant_type: "refresh_token",
83
+ refresh_token: refreshToken
84
+ },
85
+ signal: AbortSignal.timeout(3e4)
86
+ }
87
+ );
88
+ const parsed = TokenResponseSchema.safeParse(response);
89
+ if (!parsed.success) {
90
+ throw new Error(`Invalid token response: ${parsed.error.message}`);
91
+ }
92
+ const token = parsed.data;
93
+ await this.#secretStore.set(service, "accessToken", token.access_token);
94
+ if (token.refresh_token) {
95
+ await this.#secretStore.set(service, "refreshToken", token.refresh_token);
96
+ }
97
+ const issuedAt = Date.now();
98
+ const accessTokenExpiresAt = Date.now() + token.expires_in * 1e3;
99
+ this.#instance = { ...this.#instance, issuedAt, accessTokenExpiresAt };
100
+ await storage.updateInstance(this.#instance.name, {
101
+ issuedAt,
102
+ accessTokenExpiresAt
103
+ });
104
+ }
105
+ }
106
+
107
+ exports.CliAuth = CliAuth;
108
+ //# sourceMappingURL=CliAuth.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CliAuth.cjs.js","sources":["../../src/auth/CliAuth.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type StoredInstance,\n getSelectedInstance,\n getInstanceMetadata,\n updateInstanceMetadata,\n updateInstance,\n accessTokenNeedsRefresh,\n} from './storage';\nimport { getSecretStore, type SecretStore } from './secretStore';\nimport { getAuthInstanceService } from './authIdentifiers';\nimport { httpJson } from './httpJson';\nimport { z } from 'zod/v3';\n\nconst TokenResponseSchema = z.object({\n access_token: z.string().min(1),\n token_type: z.string().min(1),\n expires_in: z.number().positive().finite(),\n refresh_token: z.string().min(1).optional(),\n});\n\n/**\n * Options for creating a {@link CliAuth} instance.\n *\n * @public\n */\nexport interface CliAuthCreateOptions {\n /**\n * An explicit instance name to resolve. When omitted the currently\n * selected instance is used.\n */\n instanceName?: string;\n}\n\n/**\n * Manages authentication state for Backstage CLI commands.\n *\n * Reads the currently selected (or explicitly named) auth instance from\n * the on-disk instance store, transparently refreshes expired access\n * tokens, and exposes helpers that other CLI modules need to talk to a\n * Backstage backend.\n *\n * @public\n */\nexport class CliAuth {\n readonly #secretStore: SecretStore;\n #instance: StoredInstance;\n\n /**\n * Resolve the current auth instance and return a ready-to-use\n * {@link CliAuth} object. Throws when no instance can be found.\n */\n static async create(options?: CliAuthCreateOptions): Promise<CliAuth> {\n const instance = await getSelectedInstance(options?.instanceName);\n const secretStore = await getSecretStore();\n return new CliAuth(instance, secretStore);\n }\n\n private constructor(instance: StoredInstance, secretStore: SecretStore) {\n this.#instance = instance;\n this.#secretStore = secretStore;\n }\n\n /** Returns the name of the resolved auth instance. */\n getInstanceName(): string {\n return this.#instance.name;\n }\n\n /** Returns the base URL of the resolved auth instance. */\n getBaseUrl(): string {\n return this.#instance.baseUrl;\n }\n\n /**\n * Returns a valid access token, refreshing it first if the current\n * token is expired or about to expire.\n */\n async getAccessToken(): Promise<string> {\n if (accessTokenNeedsRefresh(this.#instance)) {\n await this.#refreshAccessToken();\n }\n\n const service = getAuthInstanceService(this.#instance.name);\n const token = await this.#secretStore.get(service, 'accessToken');\n if (!token) {\n throw new Error(\n 'No access token found. Run \"auth login\" to authenticate.',\n );\n }\n return token;\n }\n\n /**\n * Reads a per-instance metadata value previously stored by the\n * auth module (e.g. `pluginSources`).\n */\n async getMetadata(key: string): Promise<unknown> {\n return getInstanceMetadata(this.#instance.name, key);\n }\n\n /**\n * Writes a per-instance metadata value to the on-disk instance store.\n */\n async setMetadata(key: string, value: unknown): Promise<void> {\n return updateInstanceMetadata(this.#instance.name, key, value);\n }\n\n async #refreshAccessToken(): Promise<void> {\n const service = getAuthInstanceService(this.#instance.name);\n const refreshToken =\n (await this.#secretStore.get(service, 'refreshToken')) ?? '';\n if (!refreshToken) {\n throw new Error(\n 'Access token is expired and no refresh token is available',\n );\n }\n\n const response = await httpJson<unknown>(\n `${this.#instance.baseUrl}/api/auth/v1/token`,\n {\n method: 'POST',\n body: {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n },\n signal: AbortSignal.timeout(30_000),\n },\n );\n\n const parsed = TokenResponseSchema.safeParse(response);\n if (!parsed.success) {\n throw new Error(`Invalid token response: ${parsed.error.message}`);\n }\n const token = parsed.data;\n\n await this.#secretStore.set(service, 'accessToken', token.access_token);\n if (token.refresh_token) {\n await this.#secretStore.set(service, 'refreshToken', token.refresh_token);\n }\n const issuedAt = Date.now();\n const accessTokenExpiresAt = Date.now() + token.expires_in * 1000;\n this.#instance = { ...this.#instance, issuedAt, accessTokenExpiresAt };\n await updateInstance(this.#instance.name, {\n issuedAt,\n accessTokenExpiresAt,\n });\n }\n}\n"],"names":["z","getSelectedInstance","secretStore","getSecretStore","accessTokenNeedsRefresh","getAuthInstanceService","getInstanceMetadata","updateInstanceMetadata","httpJson","updateInstance"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ function getAuthInstanceService(instanceName) {
4
+ return `backstage-cli:auth-instance:${instanceName}`;
5
+ }
6
+
7
+ exports.getAuthInstanceService = getAuthInstanceService;
8
+ //# sourceMappingURL=authIdentifiers.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authIdentifiers.cjs.js","sources":["../../src/auth/authIdentifiers.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/** @internal */\nexport function getAuthInstanceService(instanceName: string): string {\n return `backstage-cli:auth-instance:${instanceName}`;\n}\n"],"names":[],"mappings":";;AAiBO,SAAS,uBAAuB,YAAA,EAA8B;AACnE,EAAA,OAAO,+BAA+B,YAAY,CAAA,CAAA;AACpD;;;;"}
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ var errors = require('@backstage/errors');
4
+
5
+ async function httpJson(url, init) {
6
+ const res = await fetch(url, {
7
+ ...init,
8
+ body: init?.body ? JSON.stringify(init.body) : void 0,
9
+ headers: {
10
+ ...init?.body ? { "Content-Type": "application/json" } : {},
11
+ ...init?.headers
12
+ }
13
+ });
14
+ if (!res.ok) {
15
+ throw await errors.ResponseError.fromResponse(res);
16
+ }
17
+ return await res.json();
18
+ }
19
+
20
+ exports.httpJson = httpJson;
21
+ //# sourceMappingURL=httpJson.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpJson.cjs.js","sources":["../../src/auth/httpJson.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ResponseError } from '@backstage/errors';\n\n/** @internal */\nexport type HttpInit = {\n headers?: Record<string, string>;\n method?: string;\n body?: any;\n signal?: AbortSignal;\n};\n\n/** @internal */\nexport async function httpJson<T>(url: string, init?: HttpInit): Promise<T> {\n const res = await fetch(url, {\n ...init,\n body: init?.body ? JSON.stringify(init.body) : undefined,\n headers: {\n ...(init?.body ? { 'Content-Type': 'application/json' } : {}),\n ...init?.headers,\n },\n });\n if (!res.ok) {\n throw await ResponseError.fromResponse(res);\n }\n return (await res.json()) as T;\n}\n"],"names":["ResponseError"],"mappings":";;;;AA2BA,eAAsB,QAAA,CAAY,KAAa,IAAA,EAA6B;AAC1E,EAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAC3B,GAAG,IAAA;AAAA,IACH,MAAM,IAAA,EAAM,IAAA,GAAO,KAAK,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA,GAAI,MAAA;AAAA,IAC/C,OAAA,EAAS;AAAA,MACP,GAAI,IAAA,EAAM,IAAA,GAAO,EAAE,cAAA,EAAgB,kBAAA,KAAuB,EAAC;AAAA,MAC3D,GAAG,IAAA,EAAM;AAAA;AACX,GACD,CAAA;AACD,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,MAAM,MAAMA,oBAAA,CAAc,YAAA,CAAa,GAAG,CAAA;AAAA,EAC5C;AACA,EAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AACzB;;;;"}
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ var node_fs = require('node:fs');
4
+ var os = require('node:os');
5
+ var path = require('node:path');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+
9
+ var os__default = /*#__PURE__*/_interopDefaultCompat(os);
10
+ var path__default = /*#__PURE__*/_interopDefaultCompat(path);
11
+
12
+ async function loadKeytar() {
13
+ try {
14
+ const keytar = require("keytar");
15
+ if (keytar && typeof keytar.getPassword === "function") {
16
+ return keytar;
17
+ }
18
+ } catch {
19
+ }
20
+ return void 0;
21
+ }
22
+ class KeytarSecretStore {
23
+ keytar;
24
+ constructor(keytar) {
25
+ this.keytar = keytar;
26
+ }
27
+ async get(service, account) {
28
+ const result = await this.keytar.getPassword(service, account);
29
+ return result ?? void 0;
30
+ }
31
+ async set(service, account, secret) {
32
+ await this.keytar.setPassword(service, account, secret);
33
+ }
34
+ async delete(service, account) {
35
+ await this.keytar.deletePassword(service, account);
36
+ }
37
+ }
38
+ async function pathExists(p) {
39
+ try {
40
+ await node_fs.promises.stat(p);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+ class FileSecretStore {
47
+ baseDir;
48
+ constructor() {
49
+ const root = process.env.XDG_DATA_HOME || (process.platform === "win32" ? process.env.APPDATA || path__default.default.join(os__default.default.homedir(), "AppData", "Roaming") : path__default.default.join(os__default.default.homedir(), ".local", "share"));
50
+ this.baseDir = path__default.default.join(root, "backstage-cli", "auth-secrets");
51
+ }
52
+ filePath(service, account) {
53
+ return path__default.default.join(
54
+ this.baseDir,
55
+ encodeURIComponent(service),
56
+ `${encodeURIComponent(account)}.secret`
57
+ );
58
+ }
59
+ async get(service, account) {
60
+ const file = this.filePath(service, account);
61
+ if (!await pathExists(file)) {
62
+ return void 0;
63
+ }
64
+ return await node_fs.promises.readFile(file, "utf8");
65
+ }
66
+ async set(service, account, secret) {
67
+ const file = this.filePath(service, account);
68
+ await node_fs.promises.mkdir(path__default.default.dirname(file), { recursive: true });
69
+ await node_fs.promises.writeFile(file, secret, { encoding: "utf8", mode: 384 });
70
+ }
71
+ async delete(service, account) {
72
+ const file = this.filePath(service, account);
73
+ try {
74
+ await node_fs.promises.unlink(file);
75
+ } catch (err) {
76
+ if (err.code !== "ENOENT") {
77
+ throw err;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ let singleton;
83
+ async function getSecretStore() {
84
+ if (!singleton) {
85
+ const keytar = await loadKeytar();
86
+ if (keytar) {
87
+ singleton = new KeytarSecretStore(keytar);
88
+ } else {
89
+ singleton = new FileSecretStore();
90
+ }
91
+ }
92
+ return singleton;
93
+ }
94
+
95
+ exports.getSecretStore = getSecretStore;
96
+ //# sourceMappingURL=secretStore.cjs.js.map