@deque/axe-auth 1.4.0 → 1.5.0-next.7a5a136d
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/README.md +43 -15
- package/credits.json +74 -0
- package/dist/cli/errors.d.ts +1 -1
- package/dist/cli/errors.js +1 -0
- package/dist/commands/run.d.ts +16 -0
- package/dist/commands/run.help.d.ts +2 -0
- package/dist/commands/run.help.js +41 -0
- package/dist/commands/run.js +108 -0
- package/dist/index.js +18 -2
- package/dist/oauth/keyringBinding.d.ts +1 -1
- package/dist/oauth/keyringBinding.js +3 -4
- package/dist/run/findFreePort.d.ts +17 -0
- package/dist/run/findFreePort.js +39 -0
- package/dist/run/pushToken.d.ts +10 -0
- package/dist/run/pushToken.js +35 -0
- package/dist/run/runSession.d.ts +62 -0
- package/dist/run/runSession.js +219 -0
- package/dist/run/supervise.d.ts +72 -0
- package/dist/run/supervise.js +234 -0
- package/dist/run/testUtils.d.ts +15 -0
- package/dist/run/testUtils.js +43 -0
- package/docs/architecture.md +63 -2
- package/docs/callback-server.md +9 -9
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -22,11 +22,12 @@ axe-auth <command> [options]
|
|
|
22
22
|
|
|
23
23
|
Commands:
|
|
24
24
|
|
|
25
|
-
| Command
|
|
26
|
-
|
|
|
27
|
-
| `login`
|
|
25
|
+
| Command | Description |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `login` | Open a browser, complete the OAuth flow, persist tokens to the OS keychain. |
|
|
28
28
|
| `logout` | Revoke the stored refresh token server-side and clear the local keychain entry. |
|
|
29
|
-
| `token`
|
|
29
|
+
| `token` | Print a currently-valid access token to stdout, refreshing silently if needed. |
|
|
30
|
+
| `run` | Launch and supervise the axe MCP server, keeping its OAuth token fresh with no restart. |
|
|
30
31
|
|
|
31
32
|
Run `axe-auth <command> --help` for command-specific options.
|
|
32
33
|
|
|
@@ -34,11 +35,11 @@ Run `axe-auth <command> --help` for command-specific options.
|
|
|
34
35
|
|
|
35
36
|
`axe-auth` discovers its OAuth coordinates by calling `<server>/api/sso-config` on the axe server. Users only supply (or default to) the axe server URL — never the underlying Keycloak URL, realm, or client ID.
|
|
36
37
|
|
|
37
|
-
| Flag
|
|
38
|
-
|
|
|
39
|
-
| `--server`
|
|
40
|
-
| `--allow-insecure-issuer`
|
|
41
|
-
| `--no-allow-insecure-issuer` | —
|
|
38
|
+
| Flag | Env var | Notes |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `--server` | `AXE_SERVER_URL` | axe server URL. Defaults to `https://axe.deque.com` (SaaS prod) when neither flag nor env var is set, so SaaS users pass no flags at all. Customers on other deployments override with their own axe server URL. |
|
|
41
|
+
| `--allow-insecure-issuer` | — | Permit non-loopback http URLs (default is https only; loopback http is always allowed). Applies to `login` only; `token` and `logout` use the policy persisted at login. |
|
|
42
|
+
| `--no-allow-insecure-issuer` | — | Force `allowInsecureIssuer=false` for the new `login` (and the entry it persists). Mutually exclusive with `--allow-insecure-issuer`. `token` and `logout` ignore this flag. |
|
|
42
43
|
|
|
43
44
|
`axe-auth` stores one set of credentials per machine. On a successful `login`, the discovered issuer / client / insecure-issuer values are persisted alongside the tokens, so subsequent `axe-auth token` and `axe-auth logout` invocations work flag-free — a typical scripted call is just `$(axe-auth token)`.
|
|
44
45
|
|
|
@@ -46,12 +47,12 @@ There is no concurrent multi-issuer support. Logging in to a second deployment o
|
|
|
46
47
|
|
|
47
48
|
### Exit codes
|
|
48
49
|
|
|
49
|
-
| Code | Meaning
|
|
50
|
-
|
|
|
51
|
-
| `0`
|
|
52
|
-
| `1`
|
|
53
|
-
| `2`
|
|
54
|
-
| `3`
|
|
50
|
+
| Code | Meaning |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `0` | Success. |
|
|
53
|
+
| `1` | Not authenticated: `axe-auth token` with no stored credentials, or stored credentials are unusable (corrupt, version-mismatch, expired without a usable refresh token, or refresh rejected by the server). Branch on this in scripts that need to trigger a `login`. |
|
|
54
|
+
| `2` | Usage or runtime error: unknown command, bad flag, missing required configuration, OAuth flow failure, or keychain failure. Details written to stderr. |
|
|
55
|
+
| `3` | Cancelled: `axe-auth login` declined at the re-authentication prompt. Distinct from `1` so scripts can tell "needs login" from "user bailed." |
|
|
55
56
|
|
|
56
57
|
### Examples
|
|
57
58
|
|
|
@@ -69,6 +70,33 @@ docker run -e AXE_ACCESS_TOKEN="$(axe-auth token)" axe-mcp-server
|
|
|
69
70
|
axe-auth logout
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
### Long-running sessions
|
|
74
|
+
|
|
75
|
+
Access tokens are short-lived, so a session that outlives the token's TTL (an agent driving the MCP server for hours, say) would otherwise start seeing auth failures. `axe-auth run` handles this automatically: it launches and supervises the server, then keeps its access token fresh for the whole session with no restart and no manual step. Point your MCP client at `axe-auth run` as the server command; it manages the server process's lifetime like any stdio server. It also shuts the server down when the session ends: on stdin close, on a termination signal, and when the launching process disappears. The server itself exits when the client stops answering its liveness probes. Each session takes a fresh refresh port from the OS unless you pin one with `--port` or `AXE_TOKEN_REFRESH_PORT`, so a server left behind by an earlier session cannot hold the port this one needs. Pin one for containers, which only reach a port you publish: `run` detects `docker`, `podman`, and `nerdctl` and refuses to guess, but any other runtime or a wrapper script is yours to pin. Your refresh token never leaves the machine; only short-lived access tokens are sent to the server.
|
|
76
|
+
|
|
77
|
+
The server just needs token refresh enabled on a port; `run` supplies the token and secret. Under Docker, publish the port on host loopback, forward the auth vars, and bind the listener to `0.0.0.0` inside the container so the published port can reach it:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
AXE_TOKEN_REFRESH_PORT=9223 npx @deque/axe-auth run -- \
|
|
81
|
+
docker run -i --rm \
|
|
82
|
+
-p 127.0.0.1:9223:9223 \
|
|
83
|
+
-e AXE_ACCESS_TOKEN \
|
|
84
|
+
-e AXE_TOKEN_REFRESH_PORT \
|
|
85
|
+
-e AXE_TOKEN_REFRESH_SECRET \
|
|
86
|
+
-e AXE_TOKEN_REFRESH_HOST=0.0.0.0 \
|
|
87
|
+
dequesystems/axe-mcp-server
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`AXE_TOKEN_REFRESH_HOST=0.0.0.0` is required under Docker: a published port forwards to the container's network interface, not its loopback, so the default loopback bind would be unreachable. The `-p 127.0.0.1:9223:9223` publish keeps the endpoint off the host's external interfaces, and the shared secret — not container isolation — is what guards the endpoint itself.
|
|
91
|
+
|
|
92
|
+
Under npm the wrapped process inherits the values directly and the listener stays on loopback:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
npx @deque/axe-auth run -- npx axe-mcp-server
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`run` picks a free loopback port per session; pin one with `AXE_TOKEN_REFRESH_PORT` or `--port` only for containers, which can reach only a port you publish. `run` generates the shared secret unless you pin one with `AXE_TOKEN_REFRESH_SECRET` / `--secret`. The secret authenticates the push; the server's listener binds loopback by default and never starts without a secret.
|
|
99
|
+
|
|
72
100
|
## Architecture
|
|
73
101
|
|
|
74
102
|
See [`docs/architecture.md`](./docs/architecture.md) for the system architecture: components, per-verb data flow, communication security, and persisted data.
|
package/credits.json
CHANGED
|
@@ -16,6 +16,30 @@
|
|
|
16
16
|
"licenseText": "# `@napi-rs/keyring-linux-x64-gnu`\n\nThis is the **x86_64-unknown-linux-gnu** binary for `@napi-rs/keyring`\n",
|
|
17
17
|
"licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring-linux-x64-gnu@1.3.0/node_modules/@napi-rs/keyring-linux-x64-gnu/README.md"
|
|
18
18
|
},
|
|
19
|
+
"cross-spawn@7.0.6": {
|
|
20
|
+
"name": "cross-spawn",
|
|
21
|
+
"version": "7.0.6",
|
|
22
|
+
"licenses": "MIT",
|
|
23
|
+
"path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn",
|
|
24
|
+
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2018 Made With MOXY Lda <hello@moxy.studio>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
|
25
|
+
"licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/LICENSE",
|
|
26
|
+
"repository": "https://github.com/moxystudio/node-cross-spawn",
|
|
27
|
+
"publisher": "André Cruz",
|
|
28
|
+
"email": "andre@moxy.studio",
|
|
29
|
+
"copyright": "Copyright (c) 2018 Made With MOXY Lda <hello@moxy.studio>"
|
|
30
|
+
},
|
|
31
|
+
"path-key@3.1.1": {
|
|
32
|
+
"name": "path-key",
|
|
33
|
+
"version": "3.1.1",
|
|
34
|
+
"licenses": "MIT",
|
|
35
|
+
"path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/path-key@3.1.1/node_modules/path-key",
|
|
36
|
+
"licenseText": "MIT License\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
|
37
|
+
"licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/path-key@3.1.1/node_modules/path-key/license",
|
|
38
|
+
"publisher": "Sindre Sorhus",
|
|
39
|
+
"email": "sindresorhus@gmail.com",
|
|
40
|
+
"url": "sindresorhus.com",
|
|
41
|
+
"copyright": "Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)"
|
|
42
|
+
},
|
|
19
43
|
"remove-trailing-slash@0.1.1": {
|
|
20
44
|
"name": "remove-trailing-slash",
|
|
21
45
|
"version": "0.1.1",
|
|
@@ -27,6 +51,30 @@
|
|
|
27
51
|
"publisher": "Stephen Mathieson",
|
|
28
52
|
"email": "me@stephenmathieson.com"
|
|
29
53
|
},
|
|
54
|
+
"shebang-command@2.0.0": {
|
|
55
|
+
"name": "shebang-command",
|
|
56
|
+
"version": "2.0.0",
|
|
57
|
+
"licenses": "MIT",
|
|
58
|
+
"path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/shebang-command@2.0.0/node_modules/shebang-command",
|
|
59
|
+
"licenseText": "MIT License\n\nCopyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> (github.com/kevva)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
|
60
|
+
"licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/shebang-command@2.0.0/node_modules/shebang-command/license",
|
|
61
|
+
"publisher": "Kevin Mårtensson",
|
|
62
|
+
"email": "kevinmartensson@gmail.com",
|
|
63
|
+
"url": "github.com/kevva",
|
|
64
|
+
"copyright": "Copyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> (github.com/kevva)"
|
|
65
|
+
},
|
|
66
|
+
"shebang-regex@3.0.0": {
|
|
67
|
+
"name": "shebang-regex",
|
|
68
|
+
"version": "3.0.0",
|
|
69
|
+
"licenses": "MIT",
|
|
70
|
+
"path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/shebang-regex@3.0.0/node_modules/shebang-regex",
|
|
71
|
+
"licenseText": "MIT License\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
|
72
|
+
"licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/shebang-regex@3.0.0/node_modules/shebang-regex/license",
|
|
73
|
+
"publisher": "Sindre Sorhus",
|
|
74
|
+
"email": "sindresorhus@gmail.com",
|
|
75
|
+
"url": "sindresorhus.com",
|
|
76
|
+
"copyright": "Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)"
|
|
77
|
+
},
|
|
30
78
|
"shlex@3.0.0": {
|
|
31
79
|
"name": "shlex",
|
|
32
80
|
"version": "3.0.0",
|
|
@@ -49,5 +97,31 @@
|
|
|
49
97
|
"publisher": "Tamino Martinius",
|
|
50
98
|
"email": "dev@zaku.eu",
|
|
51
99
|
"copyright": "Copyright (c) 2018 Tamino Martinius"
|
|
100
|
+
},
|
|
101
|
+
"isexe@2.0.0": {
|
|
102
|
+
"name": "isexe",
|
|
103
|
+
"version": "2.0.0",
|
|
104
|
+
"licenses": "ISC",
|
|
105
|
+
"path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/isexe@2.0.0/node_modules/isexe",
|
|
106
|
+
"licenseText": "The ISC License\n\nCopyright (c) Isaac Z. Schlueter and Contributors\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\nIN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n",
|
|
107
|
+
"licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/LICENSE",
|
|
108
|
+
"repository": "https://github.com/isaacs/isexe",
|
|
109
|
+
"publisher": "Isaac Z. Schlueter",
|
|
110
|
+
"email": "i@izs.me",
|
|
111
|
+
"url": "http://blog.izs.me/",
|
|
112
|
+
"copyright": "Copyright (c) Isaac Z. Schlueter and Contributors"
|
|
113
|
+
},
|
|
114
|
+
"which@2.0.2": {
|
|
115
|
+
"name": "which",
|
|
116
|
+
"version": "2.0.2",
|
|
117
|
+
"licenses": "ISC",
|
|
118
|
+
"path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/which@2.0.2/node_modules/which",
|
|
119
|
+
"licenseText": "The ISC License\n\nCopyright (c) Isaac Z. Schlueter and Contributors\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\nIN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n",
|
|
120
|
+
"licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/which@2.0.2/node_modules/which/LICENSE",
|
|
121
|
+
"repository": "https://github.com/isaacs/node-which",
|
|
122
|
+
"publisher": "Isaac Z. Schlueter",
|
|
123
|
+
"email": "i@izs.me",
|
|
124
|
+
"url": "http://blog.izs.me",
|
|
125
|
+
"copyright": "Copyright (c) Isaac Z. Schlueter and Contributors"
|
|
52
126
|
}
|
|
53
127
|
}
|
package/dist/cli/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Discriminator for `CLIError`, used by tests and the dispatcher. */
|
|
2
|
-
export type CLIErrorCode = "NOT_AUTHENTICATED" | "USER_CANCELLED" | "ALREADY_AUTHENTICATED" | "OAUTH_FAILED" | "KEYCHAIN_FAILURE";
|
|
2
|
+
export type CLIErrorCode = "NOT_AUTHENTICATED" | "USER_CANCELLED" | "ALREADY_AUTHENTICATED" | "OAUTH_FAILED" | "KEYCHAIN_FAILURE" | "REFRESH_UNREACHABLE";
|
|
3
3
|
/**
|
|
4
4
|
* Thrown from a verb's `run` to signal a known failure. The
|
|
5
5
|
* dispatcher writes `message` to stderr and exits with `exitCode`.
|
package/dist/cli/errors.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import defaultRunSession from "../run/runSession";
|
|
2
|
+
import type { CommandDeps } from "../cli/types";
|
|
3
|
+
/** Extra dependencies `run` needs beyond the shared CLI deps. */
|
|
4
|
+
export interface RunDeps extends CommandDeps {
|
|
5
|
+
/** Environment source. Defaults to `process.env`. */
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
/** Override the session supervisor (tests). */
|
|
8
|
+
runSession?: typeof defaultRunSession;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `axe-auth run [--port] [--secret] -- <command> [args...]`. Unlike the other
|
|
12
|
+
* verbs, everything after `--` is the wrapped server command, so this bypasses
|
|
13
|
+
* the shared flag-only dispatch and parses its own portion. Returns the exit
|
|
14
|
+
* code to hand to the process.
|
|
15
|
+
*/
|
|
16
|
+
export declare function dispatchRun(rest: string[], deps: RunDeps): Promise<number>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Help text for `axe-auth run --help`. */
|
|
2
|
+
export declare const HELP_RUN = "npx @deque/axe-auth run -- <server launch command>\n\nLaunch and supervise the axe MCP server so it keeps a valid OAuth access\ntoken for the whole session, refreshing before expiry without a restart. Use\nthis as your MCP client's server command; the client manages its lifetime like\nany stdio server. Everything after `--` is the command it launches.\n\nUsage:\n npx @deque/axe-auth run [--port <port>] [--secret <secret>] -- <command> [args...]\n\n The wrapped server must be started with token refresh on a loopback port.\n Under Docker, publish it with `-p 127.0.0.1:<port>:<port>`, forward the auth\n vars with `-e AXE_ACCESS_TOKEN -e AXE_TOKEN_REFRESH_PORT -e AXE_TOKEN_REFRESH_SECRET`,\n and set `-e AXE_TOKEN_REFRESH_HOST=0.0.0.0` so the published port can reach the\n listener; `run` supplies the token/port/secret values. Under npm the wrapped\n node process inherits them directly.\n\nOptions:\n --port <port> Loopback port the server's refresh listener uses.\n Defaults to AXE_TOKEN_REFRESH_PORT, otherwise a free\n port chosen per session. Pin one for a container,\n which can only reach a published port: docker,\n podman, and nerdctl are detected and refused\n without it; pin it yourself for anything else.\n --secret <secret> Shared secret for the refresh channel. Defaults to\n AXE_TOKEN_REFRESH_SECRET, otherwise a generated one.\n Prefer the env var: a value passed here is visible to other\n local users via the process list.\n -h, --help Show this help.\n\nExit codes:\n 0 The wrapped server exited normally.\n 1 Not authenticated; run `npx @deque/axe-auth login` first.\n 2 Configuration or connectivity error (an invalid port, a container\n command with no port pinned, no command after `--`, or a pinned\n refresh port the server could not be reached on).\n <n> Otherwise, the wrapped server's own exit code.";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP_RUN = void 0;
|
|
4
|
+
/** Help text for `axe-auth run --help`. */
|
|
5
|
+
exports.HELP_RUN = `npx @deque/axe-auth run -- <server launch command>
|
|
6
|
+
|
|
7
|
+
Launch and supervise the axe MCP server so it keeps a valid OAuth access
|
|
8
|
+
token for the whole session, refreshing before expiry without a restart. Use
|
|
9
|
+
this as your MCP client's server command; the client manages its lifetime like
|
|
10
|
+
any stdio server. Everything after \`--\` is the command it launches.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
npx @deque/axe-auth run [--port <port>] [--secret <secret>] -- <command> [args...]
|
|
14
|
+
|
|
15
|
+
The wrapped server must be started with token refresh on a loopback port.
|
|
16
|
+
Under Docker, publish it with \`-p 127.0.0.1:<port>:<port>\`, forward the auth
|
|
17
|
+
vars with \`-e AXE_ACCESS_TOKEN -e AXE_TOKEN_REFRESH_PORT -e AXE_TOKEN_REFRESH_SECRET\`,
|
|
18
|
+
and set \`-e AXE_TOKEN_REFRESH_HOST=0.0.0.0\` so the published port can reach the
|
|
19
|
+
listener; \`run\` supplies the token/port/secret values. Under npm the wrapped
|
|
20
|
+
node process inherits them directly.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--port <port> Loopback port the server's refresh listener uses.
|
|
24
|
+
Defaults to AXE_TOKEN_REFRESH_PORT, otherwise a free
|
|
25
|
+
port chosen per session. Pin one for a container,
|
|
26
|
+
which can only reach a published port: docker,
|
|
27
|
+
podman, and nerdctl are detected and refused
|
|
28
|
+
without it; pin it yourself for anything else.
|
|
29
|
+
--secret <secret> Shared secret for the refresh channel. Defaults to
|
|
30
|
+
AXE_TOKEN_REFRESH_SECRET, otherwise a generated one.
|
|
31
|
+
Prefer the env var: a value passed here is visible to other
|
|
32
|
+
local users via the process list.
|
|
33
|
+
-h, --help Show this help.
|
|
34
|
+
|
|
35
|
+
Exit codes:
|
|
36
|
+
0 The wrapped server exited normally.
|
|
37
|
+
1 Not authenticated; run \`npx @deque/axe-auth login\` first.
|
|
38
|
+
2 Configuration or connectivity error (an invalid port, a container
|
|
39
|
+
command with no port pinned, no command after \`--\`, or a pinned
|
|
40
|
+
refresh port the server could not be reached on).
|
|
41
|
+
<n> Otherwise, the wrapped server's own exit code.`;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.dispatchRun = dispatchRun;
|
|
7
|
+
const node_util_1 = require("node:util");
|
|
8
|
+
const runSession_1 = __importDefault(require("../run/runSession"));
|
|
9
|
+
const findFreePort_1 = __importDefault(require("../run/findFreePort"));
|
|
10
|
+
const run_help_1 = require("./run.help");
|
|
11
|
+
const errors_1 = require("../cli/errors");
|
|
12
|
+
/** Container runtimes whose child cannot reach a host port it was not told to publish. */
|
|
13
|
+
const CONTAINER_COMMANDS = new Set(["docker", "podman", "nerdctl"]);
|
|
14
|
+
/** Whether `command` launches a container runtime rather than a local process. */
|
|
15
|
+
function isContainerCommand(command) {
|
|
16
|
+
if (!command) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const name = command.split(/[/\\]/).pop() ?? command;
|
|
20
|
+
return CONTAINER_COMMANDS.has(name.replace(/\.exe$/i, "").toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `axe-auth run [--port] [--secret] -- <command> [args...]`. Unlike the other
|
|
24
|
+
* verbs, everything after `--` is the wrapped server command, so this bypasses
|
|
25
|
+
* the shared flag-only dispatch and parses its own portion. Returns the exit
|
|
26
|
+
* code to hand to the process.
|
|
27
|
+
*/
|
|
28
|
+
async function dispatchRun(rest, deps) {
|
|
29
|
+
const env = deps.env ?? process.env;
|
|
30
|
+
// Split on the first `--`: our flags precede it, the wrapped command follows.
|
|
31
|
+
const separator = rest.indexOf("--");
|
|
32
|
+
const own = separator === -1 ? rest : rest.slice(0, separator);
|
|
33
|
+
const childArgv = separator === -1 ? [] : rest.slice(separator + 1);
|
|
34
|
+
let values;
|
|
35
|
+
try {
|
|
36
|
+
values = (0, node_util_1.parseArgs)({
|
|
37
|
+
args: own,
|
|
38
|
+
options: {
|
|
39
|
+
port: { type: "string" },
|
|
40
|
+
secret: { type: "string" },
|
|
41
|
+
help: { type: "boolean", short: "h" },
|
|
42
|
+
},
|
|
43
|
+
strict: true,
|
|
44
|
+
allowPositionals: false,
|
|
45
|
+
}).values;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
deps.stderr.write(`${(0, errors_1.describeError)(err)}\n`);
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
if (values.help) {
|
|
52
|
+
deps.stdout.write(`${run_help_1.HELP_RUN}\n`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (childArgv.length === 0) {
|
|
56
|
+
deps.stderr.write("axe-auth run needs a command after `--`, e.g. `npx @deque/axe-auth run -- docker run ... axe-mcp-server`\n");
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
// No port pinned: take a fresh one, so a leftover server from an earlier
|
|
60
|
+
// session cannot collide with it (dequelabs/axe-mcp-server#1013).
|
|
61
|
+
const portRaw = values.port ?? env.AXE_TOKEN_REFRESH_PORT;
|
|
62
|
+
let port;
|
|
63
|
+
const portWasAutoSelected = !portRaw;
|
|
64
|
+
if (portRaw) {
|
|
65
|
+
port = Number(portRaw);
|
|
66
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536) {
|
|
67
|
+
deps.stderr.write(`Invalid port ${portRaw}: must be a TCP port (1-65535)\n`);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (isContainerCommand(childArgv[0])) {
|
|
72
|
+
// A container only reaches a published port, so an ephemeral one would
|
|
73
|
+
// never be reachable. Fail now, not at the first refresh push.
|
|
74
|
+
deps.stderr.write("axe-auth run needs an explicit refresh port when the wrapped command is a container: set AXE_TOKEN_REFRESH_PORT or pass --port, and publish it with `-p 127.0.0.1:<port>:<port>`\n");
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
port = await (0, findFreePort_1.default)();
|
|
79
|
+
}
|
|
80
|
+
// Mirror the server's minimum-length check so a short pinned secret fails
|
|
81
|
+
// here, at the command the user ran, rather than as a downstream server
|
|
82
|
+
// startup crash. An unset secret is generated by `runSession`.
|
|
83
|
+
const secret = values.secret ?? env.AXE_TOKEN_REFRESH_SECRET;
|
|
84
|
+
if (secret && secret.length < 16) {
|
|
85
|
+
deps.stderr.write("AXE_TOKEN_REFRESH_SECRET must be at least 16 characters\n");
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
const runSession = deps.runSession ?? runSession_1.default;
|
|
89
|
+
try {
|
|
90
|
+
return await runSession({
|
|
91
|
+
command: childArgv[0],
|
|
92
|
+
commandArgs: childArgv.slice(1),
|
|
93
|
+
port,
|
|
94
|
+
portWasAutoSelected,
|
|
95
|
+
secret,
|
|
96
|
+
env,
|
|
97
|
+
stderr: deps.stderr,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err instanceof errors_1.CLIError) {
|
|
102
|
+
deps.stderr.write(`${err.message}\n`);
|
|
103
|
+
return err.exitCode;
|
|
104
|
+
}
|
|
105
|
+
deps.stderr.write(`${(0, errors_1.describeError)(err)}\n`);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const tokenStore_1 = require("./oauth/tokenStore");
|
|
|
12
12
|
const login_1 = __importDefault(require("./commands/login"));
|
|
13
13
|
const logout_1 = __importDefault(require("./commands/logout"));
|
|
14
14
|
const token_1 = __importDefault(require("./commands/token"));
|
|
15
|
+
const run_1 = require("./commands/run");
|
|
15
16
|
const errors_1 = require("./cli/errors");
|
|
16
17
|
const safeExit_1 = require("./cli/safeExit");
|
|
17
18
|
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -20,12 +21,18 @@ const COMMANDS = [
|
|
|
20
21
|
logout_1.default,
|
|
21
22
|
token_1.default,
|
|
22
23
|
];
|
|
24
|
+
// `run` has its own dispatch (it takes a `-- <command>` passthrough), so it is
|
|
25
|
+
// not a `CommandSpec`; list it in help by hand.
|
|
26
|
+
const RUN_SUMMARY = "Launch and supervise the axe MCP server, keeping its OAuth token fresh with no restart.";
|
|
23
27
|
function findCommand(verb) {
|
|
24
28
|
return COMMANDS.find((c) => c.name === verb);
|
|
25
29
|
}
|
|
26
30
|
function topLevelHelp() {
|
|
27
|
-
const width = Math.max(...COMMANDS.map((c) => c.name.length));
|
|
28
|
-
const verbList =
|
|
31
|
+
const width = Math.max(...COMMANDS.map((c) => c.name.length), "run".length);
|
|
32
|
+
const verbList = [
|
|
33
|
+
...COMMANDS.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`),
|
|
34
|
+
` ${"run".padEnd(width)} ${RUN_SUMMARY}`,
|
|
35
|
+
].join("\n");
|
|
29
36
|
return `${pkg.name} v${pkg.version}
|
|
30
37
|
|
|
31
38
|
${pkg.description}
|
|
@@ -56,6 +63,15 @@ async function dispatch(argv) {
|
|
|
56
63
|
process.stderr.write(`Unknown option: ${first}. Run \`axe-auth --help\` for usage.\n`);
|
|
57
64
|
return 2;
|
|
58
65
|
}
|
|
66
|
+
// `run` wraps a launch command after `--`; it can't go through the shared
|
|
67
|
+
// flag-only parse below.
|
|
68
|
+
if (first === "run") {
|
|
69
|
+
return (0, run_1.dispatchRun)(rest, {
|
|
70
|
+
stdin: process.stdin,
|
|
71
|
+
stdout: process.stdout,
|
|
72
|
+
stderr: process.stderr,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
59
75
|
const command = findCommand(first);
|
|
60
76
|
if (!command) {
|
|
61
77
|
process.stderr.write(`Unknown command: ${first}. Run \`axe-auth --help\` for usage.\n`);
|
|
@@ -19,4 +19,4 @@ export type KeyringEntryFactory = (service: string, account: string) => KeyringE
|
|
|
19
19
|
* `KEYRING_UNAVAILABLE` on first store construction, not on module
|
|
20
20
|
* import.
|
|
21
21
|
*/
|
|
22
|
-
export declare
|
|
22
|
+
export declare function defaultEntryFactory(service: string, account: string): KeyringEntry;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.defaultEntryFactory =
|
|
3
|
+
exports.defaultEntryFactory = defaultEntryFactory;
|
|
4
4
|
const node_module_1 = require("node:module");
|
|
5
5
|
const errors_1 = require("./errors");
|
|
6
6
|
const requireFromHere = (0, node_module_1.createRequire)(__filename);
|
|
@@ -34,8 +34,7 @@ function resolveEntryCtor() {
|
|
|
34
34
|
* `KEYRING_UNAVAILABLE` on first store construction, not on module
|
|
35
35
|
* import.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
function defaultEntryFactory(service, account) {
|
|
38
38
|
const Ctor = resolveEntryCtor();
|
|
39
39
|
return new Ctor(service, account);
|
|
40
|
-
}
|
|
41
|
-
exports.defaultEntryFactory = defaultEntryFactory;
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the OS for a free loopback port by binding port 0 and releasing it.
|
|
3
|
+
*
|
|
4
|
+
* Sessions used to share one fixed port, so a leftover server held it and the
|
|
5
|
+
* next session lost token refresh (dequelabs/axe-mcp-server#1013). The OS
|
|
6
|
+
* never hands out a port something is still bound to.
|
|
7
|
+
*
|
|
8
|
+
* It is released before the child binds it, so another process could take it
|
|
9
|
+
* in between. That window is not small: it spans minting a token against
|
|
10
|
+
* Keycloak and starting the child. Because the port was not the user's
|
|
11
|
+
* choice, losing it degrades the session to no token refresh rather than
|
|
12
|
+
* failing it (see `portWasAutoSelected` in `runSession`).
|
|
13
|
+
*
|
|
14
|
+
* Nothing proves the listener on that port is our child, so a process that
|
|
15
|
+
* takes it receives whatever is pushed there. Tracked in #1028.
|
|
16
|
+
*/
|
|
17
|
+
export default function findFreePort(host?: string): Promise<number>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = findFreePort;
|
|
4
|
+
const node_net_1 = require("node:net");
|
|
5
|
+
/**
|
|
6
|
+
* Ask the OS for a free loopback port by binding port 0 and releasing it.
|
|
7
|
+
*
|
|
8
|
+
* Sessions used to share one fixed port, so a leftover server held it and the
|
|
9
|
+
* next session lost token refresh (dequelabs/axe-mcp-server#1013). The OS
|
|
10
|
+
* never hands out a port something is still bound to.
|
|
11
|
+
*
|
|
12
|
+
* It is released before the child binds it, so another process could take it
|
|
13
|
+
* in between. That window is not small: it spans minting a token against
|
|
14
|
+
* Keycloak and starting the child. Because the port was not the user's
|
|
15
|
+
* choice, losing it degrades the session to no token refresh rather than
|
|
16
|
+
* failing it (see `portWasAutoSelected` in `runSession`).
|
|
17
|
+
*
|
|
18
|
+
* Nothing proves the listener on that port is our child, so a process that
|
|
19
|
+
* takes it receives whatever is pushed there. Tracked in #1028.
|
|
20
|
+
*/
|
|
21
|
+
async function findFreePort(host = "127.0.0.1") {
|
|
22
|
+
const probe = (0, node_net_1.createServer)();
|
|
23
|
+
try {
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
probe.once("error", reject);
|
|
26
|
+
probe.listen(0, host, () => {
|
|
27
|
+
const address = probe.address();
|
|
28
|
+
if (address === null) {
|
|
29
|
+
reject(new Error("could not determine a free port"));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
resolve(address.port);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await new Promise((resolve) => probe.close(() => resolve()));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Header carrying the shared secret on a token-refresh push. Matches the server's listener. */
|
|
2
|
+
export declare const REFRESH_SECRET_HEADER = "x-refresh-secret";
|
|
3
|
+
/** Pushes a fresh access token to the running server's loopback listener. */
|
|
4
|
+
export type PushToken = (params: {
|
|
5
|
+
port: number;
|
|
6
|
+
secret: string;
|
|
7
|
+
accessToken: string;
|
|
8
|
+
}) => Promise<void>;
|
|
9
|
+
/** POST the fresh token to the server's loopback refresh listener. */
|
|
10
|
+
export default function pushToken({ port, secret, accessToken, }: Parameters<PushToken>[0]): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REFRESH_SECRET_HEADER = void 0;
|
|
4
|
+
exports.default = pushToken;
|
|
5
|
+
/** Header carrying the shared secret on a token-refresh push. Matches the server's listener. */
|
|
6
|
+
exports.REFRESH_SECRET_HEADER = "x-refresh-secret";
|
|
7
|
+
/** Bound the push so a stalled server can't hang the caller. */
|
|
8
|
+
const PUSH_TIMEOUT_MS = 5_000;
|
|
9
|
+
/** How much of a failed push's response body is worth repeating to the user. */
|
|
10
|
+
const MAX_DETAIL_LEN = 200;
|
|
11
|
+
/** POST the fresh token to the server's loopback refresh listener. */
|
|
12
|
+
async function pushToken({ port, secret, accessToken, }) {
|
|
13
|
+
const res = await fetch(`http://127.0.0.1:${port}/token`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
[exports.REFRESH_SECRET_HEADER]: secret,
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({ accessToken }),
|
|
20
|
+
signal: AbortSignal.timeout(PUSH_TIMEOUT_MS),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
// Whatever holds this port wrote the body, and it reaches the client's
|
|
24
|
+
// stderr, so take a little of it and strip anything that could forge log
|
|
25
|
+
// lines or redraw the terminal.
|
|
26
|
+
const detail = await res
|
|
27
|
+
.text()
|
|
28
|
+
.then((text) => text
|
|
29
|
+
.slice(0, MAX_DETAIL_LEN)
|
|
30
|
+
.replace(/[^\t\x20-\x7e]/g, " ")
|
|
31
|
+
.trim())
|
|
32
|
+
.catch(() => "");
|
|
33
|
+
throw new Error(`server responded ${res.status}${detail ? `: ${detail}` : ""}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import { getValidAccessToken } from "../oauth/getValidAccessToken";
|
|
3
|
+
import { type TokenStore } from "../oauth/tokenStore";
|
|
4
|
+
import { type SuperviseOptions } from "./supervise";
|
|
5
|
+
import { type PushToken } from "./pushToken";
|
|
6
|
+
/** Starts a repeating callback and returns a canceller. Injectable so tests drive ticks by hand. */
|
|
7
|
+
export type Scheduler = (intervalMs: number, tick: () => void) => () => void;
|
|
8
|
+
/** Options for {@link runSession}. */
|
|
9
|
+
export interface RunSessionOptions {
|
|
10
|
+
/** Executable of the wrapped server command (everything after `--`). */
|
|
11
|
+
command: string;
|
|
12
|
+
/** Arguments for the wrapped command. */
|
|
13
|
+
commandArgs: string[];
|
|
14
|
+
/** Loopback port the server's refresh listener is on. */
|
|
15
|
+
port: number;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the port was chosen by us rather than by the user. An auto-picked
|
|
18
|
+
* port can lose a race it is not the user's fault to lose, so a first push
|
|
19
|
+
* that cannot reach it degrades the session instead of failing it.
|
|
20
|
+
*/
|
|
21
|
+
portWasAutoSelected?: boolean;
|
|
22
|
+
/** Shared secret authenticating pushes. Generated if omitted. */
|
|
23
|
+
secret?: string;
|
|
24
|
+
/** Cadence of the refresh-and-push loop, ms. */
|
|
25
|
+
refreshIntervalMs?: number;
|
|
26
|
+
/** Override token minting (tests). */
|
|
27
|
+
getToken?: typeof getValidAccessToken;
|
|
28
|
+
/** Override the token store (tests). */
|
|
29
|
+
tokenStore?: TokenStore;
|
|
30
|
+
/** Override the push (tests). */
|
|
31
|
+
push?: PushToken;
|
|
32
|
+
/** Override the child supervisor (tests). */
|
|
33
|
+
supervise?: (options: SuperviseOptions) => Promise<number>;
|
|
34
|
+
/** Override the refresh scheduler (tests). */
|
|
35
|
+
scheduler?: Scheduler;
|
|
36
|
+
/** Base environment for the wrapped child. Defaults to `process.env`. */
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
/** Forwarded to the supervisor as the child's stdin. Defaults to `process.stdin`. */
|
|
39
|
+
stdin?: Readable;
|
|
40
|
+
/** Forwarded to the supervisor as the child's stdout (the MCP channel). Defaults to `process.stdout`. */
|
|
41
|
+
stdout?: Writable;
|
|
42
|
+
/** Diagnostics stream. Defaults to `process.stderr`. NEVER stdout (that is the MCP channel). */
|
|
43
|
+
stderr?: Writable;
|
|
44
|
+
/** Forwarded to the supervisor's signal registrar (tests). Defaults to `process.on`. */
|
|
45
|
+
onSignal?: (signal: NodeJS.Signals, handler: () => void) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Supervise the wrapped server command for a session: mint an initial access
|
|
49
|
+
* token, inject it plus the refresh secret and port into the child's
|
|
50
|
+
* environment, and keep the running server's token fresh by refreshing and
|
|
51
|
+
* pushing before expiry. Resolves with the child's exit code.
|
|
52
|
+
*
|
|
53
|
+
* Fails fast with a `NOT_AUTHENTICATED` {@link CLIError} if there are no stored
|
|
54
|
+
* credentials to mint from, and with `REFRESH_UNREACHABLE` if the very first
|
|
55
|
+
* push cannot reach the server (almost always a misconfigured port/secret) —
|
|
56
|
+
* exiting so the MCP client sees the failure rather than letting the session
|
|
57
|
+
* silently die at expiry. Once a push has succeeded, a later refresh failure is
|
|
58
|
+
* logged (the server keeps its last good token) rather than tearing the session
|
|
59
|
+
* down, with one louder diagnostic after {@link CONSECUTIVE_FAILURE_WARNING_THRESHOLD}
|
|
60
|
+
* consecutive failures.
|
|
61
|
+
*/
|
|
62
|
+
export default function runSession(options: RunSessionOptions): Promise<number>;
|