@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
@@ -46,6 +46,7 @@ const http_client_1 = require("../client/http-client");
46
46
  const api_request_1 = require("../client/api-request");
47
47
  const api_response_1 = require("../client/api-response");
48
48
  const tokenMethods = __importStar(require("../client/token-methods"));
49
+ const tick_util_1 = require("../push/tick-util");
49
50
  /**
50
51
  * resolveOptionTimezone returns the timezone string to use for expiry conversion.
51
52
  * Explicit tz takes precedence; otherwise infer from symbol (.HK suffix → Asia/Hong_Kong, else America/New_York).
@@ -185,12 +186,22 @@ class QuoteClient {
185
186
  async getKline(req) {
186
187
  return this.callInto('kline', req);
187
188
  }
188
- async getTimeline(symbols) {
189
- return this.callInto('timeline', { symbols });
189
+ async getTimeline(symbolsOrRequest) {
190
+ const req = Array.isArray(symbolsOrRequest) ? { symbols: symbolsOrRequest } : symbolsOrRequest;
191
+ return this.callInto('timeline', req, req.secType === 'CC' ? '3.0' : undefined);
190
192
  }
191
193
  /** Tick-by-tick trades. wire: trade_tick */
192
194
  async getTradeTick(req) {
193
- return this.callInto('trade_tick', req);
195
+ const out = await this.callInto('trade_tick', req);
196
+ for (const tick of out) {
197
+ const isUs = (0, tick_util_1.isUsStockSymbol)(tick.symbol);
198
+ for (const item of tick.items) {
199
+ if (item.cond && item.cond.length > 0) {
200
+ item.cond = (0, tick_util_1.getTradeCondByCode)(isUs, item.cond[0]);
201
+ }
202
+ }
203
+ }
204
+ return out;
194
205
  }
195
206
  /** Depth snapshot. wire: quote_depth */
