@oauth2-cli/qui-cli 0.7.16 → 1.0.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 +21 -19
- package/README.md +33 -4
- package/dist/Client.d.ts +13 -0
- package/dist/Client.js +34 -8
- package/dist/OAuth2.d.ts +40 -0
- package/dist/OAuth2.js +41 -0
- package/dist/OAuth2Plugin.d.ts +96 -14
- package/dist/OAuth2Plugin.js +116 -42
- package/dist/Token/EnvironmentStorage.d.ts +14 -0
- package/dist/Token/EnvironmentStorage.js +14 -0
- package/dist/{Unregistered.d.ts → extendable.d.ts} +1 -1
- package/dist/{Unregistered.js → extendable.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/extendable/Client.d.ts +24 -0
- package/extendable/Client.js +71 -0
- package/extendable/OAuth2Plugin.d.ts +154 -0
- package/extendable/OAuth2Plugin.js +288 -0
- package/extendable/Token/EnvironmentStorage.d.ts +21 -0
- package/extendable/Token/EnvironmentStorage.js +27 -0
- package/extendable/Token/index.d.ts +2 -0
- package/extendable/Token/index.js +2 -0
- package/extendable/extendable.d.ts +4 -0
- package/extendable/extendable.js +4 -0
- package/extendable/index.d.ts +1 -0
- package/extendable/index.js +1 -0
- package/extendable/package.json +5 -0
- package/package.extendable.json +5 -0
- package/package.json +11 -6
- package/tsconfig.extendable.json +10 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Colors } from '@qui-cli/colors';
|
|
2
|
+
import { Env } from '@qui-cli/env';
|
|
3
|
+
import { Log } from '@qui-cli/log';
|
|
4
|
+
import * as Plugin from '@qui-cli/plugin';
|
|
5
|
+
import * as OAuth2CLI from 'oauth2-cli';
|
|
6
|
+
import { URL } from 'requestish';
|
|
7
|
+
export class OAuth2Plugin {
|
|
8
|
+
name;
|
|
9
|
+
static names = [];
|
|
10
|
+
static registeredPorts = {};
|
|
11
|
+
overrideName;
|
|
12
|
+
reason;
|
|
13
|
+
/**
|
|
14
|
+
* @param name Human-readable name for client in messages. Must also be a
|
|
15
|
+
* unique qui-cli plugin name.
|
|
16
|
+
*/
|
|
17
|
+
constructor(name = '@oauth2-cli/qui-cli') {
|
|
18
|
+
this.name = name;
|
|
19
|
+
if (OAuth2Plugin.names.includes(name)) {
|
|
20
|
+
throw new Error(`A @qui-cli/plugin named ${Colors.value(name)} has already been instantiated.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Configured client credentials */
|
|
24
|
+
credentials;
|
|
25
|
+
/** Configured client base_url */
|
|
26
|
+
base_url;
|
|
27
|
+
/** Configured usage information */
|
|
28
|
+
man = {
|
|
29
|
+
heading: 'OAuth 2.0 / Open ID Connect client options'
|
|
30
|
+
};
|
|
31
|
+
/** Configured reference URLs for credentials */
|
|
32
|
+
url = undefined;
|
|
33
|
+
/** Configured hint values for credentials */
|
|
34
|
+
hint = {
|
|
35
|
+
redirect_uri: Colors.quotedValue(`"http://localhost:3000/redirect"`)
|
|
36
|
+
};
|
|
37
|
+
/** Configured environment variable names for credentials */
|
|
38
|
+
env = {
|
|
39
|
+
issuer: 'ISSUER',
|
|
40
|
+
client_id: 'CLIENT_ID',
|
|
41
|
+
client_secret: 'CLIENT_SECRET',
|
|
42
|
+
redirect_uri: 'REDIRECT_URI',
|
|
43
|
+
authorization_endpoint: 'AUTHORIZATION_ENDPOINT',
|
|
44
|
+
token_endpoint: 'TOKEN_ENDPOINT',
|
|
45
|
+
base_url: 'BASE_URL',
|
|
46
|
+
scope: 'SCOPE'
|
|
47
|
+
};
|
|
48
|
+
/** Configured credentials to suppress in usage and init */
|
|
49
|
+
suppress = {
|
|
50
|
+
scope: true
|
|
51
|
+
};
|
|
52
|
+
/** Configured request components for client to inject */
|
|
53
|
+
inject = undefined;
|
|
54
|
+
/** Configured {@link OAuth2CLI.Localhost.Options} to pass to client */
|
|
55
|
+
localhost;
|
|
56
|
+
/** Configured client refresh token storage strategy */
|
|
57
|
+
storage = undefined;
|
|
58
|
+
_client = undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Configured oauth2-cli client
|
|
61
|
+
*
|
|
62
|
+
* Do _not_ access until intitialization is complete -- the client will be
|
|
63
|
+
* configured upon first access based on available configuration known at that
|
|
64
|
+
* time
|
|
65
|
+
*/
|
|
66
|
+
get client() {
|
|
67
|
+
if (!this._client) {
|
|
68
|
+
if (!this.credentials?.client_id) {
|
|
69
|
+
throw new Error(`A ${Colors.varName(this.env.client_id)} ${Colors.keyword('must')} ` +
|
|
70
|
+
`be configured for ${this.overrideName || this.name}.`);
|
|
71
|
+
}
|
|
72
|
+
if (!this.credentials?.client_secret) {
|
|
73
|
+
throw new Error(`A ${Colors.varName(this.env.client_secret)} ${Colors.keyword('must')} ` +
|
|
74
|
+
`be configured for ${this.overrideName || this.name}.`);
|
|
75
|
+
}
|
|
76
|
+
if (!this.credentials?.redirect_uri) {
|
|
77
|
+
throw new Error(`A ${Colors.varName(this.env.redirect_uri)} ${Colors.keyword('must')} ` +
|
|
78
|
+
`be configured for ${this.overrideName || this.name}.`);
|
|
79
|
+
}
|
|
80
|
+
if (!this.credentials?.issuer) {
|
|
81
|
+
if (!this.credentials?.authorization_endpoint) {
|
|
82
|
+
throw new Error(`Either an ${Colors.varName(this.env.issuer)} or ` +
|
|
83
|
+
`${Colors.varName(this.env.authorization_endpoint)} ` +
|
|
84
|
+
`${Colors.keyword('must')} be configured for ` +
|
|
85
|
+
`${this.overrideName || this.name}.`);
|
|
86
|
+
}
|
|
87
|
+
if (!this.credentials?.token_endpoint) {
|
|
88
|
+
throw new Error(`Either an ${Colors.varName(this.env.issuer)} or ` +
|
|
89
|
+
`${Colors.varName(this.env.token_endpoint)} ` +
|
|
90
|
+
`${Colors.keyword('must')} be configured for ` +
|
|
91
|
+
`${this.overrideName || this.name}.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this._client = this.instantiateClient({
|
|
95
|
+
name: this.overrideName || this.name,
|
|
96
|
+
reason: this.reason,
|
|
97
|
+
credentials: this.credentials,
|
|
98
|
+
base_url: this.base_url,
|
|
99
|
+
inject: this.inject,
|
|
100
|
+
localhost: this.localhost,
|
|
101
|
+
storage: this.storage
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return this._client;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Configure plugin for use
|
|
108
|
+
*
|
|
109
|
+
* May be called repeatedly, overlaying different options as they become
|
|
110
|
+
* available
|
|
111
|
+
*
|
|
112
|
+
* Invoked automatically by
|
|
113
|
+
* {@link https://github.com/battis/qui-cli#readme qui-cli} during
|
|
114
|
+
* initialization
|
|
115
|
+
*
|
|
116
|
+
* @see {@link Configuration}
|
|
117
|
+
*/
|
|
118
|
+
configure(proposal = {}) {
|
|
119
|
+
function hydrate(p, c) {
|
|
120
|
+
if (p) {
|
|
121
|
+
for (const k of Object.keys(p)) {
|
|
122
|
+
if (p[k] !== undefined) {
|
|
123
|
+
if (!c) {
|
|
124
|
+
c = {};
|
|
125
|
+
}
|
|
126
|
+
c[k] = p[k];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return c;
|
|
131
|
+
}
|
|
132
|
+
this.overrideName = Plugin.hydrate(proposal.name, this.overrideName);
|
|
133
|
+
this.reason = Plugin.hydrate(proposal.reason, this.reason);
|
|
134
|
+
this.credentials = hydrate(proposal.credentials, this.credentials);
|
|
135
|
+
this.base_url = Plugin.hydrate(proposal.base_url, this.base_url);
|
|
136
|
+
this.storage = Plugin.hydrate(proposal.storage, this.storage);
|
|
137
|
+
this.inject = hydrate(proposal.inject, this.inject);
|
|
138
|
+
this.man = Plugin.hydrate(proposal.man, this.man);
|
|
139
|
+
this.url = hydrate(proposal.url, this.url);
|
|
140
|
+
this.hint = hydrate(proposal.hint, this.hint);
|
|
141
|
+
this.env = hydrate(proposal.env, this.env);
|
|
142
|
+
this.suppress = hydrate(proposal.suppress, this.suppress);
|
|
143
|
+
this.localhost = hydrate(proposal.localhost, this.localhost);
|
|
144
|
+
if (this.credentials?.redirect_uri) {
|
|
145
|
+
const url = URL.from(this.credentials.redirect_uri);
|
|
146
|
+
if (url.hostname !== 'localhost' &&
|
|
147
|
+
!/^\/https?\/localhost(:\d+)?\//.test(url.pathname)) {
|
|
148
|
+
Log.warning(`The ${Colors.varName(this.env.redirect_uri)} value ${Colors.url(this.credentials.redirect_uri)} for ${this.overrideName || this.name} may not work: it ` +
|
|
149
|
+
`${Colors.keyword('must')} redirect to ${Colors.url('localhost')}`);
|
|
150
|
+
}
|
|
151
|
+
if (url.protocol !== 'http:' &&
|
|
152
|
+
!/^\/http\/localhost(:\d+)?\//.test(url.pathname)) {
|
|
153
|
+
Log.warning(`The ${Colors.url(url.protocol)} protocol may not work without additional configuration. The out-` +
|
|
154
|
+
`of-band server listening for the ` +
|
|
155
|
+
`${this.overrideName || this.name} redirect is not automatically ` +
|
|
156
|
+
`provisioned with an SSL certificate`);
|
|
157
|
+
}
|
|
158
|
+
if (OAuth2Plugin.registeredPorts[url.port] &&
|
|
159
|
+
OAuth2Plugin.registeredPorts[url.port] !== this.name) {
|
|
160
|
+
Log.warning(`The port ${Colors.value(url.port)} has already been registered to another instance of this plugin ` +
|
|
161
|
+
`named ${Colors.value(OAuth2Plugin.registeredPorts[url.port])}. This will likely cause a failure if both ` +
|
|
162
|
+
`${this.overrideName || this.name} and ` +
|
|
163
|
+
`${OAuth2Plugin.registeredPorts[url.port]} are listening for ` +
|
|
164
|
+
`redirects at relatively proximate moments in time.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Provide usage options to
|
|
170
|
+
* {@link https://github.com/battis/qui-cli#readme qui-cli} for display to user
|
|
171
|
+
* on command line
|
|
172
|
+
*
|
|
173
|
+
* Invoked automatically by
|
|
174
|
+
* {@link https://github.com/battis/qui-cli#readme qui-cli} during
|
|
175
|
+
* initialization
|
|
176
|
+
*/
|
|
177
|
+
options() {
|
|
178
|
+
const descriptions = {
|
|
179
|
+
issuer: `The OpenID ${Colors.keyword('issuer')} URL is set from the ` +
|
|
180
|
+
`environment variable ${Colors.varName(this.env.issuer)}, if present. ` +
|
|
181
|
+
`The ${Colors.varName(this.env.issuer)} is also used as a base URL for ` +
|
|
182
|
+
`any relative URL in API requests, unless ` +
|
|
183
|
+
`${Colors.varName(this.env.base_url)} is defined.`,
|
|
184
|
+
client_id: `The OAuth 2.0 ${Colors.keyword('client_id')} is set from the ` +
|
|
185
|
+
`environment variable ${Colors.varName(this.env.client_id)}, if ` +
|
|
186
|
+
`present.`,
|
|
187
|
+
client_secret: `The OAuth 2.0 ${Colors.keyword('client_secret')} is set from the ` +
|
|
188
|
+
`environment variable ${Colors.varName(this.env.client_secret)}, if ` +
|
|
189
|
+
`present.`,
|
|
190
|
+
scope: `The OAuth 2.0 ${Colors.keyword('scope')} is set from the environment ` +
|
|
191
|
+
`variable ${Colors.varName(this.env.scope)}, if present.`,
|
|
192
|
+
redirect_uri: `The OAuth 2.0 ${Colors.keyword('redirect_uri')}, which must at least ` +
|
|
193
|
+
`redirect to ${Colors.url('localhost')}, is set from the environment ` +
|
|
194
|
+
`variable ${Colors.varName(this.env.redirect_uri)}, if present.`,
|
|
195
|
+
authorization_endpoint: `The OAuth 2.0 ${Colors.keyword('authorization_endpoint')} is set ` +
|
|
196
|
+
`from the environment variable ` +
|
|
197
|
+
`${Colors.varName(this.env.authorization_endpoint)}, if present.`,
|
|
198
|
+
token_endpoint: `The OAuth 2.0 ${Colors.keyword('token_endpoint')} is set from the ` +
|
|
199
|
+
`environment variable ${Colors.varName(this.env.token_endpoint)}, if ` +
|
|
200
|
+
`present and will fall back to ` +
|
|
201
|
+
`${Colors.varName(this.env.authorization_endpoint)} if not provided.`,
|
|
202
|
+
base_url: `The base URL to use for API requests is set from the ` +
|
|
203
|
+
`environment variable ${Colors.varName(this.env.base_url)}, if ` +
|
|
204
|
+
`present. If ${Colors.varName(this.env.base_url)} is not defined, ` +
|
|
205
|
+
`${Colors.varName(this.env.issuer)} will be used as a base URL for ` +
|
|
206
|
+
`relative URL requests.`
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
man: [
|
|
210
|
+
{ level: 1, text: this.man.heading },
|
|
211
|
+
...Object.keys(descriptions)
|
|
212
|
+
.filter((key) => !this.suppress || !this.suppress[key])
|
|
213
|
+
.map((key) => ({
|
|
214
|
+
text: descriptions[key] +
|
|
215
|
+
(this.hint[key] ? ` (e.g. ${this.hint[key]})` : '') +
|
|
216
|
+
(this.url && this.url[key]
|
|
217
|
+
? ` See ${Colors.url(this.url[key])} for more information.`
|
|
218
|
+
: '')
|
|
219
|
+
})),
|
|
220
|
+
...(this.man.text || []).map((t) => ({ text: t }))
|
|
221
|
+
]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Intialize plugin configuration from command line options and environment
|
|
226
|
+
*
|
|
227
|
+
* Invoked automatically by
|
|
228
|
+
* {@link https://github.com/battis/qui-cli#readme qui-cli} during
|
|
229
|
+
* initialization
|
|
230
|
+
*/
|
|
231
|
+
async init(_) {
|
|
232
|
+
const credentials = {};
|
|
233
|
+
const base_url = this.base_url ||
|
|
234
|
+
(await Env.get({
|
|
235
|
+
key: this.env.base_url
|
|
236
|
+
}));
|
|
237
|
+
for (const key of Object.keys(this.env)) {
|
|
238
|
+
if (key !== 'base_url') {
|
|
239
|
+
// FIXME better typing
|
|
240
|
+
// @ts-expect-error 2322
|
|
241
|
+
credentials[key] =
|
|
242
|
+
(this.credentials ? this.credentials[key] : undefined) ||
|
|
243
|
+
(await Env.get({ key: this.env[key] }));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.configure({ credentials, base_url });
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Instantiate the `oauth2-cli` client
|
|
250
|
+
*
|
|
251
|
+
* Available hook for custom configurations in plugin development
|
|
252
|
+
*/
|
|
253
|
+
instantiateClient(options) {
|
|
254
|
+
return new OAuth2CLI.Client(options);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Convenience method
|
|
258
|
+
*
|
|
259
|
+
* @see {@link OAuth2CLI.Client.request}
|
|
260
|
+
*/
|
|
261
|
+
request(...args) {
|
|
262
|
+
return this.client.request(...args);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Convenience method
|
|
266
|
+
*
|
|
267
|
+
* @see {@link OAuth2CLI.Client.requestJSON}
|
|
268
|
+
*/
|
|
269
|
+
requestJSON(...args) {
|
|
270
|
+
return this.client.requestJSON(...args);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Convenience method
|
|
274
|
+
*
|
|
275
|
+
* @see {@link OAuth2CLI.Client.fetch}
|
|
276
|
+
*/
|
|
277
|
+
fetch(...args) {
|
|
278
|
+
return this.client.fetch(...args);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Convenience method
|
|
282
|
+
*
|
|
283
|
+
* @see {@link OAuth2CLI.Client.fetchJSON}
|
|
284
|
+
*/
|
|
285
|
+
fetchJSON(...args) {
|
|
286
|
+
return this.client.fetchJSON(...args);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Token } from 'oauth2-cli';
|
|
2
|
+
/**
|
|
3
|
+
* Persist a refresh token in the local environment
|
|
4
|
+
*
|
|
5
|
+
* Care should be taken when using this persistence strategy to:
|
|
6
|
+
*
|
|
7
|
+
* 1. Ideally encrypt or otherwise secure the environment value (see
|
|
8
|
+
* {@link https://github.com/battis/oauth2-cli/tree/main/examples/qui-cli/04%201password-integration#readme 1password-integration}
|
|
9
|
+
* example for one approach)
|
|
10
|
+
* 2. Do not commit `.env` files to a public repo
|
|
11
|
+
*/
|
|
12
|
+
export declare class EnvironmentStorage implements Token.Storage {
|
|
13
|
+
private tokenEnvVar;
|
|
14
|
+
/**
|
|
15
|
+
* @param tokenEnvVar Name of the environment variable containing the refresh
|
|
16
|
+
* token, defaults to `REFRESH_TOKEN`
|
|
17
|
+
*/
|
|
18
|
+
constructor(tokenEnvVar?: string);
|
|
19
|
+
load(): Promise<string | undefined>;
|
|
20
|
+
save(refresh_token: string): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Env } from '@qui-cli/env';
|
|
2
|
+
/**
|
|
3
|
+
* Persist a refresh token in the local environment
|
|
4
|
+
*
|
|
5
|
+
* Care should be taken when using this persistence strategy to:
|
|
6
|
+
*
|
|
7
|
+
* 1. Ideally encrypt or otherwise secure the environment value (see
|
|
8
|
+
* {@link https://github.com/battis/oauth2-cli/tree/main/examples/qui-cli/04%201password-integration#readme 1password-integration}
|
|
9
|
+
* example for one approach)
|
|
10
|
+
* 2. Do not commit `.env` files to a public repo
|
|
11
|
+
*/
|
|
12
|
+
export class EnvironmentStorage {
|
|
13
|
+
tokenEnvVar;
|
|
14
|
+
/**
|
|
15
|
+
* @param tokenEnvVar Name of the environment variable containing the refresh
|
|
16
|
+
* token, defaults to `REFRESH_TOKEN`
|
|
17
|
+
*/
|
|
18
|
+
constructor(tokenEnvVar = 'REFRESH_TOKEN') {
|
|
19
|
+
this.tokenEnvVar = tokenEnvVar;
|
|
20
|
+
}
|
|
21
|
+
async load() {
|
|
22
|
+
return await Env.get({ key: this.tokenEnvVar });
|
|
23
|
+
}
|
|
24
|
+
async save(refresh_token) {
|
|
25
|
+
await Env.set({ key: this.tokenEnvVar, value: refresh_token });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./extendable.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './extendable.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oauth2-cli/qui-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "@qui-cli/plugin wrapper for oauth2-cli",
|
|
5
5
|
"homepage": "https://github.com/battis/oauth2-cli/tree/main/packages/qui-cli#readme",
|
|
6
6
|
"repository": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@qui-cli/colors": "^3.2.3",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
20
|
+
"oauth2-cli": "1.0.0",
|
|
21
|
+
"requestish": "0.1.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@battis/descriptive-types": "^0.2.6",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"@qui-cli/plugin": "^4.1.0",
|
|
29
29
|
"@tsconfig/node24": "^24.0.4",
|
|
30
30
|
"commit-and-tag-version": "^12.6.1",
|
|
31
|
+
"cpy-cli": "^7.0.0",
|
|
31
32
|
"del-cli": "^7.0.0",
|
|
32
33
|
"npm-run-all": "^4.1.5",
|
|
33
34
|
"openid-client": "^6.8.2",
|
|
@@ -39,10 +40,14 @@
|
|
|
39
40
|
"@qui-cli/plugin": ">=3"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
|
-
"clean": "
|
|
43
|
-
"
|
|
43
|
+
"clean": "run-s clean:*",
|
|
44
|
+
"clean:dist": "del ./dist",
|
|
45
|
+
"clean:extendable": "del ./extendable",
|
|
46
|
+
"build": "run-s build:**",
|
|
44
47
|
"build:clean": "run-s clean",
|
|
45
|
-
"build:
|
|
48
|
+
"build:dist": "tsc",
|
|
49
|
+
"build:extendable:compile": "tsc --project tsconfig.extendable.json",
|
|
50
|
+
"build:extendable:package": "cpy ./package.extendable.json ./extendable/package.json",
|
|
46
51
|
"release": "commit-and-tag-version"
|
|
47
52
|
}
|
|
48
53
|
}
|