@wickedevolutions/abilities-mcp 1.3.1 → 1.5.1
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 +61 -0
- package/README.md +88 -17
- package/abilities-mcp.js +182 -113
- package/lib/auth/bridge-identity-provider.js +34 -0
- package/lib/auth/browser-launcher.js +67 -0
- package/lib/auth/config-migration.js +322 -0
- package/lib/auth/dcr-client.js +123 -0
- package/lib/auth/discovery-client.js +273 -0
- package/lib/auth/errors.js +114 -0
- package/lib/auth/events.js +55 -0
- package/lib/auth/fresh-each-time-identity.js +101 -0
- package/lib/auth/http-json.js +151 -0
- package/lib/auth/index.js +88 -0
- package/lib/auth/keychain-secret-store.js +98 -0
- package/lib/auth/loopback-server.js +249 -0
- package/lib/auth/memory-secret-store.js +0 -0
- package/lib/auth/oauth-client.js +357 -0
- package/lib/auth/pkce.js +93 -0
- package/lib/auth/schema-v2.js +110 -0
- package/lib/auth/secret-store.js +78 -0
- package/lib/auth/token-manager.js +378 -0
- package/lib/cli/commands/add-site.js +226 -0
- package/lib/cli/commands/force-downgrade.js +93 -0
- package/lib/cli/commands/list-sites.js +93 -0
- package/lib/cli/commands/reauth.js +108 -0
- package/lib/cli/commands/revoke.js +127 -0
- package/lib/cli/commands/self-check.js +158 -0
- package/lib/cli/commands/test.js +174 -0
- package/lib/cli/commands/upgrade-auth.js +259 -0
- package/lib/cli/config-store.js +161 -0
- package/lib/cli/context.js +102 -0
- package/lib/cli/errors.js +227 -0
- package/lib/cli/index.js +173 -0
- package/lib/cli/output.js +175 -0
- package/lib/cli/parse-args.js +80 -0
- package/lib/config.js +248 -19
- package/lib/connection-pool.js +214 -11
- package/lib/router.js +29 -11
- package/lib/transports/oauth-http-transport.js +601 -0
- package/package.json +7 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public surface for `lib/auth/`.
|
|
5
|
+
*
|
|
6
|
+
* Every export here maps 1:1 to a future CLI subcommand or is a building
|
|
7
|
+
* block consumed by the state machine. The module is callable from any
|
|
8
|
+
* consumer (CLI today, GUI tomorrow). It contains zero CLI dependencies —
|
|
9
|
+
* no console.log, no process.exit, no readline.
|
|
10
|
+
*
|
|
11
|
+
* Copyright (C) 2026 Influencentricity | Wicked Evolutions
|
|
12
|
+
* @license GPL-2.0-or-later
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const events = require('./events');
|
|
16
|
+
const errors = require('./errors');
|
|
17
|
+
const secretStore = require('./secret-store');
|
|
18
|
+
const { MemorySecretStore } = require('./memory-secret-store');
|
|
19
|
+
const { KeychainSecretStore } = require('./keychain-secret-store');
|
|
20
|
+
const bridgeIdentity = require('./bridge-identity-provider');
|
|
21
|
+
const { FreshEachTimeIdentityProvider } = require('./fresh-each-time-identity');
|
|
22
|
+
const pkce = require('./pkce');
|
|
23
|
+
const discoveryClient = require('./discovery-client');
|
|
24
|
+
const dcrClient = require('./dcr-client');
|
|
25
|
+
const { LoopbackServer, PORT_MIN, PORT_MAX } = require('./loopback-server');
|
|
26
|
+
const { openBrowser } = require('./browser-launcher');
|
|
27
|
+
const { OAuthClient, DEFAULT_SCOPE } = require('./oauth-client');
|
|
28
|
+
const { TokenManager, REFRESH_WINDOW_SECONDS, HTTP_TIMEOUT_MS, MAX_RETRIES } = require('./token-manager');
|
|
29
|
+
const schemaV2 = require('./schema-v2');
|
|
30
|
+
const configMigration = require('./config-migration');
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
// Constants and enums
|
|
34
|
+
STATES: events.STATES,
|
|
35
|
+
TERMINAL_STATES: events.TERMINAL_STATES,
|
|
36
|
+
AUTH_STATUS: events.AUTH_STATUS,
|
|
37
|
+
EVENTS: events.EVENTS,
|
|
38
|
+
DEFAULT_SCOPE,
|
|
39
|
+
REFRESH_WINDOW_SECONDS,
|
|
40
|
+
HTTP_TIMEOUT_MS,
|
|
41
|
+
MAX_RETRIES,
|
|
42
|
+
PORT_MIN,
|
|
43
|
+
PORT_MAX,
|
|
44
|
+
IDENTITY_BUNDLE_VERSION: bridgeIdentity.IDENTITY_BUNDLE_VERSION,
|
|
45
|
+
SCHEMA_VERSION: schemaV2.SCHEMA_VERSION,
|
|
46
|
+
AUTH_METHODS: schemaV2.AUTH_METHODS,
|
|
47
|
+
VALID_AUTH_STATUS: schemaV2.VALID_AUTH_STATUS,
|
|
48
|
+
|
|
49
|
+
// Errors
|
|
50
|
+
AuthError: errors.AuthError,
|
|
51
|
+
DiscoveryError: errors.DiscoveryError,
|
|
52
|
+
CapabilityPinningError: errors.CapabilityPinningError,
|
|
53
|
+
RegistrationError: errors.RegistrationError,
|
|
54
|
+
TokenExchangeError: errors.TokenExchangeError,
|
|
55
|
+
StateMismatchError: errors.StateMismatchError,
|
|
56
|
+
UserDeniedError: errors.UserDeniedError,
|
|
57
|
+
RefreshError: errors.RefreshError,
|
|
58
|
+
SecretStoreError: errors.SecretStoreError,
|
|
59
|
+
MigrationError: errors.MigrationError,
|
|
60
|
+
|
|
61
|
+
// Secret store
|
|
62
|
+
SecretStore: {
|
|
63
|
+
KEYCHAIN_REF_SCHEME: secretStore.KEYCHAIN_REF_SCHEME,
|
|
64
|
+
makeRef: secretStore.makeRef,
|
|
65
|
+
parseRef: secretStore.parseRef,
|
|
66
|
+
resolveRef: secretStore.resolveRef,
|
|
67
|
+
},
|
|
68
|
+
MemorySecretStore,
|
|
69
|
+
KeychainSecretStore,
|
|
70
|
+
|
|
71
|
+
// Identity
|
|
72
|
+
FreshEachTimeIdentityProvider,
|
|
73
|
+
|
|
74
|
+
// Primitives
|
|
75
|
+
pkce,
|
|
76
|
+
discoveryClient,
|
|
77
|
+
dcrClient,
|
|
78
|
+
LoopbackServer,
|
|
79
|
+
openBrowser,
|
|
80
|
+
|
|
81
|
+
// Orchestration
|
|
82
|
+
OAuthClient,
|
|
83
|
+
TokenManager,
|
|
84
|
+
|
|
85
|
+
// Config schema
|
|
86
|
+
schemaV2,
|
|
87
|
+
configMigration,
|
|
88
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SecretStoreError } = require('./errors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* KeychainSecretStore — keytar-backed SecretStore.
|
|
7
|
+
*
|
|
8
|
+
* keytar wraps macOS Keychain, Windows Credential Manager, and Linux libsecret.
|
|
9
|
+
* It is declared as an `optionalDependency` so a failed native build does not
|
|
10
|
+
* break `npm install` for env-var-only operators. If keytar is unavailable at
|
|
11
|
+
* runtime, every method on this store throws `SecretStoreError` with code
|
|
12
|
+
* `keytar_unavailable` — callers can detect that and fall back to a different
|
|
13
|
+
* store (e.g. MemorySecretStore for tests, or surface to the user).
|
|
14
|
+
*
|
|
15
|
+
* Implements the SecretStore interface defined in `secret-store.js`.
|
|
16
|
+
*
|
|
17
|
+
* Copyright (C) 2026 Influencentricity | Wicked Evolutions
|
|
18
|
+
* @license GPL-2.0-or-later
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
class KeychainSecretStore {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} [opts]
|
|
24
|
+
* @param {object} [opts.keytar] Inject a keytar module — primarily for tests.
|
|
25
|
+
* When omitted, keytar is required lazily on
|
|
26
|
+
* first use.
|
|
27
|
+
*/
|
|
28
|
+
constructor(opts = {}) {
|
|
29
|
+
this._injected = opts.keytar || null;
|
|
30
|
+
this._keytar = null;
|
|
31
|
+
this._loadAttempted = false;
|
|
32
|
+
this._loadError = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_load() {
|
|
36
|
+
if (this._keytar) return this._keytar;
|
|
37
|
+
if (this._loadAttempted) {
|
|
38
|
+
if (this._loadError) {
|
|
39
|
+
throw new SecretStoreError(
|
|
40
|
+
`OS keychain unavailable: ${this._loadError.message}`,
|
|
41
|
+
{ code: 'keytar_unavailable', cause: this._loadError }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return this._keytar;
|
|
45
|
+
}
|
|
46
|
+
this._loadAttempted = true;
|
|
47
|
+
if (this._injected) {
|
|
48
|
+
this._keytar = this._injected;
|
|
49
|
+
return this._keytar;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
// eslint-disable-next-line global-require
|
|
53
|
+
this._keytar = require('keytar');
|
|
54
|
+
return this._keytar;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
this._loadError = err;
|
|
57
|
+
throw new SecretStoreError(
|
|
58
|
+
`OS keychain unavailable: ${err.message}`,
|
|
59
|
+
{ code: 'keytar_unavailable', cause: err }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @returns {Promise<boolean>} true if keytar can be loaded on this host. */
|
|
65
|
+
async isAvailable() {
|
|
66
|
+
try {
|
|
67
|
+
this._load();
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async get(service, account) {
|
|
75
|
+
const keytar = this._load();
|
|
76
|
+
return keytar.getPassword(service, account);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async set(service, account, secret) {
|
|
80
|
+
if (typeof secret !== 'string') {
|
|
81
|
+
throw new TypeError('SecretStore.set: secret must be a string');
|
|
82
|
+
}
|
|
83
|
+
const keytar = this._load();
|
|
84
|
+
await keytar.setPassword(service, account, secret);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async delete(service, account) {
|
|
88
|
+
const keytar = this._load();
|
|
89
|
+
return keytar.deletePassword(service, account);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async findAll(service) {
|
|
93
|
+
const keytar = this._load();
|
|
94
|
+
return keytar.findCredentials(service);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { KeychainSecretStore };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const { randomInt } = require('node:crypto');
|
|
5
|
+
const { URL } = require('node:url');
|
|
6
|
+
|
|
7
|
+
const { safeStateEquals } = require('./pkce');
|
|
8
|
+
const { StateMismatchError, UserDeniedError, AuthError } = require('./errors');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loopback callback server for the OAuth authorization-code flow.
|
|
12
|
+
*
|
|
13
|
+
* Per Appendix H.4.5 (binding):
|
|
14
|
+
* - Bind on 127.0.0.1, random high port in [49152, 65535].
|
|
15
|
+
* - SO_REUSEADDR=false. In Node, that maps to `listen({ exclusive: true })`
|
|
16
|
+
* — the server will not be shared with other workers and will fail with
|
|
17
|
+
* EADDRINUSE if the port is already bound.
|
|
18
|
+
* - State token validates the callback (H.3.5: timingSafeEqual on equal
|
|
19
|
+
* length, mismatch → reject without exchanging code).
|
|
20
|
+
*
|
|
21
|
+
* Public API:
|
|
22
|
+
* const server = new LoopbackServer({ expectedState });
|
|
23
|
+
* const { port, redirectUri } = await server.start();
|
|
24
|
+
* // → operator browser flow happens
|
|
25
|
+
* const callback = await server.waitForCallback({ timeoutMs });
|
|
26
|
+
* // callback = { code, state } | throws StateMismatchError | UserDeniedError | AuthError
|
|
27
|
+
* await server.stop();
|
|
28
|
+
*
|
|
29
|
+
* The server responds to the browser with a small HTML success or error page
|
|
30
|
+
* depending on the outcome. The callback Promise resolves AFTER the response
|
|
31
|
+
* is flushed so the operator's browser shows the page even if the caller
|
|
32
|
+
* stops the server immediately.
|
|
33
|
+
*
|
|
34
|
+
* Copyright (C) 2026 Influencentricity | Wicked Evolutions
|
|
35
|
+
* @license GPL-2.0-or-later
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const PORT_MIN = 49_152;
|
|
39
|
+
const PORT_MAX = 65_535;
|
|
40
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60_000; // 5 min for operator to complete browser flow
|
|
41
|
+
const DEFAULT_BIND_RETRIES = 5;
|
|
42
|
+
|
|
43
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
44
|
+
<html><head><meta charset="utf-8"><title>Authorization complete</title>
|
|
45
|
+
<style>body{font:16px/1.5 -apple-system,system-ui,sans-serif;max-width:36rem;margin:6rem auto;padding:0 1.5rem;color:#111}h1{margin:0 0 1rem}p{margin:.25rem 0}</style>
|
|
46
|
+
</head><body><h1>Authorization complete</h1>
|
|
47
|
+
<p>You can close this tab and return to your terminal.</p></body></html>`;
|
|
48
|
+
|
|
49
|
+
const DENIED_HTML = `<!DOCTYPE html>
|
|
50
|
+
<html><head><meta charset="utf-8"><title>Authorization denied</title>
|
|
51
|
+
<style>body{font:16px/1.5 -apple-system,system-ui,sans-serif;max-width:36rem;margin:6rem auto;padding:0 1.5rem;color:#111}h1{margin:0 0 1rem;color:#a00}p{margin:.25rem 0}</style>
|
|
52
|
+
</head><body><h1>Authorization denied</h1>
|
|
53
|
+
<p>The site reported that authorization was denied. You can close this tab.</p></body></html>`;
|
|
54
|
+
|
|
55
|
+
const ERROR_HTML = `<!DOCTYPE html>
|
|
56
|
+
<html><head><meta charset="utf-8"><title>Authorization error</title>
|
|
57
|
+
<style>body{font:16px/1.5 -apple-system,system-ui,sans-serif;max-width:36rem;margin:6rem auto;padding:0 1.5rem;color:#111}h1{margin:0 0 1rem;color:#a00}p{margin:.25rem 0}</style>
|
|
58
|
+
</head><body><h1>Authorization error</h1>
|
|
59
|
+
<p>An unexpected response was received. Check your terminal for details.</p></body></html>`;
|
|
60
|
+
|
|
61
|
+
class LoopbackServer {
|
|
62
|
+
/**
|
|
63
|
+
* @param {object} args
|
|
64
|
+
* @param {string} args.expectedState Bridge-generated state token
|
|
65
|
+
* @param {string} [args.callbackPath] Defaults to '/callback'
|
|
66
|
+
* @param {number} [args.bindRetries]
|
|
67
|
+
* @param {(min:number,max:number)=>number} [args.portFn] Test seam.
|
|
68
|
+
*/
|
|
69
|
+
constructor(args) {
|
|
70
|
+
if (!args || typeof args.expectedState !== 'string' || args.expectedState.length === 0) {
|
|
71
|
+
throw new Error('LoopbackServer requires expectedState');
|
|
72
|
+
}
|
|
73
|
+
this._expectedState = args.expectedState;
|
|
74
|
+
this._callbackPath = args.callbackPath || '/callback';
|
|
75
|
+
this._bindRetries = args.bindRetries ?? DEFAULT_BIND_RETRIES;
|
|
76
|
+
this._portFn = args.portFn || (() => randomInt(PORT_MIN, PORT_MAX + 1));
|
|
77
|
+
this._server = null;
|
|
78
|
+
this._port = null;
|
|
79
|
+
this._callbackPromise = null;
|
|
80
|
+
this._resolveCallback = null;
|
|
81
|
+
this._rejectCallback = null;
|
|
82
|
+
this._stopped = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @returns {{port: number, redirectUri: string}} */
|
|
86
|
+
async start() {
|
|
87
|
+
let lastErr;
|
|
88
|
+
for (let attempt = 0; attempt <= this._bindRetries; attempt++) {
|
|
89
|
+
const port = this._portFn(PORT_MIN, PORT_MAX);
|
|
90
|
+
try {
|
|
91
|
+
await this._listen(port);
|
|
92
|
+
this._port = port;
|
|
93
|
+
return { port, redirectUri: this.redirectUri };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
lastErr = err;
|
|
96
|
+
if (err && err.code !== 'EADDRINUSE') break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw new AuthError(
|
|
100
|
+
`Loopback server failed to bind on a free port after ${this._bindRetries + 1} attempts`,
|
|
101
|
+
{ code: 'loopback_bind_failed', cause: lastErr }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get redirectUri() {
|
|
106
|
+
if (this._port == null) throw new Error('LoopbackServer not started');
|
|
107
|
+
return `http://127.0.0.1:${this._port}${this._callbackPath}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Wait for the OAuth provider to redirect the operator's browser to our
|
|
112
|
+
* callback URL. Resolves with `{ code, state }` once a valid callback
|
|
113
|
+
* arrives, or rejects with a typed error.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} [opts]
|
|
116
|
+
* @param {number} [opts.timeoutMs]
|
|
117
|
+
* @returns {Promise<{code: string, state: string}>}
|
|
118
|
+
*/
|
|
119
|
+
waitForCallback(opts = {}) {
|
|
120
|
+
if (this._callbackPromise) return this._callbackPromise;
|
|
121
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
122
|
+
this._callbackPromise = new Promise((resolve, reject) => {
|
|
123
|
+
let timer = null;
|
|
124
|
+
const settle = (fn, value) => {
|
|
125
|
+
if (timer) clearTimeout(timer);
|
|
126
|
+
timer = null;
|
|
127
|
+
this._resolveCallback = null;
|
|
128
|
+
this._rejectCallback = null;
|
|
129
|
+
fn(value);
|
|
130
|
+
};
|
|
131
|
+
this._resolveCallback = (value) => settle(resolve, value);
|
|
132
|
+
this._rejectCallback = (err) => settle(reject, err);
|
|
133
|
+
if (timeoutMs > 0) {
|
|
134
|
+
timer = setTimeout(() => {
|
|
135
|
+
// The settle path clears `timer` to avoid double-clear here.
|
|
136
|
+
this._reject(new AuthError(
|
|
137
|
+
`Loopback callback timed out after ${timeoutMs}ms`,
|
|
138
|
+
{ code: 'loopback_timeout', state: 'awaiting_consent' }
|
|
139
|
+
));
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
if (timer.unref) timer.unref();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return this._callbackPromise;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async stop() {
|
|
148
|
+
this._stopped = true;
|
|
149
|
+
if (!this._server) return;
|
|
150
|
+
await new Promise((resolve) => this._server.close(() => resolve()));
|
|
151
|
+
this._server = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
_listen(port) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const server = http.createServer((req, res) => this._onRequest(req, res));
|
|
159
|
+
server.on('error', (err) => {
|
|
160
|
+
if (this._port == null) reject(err);
|
|
161
|
+
});
|
|
162
|
+
// exclusive: true → SO_REUSEADDR=false in Node's worker model. Per
|
|
163
|
+
// Appendix H.4.5 we do not share the port with other listeners.
|
|
164
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
|
|
165
|
+
this._server = server;
|
|
166
|
+
resolve();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_onRequest(req, res) {
|
|
172
|
+
if (this._stopped) {
|
|
173
|
+
res.statusCode = 503;
|
|
174
|
+
res.end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let parsed;
|
|
178
|
+
try { parsed = new URL(req.url, `http://127.0.0.1:${this._port}`); }
|
|
179
|
+
catch {
|
|
180
|
+
this._respond(res, 400, ERROR_HTML);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (parsed.pathname !== this._callbackPath) {
|
|
184
|
+
this._respond(res, 404, ERROR_HTML);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const params = parsed.searchParams;
|
|
189
|
+
const error = params.get('error');
|
|
190
|
+
const errorDescription = params.get('error_description');
|
|
191
|
+
const code = params.get('code');
|
|
192
|
+
const receivedState = params.get('state');
|
|
193
|
+
|
|
194
|
+
if (error) {
|
|
195
|
+
this._respond(res, 200, error === 'access_denied' ? DENIED_HTML : ERROR_HTML);
|
|
196
|
+
const Cls = error === 'access_denied' ? UserDeniedError : AuthError;
|
|
197
|
+
this._reject(new Cls(
|
|
198
|
+
errorDescription || `Authorization server returned error: ${error}`,
|
|
199
|
+
{ code: error, state: 'awaiting_consent' }
|
|
200
|
+
));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!code || !receivedState) {
|
|
205
|
+
this._respond(res, 400, ERROR_HTML);
|
|
206
|
+
this._reject(new AuthError(
|
|
207
|
+
'Loopback callback missing code or state',
|
|
208
|
+
{ code: 'invalid_callback', state: 'awaiting_consent' }
|
|
209
|
+
));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!safeStateEquals(this._expectedState, receivedState)) {
|
|
214
|
+
this._respond(res, 400, ERROR_HTML);
|
|
215
|
+
this._reject(new StateMismatchError(undefined, { state: 'awaiting_consent' }));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this._respond(res, 200, SUCCESS_HTML);
|
|
220
|
+
this._resolve({ code, state: receivedState });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_respond(res, status, html) {
|
|
224
|
+
res.statusCode = status;
|
|
225
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
226
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
227
|
+
res.end(html);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_resolve(value) {
|
|
231
|
+
if (this._resolveCallback) {
|
|
232
|
+
const fn = this._resolveCallback;
|
|
233
|
+
this._resolveCallback = null;
|
|
234
|
+
this._rejectCallback = null;
|
|
235
|
+
fn(value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_reject(err) {
|
|
240
|
+
if (this._rejectCallback) {
|
|
241
|
+
const fn = this._rejectCallback;
|
|
242
|
+
this._resolveCallback = null;
|
|
243
|
+
this._rejectCallback = null;
|
|
244
|
+
fn(err);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = { LoopbackServer, PORT_MIN, PORT_MAX, DEFAULT_TIMEOUT_MS };
|
|
Binary file
|