196
207
  async getQuoteDepth(req) {
@@ -356,6 +367,7 @@ class QuoteClient {
356
367
  while (acc.length < totalSize) {
357
368
  const sub = {
358
369
  symbols: req.symbol ? [req.symbol] : undefined,
370
+ secType: req.secType,
359
371
  period: req.period,
360
372
  right: req.right,
361
373
  beginTime,
@@ -406,7 +418,7 @@ class QuoteClient {
406
418
  async getStockIndustry(req) {
407
419
  return this.callInto('stock_industry', req);
408
420
  }
409
- /** Quote permission detail. wire: get_quote_permission */
421
+ /** Market data permission entries. wire: get_quote_permission */
410
422
  async getQuotePermission(req) {
411
423
  return this.callInto('get_quote_permission', req);
412
424
  }
@@ -417,9 +429,16 @@ class QuoteClient {
417
429
  // ==========================================================================
418
430
  // Batch 4: option / future extensions
419
431
  // ==========================================================================
420
- /** Option tick trades. wire: option_trade_tick */
432
+ /** Option tick trades. wire: option_trade_tick
433
+ *
434
+ * Server expects `biz_content` as a top-level JSON array (matches Java's
435
+ * `BatchApiModel` and Python's `MultipleContractParams.to_openapi_dict`).
436
+ * We unwrap the request here so the array is emitted directly instead of
437
+ * `{"contracts": [...]}`. Sending it as an object triggers
438
+ * `biz param error(failed to parse parameters in 'biz_content')`.
439
+ */
421
440
  async getOptionTradeTicks(req) {
422
- return this.callInto('option_trade_tick', req);
441
+ return this.callInto('option_trade_tick', req.contracts ?? []);
423
442
  }
424
443
  /** Option intraday timeline. wire: option_timeline */
425
444
  async getOptionTimeline(req) {
@@ -458,10 +477,16 @@ class QuoteClient {
458
477
  return this.callInto('future_main_contract', req);
459
478
  }
460
479
  /** Futures K-line. wire: future_kline
461
- * beginTime / endTime default to -1 when unset (server requires them present). */
480
+ * beginTime / endTime default to -1 when unset (server requires them present).
481
+ * Server expects `contract_codes` (plural) — normalize singular `contractCode`
482
+ * to the plural form so callers can pass either shape. */
462
483
  async getFutureKline(req) {
484
+ const contractCodes = req.contractCodes
485
+ ?? (req.contractCode ? [req.contractCode] : undefined);
486
+ const { contractCode: _drop, ...rest } = req;
463
487
  const body = {
464
- ...req,
488
+ ...rest,
489
+ ...(contractCodes ? { contractCodes } : {}),
465
490
  beginTime: req.beginTime ?? -1,
466
491
  endTime: req.endTime ?? -1,
467
492
  };
@@ -479,8 +504,9 @@ class QuoteClient {
479
504
  endTime = -1;
480
505
  const acc = [];
481
506
  while (acc.length < totalSize) {
507
+ // Server rejects singular `contract_code` — send the plural form.
482
508
  const sub = {
483
- contractCode: req.contractCode,
509
+ contractCodes: req.contractCode ? [req.contractCode] : undefined,
484
510
  period: req.period,
485
511
  beginTime,
486
512
  endTime,
@@ -559,9 +585,30 @@ class QuoteClient {
559
585
  async getWarrantFilter(req) {
560
586
  return this.callInto('warrant_filter', req);
561
587
  }
562
- /** Industry list. wire: industry_list */
563
- async getIndustryList(req) {
564
- return this.callInto('industry_list', req);
588
+ /** Industry list. wire: industry_list
589
+ *
590
+ * `industryLevel` is required by the server — omitting it returns
591
+ * `industry level error, all supported is: [GSECTOR, GGROUP, GIND, GSUBIND]`.
592
+ * Defaults to `GGROUP` when unset, matching Python SDK's
593
+ * `IndustryLevel.GGROUP` default.
594
+ *
595
+ * Wire fields differ from other endpoints (see `IndustryItem`): server sends
596
+ * `nameCN` / `nameEN` / `industryLevel`. This method hydrates the
597
+ * backwards-compat `name` (from `nameEN || nameCN`) and mirrors
598
+ * `industryLevel` to `level` so existing callers keep working.
599
+ */
600
+ async getIndustryList(req = {}) {
601
+ const payload = { industryLevel: 'GGROUP', ...req };
602
+ const items = await this.callInto('industry_list', payload);
603
+ if (!Array.isArray(items))
604
+ return [];
605
+ for (const it of items) {
606
+ if (it.name === undefined)
607
+ it.name = it.nameEN || it.nameCN;
608
+ if (it.level === undefined)
609
+ it.level = it.industryLevel;
610
+ }
611
+ return items;
565
612
  }
566
613
  /** Stocks inside an industry. wire: industry_stock_list */
567
614
  async getIndustryStocks(req) {
@@ -145,14 +145,54 @@ class TradeClient {
145
145
  });
146
146
  }
147
147
  // === Order operations ===
148
+ /**
149
+ * Prepare an order request for the wire.
150
+ *
151
+ * The gateway expects `algo_params` as a `[{tag, value}, ...]` array
152
+ * (matching the Python SDK's AlgoParams.to_dict serialization), not the
153
+ * plain object shape that reads naturally in TypeScript. This helper
154
+ * normalizes the object form into the array form on outbound payloads
155
+ * so callers can pass the natural object and TWAP/VWAP orders work end
156
+ * to end without wire-format workarounds.
157
+ *
158
+ * Fields with undefined/null values are dropped; empty algoParams
159
+ * becomes an empty array rather than being stripped, matching the
160
+ * Python SDK behavior when the user passes an empty AlgoParams().
161
+ */
162
+ marshalOrderForWire(order) {
163
+ const { algoParams, ...rest } = order;
164
+ const withAccount = { ...rest, account: this.account };
165
+ if (algoParams === undefined)
166
+ return withAccount;
167
+ // Already a tag/value array (advanced callers who want raw control)
168
+ if (Array.isArray(algoParams)) {
169
+ withAccount.algoParams = algoParams;
170
+ return withAccount;
171
+ }
172
+ // Object shape → tag/value array.
173
+ // Keys are converted to snake_case explicitly here (e.g. `startTime` →
174
+ // `start_time`) so they land correctly in the `tag` string that the
175
+ // gateway reads. `keysToSnakeCase` only rewrites object *keys*, not
176
+ // string *values*, so `tag` would be left camelCase if we didn't do
177
+ // this conversion ourselves.
178
+ const tagValues = [];
179
+ for (const [key, value] of Object.entries(algoParams)) {
180
+ if (value === undefined || value === null)
181
+ continue;
182
+ const snakeKey = key.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
183
+ tagValues.push({ tag: snakeKey, value });
184
+ }
185
+ withAccount.algoParams = tagValues;
186
+ return withAccount;
187
+ }
148
188
  async placeOrder(order) {
149
- return this.callInto('place_order', { ...order, account: this.account });
189
+ return this.callInto('place_order', this.marshalOrderForWire(order));
150
190
  }
151
191
  async previewOrder(order) {
152
- return this.callInto('preview_order', { ...order, account: this.account });
192
+ return this.callInto('preview_order', this.marshalOrderForWire(order));
153
193
  }
154
194
  async modifyOrder(id, order) {
155
- return this.callInto('modify_order', { ...order, account: this.account, id });
195
+ return this.callInto('modify_order', { ...this.marshalOrderForWire(order), id });
156
196
  }
157
197
  async cancelOrder(id, secretKey) {
158
198
  const params = { account: this.account, id };
@@ -217,11 +257,17 @@ class TradeClient {
217
257
  async getPositions(req) {
218
258
  return this.callIntoItems('positions', withAccount(req, this.account));
219
259
  }
220
- /** Query assets. v0.4.0: supports sub-account list and aggregation options. */
260
+ /**
261
+ * Query assets. v0.4.0: supports sub-account list and aggregation options.
262
+ * `assetQuoteType`(ETH / RTH / OVERNIGHT) 选择资产行情口径。
263
+ */
221
264
  async getAssets(req) {
222
265
  return this.callIntoItems('assets', withAccount(req, this.account));
223
266
  }
224
- /** Query prime assets. v0.4.0: takes AssetsRequest for filters. */
267
+ /**
268
+ * Query prime assets. v0.4.0: takes AssetsRequest for filters.
269
+ * `assetQuoteType`(ETH / RTH / OVERNIGHT) 选择资产行情口径。
270
+ */
225
271
  async getPrimeAssets(req) {
226
272
  return this.callInto('prime_assets', withAccount(req, this.account));
227
273
  }
@@ -242,7 +288,10 @@ class TradeClient {
242
288
  async getAnalyticsAsset(req) {
243
289
  return this.callIntoItems('analytics_asset', withAccount(req, this.account));
244
290
  }
245
- /** Aggregate assets (prime) by base currency. */
291
+ /**
292
+ * Aggregate assets (prime) by base currency.
293
+ * `assetQuoteType`(ETH / RTH / OVERNIGHT) 选择资产行情口径。
294
+ */
246
295
  async getAggregateAssets(req) {
247
296
  return this.callInto('aggregate_assets', withAccount(req, this.account));
248
297
  }
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
- exports.SDK_VERSION = '0.4.8';
4
+ exports.SDK_VERSION = '0.5.7';
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Request authentication interfaces.
3
+ *
4
+ * Implementations attach credentials, handle authentication retries, and release
5
+ * resources. OAuth 2.0 authorization flows live in `OAuth2SessionManager`; this module
6
+ * is the data plane and knows nothing about login.
7
+ */
8
+ /** Which kind of authentication. */
9
+ export var AuthenticationType;
10
+ (function (AuthenticationType) {
11
+ /** tigerId + RSA private key signature */
12
+ AuthenticationType["Signature"] = "SIGNATURE";
13
+ /** `Authorization: Bearer <access_token>` */
14
+ AuthenticationType["OAuth2"] = "OAUTH2";
15
+ })(AuthenticationType || (AuthenticationType = {}));
16
+ /**
17
+ * Per-request authentication context.
18
+ *
19
+ * Business parameters (`method`, `version`, `biz_content`, `timestamp`) are filled in by
20
+ * the client and must not be modified by an authentication implementation.
21
+ */
22
+ export class RequestAuthContext {
23
+ constructor(params, retryable = true) {
24
+ this.params = params;
25
+ this.retryable = retryable;
26
+ }
27
+ }
28
+ /**
29
+ * Credential snapshot used to coordinate authentication retries.
30
+ *
31
+ * Comparing the failed credential with current state detects rotation completed
32
+ * elsewhere and avoids duplicate refresh requests.
33
+ */
34
+ export class AuthenticationAttempt {
35
+ constructor(authType, credential) {
36
+ this.authType = authType;
37
+ this.credential = credential;
38
+ }
39
+ /** Never prints the credential value. */
40
+ toString() {
41
+ return `AuthenticationAttempt{type=${this.authType}, credential=${this.credential === undefined ? '<none>' : '***'}}`;
42
+ }
43
+ }
44
+ /**
45
+ * Authentication retry decision.
46
+ *
47
+ * A retry always supplies a new full `Authorization` header value, including the
48
+ * `Bearer ` prefix. There is deliberately no "retry with the same credential" option:
49
+ * that only collects the same 401 twice.
50
+ */
51
+ export class RetryDecision {
52
+ constructor(shouldRetry, authorizationHeader) {
53
+ this.shouldRetry = shouldRetry;
54
+ this.authorizationHeader = authorizationHeader;
55
+ }
56
+ static noRetry() {
57
+ return RetryDecision.NO_RETRY;
58
+ }
59
+ /** @param authorizationHeader full new header value, including `Bearer ` */
60
+ static retry(authorizationHeader) {
61
+ return new RetryDecision(true, authorizationHeader);
62
+ }
63
+ /** Never prints the header value. */
64
+ toString() {
65
+ return `RetryDecision{shouldRetry=${this.shouldRetry}}`;
66
+ }
67
+ }
68
+ RetryDecision.NO_RETRY = new RetryDecision(false, undefined);
@@ -0,0 +1,3 @@
1
+ /** Authentication layer: the request-level data plane plus the OAuth 2.0 control plane. */
2
+ export { AuthenticationAttempt, AuthenticationType, RequestAuthContext, RetryDecision, } from './authentication.js';
3
+ export * from './oauth2/index.js';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OAuth 2.0 dynamic client registration state (RFC 7591).
3
+ *
4
+ * Registration state is stored separately from tokens and retained across logout.
5
+ * `registeredScopes` records the granted scope set; scope expansion requires a new
6
+ * registration to avoid `invalid_scope`.
7
+ */
8
+ export class OAuth2ClientRegistration {
9
+ constructor(init = {}) {
10
+ this.issuer = init.issuer;
11
+ this.clientId = init.clientId;
12
+ this.clientName = init.clientName;
13
+ this.registeredScopes = init.registeredScopes;
14
+ this.clientIdIssuedAt = init.clientIdIssuedAt ?? 0;
15
+ }
16
+ toJSON() {
17
+ return {
18
+ issuer: this.issuer,
19
+ client_id: this.clientId,
20
+ client_name: this.clientName,
21
+ registered_scopes: this.registeredScopes,
22
+ client_id_issued_at: this.clientIdIssuedAt,
23
+ };
24
+ }
25
+ static fromJSON(data) {
26
+ if (!data)
27
+ return null;
28
+ return new OAuth2ClientRegistration({
29
+ issuer: data.issuer,
30
+ clientId: data.client_id,
31
+ clientName: data.client_name,
32
+ registeredScopes: data.registered_scopes,
33
+ clientIdIssuedAt: data.client_id_issued_at ?? 0,
34
+ });
35
+ }
36
+ toString() {
37
+ return `OAuth2ClientRegistration{issuer=${this.issuer}, clientId=${this.clientId}, `
38
+ + `clientName=${this.clientName}, registeredScopes=${this.registeredScopes}}`;
39
+ }
40
+ }
@@ -0,0 +1,36 @@
1
+ /** Device authorization response (RFC 8628). */
2
+ /**
3
+ * Default polling interval per RFC 8628 §3.5. Tiger's authorization server does not
4
+ * return `interval`, so this default applies in practice.
5
+ */
6
+ export const DEFAULT_DEVICE_INTERVAL_SECONDS = 5;
7
+ export class OAuth2DeviceAuthorization {
8
+ constructor(init = {}) {
9
+ this.deviceCode = init.deviceCode;
10
+ this.userCode = init.userCode;
11
+ this.verificationUri = init.verificationUri;
12
+ this.verificationUriComplete = init.verificationUriComplete;
13
+ this.expiresIn = init.expiresIn ?? 0;
14
+ this.interval = init.interval ?? 0;
15
+ }
16
+ effectiveIntervalSeconds() {
17
+ return this.interval > 0 ? this.interval : DEFAULT_DEVICE_INTERVAL_SECONDS;
18
+ }
19
+ static fromJSON(data) {
20
+ if (!data)
21
+ return null;
22
+ return new OAuth2DeviceAuthorization({
23
+ deviceCode: data.device_code,
24
+ userCode: data.user_code,
25
+ verificationUri: data.verification_uri,
26
+ verificationUriComplete: data.verification_uri_complete,
27
+ expiresIn: data.expires_in ?? 0,
28
+ interval: data.interval ?? 0,
29
+ });
30
+ }
31
+ /** Prints no deviceCode. */
32
+ toString() {
33
+ return `OAuth2DeviceAuthorization{userCode=${this.userCode}, verificationUri=${this.verificationUri}, `
34
+ + `expiresIn=${this.expiresIn}, interval=${this.interval}}`;
35
+ }
36
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Categorized errors from OAuth 2.0 authorization flows.
3
+ *
4
+ * Authorization server error details are retained in the error message.
5
+ */
6
+ /** Error category. Each one implies a different action. */
7
+ export var OAuth2Category;
8
+ (function (OAuth2Category) {
9
+ /** Cannot fetch authorization server metadata: wrong address, service down, no network */
10
+ OAuth2Category["MetadataUnavailable"] = "METADATA_UNAVAILABLE";
11
+ /** No usable client_id, and automatic registration is not possible */
12
+ OAuth2Category["ClientRegistrationRequired"] = "CLIENT_REGISTRATION_REQUIRED";
13
+ /** Callback stage failed: timeout, user denial, or state mismatch */
14
+ OAuth2Category["CallbackFailed"] = "CALLBACK_FAILED";
15
+ /**
16
+ * Device flow not approved in time, or explicitly denied. Kept separate from
17
+ * ReauthorizationRequired because only a new device authorization is needed -- the
18
+ * stored token was not touched.
19
+ */
20
+ OAuth2Category["DeviceAuthorizationFailed"] = "DEVICE_AUTHORIZATION_FAILED";
21
+ /** refresh_token expired, revoked, or never obtained. Retrying is pointless */
22
+ OAuth2Category["ReauthorizationRequired"] = "REAUTHORIZATION_REQUIRED";
23
+ /** Token endpoint call failed, and not in the terminal way above */
24
+ OAuth2Category["TokenRequestFailed"] = "TOKEN_REQUEST_FAILED";
25
+ /** Reading or writing the local token file failed */
26
+ OAuth2Category["StorageFailed"] = "STORAGE_FAILED";
27
+ })(OAuth2Category || (OAuth2Category = {}));
28
+ /** Thrown by any stage of the OAuth2 flow. */
29
+ export class OAuth2Error extends Error {
30
+ constructor(category, message, cause) {
31
+ super(message);
32
+ this.name = 'OAuth2Error';
33
+ this.category = category;
34
+ this.cause = cause;
35
+ }
36
+ get reauthorizationRequired() {
37
+ return this.category === OAuth2Category.ReauthorizationRequired;
38
+ }
39
+ toString() {
40
+ return `OAuth2Error{${this.category}: ${this.message}}`;
41
+ }
42
+ }
43
+ /**
44
+ * A grant the authorization server refused.
45
+ *
46
+ * Carries the error code so a caller can tell a rejected credential from a rejected
47
+ * client registration. Its category is `ReauthorizationRequired`, so callers that only
48
+ * know the public type behave as they would for any terminal authorization failure.
49
+ */
50
+ export class RejectedGrantError extends OAuth2Error {
51
+ constructor(error, description) {
52
+ super(OAuth2Category.ReauthorizationRequired, `token request rejected: ${error} ${description}`);
53
+ this.name = 'RejectedGrantError';
54
+ this.error = error;
55
+ }
56
+ get isClientRejected() {
57
+ return this.error === 'invalid_client';
58
+ }
59
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * HTTP helpers for OAuth 2.0 JSON and form-encoded requests.
3
+ *
4
+ * OAuth error responses retain 4xx response bodies for protocol-level error handling.
5
+ */
6
+ import { OAuth2Category, OAuth2Error } from './errors.js';
7
+ /**
8
+ * Every call here is a small JSON exchange with the authorization server, so a request
9
+ * that has not answered in this long is not going to. Same values as the Java and
10
+ * Python SDKs.
11
+ */
12
+ export const CONNECT_TIMEOUT_SECONDS = 5;
13
+ export const READ_TIMEOUT_SECONDS = 10;
14
+ /** Combined budget handed to `AbortSignal.timeout`; fetch does not split the two. */
15
+ const REQUEST_TIMEOUT_MS = (CONNECT_TIMEOUT_SECONDS + READ_TIMEOUT_SECONDS) * 1000;
16
+ const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
17
+ const FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8';
18
+ function encodeForm(form) {
19
+ const params = new URLSearchParams();
20
+ for (const [key, value] of Object.entries(form)) {
21
+ if (value !== undefined && value !== null) {
22
+ params.append(key, value);
23
+ }
24
+ }
25
+ return params.toString();
26
+ }
27
+ export async function getJson(url) {
28
+ return execute('GET', url, undefined, undefined);
29
+ }
30
+ export async function postJson(url, payload) {
31
+ return execute('POST', url, JSON.stringify(payload), JSON_CONTENT_TYPE);
32
+ }
33
+ export async function postForm(url, form) {
34
+ return execute('POST', url, encodeForm(form), FORM_CONTENT_TYPE);
35
+ }
36
+ /**
37
+ * Post a form and return the HTTP status without requiring a response body.
38
+ *
39
+ * RFC 7009 revocation returns an empty success body, so no parsing is performed.
40
+ *
41
+ * @returns the HTTP status code
42
+ * @throws OAuth2Error on connection failure
43
+ */
44
+ export async function postFormForStatus(url, form) {
45
+ let response;
46
+ try {
47
+ response = await fetch(url, {
48
+ method: 'POST',
49
+ headers: { Accept: 'application/json', 'Content-Type': FORM_CONTENT_TYPE },
50
+ body: encodeForm(form),
51
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
52
+ });
53
+ }
54
+ catch (e) {
55
+ throw new OAuth2Error(OAuth2Category.TokenRequestFailed, `request fail: ${url}`, e);
56
+ }
57
+ return response.status;
58
+ }
59
+ /**
60
+ * Send the request and parse the response body as an object.
61
+ *
62
+ * A 4xx returns its body instead of throwing: OAuth2 errors (`invalid_grant`,
63
+ * `authorization_pending` and friends) live in the 4xx body, and the caller needs the
64
+ * `error` field to tell "wait and retry" from "re-authorize". Only a connection failure
65
+ * or a non-JSON response throws.
66
+ */
67
+ async function execute(method, url, body, contentType) {
68
+ const headers = { Accept: 'application/json' };
69
+ if (contentType) {
70
+ headers['Content-Type'] = contentType;
71
+ }
72
+ let response;
73
+ try {
74
+ response = await fetch(url, {
75
+ method,
76
+ headers,
77
+ body,
78
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
79
+ });
80
+ }
81
+ catch (e) {
82
+ throw new OAuth2Error(OAuth2Category.MetadataUnavailable, `request fail: ${url}`, e);
83
+ }
84
+ let text;
85
+ try {
86
+ text = await response.text();
87
+ }
88
+ catch (e) {
89
+ throw new OAuth2Error(OAuth2Category.TokenRequestFailed, `cannot decode response from ${url}, http status ${response.status}`, e);
90
+ }
91
+ if (!text) {
92
+ throw new OAuth2Error(OAuth2Category.TokenRequestFailed, `empty response from ${url}, http status ${response.status}`);
93
+ }
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(text);
97
+ }
98
+ catch (e) {
99
+ throw new OAuth2Error(OAuth2Category.TokenRequestFailed, `non-JSON response from ${url}, http status ${response.status}`, e);
100
+ }
101
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
102
+ throw new OAuth2Error(OAuth2Category.TokenRequestFailed, `unexpected response from ${url}, http status ${response.status}`);
103
+ }
104
+ return parsed;
105
+ }
@@ -0,0 +1,11 @@
1
+ /** OAuth 2.0 authentication, authorization flows, and token persistence. */
2
+ export { OAuth2ClientRegistration } from './client-registration.js';
3
+ export { OAuth2DeviceAuthorization, DEFAULT_DEVICE_INTERVAL_SECONDS } from './device-authorization.js';
4
+ export { OAuth2Category, OAuth2Error, RejectedGrantError } from './errors.js';
5
+ export { LoopbackReceiver, CALLBACK_PATH } from './loopback-receiver.js';
6
+ export { OAuth2Metadata } from './metadata.js';
7
+ export { OAuth2Authentication, BEARER_PREFIX } from './oauth2-authentication.js';
8
+ export { OAuth2SessionManager, BUILTIN_ISSUER, CLIENT_NAME, DEFAULT_AUTHORIZE_TIMEOUT_SECONDS, DEFAULT_MANUAL_CALLBACK_PORT, DEFAULT_REFRESH_AHEAD_MILLIS, ENV_ISSUER, MIN_USABLE_REMAINING_MILLIS, } from './session-manager.js';
9
+ export { OAuth2Token, TOKEN_SCHEMA_VERSION, currentMillis } from './token.js';
10
+ export { OAuth2TokenStore, DEFAULT_SURFACE, ENV_HOME, hashIssuer } from './token-store.js';
11
+ export { CONNECT_TIMEOUT_SECONDS, READ_TIMEOUT_SECONDS } from './http-utils.js';