oauth2-cli 0.8.1 → 0.8.3

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 CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [0.8.3](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.2...oauth2-cli/0.8.3) (2026-02-18)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * continued server timeout improvements ([2e8d275](https://github.com/battis/oauth2-cli/commit/2e8d275706be411b317e2257bd97ee8f7fe4bc95))
11
+
12
+ ## [0.8.2](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.1...oauth2-cli/0.8.2) (2026-02-18)
13
+
14
+
15
+ ### Features
16
+
17
+ * isAuthorized() ([56b0436](https://github.com/battis/oauth2-cli/commit/56b0436d98b9d9f213518e806ead846321180a63))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * block thread until interactive authorization complete ([fb9987e](https://github.com/battis/oauth2-cli/commit/fb9987e41a5a8f129955bc3e88ab17994860091b)), closes [#22](https://github.com/battis/oauth2-cli/issues/22)
23
+
5
24
  ## [0.8.1](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.0...oauth2-cli/0.8.1) (2026-02-18)
6
25
 
7
26
 
package/dist/Client.d.ts CHANGED
@@ -84,6 +84,7 @@ export declare class Client<C extends Credentials = Credentials> extends EventEm
84
84
  protected getParameters(session: Session): Promise<URLSearchParams>;
85
85
  getAuthorizationUrl(session: Session): Promise<URL>;
86
86
  createSession({ views, ...options }: Omit<SessionOptions, 'client'>): Session;
87
+ isAuthorized(): Promise<boolean>;
87
88
  authorize(options?: Omit<SessionOptions, 'client'>): Promise<Token.Response>;
88
89
  handleAuthorizationCodeRedirect(req: Request, session: Session): Promise<void>;
89
90
  protected refreshTokenGrant({ refresh_token, inject: request }?: RefreshOptions): Promise<Token.Response | undefined>;
package/dist/Client.js CHANGED
@@ -91,6 +91,16 @@ export class Client extends EventEmitter {
91
91
  ...options
92
92
  });
93
93
  }
94
+ async isAuthorized() {
95
+ if (this.token?.expiresIn()) {
96
+ return true;
97
+ }
98
+ else {
99
+ return await this.tokenLock.runExclusive(async () => {
100
+ return !!(await this.refreshTokenGrant());
101
+ });
102
+ }
103
+ }
94
104
  async authorize(options = {}) {
95
105
  const session = this.createSession(options);
96
106
  const token = await this.save(await session.authorizationCodeGrant());
@@ -98,16 +108,20 @@ export class Client extends EventEmitter {
98
108
  }
99
109
  async handleAuthorizationCodeRedirect(req, session) {
100
110
  try {
101
- const response = await OpenIDClient.authorizationCodeGrant(await this.getConfiguration(), new URL(req.url, this.redirect_uri), {
111
+ /**
112
+ * Do _NOT_ await this promise: the WebServer needs to send the
113
+ * authorization complete response asynchronously before this can resolve,
114
+ * and awaiting session.resolve() will block that response.
115
+ */
116
+ session.resolve(await OpenIDClient.authorizationCodeGrant(await this.getConfiguration(), new URL(req.url, this.redirect_uri), {
102
117
  pkceCodeVerifier: session.code_verifier,
103
118
  expectedState: session.state
104
119
  }, this.inject?.search
105
120
  ? requestish.URLSearchParams.from(this.inject.search)
106
- : undefined);
107
- await session.resolve(response);
121
+ : undefined));
108
122
  }
109
- catch (error) {
110
- await session.resolve(undefined, error);
123
+ catch (cause) {
124
+ session.reject(new Error('Error making Authorization Code Grant request', { cause }));
111
125
  }
112
126
  }
113
127
  async refreshTokenGrant({ refresh_token = this.token?.refresh_token, inject: request } = {}) {
package/dist/Session.d.ts CHANGED
@@ -11,7 +11,7 @@ export type SessionOptions = {
11
11
  /** Additional request injection for authorization code grant flow */
12
12
  inject?: Injection;
13
13
  };
14
- export type Resolver = (response?: Token.Response, error?: Error) => void | Promise<void>;
14
+ export type SessionResolver = (response: Token.Response) => void | Promise<void>;
15
15
  export declare class Session {
16
16
  private readonly client;
17
17
  private readonly outOfBandRedirectServer;
@@ -21,19 +21,21 @@ export declare class Session {
21
21
  readonly state: string;
22
22
  /** Additional request injection for Authorization Code Grant request */
23
23
  readonly inject?: Injection;
24
+ private spinner;
24
25
  private _resolve?;
25
- private spinner?;
26
26
  /**
27
27
  * Method that resolves or rejects the promise returned from the
28
28
  * {@link authorizationCodeGrant}
29
29
  */
30
- get resolve(): Resolver;
30
+ get resolve(): SessionResolver;
31
+ reject(cause: unknown): void;
31
32
  constructor({ client, views, inject: request }: SessionOptions);
32
33
  /** Instantiate the web server that will listen for the out-of-band redirect */
33
- createWebServer(options: Omit<WebServer.WebServerOptions, 'session'>): WebServer.WebServerInterface;
34
+ protected instantiateWebServer(options: Omit<WebServer.WebServerOptions, 'session'>): WebServer.WebServerInterface;
34
35
  /**
35
36
  * Trigger the start of the Authorization Code Grant flow, returnig a Promise
36
- * that will resolve into the eventual token
37
+ * that will resolve into the eventual token. This will close the out-of-band
38
+ * redirect server that creating the session started.
37
39
  */
38
40
  authorizationCodeGrant(): Promise<Token.Response>;
39
41
  /** OAuth 2.0 redirect_uri that this session is handling */
package/dist/Session.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Colors } from '@qui-cli/colors';
2
2
  import * as gcrtl from 'gcrtl';
3
- import open from 'open';
4
3
  import * as OpenIDClient from 'openid-client';
5
4
  import ora from 'ora';
6
5
  import * as WebServer from './WebServer.js';
@@ -13,49 +12,87 @@ export class Session {
13
12
  state = OpenIDClient.randomState();
14
13
  /** Additional request injection for Authorization Code Grant request */
15
14
  inject;
16
- _resolve;
17
15
  spinner;
16
+ _resolve;
18
17
  /**
19
18
  * Method that resolves or rejects the promise returned from the
20
19
  * {@link authorizationCodeGrant}
21
20
  */
22
21
  get resolve() {
23
22
  if (!this._resolve) {
24
- throw new Error('callback is missing');
23
+ throw new Error(`Session resolve method is ${this._resolve}`);
25
24
  }
26
25
  return this._resolve;
27
26
  }
27
+ reject(cause) {
28
+ throw new Error('Session failed', { cause });
29
+ }
28
30
  constructor({ client, views, inject: request }) {
31
+ this.spinner = ora('Awaiting interactive authorization').start();
29
32
  this.client = client;
30
33
  this.inject = request;
31
- this.outOfBandRedirectServer = this.createWebServer({ views });
34
+ this.outOfBandRedirectServer = this.instantiateWebServer({ views });
32
35
  }
33
36
  /** Instantiate the web server that will listen for the out-of-band redirect */
34
- createWebServer(options) {
37
+ instantiateWebServer(options) {
35
38
  return new WebServer.WebServer({ session: this, ...options });
36
39
  }
37
40
  /**
38
41
  * Trigger the start of the Authorization Code Grant flow, returnig a Promise
39
- * that will resolve into the eventual token
42
+ * that will resolve into the eventual token. This will close the out-of-band
43
+ * redirect server that creating the session started.
40
44
  */
41
- authorizationCodeGrant() {
42
- return new Promise((resolve, reject) => {
43
- this._resolve = async (response, error) => {
44
- if (error) {
45
- reject(error);
46
- }
47
- if (response) {
48
- resolve(response);
49
- }
50
- else {
51
- reject(new Error('Authorization Code Grant response undefined.'));
52
- }
53
- };
54
- const url = gcrtl
55
- .expand(this.outOfBandRedirectServer.authorization_endpoint, this.client.redirect_uri)
56
- .toString();
57
- this.spinner = ora(`Waiting for interactive authorization at ${Colors.url(url)}`).start();
58
- open(url);
45
+ async authorizationCodeGrant() {
46
+ return await new Promise((resolve, reject) => {
47
+ try {
48
+ this._resolve = (response) => {
49
+ let closed = false;
50
+ this.spinner.text =
51
+ 'Waiting for out-of-band redirect server to shut down';
52
+ this.outOfBandRedirectServer.close().then(() => {
53
+ closed = true;
54
+ this.spinner.succeed('Interactive authorization complete');
55
+ resolve(response);
56
+ });
57
+ setTimeout(() => {
58
+ if (!closed) {
59
+ this.spinner.text =
60
+ 'Still waiting for out-of-band redirect server to shut down.\n' +
61
+ ' Your browser may be holding the connection to the server open.\n\n' +
62
+ ' Please close the "Authorization Complete" tab in your browser.';
63
+ }
64
+ }, 5000);
65
+ setTimeout(() => {
66
+ if (!closed) {
67
+ this.spinner.text =
68
+ 'Still waiting for out-of-band redirect server to shut down.\n' +
69
+ ' Your browser may be holding the connection to the server open.\n\n' +
70
+ ' Please close the browser window.';
71
+ }
72
+ }, 10000);
73
+ setTimeout(() => {
74
+ if (!closed) {
75
+ this.spinner.text =
76
+ 'Still waiting for out-of-band redirect server to shut down.\n' +
77
+ ' Your browser may be holding the connection to the server open.\n\n' +
78
+ ' Please quit the browser.';
79
+ }
80
+ }, 15000);
81
+ };
82
+ const url = gcrtl
83
+ .expand(this.outOfBandRedirectServer.authorization_endpoint, this.client.redirect_uri)
84
+ .toString();
85
+ //open(url);
86
+ this.spinner.text = `Please continue interactive authorization at ${Colors.url(url)} in your browser`;
87
+ }
88
+ catch (cause) {
89
+ this.spinner.text =
90
+ 'Waiting for out-of-band redirect server to shut down';
91
+ this.outOfBandRedirectServer.close().then(() => {
92
+ this.spinner.fail('Interactive authorization failed');
93
+ reject(new Error('Error in Authorization Code flow', { cause }));
94
+ });
95
+ }
59
96
  });
60
97
  }
61
98
  /** OAuth 2.0 redirect_uri that this session is handling */
@@ -70,7 +107,8 @@ export class Session {
70
107
  * Code Grant flow
71
108
  */
72
109
  async handleAuthorizationCodeRedirect(req) {
73
- this.spinner?.succeed('Interactive authorization begun');
110
+ this.spinner.text =
111
+ 'Completing access token request with provided authorization code';
74
112
  return await this.client.handleAuthorizationCodeRedirect(req, this);
75
113
  }
76
114
  }
@@ -15,11 +15,22 @@ export type WebServerOptions = {
15
15
  * URL, the first step in the Authorization Code Grant flow.
16
16
  */
17
17
  authorize_endpoint?: PathString;
18
+ /**
19
+ * The number of milliseconds of inactivity before a socket is presumed to
20
+ * have timed out. This can be reduced to limit potential wait times during
21
+ * interactive authentication, but must still be long enough to allow time for
22
+ * the authorization code to be exchanged for an access token.
23
+ *
24
+ * Defaults to 1000 milliseconds
25
+ */
26
+ timeout?: number;
18
27
  };
19
28
  export declare const DEFAULT_AUTHORIZE_ENDPOINT = "/oauth2-cli/authorize";
20
29
  export interface WebServerInterface {
21
30
  /** See {@link WebServerOptions} */
22
31
  readonly authorization_endpoint: PathString;
32
+ /** Shut down web server */
33
+ close(): Promise<void>;
23
34
  }
24
35
  /**
25
36
  * Minimal HTTP server running on localhost to handle the redirect step of
@@ -33,7 +44,7 @@ export declare class WebServer implements WebServerInterface {
33
44
  protected readonly port: string;
34
45
  readonly authorization_endpoint: PathString;
35
46
  private server;
36
- constructor({ session, views, authorize_endpoint }: WebServerOptions);
47
+ constructor({ session, views, authorize_endpoint, timeout }: WebServerOptions);
37
48
  /**
38
49
  * Set the path to folder of *.ejs templates
39
50
  *
@@ -59,6 +70,6 @@ export declare class WebServer implements WebServerInterface {
59
70
  protected handleAuthorizationEndpoint(req: Request, res: Response): Promise<void>;
60
71
  /** Handles request to `redirect_uri` */
61
72
  protected handleRedirect(req: Request, res: Response): Promise<void>;
62
- /** Close server */
73
+ /** Shut down web server */
63
74
  close(): Promise<void>;
64
75
  }
package/dist/WebServer.js CHANGED
@@ -23,7 +23,8 @@ export class WebServer {
23
23
  port;
24
24
  authorization_endpoint;
25
25
  server;
26
- constructor({ session, views, authorize_endpoint = DEFAULT_AUTHORIZE_ENDPOINT }) {
26
+ constructor({ session, views, authorize_endpoint = DEFAULT_AUTHORIZE_ENDPOINT, timeout = 1000 // milliseconds
27
+ }) {
27
28
  this.session = session;
28
29
  this.authorization_endpoint = authorize_endpoint;
29
30
  this.views = views;
@@ -37,6 +38,9 @@ export class WebServer {
37
38
  app.get(this.authorization_endpoint, this.handleAuthorizationEndpoint.bind(this));
38
39
  app.get(gcrtl.path(url), this.handleRedirect.bind(this));
39
40
  this.server = app.listen(gcrtl.port(url));
41
+ this.server.timeout = timeout;
42
+ this.server.keepAliveTimeout = 0;
43
+ this.server.keepAliveTimeoutBuffer = 0;
40
44
  }
41
45
  /**
42
46
  * Set the path to folder of *.ejs templates
@@ -79,6 +83,7 @@ export class WebServer {
79
83
  const authorization_url = await this.session.getAuthorizationUrl();
80
84
  if (!(await this.render(res, 'authorize.ejs', { authorization_url }))) {
81
85
  res.redirect(authorization_url);
86
+ res.end();
82
87
  }
83
88
  }
84
89
  /** Handles request to `redirect_uri` */
@@ -86,7 +91,7 @@ export class WebServer {
86
91
  try {
87
92
  await this.session.handleAuthorizationCodeRedirect(req);
88
93
  if (!(await this.render(res, 'complete.ejs'))) {
89
- res.send('You may close this window.');
94
+ res.send('Authorization complete. You may close this window.');
90
95
  }
91
96
  }
92
97
  catch (error) {
@@ -94,16 +99,15 @@ export class WebServer {
94
99
  res.send(error);
95
100
  }
96
101
  }
97
- finally {
98
- this.close();
99
- }
100
102
  }
101
- /** Close server */
103
+ /** Shut down web server */
102
104
  async close() {
103
105
  return new Promise((resolve, reject) => {
104
- this.server.close((err) => {
105
- if (err) {
106
- reject(err);
106
+ this.server.close((cause) => {
107
+ if (cause) {
108
+ reject(new Error('Error shutting down out-of-band redirect web server', {
109
+ cause
110
+ }));
107
111
  }
108
112
  else {
109
113
  WebServer.activePorts.splice(WebServer.activePorts.indexOf(this.port), 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth2-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Acquire API access tokens via OAuth 2.0 within CLI tools",
5
5
  "homepage": "https://github.com/battis/oauth2-cli/tree/main/packages/oauth2-cli#readme",
6
6
  "repository": {
@@ -20,7 +20,6 @@
20
20
  "async-mutex": "^0.5.0",
21
21
  "express": "^5.2.1",
22
22
  "oauth4webapi": "^3.8.5",
23
- "open": "^11.0.0",
24
23
  "openid-client": "^6.8.2",
25
24
  "ora": "^9.3.0",
26
25
  "gcrtl": "0.1.7",
package/views/error.ejs CHANGED
@@ -32,9 +32,7 @@
32
32
  <div class="center container">
33
33
  <h1>Authorization Error</h1>
34
34
  <div class="alert alert-danger" role="alert">
35
- <% if (typeof error === 'string') { %><%= error %><<% } else { %>
36
- <pre><%= JSON.stringify(error,null,2) %></pre>
37
- <% } %>
35
+ <pre><%= JSON.stringify(error, null, 2) %></pre>
38
36
  </div>
39
37
  <p>You may close this window.</p>
40
38
  </div>