@oauth2-cli/qui-cli 0.7.15 → 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.
@@ -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,2 @@
1
+ export * from 'oauth2-cli/dist/Token/index.js';
2
+ export * from './EnvironmentStorage.js';
@@ -0,0 +1,2 @@
1
+ export * from 'oauth2-cli/dist/Token/index.js';
2
+ export * from './EnvironmentStorage.js';
@@ -0,0 +1,4 @@
1
+ export { Credentials, Injection, Localhost, Options } from 'oauth2-cli';
2
+ export * from './Client.js';
3
+ export * from './OAuth2Plugin.js';
4
+ export * as Token from './Token/index.js';
@@ -0,0 +1,4 @@
1
+ export { Localhost } from 'oauth2-cli';
2
+ export * from './Client.js';
3
+ export * from './OAuth2Plugin.js';
4
+ export * as Token from './Token/index.js';
@@ -0,0 +1 @@
1
+ export * from "./extendable.js";
@@ -0,0 +1 @@
1
+ export * from './extendable.js';
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "module",
3
+ "main": "index.js",
4
+ "types": "index.d.ts"
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "module",
3
+ "main": "index.js",
4
+ "types": "index.d.ts"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oauth2-cli/qui-cli",
3
- "version": "0.7.15",
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
- "oauth2-cli": "0.8.9",
21
- "requestish": "0.1.1"
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": "del ./dist",
43
- "build": "run-s build:*",
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:compile": "tsc",
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
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@tsconfig/node24/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "declaration": true,
6
+ "outDir": "./extendable"
7
+ },
8
+ "include": ["./src"],
9
+ "exclude": ["./src/index.ts", "./src/OAuth2.ts"]
10
+ }