@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.
- package/README.md +80 -0
- package/dist/cjs/auth/authentication.js +74 -0
- package/dist/cjs/auth/index.js +24 -0
- package/dist/cjs/auth/oauth2/client-registration.js +44 -0
- package/dist/cjs/auth/oauth2/device-authorization.js +40 -0
- package/dist/cjs/auth/oauth2/errors.js +64 -0
- package/dist/cjs/auth/oauth2/http-utils.js +112 -0
- package/dist/cjs/auth/oauth2/index.js +42 -0
- package/dist/cjs/auth/oauth2/loopback-receiver.js +215 -0
- package/dist/cjs/auth/oauth2/metadata.js +34 -0
- package/dist/cjs/auth/oauth2/oauth2-authentication.js +74 -0
- package/dist/cjs/auth/oauth2/session-manager.js +762 -0
- package/dist/cjs/auth/oauth2/token-store.js +252 -0
- package/dist/cjs/auth/oauth2/token.js +76 -0
- package/dist/cjs/push/index.js +1 -0
- package/dist/cjs/push/pb/Request.js +12 -1
- package/dist/cjs/push/proto-message.js +5 -1
- package/dist/cjs/push/push-client.js +34 -16
- package/dist/cjs/push/tick-util.js +172 -0
- package/dist/cjs/quote/quote-client.js +59 -12
- package/dist/cjs/trade/trade-client.js +55 -6
- package/dist/cjs/version.js +1 -1
- package/dist/esm/auth/authentication.js +68 -0
- package/dist/esm/auth/index.js +3 -0
- package/dist/esm/auth/oauth2/client-registration.js +40 -0
- package/dist/esm/auth/oauth2/device-authorization.js +36 -0
- package/dist/esm/auth/oauth2/errors.js +59 -0
- package/dist/esm/auth/oauth2/http-utils.js +105 -0
- package/dist/esm/auth/oauth2/index.js +11 -0
- package/dist/esm/auth/oauth2/loopback-receiver.js +178 -0
- package/dist/esm/auth/oauth2/metadata.js +30 -0
- package/dist/esm/auth/oauth2/oauth2-authentication.js +70 -0
- package/dist/esm/auth/oauth2/session-manager.js +725 -0
- package/dist/esm/auth/oauth2/token-store.js +214 -0
- package/dist/esm/auth/oauth2/token.js +71 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/push/index.js +1 -0
- package/dist/esm/push/pb/Request.js +12 -1
- package/dist/esm/push/proto-message.js +5 -1
- package/dist/esm/push/push-client.js +34 -16
- package/dist/esm/push/tick-util.js +165 -0
- package/dist/esm/quote/quote-client.js +59 -12
- package/dist/esm/trade/trade-client.js +55 -6
- package/dist/esm/version.js +1 -1
- package/dist/types/auth/authentication.d.ts +98 -0
- package/dist/types/auth/index.d.ts +4 -0
- package/dist/types/auth/oauth2/client-registration.d.ts +32 -0
- package/dist/types/auth/oauth2/device-authorization.d.ts +40 -0
- package/dist/types/auth/oauth2/errors.d.ts +46 -0
- package/dist/types/auth/oauth2/http-utils.d.ts +21 -0
- package/dist/types/auth/oauth2/index.d.ts +16 -0
- package/dist/types/auth/oauth2/loopback-receiver.d.ts +29 -0
- package/dist/types/auth/oauth2/metadata.d.ts +32 -0
- package/dist/types/auth/oauth2/oauth2-authentication.d.ts +31 -0
- package/dist/types/auth/oauth2/session-manager.d.ts +241 -0
- package/dist/types/auth/oauth2/token-store.d.ts +49 -0
- package/dist/types/auth/oauth2/token.d.ts +56 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/model/order.d.ts +62 -11
- package/dist/types/model/quote-requests.d.ts +21 -2
- package/dist/types/model/quote.d.ts +72 -14
- package/dist/types/model/trade-requests.d.ts +17 -4
- package/dist/types/model/trade.d.ts +11 -1
- package/dist/types/push/callbacks.d.ts +3 -7
- package/dist/types/push/index.d.ts +2 -1
- package/dist/types/push/pb/Request.d.ts +1 -0
- package/dist/types/push/proto-message.d.ts +4 -1
- package/dist/types/push/push-client.d.ts +33 -0
- package/dist/types/push/tick-util.d.ts +30 -0
- package/dist/types/quote/quote-client.d.ts +27 -6
- package/dist/types/trade/trade-client.d.ts +27 -3
- package/dist/types/version.d.ts +1 -1
- 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(
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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',
|
|
189
|
+
return this.callInto('place_order', this.marshalOrderForWire(order));
|
|
150
190
|
}
|
|
151
191
|
async previewOrder(order) {
|
|
152
|
-
return this.callInto('preview_order',
|
|
192
|
+
return this.callInto('preview_order', this.marshalOrderForWire(order));
|
|
153
193
|
}
|
|
154
194
|
async modifyOrder(id, order) {
|
|
155
|
-
return this.callInto('modify_order', { ...
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
}
|
package/dist/cjs/version.js
CHANGED
|
@@ -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,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';
|