oauth2-cli 0.8.0 → 0.8.2

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.2](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.1...oauth2-cli/0.8.2) (2026-02-18)
6
+
7
+
8
+ ### Features
9
+
10
+ * isAuthorized() ([56b0436](https://github.com/battis/oauth2-cli/commit/56b0436d98b9d9f213518e806ead846321180a63))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * 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)
16
+
17
+ ## [0.8.1](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.0...oauth2-cli/0.8.1) (2026-02-18)
18
+
19
+
20
+ ### Features
21
+
22
+ * provide parameterized credentials with simplified typing ([5a92c67](https://github.com/battis/oauth2-cli/commit/5a92c67dbd4ab52fa4f0b02a910e788f5ae67adb))
23
+
5
24
  ## [0.8.0](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.7.3...oauth2-cli/0.8.0) (2026-02-17)
6
25
 
7
26
  ### ⚠ BREAKING CHANGES
package/dist/Client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PathString } from '@battis/descriptive-types';
2
+ import { JSONValue } from '@battis/typescript-tricks';
2
3
  import { Request } from 'express';
3
4
  import { EventEmitter } from 'node:events';
4
5
  import * as OpenIDClient from 'openid-client';
@@ -12,9 +13,9 @@ import * as Token from './Token/index.js';
12
13
  * `redirect_uri` values
13
14
  */
14
15
  export declare const DEFAULT_REDIRECT_URI = "http://localhost:3000/oauth2-cli/redirect";
15
- export type ClientOptions = {
16
+ export type ClientOptions<C extends Credentials = Credentials> = {
16
17
  /** Credentials for server access */
17
- credentials: Credentials;
18
+ credentials: C;
18
19
  /** Optional request components to inject */
19
20
  inject?: {
20
21
  search?: requestish.URLSearchParams.ish;
@@ -63,9 +64,9 @@ type GetTokenOptions = {
63
64
  *
64
65
  * Emits {@link Client.TokenEvent} whenever a new access token is received
65
66
  */
66
- export declare class Client extends EventEmitter {
67
+ export declare class Client<C extends Credentials = Credentials> extends EventEmitter {
67
68
  static readonly TokenEvent = "token";
68
- protected credentials: Credentials;
69
+ protected credentials: C;
69
70
  protected base_url?: requestish.URL.ish;
70
71
  protected config?: OpenIDClient.Configuration;
71
72
  protected inject?: Injection;
@@ -73,7 +74,7 @@ export declare class Client extends EventEmitter {
73
74
  private token?;
74
75
  private tokenLock;
75
76
  private storage?;
76
- constructor({ credentials, base_url, views, inject, storage }: ClientOptions);
77
+ constructor({ credentials, base_url, views, inject, storage }: ClientOptions<C>);
77
78
  get redirect_uri(): requestish.URL.ish;
78
79
  /**
79
80
  * @throws IndeterminateConfiguration if provided credentials combined with
@@ -83,6 +84,7 @@ export declare class Client extends EventEmitter {
83
84
  protected getParameters(session: Session): Promise<URLSearchParams>;
84
85
  getAuthorizationUrl(session: Session): Promise<URL>;
85
86
  createSession({ views, ...options }: Omit<SessionOptions, 'client'>): Session;
87
+ isAuthorized(): Promise<boolean>;
86
88
  authorize(options?: Omit<SessionOptions, 'client'>): Promise<Token.Response>;
87
89
  handleAuthorizationCodeRedirect(req: Request, session: Session): Promise<void>;
88
90
  protected refreshTokenGrant({ refresh_token, inject: request }?: RefreshOptions): Promise<Token.Response | undefined>;
@@ -107,10 +109,10 @@ export declare class Client extends EventEmitter {
107
109
  private toJSON;
108
110
  /**
109
111
  * Returns the result of {@link request} as a parsed JSON object, optionally
110
- * typed as `T`
112
+ * typed as `J`
111
113
  */
112
- requestJSON<T extends OpenIDClient.JsonValue = OpenIDClient.JsonValue>(url: requestish.URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: requestish.Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<T>;
114
+ requestJSON<J extends JSONValue = JSONValue>(url: requestish.URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: requestish.Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<J>;
113
115
  fetch(input: requestish.URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
114
- fetchJSON<T extends OpenIDClient.JsonValue = OpenIDClient.JsonValue>(input: requestish.URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<T>;
116
+ fetchJSON<J extends JSONValue = JSONValue>(input: requestish.URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<J>;
115
117
  }
116
118
  export {};
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 } = {}) {
@@ -214,7 +228,7 @@ export class Client extends EventEmitter {
214
228
  }
215
229
  /**
216
230
  * Returns the result of {@link request} as a parsed JSON object, optionally
217
- * typed as `T`
231
+ * typed as `J`
218
232
  */
219
233
  async requestJSON(url, method = 'GET', body, headers = {}, dPoPOptions) {
220
234
  return await this.toJSON(await this.request(url, method, body, headers, dPoPOptions));
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(error: Error): 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,8 +12,8 @@ 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}
@@ -25,37 +24,61 @@ export class Session {
25
24
  }
26
25
  return this._resolve;
27
26
  }
27
+ reject(error) {
28
+ throw error;
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' +
62
+ ' Please close the "Authorization Complete" tab in your browser.\n\n' +
63
+ ' If you are browsing in Chrome, close the window.\n' +
64
+ ' If you are browsing in Opera, quit the browser.';
65
+ }
66
+ }, 10000);
67
+ };
68
+ const url = gcrtl
69
+ .expand(this.outOfBandRedirectServer.authorization_endpoint, this.client.redirect_uri)
70
+ .toString();
71
+ //open(url);
72
+ this.spinner.text = `Please continue interactive authorization at ${Colors.url(url)} in your browser`;
73
+ }
74
+ catch (cause) {
75
+ this.spinner.text =
76
+ 'Waiting for out-of-band redirect server to shut down';
77
+ this.outOfBandRedirectServer.close().then(() => {
78
+ this.spinner.fail('Interactive authorization failed');
79
+ reject(new Error('Error in Authorization Code flow', { cause }));
80
+ });
81
+ }
59
82
  });
60
83
  }
61
84
  /** OAuth 2.0 redirect_uri that this session is handling */
@@ -70,7 +93,8 @@ export class Session {
70
93
  * Code Grant flow
71
94
  */
72
95
  async handleAuthorizationCodeRedirect(req) {
73
- this.spinner?.succeed('Interactive authorization begun');
96
+ this.spinner.text =
97
+ 'Completing access token request with provided authorization code';
74
98
  return await this.client.handleAuthorizationCodeRedirect(req, this);
75
99
  }
76
100
  }
@@ -20,6 +20,8 @@ export declare const DEFAULT_AUTHORIZE_ENDPOINT = "/oauth2-cli/authorize";
20
20
  export interface WebServerInterface {
21
21
  /** See {@link WebServerOptions} */
22
22
  readonly authorization_endpoint: PathString;
23
+ /** Shut down web server */
24
+ close(): Promise<void>;
23
25
  }
24
26
  /**
25
27
  * Minimal HTTP server running on localhost to handle the redirect step of
@@ -59,6 +61,6 @@ export declare class WebServer implements WebServerInterface {
59
61
  protected handleAuthorizationEndpoint(req: Request, res: Response): Promise<void>;
60
62
  /** Handles request to `redirect_uri` */
61
63
  protected handleRedirect(req: Request, res: Response): Promise<void>;
62
- /** Close server */
64
+ /** Shut down web server */
63
65
  close(): Promise<void>;
64
66
  }
package/dist/WebServer.js CHANGED
@@ -79,6 +79,7 @@ export class WebServer {
79
79
  const authorization_url = await this.session.getAuthorizationUrl();
80
80
  if (!(await this.render(res, 'authorize.ejs', { authorization_url }))) {
81
81
  res.redirect(authorization_url);
82
+ res.end();
82
83
  }
83
84
  }
84
85
  /** Handles request to `redirect_uri` */
@@ -94,16 +95,15 @@ export class WebServer {
94
95
  res.send(error);
95
96
  }
96
97
  }
97
- finally {
98
- this.close();
99
- }
100
98
  }
101
- /** Close server */
99
+ /** Shut down web server */
102
100
  async close() {
103
101
  return new Promise((resolve, reject) => {
104
- this.server.close((err) => {
105
- if (err) {
106
- reject(err);
102
+ this.server.close((cause) => {
103
+ if (cause) {
104
+ reject(new Error('Error shutting down out-of-band redirect web server', {
105
+ cause
106
+ }));
107
107
  }
108
108
  else {
109
109
  WebServer.activePorts.splice(WebServer.activePorts.indexOf(this.port), 1);
package/dist/index.d.ts CHANGED
@@ -1,2 +1,6 @@
1
- export * from './Export.js';
2
- export * from './Extend.js';
1
+ export * from './Client.js';
2
+ export * from './Credentials.js';
3
+ export * from './Injection.js';
4
+ export * as Scope from './Scope.js';
5
+ export * as Token from './Token/index.js';
6
+ export * from './WebServer.js';
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
1
- export * from './Export.js';
2
- export * from './Extend.js';
1
+ export * from './Client.js';
2
+ export * from './Credentials.js';
3
+ export * from './Injection.js';
4
+ export * as Scope from './Scope.js';
5
+ export * as Token from './Token/index.js';
6
+ export * from './WebServer.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth2-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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/dist/Export.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from './Credentials.js';
2
- export * from './Injection.js';
3
- export * as Scope from './Scope.js';
4
- export * from './WebServer.js';
package/dist/Export.js DELETED
@@ -1,4 +0,0 @@
1
- export * from './Credentials.js';
2
- export * from './Injection.js';
3
- export * as Scope from './Scope.js';
4
- export * from './WebServer.js';
package/dist/Extend.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './Client.js';
2
- export * as Token from './Token/index.js';
package/dist/Extend.js DELETED
@@ -1,2 +0,0 @@
1
- export * from './Client.js';
2
- export * as Token from './Token/index.js';