@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,178 @@
1
+ /**
2
+ * Single-use OAuth 2.0 loopback redirect receiver (RFC 8252).
3
+ *
4
+ * The receiver binds only to `127.0.0.1` to prevent authorization code exposure on the
5
+ * local network. Dynamic registration uses a system-assigned port as permitted by
6
+ * RFC 8252 §7.3; manual registration requires a port matching its redirect URI. The IP
7
+ * literal avoids the name-resolution risks described by RFC 8252 §8.3.
8
+ */
9
+ import * as http from 'http';
10
+ import { OAuth2Category, OAuth2Error } from './errors.js';
11
+ export const CALLBACK_PATH = '/callback';
12
+ /** Inline SVG avoids external resources and font-dependent glyph rendering. */
13
+ function icon(color, path) {
14
+ return `<svg width='64' height='64' viewBox='0 0 64 64' aria-hidden='true'>`
15
+ + `<circle cx='32' cy='32' r='29' fill='none' stroke='${color}' stroke-width='4'/>`
16
+ + `<path d='${path}' fill='none' stroke='${color}' stroke-width='5'`
17
+ + ` stroke-linecap='round' stroke-linejoin='round'/></svg>`;
18
+ }
19
+ /** Green tick. */
20
+ const ICON_SUCCESS = icon('#22c55e', 'M19 33l9.5 9.5L45 22');
21
+ /** Grey dash for the normal `access_denied` outcome. */
22
+ const ICON_CANCELLED = icon('#9ca3af', 'M20 32h24');
23
+ /** Red cross, for outcomes that really are failures. */
24
+ const ICON_FAILED = icon('#ef4444', 'M22 22l20 20M42 22L22 42');
25
+ /** English callback text, kept consistent with the Java and Python SDKs. */
26
+ const TEXT = {
27
+ title: 'Tiger OpenAPI',
28
+ success: ['Authorization successful', 'You can close this page and return to the application.'],
29
+ cancelled: ['Authorization cancelled', 'You declined this authorization. You can close this page.'],
30
+ failed: ['Authorization failed', 'Please return to the application and try again.'],
31
+ };
32
+ /** Classify `access_denied` as cancellation and other errors as failures. */
33
+ function outcomeOf(params) {
34
+ if ('code' in params)
35
+ return 'success';
36
+ if (params['error'] === 'access_denied')
37
+ return 'cancelled';
38
+ return 'failed';
39
+ }
40
+ /**
41
+ * Render a redacted callback outcome page.
42
+ *
43
+ * Callback parameters, including authorization codes and errors, are omitted because
44
+ * browser history, screenshots, and extensions may expose page content. Full parameters
45
+ * remain available to internal callback processing.
46
+ */
47
+ function renderPage(params) {
48
+ const outcome = outcomeOf(params);
49
+ const svg = { success: ICON_SUCCESS, cancelled: ICON_CANCELLED, failed: ICON_FAILED }[outcome];
50
+ const [heading, detail] = TEXT[outcome];
51
+ return `<!DOCTYPE html><html><head><meta charset='utf-8'>`
52
+ + `<meta name='viewport' content='width=device-width,initial-scale=1'>`
53
+ + `<title>${TEXT['title']}</title></head>`
54
+ + `<body style="margin:0;min-height:100vh;display:flex;align-items:center;`
55
+ + `justify-content:center;`
56
+ + `font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',`
57
+ + `sans-serif;background:#fafafa;color:#1f2328">`
58
+ + `<main style='text-align:center;padding:48px 24px;max-width:420px'>`
59
+ + `${svg}`
60
+ + `<h2 style='margin:24px 0 8px;font-size:20px;font-weight:600'>${heading}</h2>`
61
+ + `<p style='margin:0;font-size:14px;line-height:1.6;color:#6b7280'>${detail}</p>`
62
+ + `</main></body></html>`;
63
+ }
64
+ /** Single-shot local callback server. */
65
+ export class LoopbackReceiver {
66
+ constructor(server) {
67
+ this.port = 0;
68
+ this.params = {};
69
+ this.fired = false;
70
+ this.waiters = [];
71
+ this.server = server;
72
+ }
73
+ /**
74
+ * Bind the receiver. It must be listening before the authorization request is issued.
75
+ *
76
+ * @param port port to bind; 0 lets the system assign a free one
77
+ */
78
+ static async start(port = 0) {
79
+ const server = http.createServer();
80
+ const receiver = new LoopbackReceiver(server);
81
+ server.on('request', (req, res) => {
82
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
83
+ if (url.pathname !== CALLBACK_PATH) {
84
+ res.writeHead(404, { 'Content-Length': '0' });
85
+ res.end();
86
+ return;
87
+ }
88
+ const snapshot = receiver.record(url.searchParams);
89
+ const body = Buffer.from(renderPage(snapshot), 'utf-8');
90
+ res.writeHead(200, {
91
+ 'Content-Type': 'text/html; charset=utf-8',
92
+ 'Content-Length': String(body.length),
93
+ });
94
+ res.end(body);
95
+ receiver.release(snapshot);
96
+ });
97
+ await new Promise((resolve, reject) => {
98
+ const onError = (e) => {
99
+ // A fixed port can conflict with another listener; system-assigned ports do not.
100
+ const hint = port === 0 ? '' : ` (port ${port} is in use; free it or set another callbackPort`
101
+ + ' matching the redirect_uri registered for your client)';
102
+ reject(new OAuth2Error(OAuth2Category.CallbackFailed, `cannot bind loopback callback server on 127.0.0.1:${port}${hint}`, e));
103
+ };
104
+ server.once('error', onError);
105
+ server.listen(port, '127.0.0.1', () => {
106
+ server.removeListener('error', onError);
107
+ const address = server.address();
108
+ receiver.port = typeof address === 'object' && address ? address.port : port;
109
+ resolve();
110
+ });
111
+ });
112
+ return receiver;
113
+ }
114
+ get redirectUri() {
115
+ return `http://127.0.0.1:${this.port}${CALLBACK_PATH}`;
116
+ }
117
+ get boundPort() {
118
+ return this.port;
119
+ }
120
+ /**
121
+ * Wait for the browser callback.
122
+ *
123
+ * @returns the query parameters carried by the callback
124
+ * @throws OAuth2Error on timeout
125
+ */
126
+ waitForCallback(timeoutSeconds) {
127
+ if (this.fired) {
128
+ return Promise.resolve({ ...this.params });
129
+ }
130
+ return new Promise((resolve, reject) => {
131
+ const timer = setTimeout(() => {
132
+ this.waiters = this.waiters.filter((w) => w !== onParams);
133
+ reject(new OAuth2Error(OAuth2Category.CallbackFailed, 'waiting for authorization callback timed out'));
134
+ }, timeoutSeconds * 1000);
135
+ const onParams = (params) => {
136
+ clearTimeout(timer);
137
+ resolve(params);
138
+ };
139
+ this.waiters.push(onParams);
140
+ });
141
+ }
142
+ close() {
143
+ try {
144
+ // The browser keeps its connection alive after the redirect, and close() alone
145
+ // waits for it. That would hold the port -- which matters for the fixed port a
146
+ // manually registered client has to reuse on the next authorization.
147
+ this.server.closeAllConnections?.();
148
+ }
149
+ catch {
150
+ // Older runtimes have no such method; the idle timeout releases the port instead.
151
+ }
152
+ try {
153
+ this.server.close();
154
+ }
155
+ catch {
156
+ // Already closed; nothing to release.
157
+ }
158
+ }
159
+ /** Record the callback params (first one wins) and return a snapshot of them. */
160
+ record(search) {
161
+ if (!this.fired) {
162
+ for (const [key, value] of search.entries()) {
163
+ this.params[key] = value;
164
+ }
165
+ }
166
+ return { ...this.params };
167
+ }
168
+ release(snapshot) {
169
+ if (this.fired)
170
+ return;
171
+ this.fired = true;
172
+ const waiters = this.waiters;
173
+ this.waiters = [];
174
+ for (const waiter of waiters) {
175
+ waiter(snapshot);
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Authorization server metadata (RFC 8414).
3
+ *
4
+ * Endpoints always come from metadata, never hardcoded, so switching environments only
5
+ * means changing the issuer.
6
+ */
7
+ export class OAuth2Metadata {
8
+ constructor(init = {}) {
9
+ this.issuer = init.issuer;
10
+ this.authorizationEndpoint = init.authorizationEndpoint;
11
+ this.tokenEndpoint = init.tokenEndpoint;
12
+ this.registrationEndpoint = init.registrationEndpoint;
13
+ this.deviceAuthorizationEndpoint = init.deviceAuthorizationEndpoint;
14
+ this.revocationEndpoint = init.revocationEndpoint;
15
+ this.scopesSupported = init.scopesSupported ?? [];
16
+ }
17
+ static fromJSON(data) {
18
+ if (!data)
19
+ return null;
20
+ return new OAuth2Metadata({
21
+ issuer: data.issuer,
22
+ authorizationEndpoint: data.authorization_endpoint,
23
+ tokenEndpoint: data.token_endpoint,
24
+ registrationEndpoint: data.registration_endpoint,
25
+ deviceAuthorizationEndpoint: data.device_authorization_endpoint,
26
+ revocationEndpoint: data.revocation_endpoint,
27
+ scopesSupported: data.scopes_supported ?? [],
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,70 @@
1
+ /** Request authentication backed by an OAuth 2.0 session manager. */
2
+ import { AuthenticationAttempt, AuthenticationType, RetryDecision, } from '../authentication.js';
3
+ import { OAuth2Error } from './errors.js';
4
+ export const BEARER_PREFIX = 'Bearer ';
5
+ const UNAUTHORIZED_STATUS = 401;
6
+ /** Authenticate with an OAuth2 access token. */
7
+ export class OAuth2Authentication {
8
+ constructor(sessionManager) {
9
+ if (!sessionManager) {
10
+ throw new Error('OAuth2SessionManager is required');
11
+ }
12
+ this.sessions = sessionManager;
13
+ }
14
+ get authType() {
15
+ return AuthenticationType.OAuth2;
16
+ }
17
+ get sessionManager() {
18
+ return this.sessions;
19
+ }
20
+ /**
21
+ * Attach a Bearer token to the request context.
22
+ *
23
+ * Near-expiry refreshes use the session manager's single-flight synchronization.
24
+ * Missing authorization rejects with `ReauthorizationRequired` without starting an
25
+ * implicit browser flow.
26
+ */
27
+ async apply(context) {
28
+ const accessToken = await this.sessions.getAccessToken();
29
+ context.authorizationHeader = BEARER_PREFIX + accessToken;
30
+ return new AuthenticationAttempt(AuthenticationType.OAuth2, accessToken);
31
+ }
32
+ /**
33
+ * Process HTTP 401 responses.
34
+ *
35
+ * HTTP 403 indicates insufficient permission and cannot be resolved by refreshing a
36
+ * token with the same scope. Server errors and timeouts are not credential failures.
37
+ */
38
+ async onUnauthorized(attempt, statusCode) {
39
+ if (statusCode !== UNAUTHORIZED_STATUS) {
40
+ return RetryDecision.noRetry();
41
+ }
42
+ const credential = attempt?.credential;
43
+ try {
44
+ // Reuse a token already rotated elsewhere without another token request.
45
+ const refreshed = await this.sessions.refreshAfterUnauthorized(credential);
46
+ if (!refreshed || !refreshed.accessToken) {
47
+ return RetryDecision.noRetry();
48
+ }
49
+ if (credential !== undefined && refreshed.accessToken === credential) {
50
+ // Credential unchanged, so a retry would get the same 401.
51
+ return RetryDecision.noRetry();
52
+ }
53
+ return RetryDecision.retry(BEARER_PREFIX + refreshed.accessToken);
54
+ }
55
+ catch (e) {
56
+ if (!(e instanceof OAuth2Error)) {
57
+ // Not an OAuth2Error, so most likely a defect in the refresh path.
58
+ console.warn('[TigerOpen] oauth2 refresh after 401 error', e);
59
+ }
60
+ return RetryDecision.noRetry();
61
+ }
62
+ }
63
+ /**
64
+ * The session is owned by the application and may still be in use, so it is not
65
+ * closed here.
66
+ */
67
+ close() {
68
+ // Nothing to release.
69
+ }
70
+ }