@mercury-fw/utils 0.38.1 → 0.40.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 +31 -0
- package/README.md +1 -1
- package/cli-credentials.ts +81 -34
- package/dist/cli-credentials.d.ts +26 -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,36 @@
|
|
|
1
1
|
# @mercury-fw/utils
|
|
2
2
|
|
|
3
|
+
## 0.40.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0739f77: A plugin acts as the person Mercury is talking to, who logs in to the service through Mercury.
|
|
8
|
+
|
|
9
|
+
- Plugin contract version 4: a plugin declares `actsAs: "person" | "mercury"`, and one acting as the person contributes a `login`; its tools get the turn's `person` and `requireLogin` in their context.
|
|
10
|
+
- A person is offered only the plugins acting as the person: their prompt fragments, skills and tools. A plugin acting as Mercury, or declaring nothing, is offered on the terminal only until people can be allowed to make Mercury act as itself; the startup log names it.
|
|
11
|
+
- `createCliTool` adds `--user` with the person's id to every command, staged confirmations included, and when the CLI exits with code 3 (not logged in) returns their login instead. `createCliPersonLogin` builds a plugin's `login` on a CLI's two-step remote login. A failed `runCli` carries `exitCode`.
|
|
12
|
+
- Channel contract version 4: channels get `logins` (`accept` a callback URL, `complete` a login) and `detectLoginRequired`.
|
|
13
|
+
- The HTTP channel, with `HTTP_SURFACE_PUBLIC_URL` set, streams a `login` event with the link to open and takes the person back at `GET /login/callback`, public and protected by the single-use state.
|
|
14
|
+
- `mercury.cliCredentials` can declare `userSetup`, run by `mfw credentials setup <plugin> --user-app`: the app people log in through, set up without logging anyone in.
|
|
15
|
+
- plugin-jira and plugin-bitbucket act as the person, on CLIs 2.3.0: set up their people's app with `--user-app` and register `<HTTP_SURFACE_PUBLIC_URL>/login/callback` on it. Jira's skill now says `currentUser()` is the person.
|
|
16
|
+
- plugin-atlassian-admin acts as Mercury, on CLI 0.2.0, whose `init` asks for the key without echoing it: until permissions exist it's available on the terminal only.
|
|
17
|
+
- A third-party plugin needs `apiVersion: 4` to load, and `actsAs: "person"` with a `login` to be offered to people.
|
|
18
|
+
|
|
19
|
+
## 0.39.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 881835c: A CLI's login is set up inside the app's container, no longer carried in the env file.
|
|
24
|
+
|
|
25
|
+
- 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.
|
|
26
|
+
- `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.
|
|
27
|
+
- `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.
|
|
28
|
+
- `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.
|
|
29
|
+
- A command the model writes with `--user` is refused: which person a CLI acts as is never the model's to pick.
|
|
30
|
+
- `cliUserId` (`@mercury-fw/utils`) maps a user key to the id a CLI knows a person by.
|
|
31
|
+
- 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.
|
|
32
|
+
- 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).
|
|
33
|
+
|
|
3
34
|
## 0.38.1
|
|
4
35
|
|
|
5
36
|
## 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,58 @@
|
|
|
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, `userSetup` the app people log in through
|
|
22
|
+
* (no person logged in), `check` reports on the login, `logout` removes one
|
|
23
|
+
* identity's login (the service's, or a person's with `--user`). */
|
|
24
|
+
export type DeclaredCredentials = {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
setup: string[];
|
|
28
|
+
userSetup?: string[];
|
|
29
|
+
check?: string[];
|
|
30
|
+
logout?: string[];
|
|
31
|
+
};
|
|
19
32
|
|
|
20
|
-
/** One dependency's declaration
|
|
21
|
-
export type CliCredentials = DeclaredCredentials & { package: string
|
|
33
|
+
/** One dependency's declaration. */
|
|
34
|
+
export type CliCredentials = DeclaredCredentials & { package: string };
|
|
22
35
|
|
|
23
36
|
/** What an app's dependencies declare: the usable declarations, and one line
|
|
24
|
-
* per dependency left out (not installed, malformed,
|
|
37
|
+
* per dependency left out (not installed, malformed, clashing with another). */
|
|
25
38
|
export type AppCliCredentials = { declared: CliCredentials[]; problems: string[] };
|
|
26
39
|
|
|
27
40
|
/** A single folder name: no separators, no `.`/`..`, no leading dot. */
|
|
28
41
|
const FOLDER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
29
42
|
|
|
30
|
-
/** One segment of a home-relative path, never starting with a dash
|
|
31
|
-
*
|
|
43
|
+
/** One segment of a home-relative path, never starting with a dash; `.` and
|
|
44
|
+
* `..` are refused apart. */
|
|
32
45
|
const SEGMENT = /^[A-Za-z0-9._][A-Za-z0-9._-]*$/;
|
|
33
46
|
|
|
47
|
+
/** A binary found on the PATH: a bare name, never a path or an option. */
|
|
48
|
+
const BINARY = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
49
|
+
|
|
34
50
|
/** The credentials volume is mounted on `~/.config`; a login declared
|
|
35
51
|
* anywhere else in the home lives on it under this folder. */
|
|
36
52
|
const ELSEWHERE = ".config/mercury-home";
|
|
37
53
|
|
|
38
54
|
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";
|
|
55
|
+
"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 userSetup, check, logout) as a binary name followed by its arguments, e.g. [\"jira\", \"init\"]";
|
|
40
56
|
|
|
41
57
|
/** Whether `path` is a usable home-relative path for a login: inside the
|
|
42
58
|
* home and outside `~/.config`, the volume, where a login is a `folder`. */
|
|
@@ -46,6 +62,16 @@ function isHomePath(path: string): boolean {
|
|
|
46
62
|
return path !== ".config" && !path.startsWith(".config/");
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
/** Whether `value` is a command: a binary name, then non-empty arguments. */
|
|
66
|
+
function isCommand(value: unknown): value is string[] {
|
|
67
|
+
return (
|
|
68
|
+
Array.isArray(value) &&
|
|
69
|
+
value.length > 0 &&
|
|
70
|
+
value.every((a) => typeof a === "string" && a !== "") &&
|
|
71
|
+
BINARY.test(value[0] as string)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
/**
|
|
50
76
|
* What `mercury.cliCredentials` of a package.json-shaped object declares, or
|
|
51
77
|
* undefined when the package declares nothing. A malformed declaration throws:
|
|
@@ -61,12 +87,20 @@ export function readCliCredentials(pkg: unknown): DeclaredCredentials | undefine
|
|
|
61
87
|
if (!("cliCredentials" in mercury)) return undefined;
|
|
62
88
|
const declared = (mercury as { cliCredentials?: unknown }).cliCredentials;
|
|
63
89
|
if (typeof declared !== "object" || declared === null) throw new Error(INVALID);
|
|
64
|
-
const { folder, path } = declared as
|
|
90
|
+
const { folder, path, setup, userSetup, check, logout } = declared as Record<string, unknown>;
|
|
91
|
+
const optional = { userSetup, check, logout };
|
|
92
|
+
if (!isCommand(setup) || Object.values(optional).some((c) => c !== undefined && !isCommand(c))) {
|
|
93
|
+
throw new Error(INVALID);
|
|
94
|
+
}
|
|
95
|
+
const commands: Pick<DeclaredCredentials, "setup" | "userSetup" | "check" | "logout"> = { setup };
|
|
96
|
+
for (const [field, command] of Object.entries(optional) as [keyof typeof optional, string[] | undefined][]) {
|
|
97
|
+
if (command !== undefined) commands[field] = command;
|
|
98
|
+
}
|
|
65
99
|
if (folder !== undefined && path === undefined && typeof folder === "string" && FOLDER.test(folder)) {
|
|
66
|
-
return { name: folder, path: `.config/${folder}
|
|
100
|
+
return { name: folder, path: `.config/${folder}`, ...commands };
|
|
67
101
|
}
|
|
68
102
|
if (path !== undefined && folder === undefined && typeof path === "string" && isHomePath(path)) {
|
|
69
|
-
return { name: path, path };
|
|
103
|
+
return { name: path, path, ...commands };
|
|
70
104
|
}
|
|
71
105
|
throw new Error(INVALID);
|
|
72
106
|
}
|
|
@@ -79,21 +113,36 @@ export function volumePath(path: string): string {
|
|
|
79
113
|
return path.startsWith(".config/") ? path : `${ELSEWHERE}/${path}`;
|
|
80
114
|
}
|
|
81
115
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
/** What the CLIs accept as `--user <id>`. */
|
|
117
|
+
const CLI_USER_ID = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
|
|
118
|
+
|
|
119
|
+
/** A provider name kept readable in front of a hashed id. */
|
|
120
|
+
const PROVIDER = /^[a-z0-9][a-z0-9._-]{0,30}$/;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The id a CLI knows a person by (`--user <id>`), from Mercury's user key
|
|
124
|
+
* (`<provider>:<id>`): the key itself when the CLIs accept it, as they do
|
|
125
|
+
* `static:alice` or an OIDC key with a numeric subject, so a person's folder
|
|
126
|
+
* stays readable; otherwise the provider and the first 32 hex digits of the
|
|
127
|
+
* key's SHA-256, since an id with capitals, `/` or more than 64 characters is
|
|
128
|
+
* refused, and lowering capitals would merge two people.
|
|
129
|
+
*/
|
|
130
|
+
export function cliUserId(key: string): string {
|
|
131
|
+
if (CLI_USER_ID.test(key)) return key;
|
|
132
|
+
const provider = key.slice(0, Math.max(key.indexOf(":"), 0));
|
|
133
|
+
const hash = createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
134
|
+
return `${PROVIDER.test(provider) ? provider : "user"}:${hash}`;
|
|
86
135
|
}
|
|
87
136
|
|
|
88
137
|
/**
|
|
89
138
|
* The declarations of the app's dependencies (its package.json `dependencies`,
|
|
90
139
|
* 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
|
-
*
|
|
140
|
+
* isn't installed or declares a malformed login is left out with a problem
|
|
141
|
+
* line, and so are all the ones declaring the same login (each would set it up
|
|
142
|
+
* and log it out for the other) or logins one inside the other (one would be
|
|
143
|
+
* linked inside the other's copy on the volume): one bad plugin never costs
|
|
144
|
+
* the others their login. Throws only when the app's own package.json can't be
|
|
145
|
+
* read.
|
|
97
146
|
*/
|
|
98
147
|
export function appCliCredentials(appDir: string): AppCliCredentials {
|
|
99
148
|
const manifest = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8")) as {
|
|
@@ -109,20 +158,18 @@ export function appCliCredentials(appDir: string): AppCliCredentials {
|
|
|
109
158
|
}
|
|
110
159
|
try {
|
|
111
160
|
const declared = readCliCredentials(JSON.parse(readFileSync(path, "utf-8")));
|
|
112
|
-
if (declared !== undefined) found.push({ package: name, ...declared
|
|
161
|
+
if (declared !== undefined) found.push({ package: name, ...declared });
|
|
113
162
|
} catch (err) {
|
|
114
163
|
problems.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
164
|
}
|
|
116
165
|
}
|
|
117
166
|
const excluded = new Set<CliCredentials>();
|
|
118
167
|
for (const c of found) {
|
|
119
|
-
const sharing = found.filter((o) => o.
|
|
168
|
+
const sharing = found.filter((o) => o.path === c.path);
|
|
120
169
|
if (sharing.length === 1) continue;
|
|
121
170
|
sharing.forEach((o) => excluded.add(o));
|
|
122
171
|
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
|
-
);
|
|
172
|
+
problems.push(`${sharing.map((o) => o.package).join(" and ")} declare the same CLI credentials (${c.name}): neither is used`);
|
|
126
173
|
}
|
|
127
174
|
}
|
|
128
175
|
const inside = (outer: string, inner: string) => inner.startsWith(`${outer}/`);
|
|
@@ -1,17 +1,23 @@
|
|
|
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, `userSetup` the app people log in through
|
|
5
|
+
* (no person logged in), `check` reports on the login, `logout` removes one
|
|
6
|
+
* identity's login (the service's, or a person's with `--user`). */
|
|
4
7
|
export type DeclaredCredentials = {
|
|
5
8
|
name: string;
|
|
6
9
|
path: string;
|
|
10
|
+
setup: string[];
|
|
11
|
+
userSetup?: string[];
|
|
12
|
+
check?: string[];
|
|
13
|
+
logout?: string[];
|
|
7
14
|
};
|
|
8
|
-
/** One dependency's declaration
|
|
15
|
+
/** One dependency's declaration. */
|
|
9
16
|
export type CliCredentials = DeclaredCredentials & {
|
|
10
17
|
package: string;
|
|
11
|
-
variable: string;
|
|
12
18
|
};
|
|
13
19
|
/** What an app's dependencies declare: the usable declarations, and one line
|
|
14
|
-
* per dependency left out (not installed, malformed,
|
|
20
|
+
* per dependency left out (not installed, malformed, clashing with another). */
|
|
15
21
|
export type AppCliCredentials = {
|
|
16
22
|
declared: CliCredentials[];
|
|
17
23
|
problems: string[];
|
|
@@ -28,17 +34,23 @@ export declare function readCliCredentials(pkg: unknown): DeclaredCredentials |
|
|
|
28
34
|
* volume's mount, a declared `folder`), under `~/.config/mercury-home`
|
|
29
35
|
* otherwise, with the home path pointing there. */
|
|
30
36
|
export declare function volumePath(path: string): string;
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The id a CLI knows a person by (`--user <id>`), from Mercury's user key
|
|
39
|
+
* (`<provider>:<id>`): the key itself when the CLIs accept it, as they do
|
|
40
|
+
* `static:alice` or an OIDC key with a numeric subject, so a person's folder
|
|
41
|
+
* stays readable; otherwise the provider and the first 32 hex digits of the
|
|
42
|
+
* key's SHA-256, since an id with capitals, `/` or more than 64 characters is
|
|
43
|
+
* refused, and lowering capitals would merge two people.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cliUserId(key: string): string;
|
|
34
46
|
/**
|
|
35
47
|
* The declarations of the app's dependencies (its package.json `dependencies`,
|
|
36
48
|
* 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
|
-
*
|
|
49
|
+
* isn't installed or declares a malformed login is left out with a problem
|
|
50
|
+
* line, and so are all the ones declaring the same login (each would set it up
|
|
51
|
+
* and log it out for the other) or logins one inside the other (one would be
|
|
52
|
+
* linked inside the other's copy on the volume): one bad plugin never costs
|
|
53
|
+
* the others their login. Throws only when the app's own package.json can't be
|
|
54
|
+
* read.
|
|
43
55
|
*/
|
|
44
56
|
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