@mercury-fw/utils 0.38.1 → 0.39.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 +15 -0
- package/README.md +1 -1
- package/cli-credentials.ts +75 -34
- package/dist/cli-credentials.d.ts +24 -14
- package/dist/index.d.ts +1 -1
- package/index.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @mercury-fw/utils
|
|
2
2
|
|
|
3
|
+
## 0.39.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 881835c: A CLI's login is set up inside the app's container, no longer carried in the env file.
|
|
8
|
+
|
|
9
|
+
- A plugin declares in `mercury.cliCredentials` the commands that set its CLI's login up (`setup`, required), check it (`check`) and log an identity out (`logout`), each a binary followed by its arguments.
|
|
10
|
+
- `mfw credentials setup <plugin>` runs the declared setup in a one-off container on your terminal, so the CLI writes its login straight onto the credentials volume; `mfw credentials check <plugin>` runs the declared check.
|
|
11
|
+
- `mfw credentials reset <plugin> [--user <key>]` runs the declared logout, of the identity Mercury runs as or of one person, instead of deleting the CLI's folder.
|
|
12
|
+
- `mfw credentials set` is removed, and the core no longer unpacks `*_CONFIG_TAR_B64` variables at startup: it reports a declared login that isn't set up yet, and still links a login kept outside `~/.config` to the volume.
|
|
13
|
+
- A command the model writes with `--user` is refused: which person a CLI acts as is never the model's to pick.
|
|
14
|
+
- `cliUserId` (`@mercury-fw/utils`) maps a user key to the id a CLI knows a person by.
|
|
15
|
+
- plugin-jira and plugin-bitbucket pin their CLIs at 2.1.0, which keep the service identity and each person apart and refuse the old login folder. After updating, remove the plugin's `*_CONFIG_TAR_B64` line from the env file and run `mfw credentials setup` once; on Jira, Mercury now runs as an Atlassian Service Account.
|
|
16
|
+
- plugin-atlassian-admin keeps its CLI at 0.1.2, whose `init` doesn't prompt for the organization key: until it does, write the key from `mfw shell` with `atlassian-admin init --api-key <KEY> --org-id <ORG_ID>` (the plugin's README says so).
|
|
17
|
+
|
|
3
18
|
## 0.38.1
|
|
4
19
|
|
|
5
20
|
## 0.38.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# @mercury-fw/utils
|
|
2
2
|
|
|
3
|
-
Helpers shared by [Mercury](https://github.com/lucabro81/mercury-fw) packages, today the download of a plugin's pinned CLI binary that the CLI-based plugins run when they install,
|
|
3
|
+
Helpers shared by [Mercury](https://github.com/lucabro81/mercury-fw) packages, today the download of a plugin's pinned CLI binary that the CLI-based plugins run when they install, reading the CLI login a plugin declares (`mercury.cliCredentials`: where it lives and the commands that set it up), and the id a CLI knows a person by. You don't depend on it directly.
|
|
4
4
|
|
|
5
5
|
MIT
|
package/cli-credentials.ts
CHANGED
|
@@ -1,42 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The CLI
|
|
2
|
+
* The CLI login a plugin declares in its own package.json
|
|
3
3
|
* (`mercury.cliCredentials`): where the plugin's CLI keeps its login and reads
|
|
4
4
|
* it back at runtime, either `{ folder }` under `~/.config` (the usual place)
|
|
5
|
-
* or `{ path }` anywhere under the home
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* or `{ path }` anywhere under the home, and the commands that set it up,
|
|
6
|
+
* check it and log an identity out. `mfw credentials` runs those commands
|
|
7
|
+
* inside the app's container, so the CLI writes its login straight onto the
|
|
8
|
+
* credentials volume, and the core links a `{ path }` login to the volume at
|
|
9
|
+
* startup. Declared by the plugin rather than listed anywhere central, so a
|
|
10
|
+
* plugin from any author gets the same mechanism; read as data, so both `mfw`
|
|
11
|
+
* on the host and the core in the container find it without running the
|
|
12
|
+
* plugin's code.
|
|
11
13
|
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
12
15
|
import { readFileSync, existsSync } from "node:fs";
|
|
13
16
|
import { join } from "node:path";
|
|
14
17
|
|
|
15
18
|
/** What a plugin declares: `name` as it was declared (the folder, or the
|
|
16
|
-
* path), which `mfw credentials` takes
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
+
* path), which `mfw credentials` takes; `path` relative to the home; and the
|
|
20
|
+
* commands, each a binary on the container's PATH and its arguments: `setup`
|
|
21
|
+
* sets up the service identity, `check` reports on the login, `logout`
|
|
22
|
+
* removes one identity's login (the service's, or a person's with `--user`). */
|
|
23
|
+
export type DeclaredCredentials = {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
setup: string[];
|
|
27
|
+
check?: string[];
|
|
28
|
+
logout?: string[];
|
|
29
|
+
};
|
|
19
30
|
|
|
20
|
-
/** One dependency's declaration
|
|
21
|
-
export type CliCredentials = DeclaredCredentials & { package: string
|
|
31
|
+
/** One dependency's declaration. */
|
|
32
|
+
export type CliCredentials = DeclaredCredentials & { package: string };
|
|
22
33
|
|
|
23
34
|
/** What an app's dependencies declare: the usable declarations, and one line
|
|
24
|
-
* per dependency left out (not installed, malformed,
|
|
35
|
+
* per dependency left out (not installed, malformed, clashing with another). */
|
|
25
36
|
export type AppCliCredentials = { declared: CliCredentials[]; problems: string[] };
|
|
26
37
|
|
|
27
38
|
/** A single folder name: no separators, no `.`/`..`, no leading dot. */
|
|
28
39
|
const FOLDER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
29
40
|
|
|
30
|
-
/** One segment of a home-relative path, never starting with a dash
|
|
31
|
-
*
|
|
41
|
+
/** One segment of a home-relative path, never starting with a dash; `.` and
|
|
42
|
+
* `..` are refused apart. */
|
|
32
43
|
const SEGMENT = /^[A-Za-z0-9._][A-Za-z0-9._-]*$/;
|
|
33
44
|
|
|
45
|
+
/** A binary found on the PATH: a bare name, never a path or an option. */
|
|
46
|
+
const BINARY = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
47
|
+
|
|
34
48
|
/** The credentials volume is mounted on `~/.config`; a login declared
|
|
35
49
|
* anywhere else in the home lives on it under this folder. */
|
|
36
50
|
const ELSEWHERE = ".config/mercury-home";
|
|
37
51
|
|
|
38
52
|
const INVALID =
|
|
39
|
-
"invalid mercury.cliCredentials in package.json: expected { folder } with a single folder name under ~/.config, or { path } relative to the home and outside ~/.config";
|
|
53
|
+
"invalid mercury.cliCredentials in package.json: expected { folder } with a single folder name under ~/.config, or { path } relative to the home and outside ~/.config, with setup (and optionally check, logout) as a binary name followed by its arguments, e.g. [\"jira\", \"init\"]";
|
|
40
54
|
|
|
41
55
|
/** Whether `path` is a usable home-relative path for a login: inside the
|
|
42
56
|
* home and outside `~/.config`, the volume, where a login is a `folder`. */
|
|
@@ -46,6 +60,16 @@ function isHomePath(path: string): boolean {
|
|
|
46
60
|
return path !== ".config" && !path.startsWith(".config/");
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
/** Whether `value` is a command: a binary name, then non-empty arguments. */
|
|
64
|
+
function isCommand(value: unknown): value is string[] {
|
|
65
|
+
return (
|
|
66
|
+
Array.isArray(value) &&
|
|
67
|
+
value.length > 0 &&
|
|
68
|
+
value.every((a) => typeof a === "string" && a !== "") &&
|
|
69
|
+
BINARY.test(value[0] as string)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
/**
|
|
50
74
|
* What `mercury.cliCredentials` of a package.json-shaped object declares, or
|
|
51
75
|
* undefined when the package declares nothing. A malformed declaration throws:
|
|
@@ -61,12 +85,16 @@ export function readCliCredentials(pkg: unknown): DeclaredCredentials | undefine
|
|
|
61
85
|
if (!("cliCredentials" in mercury)) return undefined;
|
|
62
86
|
const declared = (mercury as { cliCredentials?: unknown }).cliCredentials;
|
|
63
87
|
if (typeof declared !== "object" || declared === null) throw new Error(INVALID);
|
|
64
|
-
const { folder, path } = declared as
|
|
88
|
+
const { folder, path, setup, check, logout } = declared as Record<string, unknown>;
|
|
89
|
+
if (!isCommand(setup) || (check !== undefined && !isCommand(check)) || (logout !== undefined && !isCommand(logout))) {
|
|
90
|
+
throw new Error(INVALID);
|
|
91
|
+
}
|
|
92
|
+
const commands = { setup, ...(check === undefined ? {} : { check }), ...(logout === undefined ? {} : { logout }) };
|
|
65
93
|
if (folder !== undefined && path === undefined && typeof folder === "string" && FOLDER.test(folder)) {
|
|
66
|
-
return { name: folder, path: `.config/${folder}
|
|
94
|
+
return { name: folder, path: `.config/${folder}`, ...commands };
|
|
67
95
|
}
|
|
68
96
|
if (path !== undefined && folder === undefined && typeof path === "string" && isHomePath(path)) {
|
|
69
|
-
return { name: path, path };
|
|
97
|
+
return { name: path, path, ...commands };
|
|
70
98
|
}
|
|
71
99
|
throw new Error(INVALID);
|
|
72
100
|
}
|
|
@@ -79,21 +107,36 @@ export function volumePath(path: string): string {
|
|
|
79
107
|
return path.startsWith(".config/") ? path : `${ELSEWHERE}/${path}`;
|
|
80
108
|
}
|
|
81
109
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
/** What the CLIs accept as `--user <id>`. */
|
|
111
|
+
const CLI_USER_ID = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
|
|
112
|
+
|
|
113
|
+
/** A provider name kept readable in front of a hashed id. */
|
|
114
|
+
const PROVIDER = /^[a-z0-9][a-z0-9._-]{0,30}$/;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The id a CLI knows a person by (`--user <id>`), from Mercury's user key
|
|
118
|
+
* (`<provider>:<id>`): the key itself when the CLIs accept it, as they do
|
|
119
|
+
* `static:alice` or an OIDC key with a numeric subject, so a person's folder
|
|
120
|
+
* stays readable; otherwise the provider and the first 32 hex digits of the
|
|
121
|
+
* key's SHA-256, since an id with capitals, `/` or more than 64 characters is
|
|
122
|
+
* refused, and lowering capitals would merge two people.
|
|
123
|
+
*/
|
|
124
|
+
export function cliUserId(key: string): string {
|
|
125
|
+
if (CLI_USER_ID.test(key)) return key;
|
|
126
|
+
const provider = key.slice(0, Math.max(key.indexOf(":"), 0));
|
|
127
|
+
const hash = createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
128
|
+
return `${PROVIDER.test(provider) ? provider : "user"}:${hash}`;
|
|
86
129
|
}
|
|
87
130
|
|
|
88
131
|
/**
|
|
89
132
|
* The declarations of the app's dependencies (its package.json `dependencies`,
|
|
90
133
|
* read from its `node_modules`), in the manifest's order. A dependency that
|
|
91
|
-
* isn't installed or declares a malformed
|
|
92
|
-
* line, and so are all the ones
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
134
|
+
* isn't installed or declares a malformed login is left out with a problem
|
|
135
|
+
* line, and so are all the ones declaring the same login (each would set it up
|
|
136
|
+
* and log it out for the other) or logins one inside the other (one would be
|
|
137
|
+
* linked inside the other's copy on the volume): one bad plugin never costs
|
|
138
|
+
* the others their login. Throws only when the app's own package.json can't be
|
|
139
|
+
* read.
|
|
97
140
|
*/
|
|
98
141
|
export function appCliCredentials(appDir: string): AppCliCredentials {
|
|
99
142
|
const manifest = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8")) as {
|
|
@@ -109,20 +152,18 @@ export function appCliCredentials(appDir: string): AppCliCredentials {
|
|
|
109
152
|
}
|
|
110
153
|
try {
|
|
111
154
|
const declared = readCliCredentials(JSON.parse(readFileSync(path, "utf-8")));
|
|
112
|
-
if (declared !== undefined) found.push({ package: name, ...declared
|
|
155
|
+
if (declared !== undefined) found.push({ package: name, ...declared });
|
|
113
156
|
} catch (err) {
|
|
114
157
|
problems.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
160
|
const excluded = new Set<CliCredentials>();
|
|
118
161
|
for (const c of found) {
|
|
119
|
-
const sharing = found.filter((o) => o.
|
|
162
|
+
const sharing = found.filter((o) => o.path === c.path);
|
|
120
163
|
if (sharing.length === 1) continue;
|
|
121
164
|
sharing.forEach((o) => excluded.add(o));
|
|
122
165
|
if (sharing[0] === c) {
|
|
123
|
-
problems.push(
|
|
124
|
-
`${sharing.map((o) => o.package).join(" and ")} declare CLI credentials (${sharing.map((o) => o.name).join(", ")}) carried by the same variable ${c.variable}: neither is used`,
|
|
125
|
-
);
|
|
166
|
+
problems.push(`${sharing.map((o) => o.package).join(" and ")} declare the same CLI credentials (${c.name}): neither is used`);
|
|
126
167
|
}
|
|
127
168
|
}
|
|
128
169
|
const inside = (outer: string, inner: string) => inner.startsWith(`${outer}/`);
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/** What a plugin declares: `name` as it was declared (the folder, or the
|
|
2
|
-
* path), which `mfw credentials` takes
|
|
3
|
-
*
|
|
2
|
+
* path), which `mfw credentials` takes; `path` relative to the home; and the
|
|
3
|
+
* commands, each a binary on the container's PATH and its arguments: `setup`
|
|
4
|
+
* sets up the service identity, `check` reports on the login, `logout`
|
|
5
|
+
* removes one identity's login (the service's, or a person's with `--user`). */
|
|
4
6
|
export type DeclaredCredentials = {
|
|
5
7
|
name: string;
|
|
6
8
|
path: string;
|
|
9
|
+
setup: string[];
|
|
10
|
+
check?: string[];
|
|
11
|
+
logout?: string[];
|
|
7
12
|
};
|
|
8
|
-
/** One dependency's declaration
|
|
13
|
+
/** One dependency's declaration. */
|
|
9
14
|
export type CliCredentials = DeclaredCredentials & {
|
|
10
15
|
package: string;
|
|
11
|
-
variable: string;
|
|
12
16
|
};
|
|
13
17
|
/** What an app's dependencies declare: the usable declarations, and one line
|
|
14
|
-
* per dependency left out (not installed, malformed,
|
|
18
|
+
* per dependency left out (not installed, malformed, clashing with another). */
|
|
15
19
|
export type AppCliCredentials = {
|
|
16
20
|
declared: CliCredentials[];
|
|
17
21
|
problems: string[];
|
|
@@ -28,17 +32,23 @@ export declare function readCliCredentials(pkg: unknown): DeclaredCredentials |
|
|
|
28
32
|
* volume's mount, a declared `folder`), under `~/.config/mercury-home`
|
|
29
33
|
* otherwise, with the home path pointing there. */
|
|
30
34
|
export declare function volumePath(path: string): string;
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The id a CLI knows a person by (`--user <id>`), from Mercury's user key
|
|
37
|
+
* (`<provider>:<id>`): the key itself when the CLIs accept it, as they do
|
|
38
|
+
* `static:alice` or an OIDC key with a numeric subject, so a person's folder
|
|
39
|
+
* stays readable; otherwise the provider and the first 32 hex digits of the
|
|
40
|
+
* key's SHA-256, since an id with capitals, `/` or more than 64 characters is
|
|
41
|
+
* refused, and lowering capitals would merge two people.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cliUserId(key: string): string;
|
|
34
44
|
/**
|
|
35
45
|
* The declarations of the app's dependencies (its package.json `dependencies`,
|
|
36
46
|
* read from its `node_modules`), in the manifest's order. A dependency that
|
|
37
|
-
* isn't installed or declares a malformed
|
|
38
|
-
* line, and so are all the ones
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
47
|
+
* isn't installed or declares a malformed login is left out with a problem
|
|
48
|
+
* line, and so are all the ones declaring the same login (each would set it up
|
|
49
|
+
* and log it out for the other) or logins one inside the other (one would be
|
|
50
|
+
* linked inside the other's copy on the volume): one bad plugin never costs
|
|
51
|
+
* the others their login. Throws only when the app's own package.json can't be
|
|
52
|
+
* read.
|
|
43
53
|
*/
|
|
44
54
|
export declare function appCliCredentials(appDir: string): AppCliCredentials;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* as they're factored out.
|
|
6
6
|
*/
|
|
7
7
|
export { downloadPinnedBinary, resolvePlatform, binaryAssetUrl, readPinnedBinary, type Platform, type PinnedBinary, } from "./cli-binary.ts";
|
|
8
|
-
export { readCliCredentials,
|
|
8
|
+
export { readCliCredentials, appCliCredentials, volumePath, cliUserId, type DeclaredCredentials, type CliCredentials, type AppCliCredentials, } from "./cli-credentials.ts";
|
package/index.ts
CHANGED