@tigeropenapi/tigeropen 0.5.5 → 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 +27 -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/pb/Request.js +12 -1
- package/dist/cjs/push/proto-message.js +5 -1
- package/dist/cjs/push/push-client.js +29 -13
- package/dist/cjs/trade/trade-client.js +12 -3
- 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/pb/Request.js +12 -1
- package/dist/esm/push/proto-message.js +5 -1
- package/dist/esm/push/push-client.js +29 -13
- package/dist/esm/trade/trade-client.js +12 -3
- 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 +1 -1
- package/dist/types/model/quote.d.ts +1 -0
- package/dist/types/model/trade-requests.d.ts +5 -0
- package/dist/types/push/index.d.ts +1 -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/trade/trade-client.d.ts +12 -3
- package/dist/types/version.d.ts +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed OAuth 2.0 authorization state.
|
|
3
|
+
*
|
|
4
|
+
* Storage layout:
|
|
5
|
+
*
|
|
6
|
+
* ~/.tiger/openapi/
|
|
7
|
+
* tokens/{clientId}.json authorization state, mode 0600, removed on logout
|
|
8
|
+
* clients/{issuerHash}-{surface}.json dynamic registration, retained across logout
|
|
9
|
+
*
|
|
10
|
+
* Registration files are keyed by issuer because the client ID is not yet available
|
|
11
|
+
* during lookup, and by surface so that each entry point registers its own client.
|
|
12
|
+
* Without the surface, whichever entry point ran first would own the registration and
|
|
13
|
+
* every other one would silently inherit its identity: one name on the user's
|
|
14
|
+
* authorization page, one revocation switch for all of them, and one shared token file
|
|
15
|
+
* whose refresh-token rotation they would race over.
|
|
16
|
+
*
|
|
17
|
+
* Token files need no surface: each surface registers a distinct client ID, so their
|
|
18
|
+
* token files are already distinct. A manually configured client ID is shared on
|
|
19
|
+
* purpose -- one client ID means one application.
|
|
20
|
+
*
|
|
21
|
+
* Storage root precedence is: explicit argument, `TIGEROPEN_HOME`, then
|
|
22
|
+
* `~/.tiger/openapi`. The format is shared with the Java and Python SDKs.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from 'crypto';
|
|
25
|
+
import * as fs from 'fs';
|
|
26
|
+
import { homedir } from 'os';
|
|
27
|
+
import * as path from 'path';
|
|
28
|
+
import { OAuth2ClientRegistration } from './client-registration.js';
|
|
29
|
+
import { OAuth2Category, OAuth2Error } from './errors.js';
|
|
30
|
+
import { OAuth2Token, TOKEN_SCHEMA_VERSION } from './token.js';
|
|
31
|
+
export const ENV_HOME = 'TIGEROPEN_HOME';
|
|
32
|
+
/**
|
|
33
|
+
* Entry point owning a registration. Tools built on this SDK must pass their own value,
|
|
34
|
+
* otherwise they would be filed as the TypeScript SDK and share its client. Keep in
|
|
35
|
+
* step with the Java SDK's `sdk-java` and the Python SDK's `sdk-python`.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_SURFACE = 'sdk-typescript';
|
|
38
|
+
const SAFE_CHARS = /[^a-zA-Z0-9\-_.]/g;
|
|
39
|
+
/**
|
|
40
|
+
* Surfaces become part of a file name, so they are restricted rather than sanitised: a
|
|
41
|
+
* silently rewritten surface would file two entry points under one name.
|
|
42
|
+
*/
|
|
43
|
+
const SURFACE_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
44
|
+
export class OAuth2TokenStore {
|
|
45
|
+
/**
|
|
46
|
+
* @param home storage root; defaults to `TIGEROPEN_HOME` then `~/.tiger/openapi`
|
|
47
|
+
* @param surface entry point owning the registration, e.g. `sdk-typescript`, `cli`.
|
|
48
|
+
* Lowercase alphanumerics and hyphens only
|
|
49
|
+
*/
|
|
50
|
+
constructor(home, surface = DEFAULT_SURFACE) {
|
|
51
|
+
const base = home || process.env[ENV_HOME] || path.join(homedir(), '.tiger', 'openapi');
|
|
52
|
+
this.home = path.resolve(expandHome(base));
|
|
53
|
+
this.surface = validateSurface(surface);
|
|
54
|
+
}
|
|
55
|
+
/** Load a token, treating a missing file or issuer mismatch as a cache miss. */
|
|
56
|
+
loadToken(clientId, expectedIssuer) {
|
|
57
|
+
const file = this.tokenFile(clientId);
|
|
58
|
+
if (!fs.existsSync(file))
|
|
59
|
+
return null;
|
|
60
|
+
let token;
|
|
61
|
+
try {
|
|
62
|
+
token = OAuth2Token.fromJSON(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
// A corrupt file tells us nothing; treat it as no authorization rather than crash.
|
|
66
|
+
console.error(`[TigerOpen] read token file fail: ${file}`, e);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (!token)
|
|
70
|
+
return null;
|
|
71
|
+
if (token.schemaVersion !== TOKEN_SCHEMA_VERSION) {
|
|
72
|
+
// Unknown schema versions are not interpreted.
|
|
73
|
+
throw new OAuth2Error(OAuth2Category.StorageFailed, `unsupported token schema_version: ${token.schemaVersion}`);
|
|
74
|
+
}
|
|
75
|
+
if (expectedIssuer !== undefined && expectedIssuer !== token.issuer) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
saveToken(token) {
|
|
81
|
+
if (!token || !token.clientId) {
|
|
82
|
+
throw new OAuth2Error(OAuth2Category.StorageFailed, 'token or clientId is empty, cannot save');
|
|
83
|
+
}
|
|
84
|
+
writeAtomic(this.tokenFile(token.clientId), JSON.stringify(token.toJSON(), null, 2));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Delete authorization state while retaining client registration.
|
|
88
|
+
*
|
|
89
|
+
* @returns the path removed, or null when there was nothing to remove. A clientId is
|
|
90
|
+
* not its own file name, so a caller reporting what it deleted has to be told rather
|
|
91
|
+
* than build the path itself
|
|
92
|
+
*/
|
|
93
|
+
deleteToken(clientId) {
|
|
94
|
+
if (!clientId)
|
|
95
|
+
return null;
|
|
96
|
+
const file = this.tokenFile(clientId);
|
|
97
|
+
return deleteFile(file) ? file : null;
|
|
98
|
+
}
|
|
99
|
+
loadClientId(issuer) {
|
|
100
|
+
return this.loadClientRegistration(issuer)?.clientId ?? null;
|
|
101
|
+
}
|
|
102
|
+
loadClientRegistration(issuer) {
|
|
103
|
+
const file = this.clientFile(issuer);
|
|
104
|
+
if (!fs.existsSync(file))
|
|
105
|
+
return null;
|
|
106
|
+
try {
|
|
107
|
+
return OAuth2ClientRegistration.fromJSON(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.error(`[TigerOpen] read client registration file fail: ${file}`, e);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
saveClientRegistration(registration) {
|
|
115
|
+
if (!registration || !registration.issuer) {
|
|
116
|
+
throw new OAuth2Error(OAuth2Category.StorageFailed, 'registration or issuer is empty, cannot save');
|
|
117
|
+
}
|
|
118
|
+
writeAtomic(this.clientFile(registration.issuer), JSON.stringify(registration.toJSON(), null, 2));
|
|
119
|
+
}
|
|
120
|
+
/** Delete registration state after an `invalid_client` response. */
|
|
121
|
+
deleteClientRegistration(issuer) {
|
|
122
|
+
if (!issuer)
|
|
123
|
+
return;
|
|
124
|
+
deleteFile(this.clientFile(issuer));
|
|
125
|
+
}
|
|
126
|
+
tokenFile(clientId) {
|
|
127
|
+
return path.join(this.home, 'tokens', `${safeFileName(clientId)}.json`);
|
|
128
|
+
}
|
|
129
|
+
clientFile(issuer) {
|
|
130
|
+
return path.join(this.home, 'clients', `${hashIssuer(issuer)}-${this.surface}.json`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Return the Java-compatible 32-character SHA-256 issuer file key. */
|
|
134
|
+
export function hashIssuer(issuer) {
|
|
135
|
+
return createHash('sha256').update(issuer || '', 'utf-8').digest('hex').slice(0, 32);
|
|
136
|
+
}
|
|
137
|
+
/** Sanitize an untrusted client ID for use as a file name. */
|
|
138
|
+
function safeFileName(raw) {
|
|
139
|
+
let name = (raw || '').replace(SAFE_CHARS, '_');
|
|
140
|
+
while (name.includes('..')) {
|
|
141
|
+
name = name.replace(/\.\./g, '__');
|
|
142
|
+
}
|
|
143
|
+
return name || '_';
|
|
144
|
+
}
|
|
145
|
+
/** Reject a surface that cannot be used verbatim as part of a file name. */
|
|
146
|
+
function validateSurface(surface) {
|
|
147
|
+
if (typeof surface !== 'string' || !SURFACE_PATTERN.test(surface)) {
|
|
148
|
+
throw new OAuth2Error(OAuth2Category.StorageFailed, `invalid surface '${surface}': expected lowercase alphanumerics and hyphens, e.g. 'cli'`);
|
|
149
|
+
}
|
|
150
|
+
return surface;
|
|
151
|
+
}
|
|
152
|
+
function expandHome(value) {
|
|
153
|
+
if (value === '~')
|
|
154
|
+
return homedir();
|
|
155
|
+
if (value.startsWith('~/') || value.startsWith(`~${path.sep}`)) {
|
|
156
|
+
return path.join(homedir(), value.slice(2));
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
/** @returns true when the file was there and is now gone */
|
|
161
|
+
function deleteFile(file) {
|
|
162
|
+
try {
|
|
163
|
+
if (fs.existsSync(file)) {
|
|
164
|
+
fs.unlinkSync(file);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.error(`[TigerOpen] delete file fail: ${file}`, e);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Atomically replace a file through a same-directory temporary file.
|
|
175
|
+
*
|
|
176
|
+
* Same-filesystem replacement prevents partial JSON writes. Directories and files are
|
|
177
|
+
* restricted to modes 0700 and 0600 where supported.
|
|
178
|
+
*/
|
|
179
|
+
function writeAtomic(file, content) {
|
|
180
|
+
const dir = path.dirname(file);
|
|
181
|
+
try {
|
|
182
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
183
|
+
restrict(dir, 0o700);
|
|
184
|
+
// Same directory, so the rename below stays on one filesystem and is atomic.
|
|
185
|
+
const tmp = path.join(dir, `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
186
|
+
try {
|
|
187
|
+
fs.writeFileSync(tmp, content, { encoding: 'utf-8', mode: 0o600 });
|
|
188
|
+
restrict(tmp, 0o600);
|
|
189
|
+
fs.renameSync(tmp, file);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
try {
|
|
193
|
+
fs.unlinkSync(tmp);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// The temporary file was never created; nothing to clean up.
|
|
197
|
+
}
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
restrict(file, 0o600);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
throw new OAuth2Error(OAuth2Category.StorageFailed, `write file fail: ${file}`, e);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Restrict a credential file or directory where the platform supports it. */
|
|
207
|
+
function restrict(target, mode) {
|
|
208
|
+
try {
|
|
209
|
+
fs.chmodSync(target, mode);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Windows and some network filesystems have no POSIX modes; the write still stands.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted OAuth 2.0 authorization state.
|
|
3
|
+
*
|
|
4
|
+
* The `issuer` and `clientId` bind credentials to an authorization server and client,
|
|
5
|
+
* preventing credential reuse across environments. The on-disk field names are
|
|
6
|
+
* snake_case and shared with the Java and Python SDKs.
|
|
7
|
+
*/
|
|
8
|
+
/** On-disk format version. An unrecognised version is an error, not a guess. */
|
|
9
|
+
export const TOKEN_SCHEMA_VERSION = 1;
|
|
10
|
+
export function currentMillis() {
|
|
11
|
+
return Date.now();
|
|
12
|
+
}
|
|
13
|
+
/** An access token and its metadata. */
|
|
14
|
+
export class OAuth2Token {
|
|
15
|
+
constructor(init = {}) {
|
|
16
|
+
this.schemaVersion = init.schemaVersion ?? TOKEN_SCHEMA_VERSION;
|
|
17
|
+
this.issuer = init.issuer;
|
|
18
|
+
this.clientId = init.clientId;
|
|
19
|
+
this.accessToken = init.accessToken;
|
|
20
|
+
this.refreshToken = init.refreshToken;
|
|
21
|
+
this.tokenType = init.tokenType || 'Bearer';
|
|
22
|
+
this.scope = init.scope;
|
|
23
|
+
this.expiresAt = init.expiresAt ?? 0;
|
|
24
|
+
}
|
|
25
|
+
/** Time until expiry; negative once already expired. */
|
|
26
|
+
remainingMillis() {
|
|
27
|
+
return this.expiresAt - currentMillis();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @param aheadMillis how early to refresh. The window exists because time passes
|
|
31
|
+
* between sending a request and the server validating it, so using a token right up
|
|
32
|
+
* to its expiry produces intermittent 401s
|
|
33
|
+
*/
|
|
34
|
+
needsRefresh(aheadMillis) {
|
|
35
|
+
return this.remainingMillis() <= aheadMillis;
|
|
36
|
+
}
|
|
37
|
+
hasRefreshToken() {
|
|
38
|
+
return !!this.refreshToken;
|
|
39
|
+
}
|
|
40
|
+
toJSON() {
|
|
41
|
+
return {
|
|
42
|
+
schema_version: this.schemaVersion,
|
|
43
|
+
issuer: this.issuer,
|
|
44
|
+
client_id: this.clientId,
|
|
45
|
+
access_token: this.accessToken,
|
|
46
|
+
refresh_token: this.refreshToken,
|
|
47
|
+
token_type: this.tokenType,
|
|
48
|
+
scope: this.scope,
|
|
49
|
+
expires_at: this.expiresAt,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
static fromJSON(data) {
|
|
53
|
+
if (!data)
|
|
54
|
+
return null;
|
|
55
|
+
return new OAuth2Token({
|
|
56
|
+
schemaVersion: data.schema_version ?? TOKEN_SCHEMA_VERSION,
|
|
57
|
+
issuer: data.issuer,
|
|
58
|
+
clientId: data.client_id,
|
|
59
|
+
accessToken: data.access_token,
|
|
60
|
+
refreshToken: data.refresh_token,
|
|
61
|
+
tokenType: data.token_type || 'Bearer',
|
|
62
|
+
scope: data.scope,
|
|
63
|
+
expiresAt: data.expires_at ?? 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Prints no token values. */
|
|
67
|
+
toString() {
|
|
68
|
+
return `OAuth2Token{clientId=${this.clientId}, issuer=${this.issuer}, scope=${this.scope}, `
|
|
69
|
+
+ `expiresAt=${this.expiresAt}, hasRefreshToken=${this.hasRefreshToken()}}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -200,7 +200,7 @@ export const Request_Connect = {
|
|
|
200
200
|
},
|
|
201
201
|
};
|
|
202
202
|
function createBaseRequest_Subscribe() {
|
|
203
|
-
return { dataType: 0, symbols: undefined, account: undefined, market: undefined };
|
|
203
|
+
return { dataType: 0, symbols: undefined, account: undefined, market: undefined, assetQuoteType: undefined };
|
|
204
204
|
}
|
|
205
205
|
export const Request_Subscribe = {
|
|
206
206
|
encode(message, writer = new BinaryWriter()) {
|
|
@@ -216,6 +216,9 @@ export const Request_Subscribe = {
|
|
|
216
216
|
if (message.market !== undefined) {
|
|
217
217
|
writer.uint32(34).string(message.market);
|
|
218
218
|
}
|
|
219
|
+
if (message.assetQuoteType !== undefined) {
|
|
220
|
+
writer.uint32(42).string(message.assetQuoteType);
|
|
221
|
+
}
|
|
219
222
|
return writer;
|
|
220
223
|
},
|
|
221
224
|
decode(input, length) {
|
|
@@ -253,6 +256,13 @@ export const Request_Subscribe = {
|
|
|
253
256
|
message.market = reader.string();
|
|
254
257
|
continue;
|
|
255
258
|
}
|
|
259
|
+
case 5: {
|
|
260
|
+
if (tag !== 42) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
message.assetQuoteType = reader.string();
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
256
266
|
}
|
|
257
267
|
if ((tag & 7) === 4 || tag === 0) {
|
|
258
268
|
break;
|
|
@@ -270,6 +280,7 @@ export const Request_Subscribe = {
|
|
|
270
280
|
message.symbols = object.symbols ?? undefined;
|
|
271
281
|
message.account = object.account ?? undefined;
|
|
272
282
|
message.market = object.market ?? undefined;
|
|
283
|
+
message.assetQuoteType = object.assetQuoteType ?? undefined;
|
|
273
284
|
return message;
|
|
274
285
|
},
|
|
275
286
|
};
|
|
@@ -45,8 +45,11 @@ export function buildHeartBeatMessage() {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* 构建 SUBSCRIBE 请求消息
|
|
48
|
+
*
|
|
49
|
+
* @param assetQuoteType 资产行情类型枚举名(ETH / RTH / OVERNIGHT),
|
|
50
|
+
* 仅对 Asset / Position 两种订阅有意义,其余场景传 undefined
|
|
48
51
|
*/
|
|
49
|
-
export function buildSubscribeMessage(dataType, symbols, account, market) {
|
|
52
|
+
export function buildSubscribeMessage(dataType, symbols, account, market, assetQuoteType) {
|
|
50
53
|
return Request.fromPartial({
|
|
51
54
|
command: SocketCommon_Command.SUBSCRIBE,
|
|
52
55
|
id: nextRequestId(),
|
|
@@ -55,6 +58,7 @@ export function buildSubscribeMessage(dataType, symbols, account, market) {
|
|
|
55
58
|
symbols,
|
|
56
59
|
account,
|
|
57
60
|
market,
|
|
61
|
+
assetQuoteType,
|
|
58
62
|
}),
|
|
59
63
|
});
|
|
60
64
|
}
|
|
@@ -59,6 +59,8 @@ export class PushClient {
|
|
|
59
59
|
this.subscriptions = new Map();
|
|
60
60
|
/** Account-level subscriptions: subject -> account string used at subscribe time */
|
|
61
61
|
this.accountSubs = new Map();
|
|
62
|
+
/** Account-level subscriptions: subject -> assetQuoteType used at subscribe time (reconnect replay) */
|
|
63
|
+
this.accountSubQuoteTypes = new Map();
|
|
62
64
|
this.heartbeatTimer = null;
|
|
63
65
|
this.reconnectTimer = null;
|
|
64
66
|
this.stopped = false;
|
|
@@ -319,7 +321,7 @@ export class PushClient {
|
|
|
319
321
|
this.doSubscribe(subject, Array.from(symbols));
|
|
320
322
|
}
|
|
321
323
|
for (const [subject, account] of this.accountSubs) {
|
|
322
|
-
this.doSubscribe(subject, undefined, account);
|
|
324
|
+
this.doSubscribe(subject, undefined, account, undefined, this.accountSubQuoteTypes.get(subject));
|
|
323
325
|
}
|
|
324
326
|
}
|
|
325
327
|
/** Dispatch push data to the appropriate callback based on dataType */
|
|
@@ -389,10 +391,10 @@ export class PushClient {
|
|
|
389
391
|
}
|
|
390
392
|
}
|
|
391
393
|
/** Internal subscribe method */
|
|
392
|
-
doSubscribe(subject, symbols, account, market) {
|
|
394
|
+
doSubscribe(subject, symbols, account, market, assetQuoteType) {
|
|
393
395
|
const dataType = subjectToDataType(subject);
|
|
394
396
|
const symbolsStr = symbols ? symbols.join(',') : undefined;
|
|
395
|
-
const req = buildSubscribeMessage(dataType, symbolsStr, account, market);
|
|
397
|
+
const req = buildSubscribeMessage(dataType, symbolsStr, account, market, assetQuoteType);
|
|
396
398
|
this.sendMessage(req);
|
|
397
399
|
}
|
|
398
400
|
/** Internal unsubscribe method */
|
|
@@ -552,27 +554,41 @@ export class PushClient {
|
|
|
552
554
|
this.removeSubscription(SubjectType.Market, [market]);
|
|
553
555
|
}
|
|
554
556
|
// ===== Account push subscribe/unsubscribe =====
|
|
555
|
-
/**
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Shared implementation for the account/options overloads of
|
|
559
|
+
* subscribeAsset / subscribePosition.
|
|
560
|
+
*/
|
|
561
|
+
subscribeAccount(subject, accountOrOptions) {
|
|
562
|
+
const opts = typeof accountOrOptions === 'string'
|
|
563
|
+
? { account: accountOrOptions }
|
|
564
|
+
: (accountOrOptions ?? {});
|
|
565
|
+
const acct = opts.account || this.config.account;
|
|
566
|
+
this.doSubscribe(subject, undefined, acct, undefined, opts.assetQuoteType);
|
|
567
|
+
this.accountSubs.set(subject, acct);
|
|
568
|
+
if (opts.assetQuoteType) {
|
|
569
|
+
this.accountSubQuoteTypes.set(subject, opts.assetQuoteType);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
this.accountSubQuoteTypes.delete(subject);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
subscribeAsset(accountOrOptions) {
|
|
576
|
+
this.subscribeAccount(SubjectType.Asset, accountOrOptions);
|
|
560
577
|
}
|
|
561
578
|
/** Unsubscribe from asset changes */
|
|
562
579
|
unsubscribeAsset() {
|
|
563
580
|
this.doUnsubscribe(SubjectType.Asset);
|
|
564
581
|
this.accountSubs.delete(SubjectType.Asset);
|
|
582
|
+
this.accountSubQuoteTypes.delete(SubjectType.Asset);
|
|
565
583
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const acct = account || this.config.account;
|
|
569
|
-
this.doSubscribe(SubjectType.Position, undefined, acct);
|
|
570
|
-
this.accountSubs.set(SubjectType.Position, acct);
|
|
584
|
+
subscribePosition(accountOrOptions) {
|
|
585
|
+
this.subscribeAccount(SubjectType.Position, accountOrOptions);
|
|
571
586
|
}
|
|
572
587
|
/** Unsubscribe from position changes */
|
|
573
588
|
unsubscribePosition() {
|
|
574
589
|
this.doUnsubscribe(SubjectType.Position);
|
|
575
590
|
this.accountSubs.delete(SubjectType.Position);
|
|
591
|
+
this.accountSubQuoteTypes.delete(SubjectType.Position);
|
|
576
592
|
}
|
|
577
593
|
/** Subscribe to order status */
|
|
578
594
|
subscribeOrder(account) {
|
|
@@ -221,11 +221,17 @@ export class TradeClient {
|
|
|
221
221
|
async getPositions(req) {
|
|
222
222
|
return this.callIntoItems('positions', withAccount(req, this.account));
|
|
223
223
|
}
|
|
224
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* Query assets. v0.4.0: supports sub-account list and aggregation options.
|
|
226
|
+
* `assetQuoteType`(ETH / RTH / OVERNIGHT) 选择资产行情口径。
|
|
227
|
+
*/
|
|
225
228
|
async getAssets(req) {
|
|
226
229
|
return this.callIntoItems('assets', withAccount(req, this.account));
|
|
227
230
|
}
|
|
228
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Query prime assets. v0.4.0: takes AssetsRequest for filters.
|
|
233
|
+
* `assetQuoteType`(ETH / RTH / OVERNIGHT) 选择资产行情口径。
|
|
234
|
+
*/
|
|
229
235
|
async getPrimeAssets(req) {
|
|
230
236
|
return this.callInto('prime_assets', withAccount(req, this.account));
|
|
231
237
|
}
|
|
@@ -246,7 +252,10 @@ export class TradeClient {
|
|
|
246
252
|
async getAnalyticsAsset(req) {
|
|
247
253
|
return this.callIntoItems('analytics_asset', withAccount(req, this.account));
|
|
248
254
|
}
|
|
249
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Aggregate assets (prime) by base currency.
|
|
257
|
+
* `assetQuoteType`(ETH / RTH / OVERNIGHT) 选择资产行情口径。
|
|
258
|
+
*/
|
|
250
259
|
async getAggregateAssets(req) {
|
|
251
260
|
return this.callInto('aggregate_assets', withAccount(req, this.account));
|
|
252
261
|
}
|
package/dist/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '0.
|
|
1
|
+
export const SDK_VERSION = '0.5.7';
|
|
@@ -0,0 +1,98 @@
|
|
|
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 declare enum AuthenticationType {
|
|
10
|
+
/** tigerId + RSA private key signature */
|
|
11
|
+
Signature = "SIGNATURE",
|
|
12
|
+
/** `Authorization: Bearer <access_token>` */
|
|
13
|
+
OAuth2 = "OAUTH2"
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Per-request authentication context.
|
|
17
|
+
*
|
|
18
|
+
* Business parameters (`method`, `version`, `biz_content`, `timestamp`) are filled in by
|
|
19
|
+
* the client and must not be modified by an authentication implementation.
|
|
20
|
+
*/
|
|
21
|
+
export declare class RequestAuthContext {
|
|
22
|
+
/** Request body parameters. Read-only as far as authentication is concerned. */
|
|
23
|
+
readonly params: Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a request may be retried with a new credential after a 401.
|
|
26
|
+
*
|
|
27
|
+
* Order placement, modification, and cancellation are not retryable because a 401
|
|
28
|
+
* does not prove that the operation was not executed.
|
|
29
|
+
*/
|
|
30
|
+
readonly retryable: boolean;
|
|
31
|
+
/** Full `Authorization` header value; undefined means do not send the header. */
|
|
32
|
+
authorizationHeader?: string;
|
|
33
|
+
constructor(params: Record<string, string>, retryable?: boolean);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Credential snapshot used to coordinate authentication retries.
|
|
37
|
+
*
|
|
38
|
+
* Comparing the failed credential with current state detects rotation completed
|
|
39
|
+
* elsewhere and avoids duplicate refresh requests.
|
|
40
|
+
*/
|
|
41
|
+
export declare class AuthenticationAttempt {
|
|
42
|
+
readonly authType: AuthenticationType;
|
|
43
|
+
/**
|
|
44
|
+
* Credential used by the request, including a raw OAuth 2.0 access token.
|
|
45
|
+
*
|
|
46
|
+
* This value is restricted to equality comparisons and must not be logged or exposed.
|
|
47
|
+
*/
|
|
48
|
+
readonly credential: string | undefined;
|
|
49
|
+
constructor(authType: AuthenticationType, credential: string | undefined);
|
|
50
|
+
/** Never prints the credential value. */
|
|
51
|
+
toString(): string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Authentication retry decision.
|
|
55
|
+
*
|
|
56
|
+
* A retry always supplies a new full `Authorization` header value, including the
|
|
57
|
+
* `Bearer ` prefix. There is deliberately no "retry with the same credential" option:
|
|
58
|
+
* that only collects the same 401 twice.
|
|
59
|
+
*/
|
|
60
|
+
export declare class RetryDecision {
|
|
61
|
+
private static readonly NO_RETRY;
|
|
62
|
+
readonly shouldRetry: boolean;
|
|
63
|
+
readonly authorizationHeader: string | undefined;
|
|
64
|
+
private constructor();
|
|
65
|
+
static noRetry(): RetryDecision;
|
|
66
|
+
/** @param authorizationHeader full new header value, including `Bearer ` */
|
|
67
|
+
static retry(authorizationHeader: string): RetryDecision;
|
|
68
|
+
/** Never prints the header value. */
|
|
69
|
+
toString(): string;
|
|
70
|
+
}
|
|
71
|
+
/** Authentication interface. */
|
|
72
|
+
export interface Authentication {
|
|
73
|
+
/**
|
|
74
|
+
* Which kind of authentication. For logging and assertions; business logic should not
|
|
75
|
+
* branch on it.
|
|
76
|
+
*/
|
|
77
|
+
readonly authType: AuthenticationType;
|
|
78
|
+
/**
|
|
79
|
+
* Write the credential into the request and return a snapshot of which credential was
|
|
80
|
+
* used.
|
|
81
|
+
*
|
|
82
|
+
* The OAuth2 implementation may refresh here, so this may reject with an
|
|
83
|
+
* `OAuth2Error`.
|
|
84
|
+
*
|
|
85
|
+
* @returns snapshot, passed back to {@link onUnauthorized}
|
|
86
|
+
*/
|
|
87
|
+
apply(context: RequestAuthContext): Promise<AuthenticationAttempt>;
|
|
88
|
+
/**
|
|
89
|
+
* Return the retry decision for an unauthorized response.
|
|
90
|
+
*
|
|
91
|
+
* Refresh applies only to HTTP 401 credential failures, never to HTTP 403, server
|
|
92
|
+
* errors, or timeouts. Authentication retries occur at most once per request and
|
|
93
|
+
* require a different credential.
|
|
94
|
+
*/
|
|
95
|
+
onUnauthorized(attempt: AuthenticationAttempt | undefined, statusCode: number): Promise<RetryDecision>;
|
|
96
|
+
/** Release resources. Must be called when switching authentication method. */
|
|
97
|
+
close(): void;
|
|
98
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Authentication layer: the request-level data plane plus the OAuth 2.0 control plane. */
|
|
2
|
+
export { AuthenticationAttempt, AuthenticationType, RequestAuthContext, RetryDecision, } from './authentication';
|
|
3
|
+
export type { Authentication } from './authentication';
|
|
4
|
+
export * from './oauth2';
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/** Raw on-disk shape, shared with the Java and Python SDKs. */
|
|
9
|
+
export interface OAuth2ClientRegistrationJson {
|
|
10
|
+
issuer?: string;
|
|
11
|
+
client_id?: string;
|
|
12
|
+
client_name?: string;
|
|
13
|
+
registered_scopes?: string;
|
|
14
|
+
client_id_issued_at?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class OAuth2ClientRegistration {
|
|
17
|
+
issuer?: string;
|
|
18
|
+
clientId?: string;
|
|
19
|
+
clientName?: string;
|
|
20
|
+
registeredScopes?: string;
|
|
21
|
+
clientIdIssuedAt: number;
|
|
22
|
+
constructor(init?: {
|
|
23
|
+
issuer?: string;
|
|
24
|
+
clientId?: string;
|
|
25
|
+
clientName?: string;
|
|
26
|
+
registeredScopes?: string;
|
|
27
|
+
clientIdIssuedAt?: number;
|
|
28
|
+
});
|
|
29
|
+
toJSON(): OAuth2ClientRegistrationJson;
|
|
30
|
+
static fromJSON(data: OAuth2ClientRegistrationJson | null | undefined): OAuth2ClientRegistration | null;
|
|
31
|
+
toString(): string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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 declare const DEFAULT_DEVICE_INTERVAL_SECONDS = 5;
|
|
7
|
+
/** Raw shape of the device authorization response. */
|
|
8
|
+
export interface OAuth2DeviceAuthorizationJson {
|
|
9
|
+
device_code?: string;
|
|
10
|
+
user_code?: string;
|
|
11
|
+
verification_uri?: string;
|
|
12
|
+
verification_uri_complete?: string;
|
|
13
|
+
expires_in?: number;
|
|
14
|
+
interval?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class OAuth2DeviceAuthorization {
|
|
17
|
+
/** Credential used to redeem the token; it must not be displayed or logged. */
|
|
18
|
+
deviceCode?: string;
|
|
19
|
+
/** Short code presented to the resource owner. */
|
|
20
|
+
userCode?: string;
|
|
21
|
+
/** Verification URI presented to the resource owner. */
|
|
22
|
+
verificationUri?: string;
|
|
23
|
+
/** Verification URI with the user code included. */
|
|
24
|
+
verificationUriComplete?: string;
|
|
25
|
+
expiresIn: number;
|
|
26
|
+
/** Polling interval in seconds; 0 means the server sent none */
|
|
27
|
+
interval: number;
|
|
28
|
+
constructor(init?: {
|
|
29
|
+
deviceCode?: string;
|
|
30
|
+
userCode?: string;
|
|
31
|
+
verificationUri?: string;
|
|
32
|
+
verificationUriComplete?: string;
|
|
33
|
+
expiresIn?: number;
|
|
34
|
+
interval?: number;
|
|
35
|
+
});
|
|
36
|
+
effectiveIntervalSeconds(): number;
|
|
37
|
+
static fromJSON(data: OAuth2DeviceAuthorizationJson | null | undefined): OAuth2DeviceAuthorization | null;
|
|
38
|
+
/** Prints no deviceCode. */
|
|
39
|
+
toString(): string;
|
|
40
|
+
}
|