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.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/dist/authentication/halo-authentication-client.d.ts +4 -5
  3. package/dist/authentication/halo-authentication-client.js +23 -69
  4. package/dist/authentication/halo-authentication-client.js.map +1 -1
  5. package/dist/authentication/xbox-authentication-client.d.ts +23 -10
  6. package/dist/authentication/xbox-authentication-client.js +92 -64
  7. package/dist/authentication/xbox-authentication-client.js.map +1 -1
  8. package/dist/core/halo-infinite-client.d.ts +9 -8
  9. package/dist/core/halo-infinite-client.js +20 -35
  10. package/dist/core/halo-infinite-client.js.map +1 -1
  11. package/dist/core/spartan-token-fetchers/auto-xsts-sartan-token-provider.d.ts +11 -0
  12. package/dist/core/spartan-token-fetchers/auto-xsts-sartan-token-provider.js +43 -0
  13. package/dist/core/spartan-token-fetchers/auto-xsts-sartan-token-provider.js.map +1 -0
  14. package/dist/core/spartan-token-fetchers/index.d.ts +3 -0
  15. package/dist/core/spartan-token-fetchers/index.js +2 -0
  16. package/dist/core/spartan-token-fetchers/index.js.map +1 -0
  17. package/dist/core/spartan-token-fetchers/static-xsts-ticket-token-spartan-token-provider.d.ts +12 -0
  18. package/dist/core/spartan-token-fetchers/static-xsts-ticket-token-spartan-token-provider.js +26 -0
  19. package/dist/core/spartan-token-fetchers/static-xsts-ticket-token-spartan-token-provider.js.map +1 -0
  20. package/dist/core/spartan-token-providers/auto-xsts-spartan-token-provider.d.ts +11 -0
  21. package/dist/core/spartan-token-providers/auto-xsts-spartan-token-provider.js +31 -0
  22. package/dist/core/spartan-token-providers/auto-xsts-spartan-token-provider.js.map +1 -0
  23. package/dist/core/spartan-token-providers/index.d.ts +3 -0
  24. package/dist/core/spartan-token-providers/index.js +2 -0
  25. package/dist/core/spartan-token-providers/index.js.map +1 -0
  26. package/dist/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.d.ts +12 -0
  27. package/dist/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.js +25 -0
  28. package/dist/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.js.map +1 -0
  29. package/dist/core/token-persister.d.ts +4 -0
  30. package/dist/core/token-persister.js +2 -0
  31. package/dist/core/token-persister.js.map +1 -0
  32. package/dist/core/token-persisters/in-memory-token-persister.d.ts +2 -0
  33. package/dist/core/token-persisters/in-memory-token-persister.js +10 -0
  34. package/dist/core/token-persisters/in-memory-token-persister.js.map +1 -0
  35. package/dist/core/token-persisters/index.d.ts +4 -0
  36. package/dist/core/token-persisters/index.js +2 -0
  37. package/dist/core/token-persisters/index.js.map +1 -0
  38. package/dist/core/token-persisters/local-storage-token-persister.d.ts +2 -0
  39. package/dist/core/token-persisters/local-storage-token-persister.js +15 -0
  40. package/dist/core/token-persisters/local-storage-token-persister.js.map +1 -0
  41. package/dist/core/token-persisters/node-fs-token-persister.d.ts +2 -0
  42. package/dist/core/token-persisters/node-fs-token-persister.js +24 -0
  43. package/dist/core/token-persisters/node-fs-token-persister.js.map +1 -0
  44. package/dist/index.d.ts +5 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/util/expiry-token-cache.d.ts +9 -0
  48. package/dist/util/expiry-token-cache.js +48 -0
  49. package/dist/util/expiry-token-cache.js.map +1 -0
  50. package/dist/util/resolvable-promise.d.ts +8 -0
  51. package/dist/util/resolvable-promise.js +31 -0
  52. package/dist/util/resolvable-promise.js.map +1 -0
  53. package/package.json +3 -2
  54. package/src/authentication/halo-authentication-client.ts +30 -72
  55. package/src/authentication/xbox-authentication-client.ts +120 -83
  56. package/src/core/halo-infinite-client.ts +30 -66
  57. package/src/core/spartan-token-providers/auto-xsts-spartan-token-provider.ts +54 -0
  58. package/src/core/spartan-token-providers/index.ts +3 -0
  59. package/src/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.ts +35 -0
  60. package/src/core/token-persisters/in-memory-token-persister.ts +12 -0
  61. package/src/core/token-persisters/index.ts +4 -0
  62. package/src/core/token-persisters/local-storage-token-persister.ts +15 -0
  63. package/src/core/token-persisters/node-fs-token-persister.ts +23 -0
  64. package/src/index.ts +8 -0
  65. package/src/util/expiry-token-cache.ts +51 -0
  66. 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 currentTokenPromise: Promise<XboxAuthenticationToken> | undefined =
22
- undefined;
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 loadToken: () => Promise<{
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
- return pkceChallenge(43);
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.currentTokenPromise) {
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.currentTokenPromise;
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
- let promiseResolver!: (token: XboxAuthenticationToken) => void;
57
- let promiseRejector!: (error: unknown) => void;
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
- promiseResolver(newToken);
70
- await this.saveToken(newToken);
146
+ this.accessTokenPromise.resolve(newToken);
147
+ await this.tokenPersister?.save("xbox.accessToken", newToken);
71
148
  return newToken.token;
72
149
  } catch (e) {
73
- promiseRejector(e);
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
- let promiseResolver!: (token: XboxAuthenticationToken) => void;
80
- let promiseRejector!: (error: unknown) => void;
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 = await this.loadToken();
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
- promiseResolver(currentToken as XboxAuthenticationToken);
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
- promiseResolver(newToken);
103
- await this.saveToken(newToken);
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
- promiseRejector(e);
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 } = await this.getPkce();
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: responseDate.plus({ seconds: response.data.expires_in }),
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 response = await this.httpClient.post<XboxTicket>(
194
- "https://user.auth.xboxlive.com/user/authenticate",
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 async getXstsTicket(userToken: string, relyingParty: RelyingParty) {
217
- const response = await this.httpClient.post<XboxTicket>(
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
- export class HaloInfiniteClient {
70
- private readonly haloAuthClient: HaloAuthenticationClient;
71
-
72
- constructor(
73
- clientId: string,
74
- redirectUri: string,
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.haloAuthClient.getSpartanToken()
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,3 @@
1
+ export interface SpartanTokenProvider {
2
+ getSpartanToken(): Promise<string>;
3
+ }
@@ -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,4 @@
1
+ export interface TokenPersister {
2
+ load: <T>(tokenName: string) => Promise<T> | T;
3
+ save: (tokenName: string, token: unknown) => Promise<void> | void;
4
+ }
@@ -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
+ }