@tigeropenapi/tigeropen 0.5.4 → 0.5.7

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.
Files changed (73) hide show
  1. package/README.md +80 -0
  2. package/dist/cjs/auth/authentication.js +74 -0
  3. package/dist/cjs/auth/index.js +24 -0
  4. package/dist/cjs/auth/oauth2/client-registration.js +44 -0
  5. package/dist/cjs/auth/oauth2/device-authorization.js +40 -0
  6. package/dist/cjs/auth/oauth2/errors.js +64 -0
  7. package/dist/cjs/auth/oauth2/http-utils.js +112 -0
  8. package/dist/cjs/auth/oauth2/index.js +42 -0
  9. package/dist/cjs/auth/oauth2/loopback-receiver.js +215 -0
  10. package/dist/cjs/auth/oauth2/metadata.js +34 -0
  11. package/dist/cjs/auth/oauth2/oauth2-authentication.js +74 -0
  12. package/dist/cjs/auth/oauth2/session-manager.js +762 -0
  13. package/dist/cjs/auth/oauth2/token-store.js +252 -0
  14. package/dist/cjs/auth/oauth2/token.js +76 -0
  15. package/dist/cjs/push/index.js +1 -0
  16. package/dist/cjs/push/pb/Request.js +12 -1
  17. package/dist/cjs/push/proto-message.js +5 -1
  18. package/dist/cjs/push/push-client.js +34 -16
  19. package/dist/cjs/push/tick-util.js +172 -0
  20. package/dist/cjs/quote/quote-client.js +59 -12
  21. package/dist/cjs/trade/trade-client.js +55 -6
  22. package/dist/cjs/version.js +1 -1
  23. package/dist/esm/auth/authentication.js +68 -0
  24. package/dist/esm/auth/index.js +3 -0
  25. package/dist/esm/auth/oauth2/client-registration.js +40 -0
  26. package/dist/esm/auth/oauth2/device-authorization.js +36 -0
  27. package/dist/esm/auth/oauth2/errors.js +59 -0
  28. package/dist/esm/auth/oauth2/http-utils.js +105 -0
  29. package/dist/esm/auth/oauth2/index.js +11 -0
  30. package/dist/esm/auth/oauth2/loopback-receiver.js +178 -0
  31. package/dist/esm/auth/oauth2/metadata.js +30 -0
  32. package/dist/esm/auth/oauth2/oauth2-authentication.js +70 -0
  33. package/dist/esm/auth/oauth2/session-manager.js +725 -0
  34. package/dist/esm/auth/oauth2/token-store.js +214 -0
  35. package/dist/esm/auth/oauth2/token.js +71 -0
  36. package/dist/esm/package.json +3 -0
  37. package/dist/esm/push/index.js +1 -0
  38. package/dist/esm/push/pb/Request.js +12 -1
  39. package/dist/esm/push/proto-message.js +5 -1
  40. package/dist/esm/push/push-client.js +34 -16
  41. package/dist/esm/push/tick-util.js +165 -0
  42. package/dist/esm/quote/quote-client.js +59 -12
  43. package/dist/esm/trade/trade-client.js +55 -6
  44. package/dist/esm/version.js +1 -1
  45. package/dist/types/auth/authentication.d.ts +98 -0
  46. package/dist/types/auth/index.d.ts +4 -0
  47. package/dist/types/auth/oauth2/client-registration.d.ts +32 -0
  48. package/dist/types/auth/oauth2/device-authorization.d.ts +40 -0
  49. package/dist/types/auth/oauth2/errors.d.ts +46 -0
  50. package/dist/types/auth/oauth2/http-utils.d.ts +21 -0
  51. package/dist/types/auth/oauth2/index.d.ts +16 -0
  52. package/dist/types/auth/oauth2/loopback-receiver.d.ts +29 -0
  53. package/dist/types/auth/oauth2/metadata.d.ts +32 -0
  54. package/dist/types/auth/oauth2/oauth2-authentication.d.ts +31 -0
  55. package/dist/types/auth/oauth2/session-manager.d.ts +241 -0
  56. package/dist/types/auth/oauth2/token-store.d.ts +49 -0
  57. package/dist/types/auth/oauth2/token.d.ts +56 -0
  58. package/dist/types/index.d.ts +2 -2
  59. package/dist/types/model/order.d.ts +62 -11
  60. package/dist/types/model/quote-requests.d.ts +21 -2
  61. package/dist/types/model/quote.d.ts +72 -14
  62. package/dist/types/model/trade-requests.d.ts +17 -4
  63. package/dist/types/model/trade.d.ts +11 -1
  64. package/dist/types/push/callbacks.d.ts +3 -7
  65. package/dist/types/push/index.d.ts +2 -1
  66. package/dist/types/push/pb/Request.d.ts +1 -0
  67. package/dist/types/push/proto-message.d.ts +4 -1
  68. package/dist/types/push/push-client.d.ts +33 -0
  69. package/dist/types/push/tick-util.d.ts +30 -0
  70. package/dist/types/quote/quote-client.d.ts +27 -6
  71. package/dist/types/trade/trade-client.d.ts +27 -3
  72. package/dist/types/version.d.ts +1 -1
  73. package/package.json +8 -4
