halo-infinite-api 1.2.3 → 2.1.0
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 +1 -1
- package/dist/authentication/halo-authentication-client.d.ts +4 -5
- package/dist/authentication/halo-authentication-client.js +23 -69
- package/dist/authentication/halo-authentication-client.js.map +1 -1
- package/dist/authentication/xbox-authentication-client.d.ts +23 -10
- package/dist/authentication/xbox-authentication-client.js +92 -64
- package/dist/authentication/xbox-authentication-client.js.map +1 -1
- package/dist/core/halo-infinite-client.d.ts +9 -8
- package/dist/core/halo-infinite-client.js +20 -35
- package/dist/core/halo-infinite-client.js.map +1 -1
- package/dist/core/spartan-token-fetchers/auto-xsts-sartan-token-provider.d.ts +11 -0
- package/dist/core/spartan-token-fetchers/auto-xsts-sartan-token-provider.js +43 -0
- package/dist/core/spartan-token-fetchers/auto-xsts-sartan-token-provider.js.map +1 -0
- package/dist/core/spartan-token-fetchers/index.d.ts +3 -0
- package/dist/core/spartan-token-fetchers/index.js +2 -0
- package/dist/core/spartan-token-fetchers/index.js.map +1 -0
- package/dist/core/spartan-token-fetchers/static-xsts-ticket-token-spartan-token-provider.d.ts +12 -0
- package/dist/core/spartan-token-fetchers/static-xsts-ticket-token-spartan-token-provider.js +26 -0
- package/dist/core/spartan-token-fetchers/static-xsts-ticket-token-spartan-token-provider.js.map +1 -0
- package/dist/core/spartan-token-providers/auto-xsts-spartan-token-provider.d.ts +11 -0
- package/dist/core/spartan-token-providers/auto-xsts-spartan-token-provider.js +31 -0
- package/dist/core/spartan-token-providers/auto-xsts-spartan-token-provider.js.map +1 -0
- package/dist/core/spartan-token-providers/index.d.ts +3 -0
- package/dist/core/spartan-token-providers/index.js +2 -0
- package/dist/core/spartan-token-providers/index.js.map +1 -0
- package/dist/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.d.ts +12 -0
- package/dist/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.js +25 -0
- package/dist/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.js.map +1 -0
- package/dist/core/token-persister.d.ts +4 -0
- package/dist/core/token-persister.js +2 -0
- package/dist/core/token-persister.js.map +1 -0
- package/dist/core/token-persisters/in-memory-token-persister.d.ts +2 -0
- package/dist/core/token-persisters/in-memory-token-persister.js +10 -0
- package/dist/core/token-persisters/in-memory-token-persister.js.map +1 -0
- package/dist/core/token-persisters/index.d.ts +4 -0
- package/dist/core/token-persisters/index.js +2 -0
- package/dist/core/token-persisters/index.js.map +1 -0
- package/dist/core/token-persisters/local-storage-token-persister.d.ts +2 -0
- package/dist/core/token-persisters/local-storage-token-persister.js +15 -0
- package/dist/core/token-persisters/local-storage-token-persister.js.map +1 -0
- package/dist/core/token-persisters/node-fs-token-persister.d.ts +2 -0
- package/dist/core/token-persisters/node-fs-token-persister.js +24 -0
- package/dist/core/token-persisters/node-fs-token-persister.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/util/expiry-token-cache.d.ts +9 -0
- package/dist/util/expiry-token-cache.js +48 -0
- package/dist/util/expiry-token-cache.js.map +1 -0
- package/dist/util/resolvable-promise.d.ts +8 -0
- package/dist/util/resolvable-promise.js +31 -0
- package/dist/util/resolvable-promise.js.map +1 -0
- package/package.json +3 -2
- package/src/authentication/halo-authentication-client.ts +30 -72
- package/src/authentication/xbox-authentication-client.ts +120 -83
- package/src/core/halo-infinite-client.ts +30 -66
- package/src/core/spartan-token-providers/auto-xsts-spartan-token-provider.ts +54 -0
- package/src/core/spartan-token-providers/index.ts +3 -0
- package/src/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.ts +35 -0
- package/src/core/token-persisters/in-memory-token-persister.ts +12 -0
- package/src/core/token-persisters/index.ts +4 -0
- package/src/core/token-persisters/local-storage-token-persister.ts +15 -0
- package/src/core/token-persisters/node-fs-token-persister.ts +23 -0
- package/src/index.ts +8 -0
- package/src/util/expiry-token-cache.ts +51 -0
- package/src/util/resolvable-promise.ts +32 -0
|
@@ -3,6 +3,9 @@ import pkceChallenge from "pkce-challenge";
|
|
|
3
3
|
import { DateTime } from "luxon";
|
|
4
4
|
import { XboxTicket } from "../models/xbox-ticket";
|
|
5
5
|
import { coalesceDateTime } from "../util/date-time";
|
|
6
|
+
import { ResolvablePromise } from "../util/resolvable-promise";
|
|
7
|
+
import { ExpiryTokenCache } from "../util/expiry-token-cache";
|
|
8
|
+
import { TokenPersister } from "../core/token-persisters";
|
|
6
9
|
|
|
7
10
|
const SCOPES = ["Xboxlive.signin", "Xboxlive.offline_access"];
|
|
8
11
|
|
|
@@ -18,75 +21,146 @@ export interface XboxAuthenticationToken {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export class XboxAuthenticationClient {
|
|
21
|
-
private
|
|
22
|
-
|
|
24
|
+
private accessTokenPromise:
|
|
25
|
+
| ResolvablePromise<XboxAuthenticationToken>
|
|
26
|
+
| undefined = undefined;
|
|
27
|
+
private userTokenCache = new ExpiryTokenCache(async (accessToken: string) => {
|
|
28
|
+
const persistedToken = await this.tokenPersister?.load<
|
|
29
|
+
XboxTicket & { expiresAt?: unknown }
|
|
30
|
+
>("xbox.userToken");
|
|
31
|
+
|
|
32
|
+
if (persistedToken?.expiresAt) {
|
|
33
|
+
const expiresAt = coalesceDateTime(persistedToken.expiresAt);
|
|
34
|
+
if (expiresAt && expiresAt > DateTime.now()) {
|
|
35
|
+
return { ...persistedToken, expiresAt };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const response = await this.httpClient.post<XboxTicket>(
|
|
40
|
+
"https://user.auth.xboxlive.com/user/authenticate",
|
|
41
|
+
{
|
|
42
|
+
RelyingParty: "http://auth.xboxlive.com",
|
|
43
|
+
TokenType: "JWT",
|
|
44
|
+
Properties: {
|
|
45
|
+
AuthMethod: "RPS",
|
|
46
|
+
SiteName: "user.auth.xboxlive.com",
|
|
47
|
+
RpsTicket: `d=${accessToken}`,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
Accept: "application/json",
|
|
54
|
+
"x-xbl-contract-version": "1",
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const result = {
|
|
60
|
+
...response.data,
|
|
61
|
+
expiresAt: DateTime.fromISO(response.data.NotAfter),
|
|
62
|
+
};
|
|
63
|
+
await this.tokenPersister?.save("xbox.userToken", result);
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
66
|
+
private xstsTicketCache = new ExpiryTokenCache(
|
|
67
|
+
async (userToken: string, relyingParty: RelyingParty) => {
|
|
68
|
+
const persistedToken = await this.tokenPersister?.load<
|
|
69
|
+
XboxTicket & { expiresAt: DateTime }
|
|
70
|
+
>("xbox.xstsTicket");
|
|
71
|
+
|
|
72
|
+
if (persistedToken?.expiresAt) {
|
|
73
|
+
const expiresAt = coalesceDateTime(persistedToken.expiresAt);
|
|
74
|
+
if (expiresAt && expiresAt > DateTime.now()) {
|
|
75
|
+
return { ...persistedToken, expiresAt };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const response = await this.httpClient.post<XboxTicket>(
|
|
80
|
+
"https://xsts.auth.xboxlive.com/xsts/authorize",
|
|
81
|
+
{
|
|
82
|
+
RelyingParty: relyingParty,
|
|
83
|
+
TokenType: "JWT",
|
|
84
|
+
Properties: {
|
|
85
|
+
SandboxId: "RETAIL",
|
|
86
|
+
UserTokens: [userToken],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
Accept: "application/json",
|
|
93
|
+
"x-xbl-contract-version": "1",
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const result = {
|
|
99
|
+
...response.data,
|
|
100
|
+
expiresAt: DateTime.fromISO(response.data.NotAfter),
|
|
101
|
+
};
|
|
102
|
+
await this.tokenPersister?.save("xbox.xstsTicket", result);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
23
107
|
private readonly httpClient: AxiosInstance;
|
|
24
108
|
|
|
25
109
|
constructor(
|
|
26
110
|
private readonly clientId: string,
|
|
27
111
|
private readonly redirectUri: string,
|
|
28
112
|
private readonly getAuthCode: (authorizeUrl: string) => Promise<string>,
|
|
29
|
-
private readonly
|
|
30
|
-
token?: string;
|
|
31
|
-
expiresAt?: unknown;
|
|
32
|
-
refreshToken?: string;
|
|
33
|
-
} | null>,
|
|
34
|
-
private readonly saveToken: (
|
|
35
|
-
token: XboxAuthenticationToken
|
|
36
|
-
) => Promise<void>
|
|
113
|
+
private readonly tokenPersister?: TokenPersister
|
|
37
114
|
) {
|
|
38
115
|
this.httpClient = axios.create();
|
|
39
116
|
}
|
|
40
117
|
|
|
41
118
|
private getPkce() {
|
|
42
|
-
|
|
119
|
+
// Some sort of module issue here, we work around it
|
|
120
|
+
type PkceChallenge = typeof pkceChallenge;
|
|
121
|
+
if (typeof pkceChallenge === "function") {
|
|
122
|
+
return pkceChallenge(43);
|
|
123
|
+
} else {
|
|
124
|
+
return (pkceChallenge as { default: PkceChallenge }).default(43);
|
|
125
|
+
}
|
|
43
126
|
}
|
|
44
127
|
|
|
45
128
|
public async getAccessToken() {
|
|
46
|
-
if (this.
|
|
129
|
+
if (this.accessTokenPromise) {
|
|
47
130
|
// Someone either already has a token or is in the process of getting one
|
|
48
131
|
// Wait for them to finish, then check for validity
|
|
49
|
-
const currentToken = await this.
|
|
132
|
+
const currentToken = await this.accessTokenPromise;
|
|
50
133
|
|
|
51
134
|
if (currentToken.expiresAt > DateTime.now()) {
|
|
52
135
|
// Current token is valid, return it
|
|
53
136
|
return currentToken.token;
|
|
54
137
|
} else {
|
|
55
138
|
// Current token expired, start a new promise
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.currentTokenPromise = new Promise<XboxAuthenticationToken>(
|
|
59
|
-
(resolve, reject) => {
|
|
60
|
-
promiseResolver = resolve;
|
|
61
|
-
promiseRejector = reject;
|
|
62
|
-
}
|
|
63
|
-
);
|
|
139
|
+
this.accessTokenPromise =
|
|
140
|
+
new ResolvablePromise<XboxAuthenticationToken>();
|
|
64
141
|
|
|
65
142
|
try {
|
|
66
143
|
const newToken = await this.refreshOAuth2Token(
|
|
67
144
|
currentToken.refreshToken
|
|
68
145
|
);
|
|
69
|
-
|
|
70
|
-
await this.
|
|
146
|
+
this.accessTokenPromise.resolve(newToken);
|
|
147
|
+
await this.tokenPersister?.save("xbox.accessToken", newToken);
|
|
71
148
|
return newToken.token;
|
|
72
149
|
} catch (e) {
|
|
73
|
-
|
|
150
|
+
this.accessTokenPromise.reject(e);
|
|
74
151
|
throw e;
|
|
75
152
|
}
|
|
76
153
|
}
|
|
77
154
|
} else {
|
|
78
155
|
// We are the first caller, create a promise to block subsequent callers
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.currentTokenPromise = new Promise<XboxAuthenticationToken>(
|
|
82
|
-
(resolve, reject) => {
|
|
83
|
-
promiseResolver = resolve;
|
|
84
|
-
promiseRejector = reject;
|
|
85
|
-
}
|
|
86
|
-
);
|
|
156
|
+
this.accessTokenPromise =
|
|
157
|
+
new ResolvablePromise<XboxAuthenticationToken>();
|
|
87
158
|
|
|
88
159
|
try {
|
|
89
|
-
const loadedToken =
|
|
160
|
+
const loadedToken =
|
|
161
|
+
await this.tokenPersister?.load<XboxAuthenticationToken>(
|
|
162
|
+
"xbox.accessToken"
|
|
163
|
+
);
|
|
90
164
|
const currentToken = {
|
|
91
165
|
...loadedToken,
|
|
92
166
|
token: loadedToken?.token ?? "",
|
|
@@ -95,23 +169,25 @@ export class XboxAuthenticationClient {
|
|
|
95
169
|
|
|
96
170
|
if (currentToken.expiresAt && currentToken.expiresAt > DateTime.now()) {
|
|
97
171
|
// Current token is valid, return it and alert other callers if applicable
|
|
98
|
-
|
|
172
|
+
this.accessTokenPromise.resolve(
|
|
173
|
+
currentToken as XboxAuthenticationToken
|
|
174
|
+
);
|
|
99
175
|
return currentToken.token;
|
|
100
176
|
} else {
|
|
101
177
|
const newToken = await this.fetchOauth2Token();
|
|
102
|
-
|
|
103
|
-
await this.
|
|
178
|
+
this.accessTokenPromise.resolve(newToken);
|
|
179
|
+
await this.tokenPersister?.save("xbox.accessToken", newToken);
|
|
104
180
|
return newToken.token;
|
|
105
181
|
}
|
|
106
182
|
} catch (e) {
|
|
107
|
-
|
|
183
|
+
this.accessTokenPromise.reject(e);
|
|
108
184
|
throw e;
|
|
109
185
|
}
|
|
110
186
|
}
|
|
111
187
|
}
|
|
112
188
|
|
|
113
189
|
private async fetchOauth2Token(): Promise<XboxAuthenticationToken> {
|
|
114
|
-
const { code_verifier, code_challenge } =
|
|
190
|
+
const { code_verifier, code_challenge } = this.getPkce();
|
|
115
191
|
|
|
116
192
|
const authorizeUrl = `https://login.live.com/oauth20_authorize.srf?${new URLSearchParams(
|
|
117
193
|
{
|
|
@@ -160,6 +236,7 @@ export class XboxAuthenticationClient {
|
|
|
160
236
|
private async refreshOAuth2Token(
|
|
161
237
|
refreshToken: string
|
|
162
238
|
): Promise<XboxAuthenticationToken> {
|
|
239
|
+
const requestStart = DateTime.now();
|
|
163
240
|
const response = await this.httpClient.post<{
|
|
164
241
|
access_token: string;
|
|
165
242
|
expires_in: number;
|
|
@@ -180,60 +257,20 @@ export class XboxAuthenticationClient {
|
|
|
180
257
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
181
258
|
}
|
|
182
259
|
);
|
|
183
|
-
|
|
184
|
-
const responseDate = DateTime.fromRFC2822(response.headers["date"]);
|
|
185
260
|
return {
|
|
186
261
|
token: response.data.access_token,
|
|
187
|
-
expiresAt:
|
|
262
|
+
expiresAt: requestStart.plus({ seconds: response.data.expires_in }),
|
|
188
263
|
refreshToken: response.data.refresh_token,
|
|
189
264
|
};
|
|
190
265
|
}
|
|
191
266
|
|
|
192
267
|
public async getUserToken(accessToken: string) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
{
|
|
196
|
-
RelyingParty: "http://auth.xboxlive.com",
|
|
197
|
-
TokenType: "JWT",
|
|
198
|
-
Properties: {
|
|
199
|
-
AuthMethod: "RPS",
|
|
200
|
-
SiteName: "user.auth.xboxlive.com",
|
|
201
|
-
RpsTicket: `d=${accessToken}`,
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
headers: {
|
|
206
|
-
"Content-Type": "application/json",
|
|
207
|
-
Accept: "application/json",
|
|
208
|
-
"x-xbl-contract-version": "1",
|
|
209
|
-
},
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
return response.data.Token;
|
|
268
|
+
const { Token } = await this.userTokenCache.getToken(accessToken);
|
|
269
|
+
return Token;
|
|
214
270
|
}
|
|
215
271
|
|
|
216
|
-
public
|
|
217
|
-
|
|
218
|
-
"https://xsts.auth.xboxlive.com/xsts/authorize",
|
|
219
|
-
{
|
|
220
|
-
RelyingParty: relyingParty,
|
|
221
|
-
TokenType: "JWT",
|
|
222
|
-
Properties: {
|
|
223
|
-
SandboxId: "RETAIL",
|
|
224
|
-
UserTokens: [userToken],
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
headers: {
|
|
229
|
-
"Content-Type": "application/json",
|
|
230
|
-
Accept: "application/json",
|
|
231
|
-
"x-xbl-contract-version": "1",
|
|
232
|
-
},
|
|
233
|
-
}
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
return response.data;
|
|
272
|
+
public getXstsTicket(userToken: string, relyingParty: RelyingParty) {
|
|
273
|
+
return this.xstsTicketCache.getToken(userToken, relyingParty);
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
public getXboxLiveV3Token = (userHash: string, userToken: string) =>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import axios, { AxiosError, AxiosHeaders, Method } from "axios";
|
|
2
|
-
import { HaloAuthenticationClient } from "../authentication/halo-authentication-client";
|
|
3
|
-
import {
|
|
4
|
-
RelyingParty,
|
|
5
|
-
XboxAuthenticationClient,
|
|
6
|
-
} from "../authentication/xbox-authentication-client";
|
|
7
2
|
import { HaloCoreEndpoints } from "../endpoints/halo-core-endpoints";
|
|
3
|
+
import {
|
|
4
|
+
MapAsset,
|
|
5
|
+
PlaylistAsset,
|
|
6
|
+
UgcGameVariantAsset,
|
|
7
|
+
} from "../models/halo-infinite/asset";
|
|
8
|
+
import { AssetKind } from "../models/halo-infinite/asset-kind";
|
|
8
9
|
import { MatchSkill } from "../models/halo-infinite/match-skill";
|
|
9
10
|
import { MatchStats } from "../models/halo-infinite/match-stats";
|
|
10
11
|
import { MatchType } from "../models/halo-infinite/match-type";
|
|
@@ -14,12 +15,7 @@ import { PlaylistCsrContainer } from "../models/halo-infinite/playlist-csr-conta
|
|
|
14
15
|
import { ServiceRecord } from "../models/halo-infinite/service-record";
|
|
15
16
|
import { UserInfo } from "../models/halo-infinite/user-info";
|
|
16
17
|
import { GlobalConstants } from "../util/global-contants";
|
|
17
|
-
import {
|
|
18
|
-
MapAsset,
|
|
19
|
-
PlaylistAsset,
|
|
20
|
-
UgcGameVariantAsset,
|
|
21
|
-
} from "../models/halo-infinite/asset";
|
|
22
|
-
import { AssetKind } from "../models/halo-infinite/asset-kind";
|
|
18
|
+
import { SpartanTokenProvider } from "./spartan-token-providers";
|
|
23
19
|
|
|
24
20
|
interface ResultContainer<TValue> {
|
|
25
21
|
Id: string;
|
|
@@ -38,11 +34,6 @@ interface PaginationContainer<TValue> {
|
|
|
38
34
|
Results: TValue[];
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
interface TokenPersister {
|
|
42
|
-
load: <T>(tokenName: string) => Promise<T> | T;
|
|
43
|
-
save: (tokenName: string, token: unknown) => Promise<void> | void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
37
|
export type AssetKindTypeMap = {
|
|
47
38
|
[AssetKind.Map]: MapAsset;
|
|
48
39
|
[AssetKind.UgcGameVariant]: UgcGameVariantAsset;
|
|
@@ -66,56 +57,17 @@ function wrapPlayerId(playerId: string) {
|
|
|
66
57
|
}
|
|
67
58
|
}
|
|
68
59
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
getAuthCode: (authorizeUrl: string) => Promise<string>,
|
|
76
|
-
tokenPersister?: TokenPersister
|
|
77
|
-
) {
|
|
78
|
-
const xboxAuthClient = new XboxAuthenticationClient(
|
|
79
|
-
clientId,
|
|
80
|
-
redirectUri,
|
|
81
|
-
getAuthCode,
|
|
82
|
-
async () => {
|
|
83
|
-
if (tokenPersister) {
|
|
84
|
-
return await tokenPersister.load("xbox.authToken");
|
|
85
|
-
} else {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
async (token) => {
|
|
90
|
-
if (tokenPersister) {
|
|
91
|
-
await tokenPersister.save("xbox.authToken", token);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
this.haloAuthClient = new HaloAuthenticationClient(
|
|
96
|
-
async () => {
|
|
97
|
-
const accessToken = await xboxAuthClient.getAccessToken();
|
|
98
|
-
const userToken = await xboxAuthClient.getUserToken(accessToken);
|
|
99
|
-
const xstsTicket = await xboxAuthClient.getXstsTicket(
|
|
100
|
-
userToken,
|
|
101
|
-
RelyingParty.Halo
|
|
102
|
-
);
|
|
103
|
-
return xstsTicket.Token;
|
|
104
|
-
},
|
|
105
|
-
async () => {
|
|
106
|
-
if (tokenPersister) {
|
|
107
|
-
return await tokenPersister.load("halo.authToken");
|
|
108
|
-
} else {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
async (token) => {
|
|
113
|
-
if (tokenPersister) {
|
|
114
|
-
await tokenPersister.save("halo.authToken", token);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
);
|
|
60
|
+
function unwrapPlayerId(playerId: string) {
|
|
61
|
+
const match = /^\w+\((\d+)\)$/.exec(playerId);
|
|
62
|
+
if (match) {
|
|
63
|
+
return match[1];
|
|
64
|
+
} else {
|
|
65
|
+
return playerId;
|
|
118
66
|
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class HaloInfiniteClient {
|
|
70
|
+
constructor(private spartanTokenProvider: SpartanTokenProvider) {}
|
|
119
71
|
|
|
120
72
|
private async executeRequest<T>(
|
|
121
73
|
url: string,
|
|
@@ -132,7 +84,7 @@ export class HaloInfiniteClient {
|
|
|
132
84
|
if (useSpartanToken) {
|
|
133
85
|
headers.set(
|
|
134
86
|
"x-343-authorization-spartan",
|
|
135
|
-
await this.
|
|
87
|
+
await this.spartanTokenProvider.getSpartanToken()
|
|
136
88
|
);
|
|
137
89
|
}
|
|
138
90
|
|
|
@@ -199,6 +151,18 @@ export class HaloInfiniteClient {
|
|
|
199
151
|
"get"
|
|
200
152
|
);
|
|
201
153
|
|
|
154
|
+
/** Get gamertag info for several players.
|
|
155
|
+
* @param xuids - Xuids to lookup.
|
|
156
|
+
*/
|
|
157
|
+
public getUsers = (xuids: string[]) => {
|
|
158
|
+
return this.executeRequest<UserInfo[]>(
|
|
159
|
+
`https://${HaloCoreEndpoints.Profile}.${
|
|
160
|
+
HaloCoreEndpoints.ServiceDomain
|
|
161
|
+
}/users?xuids=${xuids.map((x) => unwrapPlayerId(x)).join(",")}`,
|
|
162
|
+
"get"
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
202
166
|
/** Get service record for a player.
|
|
203
167
|
* @param gamerTag - Gamertag to lookup.
|
|
204
168
|
*/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RelyingParty,
|
|
3
|
+
XboxAuthenticationClient,
|
|
4
|
+
} from "../../authentication/xbox-authentication-client";
|
|
5
|
+
import { TokenPersister } from "../token-persisters";
|
|
6
|
+
import { HaloAuthenticationClient } from "../../authentication/halo-authentication-client";
|
|
7
|
+
import { SpartanTokenProvider } from ".";
|
|
8
|
+
import { inMemoryTokenPersister } from "../token-persisters/in-memory-token-persister";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A SpartanTokenProvider that fetches both the Xbox and Halo tokens in the same
|
|
12
|
+
* process. This is useful for applications that do not need to contend with
|
|
13
|
+
* CORS restrictions.
|
|
14
|
+
*/
|
|
15
|
+
export class AutoXstsSpartanTokenProvider implements SpartanTokenProvider {
|
|
16
|
+
public readonly getSpartanToken: () => Promise<string>;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
clientId: string,
|
|
20
|
+
redirectUri: string,
|
|
21
|
+
getAuthCode: (authorizeUrl: string) => Promise<string>,
|
|
22
|
+
tokenPersister?: TokenPersister
|
|
23
|
+
) {
|
|
24
|
+
let actualTokenPersister: TokenPersister;
|
|
25
|
+
if (tokenPersister) {
|
|
26
|
+
actualTokenPersister = tokenPersister;
|
|
27
|
+
} else {
|
|
28
|
+
actualTokenPersister = inMemoryTokenPersister;
|
|
29
|
+
}
|
|
30
|
+
const xboxAuthClient = new XboxAuthenticationClient(
|
|
31
|
+
clientId,
|
|
32
|
+
redirectUri,
|
|
33
|
+
getAuthCode,
|
|
34
|
+
tokenPersister
|
|
35
|
+
);
|
|
36
|
+
const haloAuthClient = new HaloAuthenticationClient(
|
|
37
|
+
async () => {
|
|
38
|
+
const accessToken = await xboxAuthClient.getAccessToken();
|
|
39
|
+
const userToken = await xboxAuthClient.getUserToken(accessToken);
|
|
40
|
+
const xstsTicket = await xboxAuthClient.getXstsTicket(
|
|
41
|
+
userToken,
|
|
42
|
+
RelyingParty.Halo
|
|
43
|
+
);
|
|
44
|
+
return xstsTicket.Token;
|
|
45
|
+
},
|
|
46
|
+
async () => await actualTokenPersister.load("halo.authToken"),
|
|
47
|
+
async (token) => {
|
|
48
|
+
await actualTokenPersister.save("halo.authToken", token);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
this.getSpartanToken = () => haloAuthClient.getSpartanToken();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TokenPersister } from "../token-persisters";
|
|
2
|
+
import { HaloAuthenticationClient } from "../../authentication/halo-authentication-client";
|
|
3
|
+
import { SpartanTokenProvider } from ".";
|
|
4
|
+
import { inMemoryTokenPersister } from "../token-persisters/in-memory-token-persister";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A SpartanTokenProvider that fetches uses a pre-fetched XSTS ticket token.
|
|
8
|
+
* Since requests to the Halo API are subject to CORS restrictions a
|
|
9
|
+
* HaloAuthenticationClient can be instantitated with a pre-fetched XSTS ticket
|
|
10
|
+
* and run on a server (such as one provided by the user).
|
|
11
|
+
*/
|
|
12
|
+
export class StaticXstsTicketTokenSpartanTokenProvider
|
|
13
|
+
implements SpartanTokenProvider
|
|
14
|
+
{
|
|
15
|
+
public readonly getSpartanToken: () => Promise<string>;
|
|
16
|
+
|
|
17
|
+
constructor(xstsTicketToken: string, tokenPersister?: TokenPersister) {
|
|
18
|
+
let actualTokenPersister: TokenPersister;
|
|
19
|
+
if (tokenPersister) {
|
|
20
|
+
actualTokenPersister = tokenPersister;
|
|
21
|
+
} else {
|
|
22
|
+
actualTokenPersister = inMemoryTokenPersister;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const haloAuthClient = new HaloAuthenticationClient(
|
|
26
|
+
() => xstsTicketToken,
|
|
27
|
+
async () => await actualTokenPersister.load("halo.authToken"),
|
|
28
|
+
async (token) => {
|
|
29
|
+
await actualTokenPersister.save("halo.authToken", token);
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
this.getSpartanToken = () => haloAuthClient.getSpartanToken();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TokenPersister } from ".";
|
|
2
|
+
|
|
3
|
+
const tokens = new Map<string, any>();
|
|
4
|
+
|
|
5
|
+
export const inMemoryTokenPersister: TokenPersister = {
|
|
6
|
+
load: (tokenName) => {
|
|
7
|
+
return tokens.get(tokenName);
|
|
8
|
+
},
|
|
9
|
+
save: (tokenName, token) => {
|
|
10
|
+
tokens.set(tokenName, token);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TokenPersister } from ".";
|
|
2
|
+
|
|
3
|
+
export const localStorageTokenPersister: TokenPersister = {
|
|
4
|
+
load: (tokenName) => {
|
|
5
|
+
const json = localStorage.getItem(tokenName);
|
|
6
|
+
if (json) {
|
|
7
|
+
return JSON.parse(json);
|
|
8
|
+
} else {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
save: (tokenName, token) => {
|
|
13
|
+
localStorage.setItem(tokenName, JSON.stringify(token));
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { TokenPersister } from ".";
|
|
3
|
+
|
|
4
|
+
export const nodeFsTokenPersister: TokenPersister = {
|
|
5
|
+
load: async (tokenName) => {
|
|
6
|
+
const storageFileName = `./tokens/${tokenName}`;
|
|
7
|
+
try {
|
|
8
|
+
const json = await fs.readFile(storageFileName, { encoding: "utf-8" });
|
|
9
|
+
return JSON.parse(json);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
if (e && typeof e === "object" && "code" in e && e.code === "ENOENT") {
|
|
12
|
+
return null;
|
|
13
|
+
} else {
|
|
14
|
+
throw e;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
save: async (tokenName, token) => {
|
|
19
|
+
const storageFileName = `./tokens/${tokenName}`;
|
|
20
|
+
await fs.mkdir(`./tokens/`, { recursive: true });
|
|
21
|
+
await fs.writeFile(storageFileName, JSON.stringify(token));
|
|
22
|
+
},
|
|
23
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,10 @@ export {
|
|
|
2
2
|
HaloInfiniteClient,
|
|
3
3
|
AssetKindTypeMap,
|
|
4
4
|
} from "./core/halo-infinite-client";
|
|
5
|
+
export {
|
|
6
|
+
XboxAuthenticationClient,
|
|
7
|
+
RelyingParty,
|
|
8
|
+
} from "./authentication/xbox-authentication-client";
|
|
5
9
|
export { Playlist } from "./models/halo-infinite/playlist";
|
|
6
10
|
export { PlaylistCsrContainer } from "./models/halo-infinite/playlist-csr-container";
|
|
7
11
|
export { UserInfo } from "./models/halo-infinite/user-info";
|
|
@@ -17,3 +21,7 @@ export { MatchOutcome } from "./models/halo-infinite/match-outcome";
|
|
|
17
21
|
export { MatchSkill } from "./models/halo-infinite/match-skill";
|
|
18
22
|
export { AssetVersionLink } from "./models/halo-infinite/asset-version-link";
|
|
19
23
|
export { MatchInfo } from "./models/halo-infinite/match-info";
|
|
24
|
+
export { SpartanTokenProvider } from "./core/spartan-token-providers";
|
|
25
|
+
export { AutoXstsSpartanTokenProvider } from "./core/spartan-token-providers/auto-xsts-spartan-token-provider";
|
|
26
|
+
export { StaticXstsTicketTokenSpartanTokenProvider } from "./core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider";
|
|
27
|
+
export { TokenPersister } from "./core/token-persisters";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { ResolvablePromise } from "./resolvable-promise";
|
|
3
|
+
|
|
4
|
+
export class ExpiryTokenCache<
|
|
5
|
+
TToken extends { expiresAt: DateTime },
|
|
6
|
+
TArgs extends any[]
|
|
7
|
+
> {
|
|
8
|
+
private tokenFetchPromise: ResolvablePromise<TToken> | undefined = undefined;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly tokenFetcher: (...args: TArgs) => Promise<TToken>
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
// TODO: Compare args and separate cache entries based on input
|
|
15
|
+
async getToken(...args: TArgs): Promise<TToken> {
|
|
16
|
+
if (this.tokenFetchPromise) {
|
|
17
|
+
// Someone either already has a token or is in the process of getting one
|
|
18
|
+
// Wait for them to finish, then check for validity
|
|
19
|
+
const currentToken = await this.tokenFetchPromise;
|
|
20
|
+
|
|
21
|
+
if (currentToken.expiresAt > DateTime.now()) {
|
|
22
|
+
// Current token is valid, return it
|
|
23
|
+
return currentToken;
|
|
24
|
+
} else {
|
|
25
|
+
// Current token expired, start a new promise
|
|
26
|
+
this.tokenFetchPromise = new ResolvablePromise<TToken>();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const newToken = await this.tokenFetcher(...args);
|
|
30
|
+
this.tokenFetchPromise.resolve(newToken);
|
|
31
|
+
return newToken;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
this.tokenFetchPromise.reject(e);
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// No one has a token, start a new promise
|
|
39
|
+
this.tokenFetchPromise = new ResolvablePromise<TToken>();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const newToken = await this.tokenFetcher(...args);
|
|
43
|
+
this.tokenFetchPromise.resolve(newToken);
|
|
44
|
+
return newToken;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
this.tokenFetchPromise.reject(e);
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|