oauth2-cli 0.2.0 → 0.2.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,15 @@
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.2.2](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.2.1...oauth2-cli/0.2.2) (2025-12-23)
6
+
7
+ ## [0.2.1](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.2.0...oauth2-cli/0.2.1) (2025-09-11)
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * update dependencies to address transient openid-client config error ([f0ca9a8](https://github.com/battis/oauth2-cli/commit/f0ca9a8d2bb4551b80a49e48aa43df5ba66a5a9b))
13
+
5
14
  ## [0.2.0](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.1.6...oauth2-cli/0.2.0) (2025-03-09)
6
15
 
7
16
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth2-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.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": {
@@ -17,19 +17,19 @@
17
17
  "types": "./dist/index.d.ts",
18
18
  "dependencies": {
19
19
  "async-mutex": "^0.5.0",
20
- "express": "^4.21.2",
21
- "open": "^10.1.0",
22
- "openid-client": "^6.3.3",
23
- "@battis/oauth2-configure": "0.1.2"
20
+ "express": "^5.2.1",
21
+ "open": "^11.0.0",
22
+ "openid-client": "^6.8.1",
23
+ "@battis/oauth2-configure": "0.1.3"
24
24
  },
25
25
  "devDependencies": {
26
- "@tsconfig/node20": "^20.1.4",
26
+ "@tsconfig/node20": "^20.1.8",
27
27
  "@types/ejs": "^3.1.5",
28
- "@types/express": "^5.0.0",
29
- "commit-and-tag-version": "^12.5.0",
28
+ "@types/express": "^5.0.6",
29
+ "commit-and-tag-version": "^12.6.1",
30
30
  "del-cli": "^6.0.0",
31
31
  "npm-run-all": "^4.1.5",
32
- "typescript": "^5.8.2"
32
+ "typescript": "^5.9.3"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "ejs": "*"
package/.versionrc.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "path": ".",
3
- "tag-prefix": "oauth2-cli/",
4
- "releaseCommitMessageFormat": "chore(oauth2-cli): oauth2-cli@{{currentTag}}"
5
- }
package/src/Client.ts DELETED
@@ -1,154 +0,0 @@
1
- import * as Configuration from '@battis/oauth2-configure';
2
- import { Mutex } from 'async-mutex';
3
- import * as OpenIDClient from 'openid-client';
4
- import { FileStorage } from './FileStorage.js';
5
- import * as Localhost from './Localhost.js';
6
- import { Token } from './Token.js';
7
- import { TokenStorage } from './TokenStorage.js';
8
-
9
- export type Options = Configuration.Options & {
10
- scope?: string;
11
- headers?: Record<string, string>;
12
- parameters?: Record<string, string>;
13
- store?: TokenStorage | string;
14
- };
15
-
16
- export class Client {
17
- private tokenMutex = new Mutex();
18
- private config?: OpenIDClient.Configuration;
19
- private token?: Token;
20
- private store?: TokenStorage;
21
-
22
- public constructor(private options: Options) {
23
- if (this.options.store) {
24
- if (typeof this.options.store === 'string') {
25
- this.store = new FileStorage(this.options.store);
26
- } else {
27
- this.store = this.options.store;
28
- }
29
- }
30
- }
31
-
32
- public async getToken() {
33
- return await this.tokenMutex.runExclusive(
34
- (async () => {
35
- if (!this.token) {
36
- this.token = await this.store?.load();
37
- }
38
- if (this.token?.hasExpired()) {
39
- this.token = await this.refreshToken(this.token);
40
- }
41
- if (!this.token) {
42
- this.token = await this.authorize();
43
- }
44
- return this.token;
45
- }).bind(this)
46
- );
47
- }
48
-
49
- private async refreshToken(token: Token): Promise<Token | undefined> {
50
- if (token.refresh_token) {
51
- const { headers, parameters } = this.options;
52
- let freshTokens;
53
- if (
54
- (freshTokens = Token.fromResponse(
55
- await OpenIDClient.refreshTokenGrant(
56
- await Configuration.acquire(this.options),
57
- token.refresh_token,
58
- parameters,
59
- // @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
60
- { headers }
61
- ),
62
- token.refresh_token
63
- ))
64
- ) {
65
- return this.store?.save(freshTokens) || freshTokens;
66
- }
67
- }
68
- return this.authorize();
69
- }
70
-
71
- private async getConfiguration() {
72
- if (!this.config) {
73
- this.config = await Configuration.acquire(this.options);
74
- }
75
- return this.config;
76
- }
77
-
78
- private async authorize(): Promise<Token | undefined> {
79
- const {
80
- scope,
81
- redirect_uri,
82
- parameters: additionalParameters
83
- } = this.options;
84
-
85
- return new Promise(async (resolve, reject) => {
86
- const code_verifier = OpenIDClient.randomPKCECodeVerifier();
87
- const code_challenge =
88
- await OpenIDClient.calculatePKCECodeChallenge(code_verifier);
89
- let state: string | undefined = undefined;
90
- const parameters: Record<string, string> = {
91
- ...additionalParameters,
92
- redirect_uri,
93
- code_challenge,
94
- code_challenge_method: 'S256' // TODO make code challenge method configurable?
95
- };
96
-
97
- if (scope) {
98
- parameters.scope = scope;
99
- }
100
- if (!(await this.getConfiguration()).serverMetadata().supportsPKCE()) {
101
- state = OpenIDClient.randomState();
102
- parameters.state = state;
103
- }
104
-
105
- await Localhost.redirectServer({
106
- ...this.options,
107
- authorization_url: OpenIDClient.buildAuthorizationUrl(
108
- await this.getConfiguration(),
109
- parameters
110
- ).href,
111
- code_verifier,
112
- state,
113
- resolve: (async (response?: OpenIDClient.TokenEndpointResponse) => {
114
- this.token = Token.fromResponse(response);
115
- if (this.token && this.store) {
116
- await this.store.save(this.token);
117
- }
118
- resolve(this.token);
119
- }).bind(this),
120
- reject
121
- });
122
- });
123
- }
124
-
125
- public async request(
126
- url: URL | string,
127
- method: string = 'GET',
128
- body?: OpenIDClient.FetchBody,
129
- headers?: Record<string, string>,
130
- options?: OpenIDClient.DPoPOptions
131
- ) {
132
- return await OpenIDClient.fetchProtectedResource(
133
- await this.getConfiguration(),
134
- (await this.getToken())!.access_token,
135
- new URL(url),
136
- method,
137
- body,
138
- new Headers({ ...this.options.headers, ...headers }),
139
- options
140
- );
141
- }
142
-
143
- public async requestJSON(
144
- url: URL | string,
145
- method: string = 'GET',
146
- body?: OpenIDClient.FetchBody,
147
- headers?: Record<string, string>,
148
- options?: OpenIDClient.DPoPOptions
149
- ) {
150
- return await (
151
- await this.request(url, method, body, headers, options)
152
- ).json();
153
- }
154
- }
@@ -1,40 +0,0 @@
1
- import { Mutex } from 'async-mutex';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { Token } from './Token.js';
5
- import { TokenStorage } from './TokenStorage.js';
6
-
7
- export class FileStorage implements TokenStorage {
8
- private fileLock = new Mutex();
9
- private readonly filePath: string;
10
-
11
- public constructor(filePath: string) {
12
- this.filePath = path.resolve(process.cwd(), filePath);
13
- }
14
-
15
- public async load() {
16
- return await this.fileLock.runExclusive(
17
- (async () => {
18
- if (fs.existsSync(this.filePath)) {
19
- return Token.fromResponse(
20
- JSON.parse(fs.readFileSync(this.filePath).toString())
21
- );
22
- }
23
- return undefined;
24
- }).bind(this)
25
- );
26
- }
27
-
28
- public async save(tokens: Token) {
29
- await this.fileLock.runExclusive(
30
- (async () => {
31
- const dirPath = path.dirname(this.filePath);
32
- if (!fs.existsSync(dirPath)) {
33
- fs.mkdirSync(dirPath, { recursive: true });
34
- }
35
- fs.writeFileSync(this.filePath, JSON.stringify(tokens));
36
- }).bind(this)
37
- );
38
- return tokens;
39
- }
40
- }
package/src/Localhost.ts DELETED
@@ -1,119 +0,0 @@
1
- import * as Configuration from '@battis/oauth2-configure';
2
- import express from 'express';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import open from 'open';
6
- import * as OpenIDClient from 'openid-client';
7
-
8
- const ejs = await import('ejs');
9
-
10
- const portRegistry: (number | string)[] = [];
11
-
12
- type Options = Configuration.Options & {
13
- authorization_url: string;
14
- redirect_uri: string;
15
- headers?: Record<string, string>;
16
- code_verifier?: string;
17
- state?: string;
18
- resolve: (tokens?: OpenIDClient.TokenEndpointResponse) => void;
19
- reject: (error: unknown) => void;
20
- views?: string;
21
- };
22
-
23
- export async function redirectServer(options: Options) {
24
- const {
25
- authorization_url,
26
- redirect_uri,
27
- code_verifier,
28
- state,
29
- headers,
30
- resolve,
31
- reject,
32
- views = '../views'
33
- } = options;
34
- const redirectUrl = new URL(redirect_uri);
35
-
36
- const configuration = await Configuration.acquire(options);
37
-
38
- const app = express();
39
- const port = redirectUrl.port !== '' ? redirectUrl.port : 80;
40
- const server = app.listen(port);
41
- let view = 'complete.ejs';
42
- let tokens: OpenIDClient.TokenEndpointResponse | undefined = undefined;
43
- let error: unknown = undefined;
44
-
45
- /*
46
- * FIXME Multiple clients with `redirect_uri` on the same localhost port
47
- * This seems to be some sort of an issue with Express (or node http?) in
48
- * which, despite a fresh invocation of express() for each redirect
49
- * listener, every listener subsequent to the first _on the same port_
50
- * retains the original routing stack of the first listener on that port.
51
- * I have tried:
52
- * - Setting a manual delay (up to 10 seconds) between the receipt of
53
- * the token and resolving it to allow the server to close.
54
- * - Using a mutex semaphore to ensure that no two instances of
55
- * Localhost are running simultaneously
56
- * - Separating the routing into a separate Router middleware
57
- * - Manually removing the routing stack (which does, at least, break
58
- * the routing of the subsequent instances, supporting the idea that
59
- * the app is getting reused by Express)
60
- */
61
- if (portRegistry.includes(port)) {
62
- throw new Error(
63
- `Multiple OAuth clients are attempting to redirect to port ${port}. This will result in failure. Please reconfigure your credentials so that each client is redirecting to a distinct port on http://localhost (e.g. 3000, 3001, 3002)`
64
- );
65
- } else {
66
- portRegistry.push(port);
67
- }
68
-
69
- app.get('/authorize', async (req, res) => {
70
- const viewPath = path.resolve(import.meta.dirname, views, 'authorize');
71
- if (ejs && fs.existsSync(viewPath)) {
72
- res.send(await ejs.renderFile(viewPath));
73
- } else {
74
- res.redirect(authorization_url);
75
- }
76
- });
77
- app.get(redirectUrl.pathname, async (req, res) => {
78
- try {
79
- const currentUrl = new URL(req.originalUrl, redirect_uri);
80
- tokens = await OpenIDClient.authorizationCodeGrant(
81
- configuration,
82
- currentUrl,
83
- {
84
- pkceCodeVerifier: code_verifier,
85
- expectedState: state
86
- },
87
- // @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
88
- { headers }
89
- );
90
- } catch (e) {
91
- error = e;
92
- view = 'error.ejs';
93
- }
94
- if (!tokens && !error) {
95
- error = 'No tokens received in response to authorization code';
96
- }
97
- if (ejs) {
98
- res.send(
99
- await ejs.renderFile(path.resolve(import.meta.dirname, views, view), {
100
- tokens,
101
- error
102
- })
103
- );
104
- } else {
105
- res.send(error || 'You may close this window.');
106
- }
107
- server.close();
108
- if (error) {
109
- reject(error);
110
- } else {
111
- resolve(tokens);
112
- }
113
- });
114
- app.get('*', (_, res) => {
115
- res.status(404).send();
116
- });
117
-
118
- open(`http://localhost:${port}/authorize`);
119
- }
package/src/Token.ts DELETED
@@ -1,40 +0,0 @@
1
- import * as OpenIDClient from 'openid-client';
2
-
3
- interface TokenResponse extends OpenIDClient.TokenEndpointResponse {
4
- [key: string]: any;
5
- }
6
-
7
- export class Token implements TokenResponse {
8
- public readonly access_token: string;
9
- public readonly token_type: Lowercase<string>;
10
- public readonly timestamp?: any;
11
- public readonly refresh_token?: string;
12
- public readonly refresh_token_expires_in?: number;
13
- public readonly scope?: string;
14
- public readonly id_token?: string;
15
- public readonly expires_in?: number;
16
-
17
- private constructor(response: OpenIDClient.TokenEndpointResponse) {
18
- this.access_token = response.access_token;
19
- this.token_type = response.token_type;
20
- this.timestamp = response.timestamp;
21
- Object.assign(this, response);
22
- }
23
-
24
- public static fromResponse(
25
- response?: OpenIDClient.TokenEndpointResponse,
26
- refresh_token?: string
27
- ) {
28
- if (response) {
29
- return new Token({ refresh_token, timestamp: Date.now(), ...response });
30
- }
31
- return undefined;
32
- }
33
-
34
- public hasExpired() {
35
- return (
36
- this.expires_in === undefined ||
37
- Date.now() > this.timestamp + this.expires_in
38
- );
39
- }
40
- }
@@ -1,6 +0,0 @@
1
- import { Token } from './Token.js';
2
-
3
- export interface TokenStorage {
4
- load(): Promise<Token | undefined>;
5
- save(tokens: Token): Promise<Token>;
6
- }
package/src/index.ts DELETED
@@ -1,11 +0,0 @@
1
- import { Client } from './Client.js';
2
-
3
- export { Client, Options as Credentials } from './Client.js';
4
- export { FileStorage } from './FileStorage.js';
5
- export { Token } from './Token.js';
6
- export { TokenStorage } from './TokenStorage.js';
7
-
8
- export {
9
- /** @deprecated use Client */
10
- Client as TokenManager
11
- };
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "@tsconfig/node20/tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "declaration": true
6
- },
7
- "include": ["./src"]
8
- }