@@ -0,0 +1,762 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.OAuth2SessionManager = exports.CLIENT_NAME = exports.DEFAULT_MANUAL_CALLBACK_PORT = exports.DEFAULT_AUTHORIZE_TIMEOUT_SECONDS = exports.MIN_USABLE_REMAINING_MILLIS = exports.DEFAULT_REFRESH_AHEAD_MILLIS = exports.BUILTIN_ISSUER = exports.ENV_ISSUER = void 0;
37
+ /**
38
+ * OAuth 2.0 authorization session management.
39
+ *
40
+ * The manager performs metadata discovery, dynamic registration, loopback or device
41
+ * authorization, token refresh, and persistence. Requested scopes are derived from
42
+ * `scopes_supported` metadata. Construction performs no network requests.
43
+ */
44
+ const crypto_1 = require("crypto");
45
+ const client_registration_1 = require("./client-registration");
46
+ const device_authorization_1 = require("./device-authorization");
47
+ const errors_1 = require("./errors");
48
+ const httpUtils = __importStar(require("./http-utils"));
49
+ const loopback_receiver_1 = require("./loopback-receiver");
50
+ const metadata_1 = require("./metadata");
51
+ const token_1 = require("./token");
52
+ const token_store_1 = require("./token-store");
53
+ exports.ENV_ISSUER = 'TIGEROPEN_OAUTH2_ISSUER';
54
+ /**
55
+ * Production authorization server. The environment variable or constructor argument
56
+ * overrides this value.
57
+ */
58
+ exports.BUILTIN_ISSUER = 'https://openapi-oauth2.tigerfintech.com';
59
+ /** How early to refresh an access token. */
60
+ exports.DEFAULT_REFRESH_AHEAD_MILLIS = 300 * 1000;
61
+ /** Remaining lifetime below which a token is no longer handed out. */
62
+ exports.MIN_USABLE_REMAINING_MILLIS = 30 * 1000;
63
+ /** How long to wait for the browser callback. */
64
+ exports.DEFAULT_AUTHORIZE_TIMEOUT_SECONDS = 5 * 60;
65
+ /** Added to the polling interval on each `slow_down`. */
66
+ const DEVICE_SLOW_DOWN_INCREMENT_SECONDS = 5;
67
+ /** Fallback when the device authorization response omits `expires_in`. */
68
+ const DEFAULT_DEVICE_EXPIRES_IN_SECONDS = 600;
69
+ /**
70
+ * Fixed callback port for a manually configured clientId. Chosen to stay clear of the
71
+ * default Linux ephemeral range (from 32768) and of common dev ports.
72
+ */
73
+ exports.DEFAULT_MANUAL_CALLBACK_PORT = 18888;
74
+ const GRANT_TYPE_AUTHORIZATION_CODE = 'authorization_code';
75
+ const GRANT_TYPE_REFRESH_TOKEN = 'refresh_token';
76
+ const GRANT_TYPE_DEVICE_CODE = 'urn:ietf:params:oauth:grant-type:device_code';
77
+ /**
78
+ * Application name registered by the SDK itself. Shown on the user's authorization
79
+ * page, so an entry point that registers its own client passes its own name.
80
+ */
81
+ exports.CLIENT_NAME = 'Tiger TypeScript SDK';
82
+ /** OAuth 2.0 authorization session manager. */
83
+ class OAuth2SessionManager {
84
+ constructor(options = {}) {
85
+ /** In-flight refresh, so concurrent callers share one token request (single-flight). */
86
+ this.refreshInFlight = null;
87
+ /** In-flight metadata discovery, so concurrent callers share one round trip. */
88
+ this.metadataInFlight = null;
89
+ this._issuer = trimTrailingSlash(options.issuer || OAuth2SessionManager.defaultIssuer());
90
+ this._callbackPort = options.callbackPort || 0;
91
+ this.explicitClientId = options.clientId || undefined;
92
+ this.authorizeTimeoutSeconds = options.authorizeTimeoutSeconds ?? exports.DEFAULT_AUTHORIZE_TIMEOUT_SECONDS;
93
+ this.clientName = options.clientName || exports.CLIENT_NAME;
94
+ this.store = options.store ?? new token_store_1.OAuth2TokenStore(options.home, options.surface ?? token_store_1.DEFAULT_SURFACE);
95
+ }
96
+ static defaultIssuer() {
97
+ return trimTrailingSlash(process.env[exports.ENV_ISSUER] || exports.BUILTIN_ISSUER);
98
+ }
99
+ get issuer() {
100
+ return this._issuer;
101
+ }
102
+ /** Currently known clientId; undefined before registration in dynamic mode. */
103
+ get clientId() {
104
+ return this.knownClientId();
105
+ }
106
+ get hasExplicitClientId() {
107
+ return this.explicitClientId !== undefined;
108
+ }
109
+ get callbackPort() {
110
+ return this._callbackPort;
111
+ }
112
+ /** Scopes requested by this session, space-separated. Fetches metadata. */
113
+ async scopes() {
114
+ return this.resolveScopes();
115
+ }
116
+ /**
117
+ * Create a default session and perform browser authorization when required.
118
+ *
119
+ * The callback receives the authorization URL; the SDK does not open a browser.
120
+ */
121
+ static async loginWithDefaults(onAuthorizationUrl) {
122
+ const sessions = new OAuth2SessionManager();
123
+ await sessions.loginIfNeeded(onAuthorizationUrl);
124
+ return sessions;
125
+ }
126
+ /** Create a default session and perform device authorization when required. */
127
+ static async loginWithDeviceCodeDefaults(onDeviceAuthorization) {
128
+ const sessions = new OAuth2SessionManager();
129
+ await sessions.loginWithDeviceCodeIfNeeded(onDeviceAuthorization);
130
+ return sessions;
131
+ }
132
+ /**
133
+ * Perform browser authorization with a loopback redirect and PKCE.
134
+ *
135
+ * @param onAuthorizationUrl callback receiving the authorization URL
136
+ */
137
+ async login(onAuthorizationUrl) {
138
+ const metadata = await this.metadataOf();
139
+ const clientId = await this.resolveClientId();
140
+ const scopes = await this.resolveScopes();
141
+ // The receiver must be listening before the authorization request is issued.
142
+ const receiver = await loopback_receiver_1.LoopbackReceiver.start(this.effectiveCallbackPort());
143
+ const verifier = randomUrlSafe(64);
144
+ const state = randomUrlSafe(24);
145
+ let params;
146
+ try {
147
+ const url = `${metadata.authorizationEndpoint}?` + query({
148
+ response_type: 'code',
149
+ client_id: clientId,
150
+ redirect_uri: receiver.redirectUri,
151
+ scope: scopes,
152
+ state,
153
+ code_challenge: s256(verifier),
154
+ code_challenge_method: 'S256',
155
+ });
156
+ await onAuthorizationUrl(url);
157
+ params = await receiver.waitForCallback(this.authorizeTimeoutSeconds);
158
+ }
159
+ finally {
160
+ receiver.close();
161
+ }
162
+ const error = params['error'];
163
+ if (error) {
164
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.CallbackFailed, `authorization denied: ${error} ${params['error_description'] ?? ''}`);
165
+ }
166
+ if (params['state'] !== state) {
167
+ // A mismatch indicates that the callback does not belong to the active flow.
168
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.CallbackFailed, 'state mismatch in authorization callback');
169
+ }
170
+ const code = params['code'];
171
+ if (!code) {
172
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.CallbackFailed, 'authorization callback has no code');
173
+ }
174
+ let token;
175
+ try {
176
+ token = await this.exchange({
177
+ grant_type: GRANT_TYPE_AUTHORIZATION_CODE,
178
+ code,
179
+ redirect_uri: receiver.redirectUri,
180
+ client_id: clientId,
181
+ code_verifier: verifier,
182
+ }, scopes, clientId);
183
+ }
184
+ catch (e) {
185
+ // A refused authorization code says nothing about the persisted token, so that is
186
+ // left alone. A refused client registration is removed so it is not reused.
187
+ if (e instanceof errors_1.RejectedGrantError) {
188
+ this.forgetRejectedRegistration(e);
189
+ }
190
+ throw e;
191
+ }
192
+ this.persist(token);
193
+ return token;
194
+ }
195
+ /**
196
+ * Use the existing local authorization if usable, otherwise run a full loopback +
197
+ * PKCE authorization.
198
+ */
199
+ async loginIfNeeded(onAuthorizationUrl) {
200
+ const reusable = await this.reusableToken();
201
+ return reusable ?? this.login(onAuthorizationUrl);
202
+ }
203
+ /**
204
+ * Reuse local authorization or perform device authorization (RFC 8628).
205
+ *
206
+ * Device authorization supports environments without a local browser. Unlike loopback
207
+ * authorization with PKCE, it has no redirect binding or proof that the initiating and
208
+ * redeeming clients are the same party, increasing phishing risk. Loopback
209
+ * authorization is preferred when a local browser is available.
210
+ */
211
+ async loginWithDeviceCodeIfNeeded(onDeviceAuthorization) {
212
+ const reusable = await this.reusableToken();
213
+ return reusable ?? this.loginWithDeviceCode(onDeviceAuthorization);
214
+ }
215
+ /**
216
+ * Perform device authorization as defined by RFC 8628.
217
+ *
218
+ * This flow is intended for environments without a local browser and has the security
219
+ * limitations documented by {@link loginWithDeviceCodeIfNeeded}.
220
+ */
221
+ async loginWithDeviceCode(onDeviceAuthorization) {
222
+ const metadata = await this.metadataOf();
223
+ if (!metadata.deviceAuthorizationEndpoint) {
224
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.MetadataUnavailable, 'authorization server does not advertise a device_authorization_endpoint');
225
+ }
226
+ const clientId = await this.resolveClientId();
227
+ const scopes = await this.resolveScopes();
228
+ const device = await this.requestDeviceAuthorization(metadata, clientId, scopes);
229
+ await onDeviceAuthorization(device);
230
+ const token = await this.pollForDeviceToken(metadata, device, clientId, scopes);
231
+ this.persist(token);
232
+ return token;
233
+ }
234
+ /**
235
+ * Revoke authorization under RFC 7009 and delete the local token.
236
+ *
237
+ * Dynamic client registration is retained across logout. The local token is deleted
238
+ * even if server-side revocation fails.
239
+ *
240
+ * @returns true when server-side revocation succeeds; false when only local state is
241
+ * cleared and the server credential may remain valid
242
+ */
243
+ async logout() {
244
+ const clientId = this.knownClientId();
245
+ const revoked = await this.revoke(this.currentToken(), clientId);
246
+ if (clientId) {
247
+ this.store.deleteToken(clientId);
248
+ }
249
+ this.token = undefined;
250
+ return revoked;
251
+ }
252
+ /**
253
+ * Return a usable access token, refreshing near expiry.
254
+ *
255
+ * Throws `ReauthorizationRequired` when no authorization is available. Browser
256
+ * authorization is never started implicitly.
257
+ *
258
+ * A refresh being due does not make the caller wait for one. While the current token
259
+ * still has {@link MIN_USABLE_REMAINING_MILLIS} of life, a refresh already running
260
+ * elsewhere is left to finish and the current token is returned.
261
+ */
262
+ async getAccessToken() {
263
+ const token = this.currentToken();
264
+ if (!token || !token.accessToken) {
265
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.ReauthorizationRequired, 'not authorized yet, call login() first');
266
+ }
267
+ if (!token.needsRefresh(exports.DEFAULT_REFRESH_AHEAD_MILLIS)) {
268
+ return token.accessToken;
269
+ }
270
+ if (token.remainingMillis() > exports.MIN_USABLE_REMAINING_MILLIS && this.refreshInFlight !== null) {
271
+ // Refresh due, token still good, and somebody else already has the exchange.
272
+ return token.accessToken;
273
+ }
274
+ const refreshed = await this.refresh(token.accessToken);
275
+ return refreshed.accessToken;
276
+ }
277
+ /** The current token, or undefined. Sends no requests and triggers no authorization. */
278
+ status() {
279
+ return this.currentToken();
280
+ }
281
+ /**
282
+ * Whether asking for a token now would trigger a refresh. Reads memory only.
283
+ *
284
+ * Exists so no other component needs a refresh-ahead window of its own.
285
+ */
286
+ shouldRefreshSoon() {
287
+ const token = this.currentToken();
288
+ return !!token && token.needsRefresh(exports.DEFAULT_REFRESH_AHEAD_MILLIS);
289
+ }
290
+ isAuthorized() {
291
+ return this.currentToken() !== undefined;
292
+ }
293
+ /** Refresh because the token is close to expiry. */
294
+ refresh(failedAccessToken) {
295
+ return this.doRefresh(failedAccessToken, false);
296
+ }
297
+ /**
298
+ * Refresh after a 401 even when the token is not near expiry.
299
+ *
300
+ * @param failedAccessToken token used by the failed request; comparison with the
301
+ * current token detects rotation completed elsewhere and preserves single-flight
302
+ * refresh behavior
303
+ */
304
+ refreshAfterUnauthorized(failedAccessToken) {
305
+ return this.doRefresh(failedAccessToken, true);
306
+ }
307
+ /**
308
+ * Single-flight refresh: the first caller performs rotation while others await the
309
+ * same promise.
310
+ */
311
+ async doRefresh(failedAccessToken, force) {
312
+ while (this.refreshInFlight) {
313
+ // Re-run the guards once the running exchange settles: it may already have
314
+ // produced exactly the token this caller needs. Its failure belongs to whoever
315
+ // started it; this caller re-evaluates from the state it left behind.
316
+ try {
317
+ await this.refreshInFlight;
318
+ }
319
+ catch {
320
+ // Reported to the caller that owns the exchange.
321
+ }
322
+ // Another waiter may have claimed the next exchange while this one resumed, so
323
+ // the loop re-reads rather than falling straight through.
324
+ }
325
+ // No await between the loop exit and the claim below, so exactly one caller claims.
326
+ return this.refreshOnce(failedAccessToken, force);
327
+ }
328
+ refreshOnce(failedAccessToken, force) {
329
+ const current = this.currentToken();
330
+ if (!current) {
331
+ return Promise.reject(new errors_1.OAuth2Error(errors_1.OAuth2Category.ReauthorizationRequired, 'not authorized yet, call login() first'));
332
+ }
333
+ if (failedAccessToken !== undefined && current.accessToken !== failedAccessToken) {
334
+ // Already rotated elsewhere. Compares token values rather than timestamps: equal
335
+ // timestamps do not imply the same token.
336
+ return Promise.resolve(current);
337
+ }
338
+ if (!force && !current.needsRefresh(exports.DEFAULT_REFRESH_AHEAD_MILLIS)) {
339
+ return Promise.resolve(current);
340
+ }
341
+ if (!current.hasRefreshToken()) {
342
+ return Promise.reject(new errors_1.OAuth2Error(errors_1.OAuth2Category.ReauthorizationRequired, 'no refresh_token available, re-authorization required'));
343
+ }
344
+ const running = this.exchangeRefreshToken(current)
345
+ .catch((e) => {
346
+ if (e instanceof errors_1.RejectedGrantError) {
347
+ return this.recoverOrGiveUp(current, e);
348
+ }
349
+ throw e;
350
+ })
351
+ .finally(() => {
352
+ if (this.refreshInFlight === running) {
353
+ this.refreshInFlight = null;
354
+ }
355
+ });
356
+ this.refreshInFlight = running;
357
+ return running;
358
+ }
359
+ /**
360
+ * Handle a refresh the authorization server rejected.
361
+ *
362
+ * Refresh tokens rotate, so the rejected value may already have been replaced by
363
+ * another process sharing this store. The store is re-read before any local state is
364
+ * removed, and the authorization is treated as ended only once the newest stored
365
+ * credential is rejected too.
366
+ */
367
+ async recoverOrGiveUp(rejectedToken, rejected) {
368
+ const stored = this.reloadFromStore(rejectedToken.clientId);
369
+ const superseded = stored !== null && stored.accessToken !== rejectedToken.accessToken;
370
+ if (!superseded) {
371
+ // Nothing newer exists, so the rejection is about the authorization itself.
372
+ this.forgetRejectedAuthorization(rejected, rejectedToken);
373
+ throw rejected;
374
+ }
375
+ this.token = stored;
376
+ if (!stored.needsRefresh(exports.DEFAULT_REFRESH_AHEAD_MILLIS)) {
377
+ // Already refreshed elsewhere, so no second request is needed.
378
+ return stored;
379
+ }
380
+ if (!stored.hasRefreshToken()) {
381
+ this.forgetRejectedAuthorization(rejected, rejectedToken);
382
+ throw rejected;
383
+ }
384
+ try {
385
+ return await this.exchangeRefreshToken(stored);
386
+ }
387
+ catch (e) {
388
+ // The credential adopted above was refused too.
389
+ if (e instanceof errors_1.RejectedGrantError) {
390
+ this.forgetRejectedAuthorization(e, stored);
391
+ }
392
+ throw e;
393
+ }
394
+ }
395
+ /** Exchange the given token's refresh token for a new access token and persist it. */
396
+ async exchangeRefreshToken(base) {
397
+ const clientId = base.clientId || this.knownClientId();
398
+ const token = await this.exchange({
399
+ grant_type: GRANT_TYPE_REFRESH_TOKEN,
400
+ refresh_token: base.refreshToken,
401
+ client_id: clientId,
402
+ }, base.scope, clientId);
403
+ if (!token.refreshToken) {
404
+ // Preserve the existing refresh token when rotation omits a replacement.
405
+ token.refreshToken = base.refreshToken;
406
+ }
407
+ this.persist(token);
408
+ return token;
409
+ }
410
+ /**
411
+ * Read the stored token from disk, bypassing the in-memory copy.
412
+ *
413
+ * @returns the stored token, or null when there is none or it cannot be read
414
+ */
415
+ reloadFromStore(clientId) {
416
+ if (!clientId)
417
+ return null;
418
+ try {
419
+ return this.store.loadToken(clientId, this._issuer);
420
+ }
421
+ catch {
422
+ // An unreadable store tells us nothing about the grant, so the rejection stands.
423
+ return null;
424
+ }
425
+ }
426
+ /**
427
+ * Remove the local token, and the dynamic registration when that was refused.
428
+ *
429
+ * Only the credential that was refused is removed, and only while it is still the one
430
+ * on disk. Another process sharing the store may have replaced it in the meantime, and
431
+ * that replacement was never refused.
432
+ */
433
+ forgetRejectedAuthorization(rejected, rejectedToken) {
434
+ const clientId = rejectedToken?.clientId;
435
+ const stored = clientId ? this.reloadFromStore(clientId) : null;
436
+ if (stored !== null && stored.accessToken !== rejectedToken?.accessToken) {
437
+ // A newer credential was stored while this one was refused; keep it.
438
+ }
439
+ else if (clientId) {
440
+ this.store.deleteToken(clientId);
441
+ }
442
+ // The in-memory copy is the refused one either way. Dropping it makes the next call
443
+ // read whatever is on disk.
444
+ this.token = undefined;
445
+ this.forgetRejectedRegistration(rejected);
446
+ }
447
+ /**
448
+ * Remove a refused dynamic client registration so the next authorization registers a
449
+ * new one. Explicit clientIds are left alone.
450
+ */
451
+ forgetRejectedRegistration(rejected) {
452
+ if (rejected.isClientRejected && this.explicitClientId === undefined) {
453
+ this.store.deleteClientRegistration(this._issuer);
454
+ this.registeredClientId = undefined;
455
+ }
456
+ }
457
+ /**
458
+ * Return a reusable local token, refreshing it when required.
459
+ *
460
+ * @returns undefined when interactive authorization is required
461
+ */
462
+ async reusableToken() {
463
+ const existing = this.currentToken();
464
+ if (!existing || !existing.accessToken)
465
+ return undefined;
466
+ if (!existing.needsRefresh(exports.DEFAULT_REFRESH_AHEAD_MILLIS))
467
+ return existing;
468
+ if (existing.hasRefreshToken()) {
469
+ try {
470
+ return await this.refresh(existing.accessToken);
471
+ }
472
+ catch (e) {
473
+ // Only terminal authorization errors require interactive authorization;
474
+ // transient network and server errors remain retryable.
475
+ if (!(e instanceof errors_1.OAuth2Error) || e.category !== errors_1.OAuth2Category.ReauthorizationRequired) {
476
+ throw e;
477
+ }
478
+ }
479
+ }
480
+ return undefined;
481
+ }
482
+ currentToken() {
483
+ if (this.token)
484
+ return this.token;
485
+ const clientId = this.knownClientId();
486
+ if (!clientId)
487
+ return undefined;
488
+ const token = this.store.loadToken(clientId, this._issuer);
489
+ if (token) {
490
+ this.token = token;
491
+ }
492
+ return token ?? undefined;
493
+ }
494
+ /**
495
+ * The known clientId, without triggering registration. Manual mode never reads
496
+ * `clients/`.
497
+ */
498
+ knownClientId() {
499
+ if (this.explicitClientId)
500
+ return this.explicitClientId;
501
+ if (this.registeredClientId)
502
+ return this.registeredClientId;
503
+ const clientId = this.store.loadClientId(this._issuer);
504
+ if (clientId) {
505
+ this.registeredClientId = clientId;
506
+ }
507
+ return clientId ?? undefined;
508
+ }
509
+ persist(token) {
510
+ this.token = token;
511
+ this.store.saveToken(token);
512
+ }
513
+ effectiveCallbackPort() {
514
+ if (this._callbackPort > 0)
515
+ return this._callbackPort;
516
+ // A manual clientId was registered by hand with a concrete redirect_uri the SDK
517
+ // cannot see, so both sides agree on this port; dynamic registration sends a
518
+ // portless redirect_uri and can let the system assign one.
519
+ return this.explicitClientId ? exports.DEFAULT_MANUAL_CALLBACK_PORT : 0;
520
+ }
521
+ async resolveScopes() {
522
+ if (this.resolvedScopes !== undefined)
523
+ return this.resolvedScopes;
524
+ const metadata = await this.metadataOf();
525
+ if (!metadata.scopesSupported.length) {
526
+ // Missing scope metadata is rejected instead of inferring incomplete scopes.
527
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.MetadataUnavailable, 'authorization server metadata has no scopes_supported');
528
+ }
529
+ this.resolvedScopes = normalizeScopes(metadata.scopesSupported.join(' '));
530
+ return this.resolvedScopes;
531
+ }
532
+ metadataOf() {
533
+ if (this.metadata)
534
+ return Promise.resolve(this.metadata);
535
+ if (this.metadataInFlight)
536
+ return this.metadataInFlight;
537
+ const url = `${this._issuer}/.well-known/oauth-authorization-server`;
538
+ const running = httpUtils.getJson(url)
539
+ .then((data) => {
540
+ const metadata = metadata_1.OAuth2Metadata.fromJSON(data);
541
+ if (!metadata || !metadata.authorizationEndpoint || !metadata.tokenEndpoint) {
542
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.MetadataUnavailable, `incomplete authorization server metadata from ${url}`);
543
+ }
544
+ this.metadata = metadata;
545
+ return metadata;
546
+ })
547
+ .finally(() => {
548
+ if (this.metadataInFlight === running) {
549
+ this.metadataInFlight = null;
550
+ }
551
+ });
552
+ this.metadataInFlight = running;
553
+ return running;
554
+ }
555
+ /** Manual clientId > saved registration whose scopes still cover > register. */
556
+ async resolveClientId() {
557
+ if (this.explicitClientId)
558
+ return this.explicitClientId;
559
+ const required = await this.resolveScopes();
560
+ const registration = this.store.loadClientRegistration(this._issuer);
561
+ if (registration?.clientId && scopesCover(registration.registeredScopes, required)) {
562
+ this.registeredClientId = registration.clientId;
563
+ return registration.clientId;
564
+ }
565
+ return this.register(required);
566
+ }
567
+ /**
568
+ * Register a public client under RFC 7591.
569
+ *
570
+ * Registration omits scope. The granted scope set is stored separately so a wider
571
+ * requirement triggers re-registration instead of `invalid_scope`.
572
+ */
573
+ async register(scopes) {
574
+ const metadata = await this.metadataOf();
575
+ if (!metadata.registrationEndpoint) {
576
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.ClientRegistrationRequired, 'authorization server does not support dynamic registration; pass clientId explicitly');
577
+ }
578
+ const port = this._callbackPort;
579
+ const redirectUri = port > 0 ? `http://127.0.0.1:${port}/callback` : 'http://127.0.0.1/callback';
580
+ const response = await httpUtils.postJson(metadata.registrationEndpoint, {
581
+ client_name: this.clientName,
582
+ redirect_uris: [redirectUri],
583
+ grant_types: [GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN, GRANT_TYPE_DEVICE_CODE],
584
+ response_types: ['code'],
585
+ // Public client: there is no client_secret to hide, PKCE is the protection
586
+ token_endpoint_auth_method: 'none',
587
+ });
588
+ const error = response['error'];
589
+ if (error) {
590
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.ClientRegistrationRequired, `dynamic registration failed: ${String(error)} ${String(response['error_description'] ?? '')}`);
591
+ }
592
+ const clientId = response['client_id'];
593
+ if (typeof clientId !== 'string' || !clientId) {
594
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.ClientRegistrationRequired, 'dynamic registration response has no client_id');
595
+ }
596
+ this.store.saveClientRegistration(new client_registration_1.OAuth2ClientRegistration({
597
+ issuer: this._issuer,
598
+ clientId,
599
+ clientName: this.clientName,
600
+ registeredScopes: normalizeScopes(response['scope']) ?? scopes,
601
+ clientIdIssuedAt: typeof response['client_id_issued_at'] === 'number'
602
+ ? response['client_id_issued_at']
603
+ : Math.floor((0, token_1.currentMillis)() / 1000),
604
+ }));
605
+ this.registeredClientId = clientId;
606
+ return clientId;
607
+ }
608
+ async requestDeviceAuthorization(metadata, clientId, scopes) {
609
+ const response = await httpUtils.postForm(metadata.deviceAuthorizationEndpoint, {
610
+ client_id: clientId,
611
+ scope: scopes,
612
+ });
613
+ const error = response['error'];
614
+ if (error) {
615
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.DeviceAuthorizationFailed, `device authorization failed: ${String(error)} ${String(response['error_description'] ?? '')}`);
616
+ }
617
+ const device = device_authorization_1.OAuth2DeviceAuthorization.fromJSON(response);
618
+ if (!device || !device.deviceCode || !device.userCode) {
619
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.DeviceAuthorizationFailed, 'incomplete device authorization response');
620
+ }
621
+ return device;
622
+ }
623
+ /** Poll the token endpoint according to RFC 8628 response semantics. */
624
+ async pollForDeviceToken(metadata, device, clientId, scopes) {
625
+ let interval = device.effectiveIntervalSeconds();
626
+ const expiresIn = device.expiresIn || DEFAULT_DEVICE_EXPIRES_IN_SECONDS;
627
+ const deadline = (0, token_1.currentMillis)() + expiresIn * 1000;
628
+ for (;;) {
629
+ if ((0, token_1.currentMillis)() >= deadline) {
630
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.DeviceAuthorizationFailed, 'device authorization was not approved in time');
631
+ }
632
+ await sleep(interval * 1000);
633
+ const response = await httpUtils.postForm(metadata.tokenEndpoint, {
634
+ grant_type: GRANT_TYPE_DEVICE_CODE,
635
+ device_code: device.deviceCode,
636
+ client_id: clientId,
637
+ });
638
+ const error = response['error'];
639
+ if (!error) {
640
+ return this.toToken(response, scopes, clientId);
641
+ }
642
+ if (error === 'authorization_pending')
643
+ continue;
644
+ if (error === 'slow_down') {
645
+ interval += DEVICE_SLOW_DOWN_INCREMENT_SECONDS;
646
+ continue;
647
+ }
648
+ if (error === 'access_denied') {
649
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.DeviceAuthorizationFailed, 'device authorization was denied by the user');
650
+ }
651
+ if (error === 'expired_token') {
652
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.DeviceAuthorizationFailed, 'device code expired before it was approved');
653
+ }
654
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.TokenRequestFailed, `device token request failed: ${String(error)} ${String(response['error_description'] ?? '')}`);
655
+ }
656
+ }
657
+ async exchange(form, requestedScopes, clientId) {
658
+ const metadata = await this.metadataOf();
659
+ const response = await httpUtils.postForm(metadata.tokenEndpoint, form);
660
+ const error = response['error'];
661
+ if (error) {
662
+ const description = String(response['error_description'] ?? '');
663
+ if (error === 'invalid_grant' || error === 'invalid_client') {
664
+ // Reported only; removing local state is left to the caller.
665
+ throw new errors_1.RejectedGrantError(String(error), description);
666
+ }
667
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.TokenRequestFailed, `token request failed: ${String(error)} ${description}`);
668
+ }
669
+ return this.toToken(response, requestedScopes, clientId);
670
+ }
671
+ toToken(response, requestedScopes, clientId) {
672
+ const accessToken = response['access_token'];
673
+ if (typeof accessToken !== 'string' || !accessToken) {
674
+ throw new errors_1.OAuth2Error(errors_1.OAuth2Category.TokenRequestFailed, 'token response has no access_token');
675
+ }
676
+ const expiresIn = typeof response['expires_in'] === 'number' ? response['expires_in'] : 3600;
677
+ return new token_1.OAuth2Token({
678
+ issuer: this._issuer,
679
+ clientId,
680
+ accessToken,
681
+ refreshToken: typeof response['refresh_token'] === 'string' ? response['refresh_token'] : undefined,
682
+ tokenType: typeof response['token_type'] === 'string' ? response['token_type'] : 'Bearer',
683
+ // The response scope wins: what was granted may be narrower than requested
684
+ scope: normalizeScopes(response['scope']) ?? requestedScopes,
685
+ expiresAt: (0, token_1.currentMillis)() + expiresIn * 1000,
686
+ });
687
+ }
688
+ /**
689
+ * Attempt server-side revocation without throwing.
690
+ *
691
+ * The refresh token is preferred because RFC 7009 §2.1 permits revocation of its
692
+ * associated access tokens. RFC 7009 §2.2 also returns success for unknown or
693
+ * previously revoked tokens.
694
+ */
695
+ async revoke(current, clientId) {
696
+ if (!current || !clientId)
697
+ return false;
698
+ const value = current.refreshToken || current.accessToken;
699
+ const hint = current.refreshToken ? 'refresh_token' : 'access_token';
700
+ if (!value)
701
+ return false;
702
+ try {
703
+ const endpoint = (await this.metadataOf()).revocationEndpoint;
704
+ if (!endpoint) {
705
+ // Revocation is optional under RFC 8414.
706
+ return false;
707
+ }
708
+ const status = await httpUtils.postFormForStatus(endpoint, {
709
+ token: value,
710
+ token_type_hint: hint,
711
+ client_id: clientId,
712
+ });
713
+ // HTTP 400 and 401 indicate that the server rejected the client.
714
+ return status >= 200 && status < 300;
715
+ }
716
+ catch {
717
+ return false;
718
+ }
719
+ }
720
+ }
721
+ exports.OAuth2SessionManager = OAuth2SessionManager;
722
+ function scopesCover(granted, required) {
723
+ if (!granted || !required)
724
+ return false;
725
+ const grantedSet = new Set(granted.split(' '));
726
+ return required.split(' ').every((scope) => grantedSet.has(scope));
727
+ }
728
+ /**
729
+ * Split on whitespace, dedupe, sort, join with spaces.
730
+ *
731
+ * Sorting keeps {@link scopesCover} and the stored value order-independent: the
732
+ * authorization server does not guarantee a stable order, and without normalization
733
+ * that would spuriously trigger re-registration.
734
+ */
735
+ function normalizeScopes(raw) {
736
+ if (typeof raw !== 'string')
737
+ return undefined;
738
+ const parts = raw.trim().split(/\s+/).filter(Boolean);
739
+ if (!parts.length)
740
+ return undefined;
741
+ return Array.from(new Set(parts)).sort().join(' ');
742
+ }
743
+ function trimTrailingSlash(value) {
744
+ return value.endsWith('/') ? value.slice(0, -1) : value;
745
+ }
746
+ function query(params) {
747
+ const search = new URLSearchParams();
748
+ for (const [key, value] of Object.entries(params)) {
749
+ if (value !== undefined)
750
+ search.append(key, value);
751
+ }
752
+ return search.toString();
753
+ }
754
+ function randomUrlSafe(numBytes) {
755
+ return (0, crypto_1.randomBytes)(numBytes).toString('base64url');
756
+ }
757
+ function s256(verifier) {
758
+ return (0, crypto_1.createHash)('sha256').update(verifier, 'ascii').digest('base64url');
759
+ }
760
+ function sleep(ms) {
761
+ return new Promise((resolve) => setTimeout(resolve, ms));
762
+ }