halo-infinite-api 1.2.2 → 2.0.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 (62) 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 +9 -9
  6. package/dist/authentication/xbox-authentication-client.js +80 -63
  7. package/dist/authentication/xbox-authentication-client.js.map +1 -1
  8. package/dist/core/halo-infinite-client.d.ts +5 -8
  9. package/dist/core/halo-infinite-client.js +5 -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 +32 -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 +26 -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/index.d.ts +4 -0
  33. package/dist/core/token-persisters/index.js +2 -0
  34. package/dist/core/token-persisters/index.js.map +1 -0
  35. package/dist/core/token-persisters/local-storage-token-persister.d.ts +2 -0
  36. package/dist/core/token-persisters/local-storage-token-persister.js +15 -0
  37. package/dist/core/token-persisters/local-storage-token-persister.js.map +1 -0
  38. package/dist/core/token-persisters/node-fs-token-persister.d.ts +2 -0
  39. package/dist/core/token-persisters/node-fs-token-persister.js +24 -0
  40. package/dist/core/token-persisters/node-fs-token-persister.js.map +1 -0
  41. package/dist/index.d.ts +5 -0
  42. package/dist/index.js +3 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/util/expiry-token-cache.d.ts +9 -0
  45. package/dist/util/expiry-token-cache.js +48 -0
  46. package/dist/util/expiry-token-cache.js.map +1 -0
  47. package/dist/util/resolvable-promise.d.ts +8 -0
  48. package/dist/util/resolvable-promise.js +31 -0
  49. package/dist/util/resolvable-promise.js.map +1 -0
  50. package/package.json +3 -2
  51. package/src/authentication/halo-authentication-client.ts +30 -72
  52. package/src/authentication/xbox-authentication-client.ts +107 -81
  53. package/src/core/halo-infinite-client.ts +9 -66
  54. package/src/core/spartan-token-providers/auto-xsts-spartan-token-provider.ts +55 -0
  55. package/src/core/spartan-token-providers/index.ts +3 -0
  56. package/src/core/spartan-token-providers/static-xsts-ticket-token-spartan-token-provider.ts +36 -0
  57. package/src/core/token-persisters/index.ts +4 -0
  58. package/src/core/token-persisters/local-storage-token-persister.ts +15 -0
  59. package/src/core/token-persisters/node-fs-token-persister.ts +23 -0
  60. package/src/index.ts +8 -0
  61. package/src/util/expiry-token-cache.ts +51 -0
  62. 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,22 +21,90 @@ 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: DateTime }
30
+ >("xbox.userToken");
31
+
32
+ if (persistedToken && persistedToken.expiresAt > DateTime.now()) {
33
+ return persistedToken;
34
+ }
35
+
36
+ const response = await this.httpClient.post<XboxTicket>(
37
+ "https://user.auth.xboxlive.com/user/authenticate",
38
+ {
39
+ RelyingParty: "http://auth.xboxlive.com",
40
+ TokenType: "JWT",
41
+ Properties: {
42
+ AuthMethod: "RPS",
43
+ SiteName: "user.auth.xboxlive.com",
44
+ RpsTicket: `d=${accessToken}`,
45
+ },
46
+ },
47
+ {
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ Accept: "application/json",
51
+ "x-xbl-contract-version": "1",
52
+ },
53
+ }
54
+ );
55
+
56
+ const result = {
57
+ ...response.data,
58
+ expiresAt: DateTime.fromISO(response.data.NotAfter),
59
+ };
60
+ await this.tokenPersister?.save("xbox.userToken", result);
61
+ return result;
62
+ });
63
+ private xstsTicketCache = new ExpiryTokenCache(
64
+ async (userToken: string, relyingParty: RelyingParty) => {
65
+ const persistedToken = await this.tokenPersister?.load<
66
+ XboxTicket & { expiresAt: DateTime }
67
+ >("xbox.xstsTicket");
68
+
69
+ if (persistedToken && persistedToken.expiresAt > DateTime.now()) {
70
+ return persistedToken;
71
+ }
72
+
73
+ const response = await this.httpClient.post<XboxTicket>(
74
+ "https://xsts.auth.xboxlive.com/xsts/authorize",
75
+ {
76
+ RelyingParty: relyingParty,
77
+ TokenType: "JWT",
78
+ Properties: {
79
+ SandboxId: "RETAIL",
80
+ UserTokens: [userToken],
81
+ },
82
+ },
83
+ {
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ Accept: "application/json",
87
+ "x-xbl-contract-version": "1",
88
+ },
89
+ }
90
+ );
91
+
92
+ const result = {
93
+ ...response.data,
94
+ expiresAt: DateTime.fromISO(response.data.NotAfter),
95
+ };
96
+ await this.tokenPersister?.save("xbox.xstsTicket", result);
97
+ return result;
98
+ }
99
+ );
100
+
23
101
  private readonly httpClient: AxiosInstance;
24
102
 
25
103
  constructor(
26
104
  private readonly clientId: string,
27
105
  private readonly redirectUri: string,
28
106
  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>
107
+ private readonly tokenPersister?: TokenPersister
37
108
  ) {
38
109
  this.httpClient = axios.create();
39
110
  }
@@ -43,50 +114,41 @@ export class XboxAuthenticationClient {
43
114
  }
44
115
 
45
116
  public async getAccessToken() {
46
- if (this.currentTokenPromise) {
117
+ if (this.accessTokenPromise) {
47
118
  // Someone either already has a token or is in the process of getting one
48
119
  // Wait for them to finish, then check for validity
49
- const currentToken = await this.currentTokenPromise;
120
+ const currentToken = await this.accessTokenPromise;
50
121
 
51
122
  if (currentToken.expiresAt > DateTime.now()) {
52
123
  // Current token is valid, return it
53
124
  return currentToken.token;
54
125
  } else {
55
126
  // 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
- );
127
+ this.accessTokenPromise =
128
+ new ResolvablePromise<XboxAuthenticationToken>();
64
129
 
65
130
  try {
66
131
  const newToken = await this.refreshOAuth2Token(
67
132
  currentToken.refreshToken
68
133
  );
69
- promiseResolver(newToken);
70
- await this.saveToken(newToken);
134
+ this.accessTokenPromise.resolve(newToken);
135
+ await this.tokenPersister?.save("xbox.accessToken", newToken);
71
136
  return newToken.token;
72
137
  } catch (e) {
73
- promiseRejector(e);
138
+ this.accessTokenPromise.reject(e);
74
139
  throw e;
75
140
  }
76
141
  }
77
142
  } else {
78
143
  // 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
- );
144
+ this.accessTokenPromise =
145
+ new ResolvablePromise<XboxAuthenticationToken>();
87
146
 
88
147
  try {
89
- const loadedToken = await this.loadToken();
148
+ const loadedToken =
149
+ await this.tokenPersister?.load<XboxAuthenticationToken>(
150
+ "xbox.accessToken"
151
+ );
90
152
  const currentToken = {
91
153
  ...loadedToken,
92
154
  token: loadedToken?.token ?? "",
@@ -95,23 +157,25 @@ export class XboxAuthenticationClient {
95
157
 
96
158
  if (currentToken.expiresAt && currentToken.expiresAt > DateTime.now()) {
97
159
  // Current token is valid, return it and alert other callers if applicable
98
- promiseResolver(currentToken as XboxAuthenticationToken);
160
+ this.accessTokenPromise.resolve(
161
+ currentToken as XboxAuthenticationToken
162
+ );
99
163
  return currentToken.token;
100
164
  } else {
101
165
  const newToken = await this.fetchOauth2Token();
102
- promiseResolver(newToken);
103
- await this.saveToken(newToken);
166
+ this.accessTokenPromise.resolve(newToken);
167
+ await this.tokenPersister?.save("xbox.accessToken", newToken);
104
168
  return newToken.token;
105
169
  }
106
170
  } catch (e) {
107
- promiseRejector(e);
171
+ this.accessTokenPromise.reject(e);
108
172
  throw e;
109
173
  }
110
174
  }
111
175
  }
112
176
 
113
177
  private async fetchOauth2Token(): Promise<XboxAuthenticationToken> {
114
- const { code_verifier, code_challenge } = await this.getPkce();
178
+ const { code_verifier, code_challenge } = this.getPkce();
115
179
 
116
180
  const authorizeUrl = `https://login.live.com/oauth20_authorize.srf?${new URLSearchParams(
117
181
  {
@@ -126,6 +190,7 @@ export class XboxAuthenticationClient {
126
190
 
127
191
  const code = await this.getAuthCode(authorizeUrl);
128
192
 
193
+ const requestStart = DateTime.now();
129
194
  const response = await this.httpClient.post<{
130
195
  access_token: string;
131
196
  expires_in: number;
@@ -149,10 +214,9 @@ export class XboxAuthenticationClient {
149
214
  }
150
215
  );
151
216
 
152
- const responseDate = DateTime.fromRFC2822(response.headers["date"]);
153
217
  return {
154
218
  token: response.data.access_token,
155
- expiresAt: responseDate.plus({ seconds: response.data.expires_in }),
219
+ expiresAt: requestStart.plus({ seconds: response.data.expires_in }),
156
220
  refreshToken: response.data.refresh_token,
157
221
  };
158
222
  }
@@ -190,50 +254,12 @@ export class XboxAuthenticationClient {
190
254
  }
191
255
 
192
256
  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;
257
+ const { Token } = await this.userTokenCache.getToken(accessToken);
258
+ return Token;
214
259
  }
215
260
 
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;
261
+ public getXstsTicket(userToken: string, relyingParty: RelyingParty) {
262
+ return this.xstsTicketCache.getToken(userToken, relyingParty);
237
263
  }
238
264
 
239
265
  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>;
43
- save: (tokenName: string, token: unknown) => Promise<void>;
44
- }
45
-
46
37
  export type AssetKindTypeMap = {
47
38
  [AssetKind.Map]: MapAsset;
48
39
  [AssetKind.UgcGameVariant]: UgcGameVariantAsset;
@@ -67,55 +58,7 @@ function wrapPlayerId(playerId: string) {
67
58
  }
68
59
 
69
60
  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
- );
118
- }
61
+ constructor(private spartanTokenProvider: SpartanTokenProvider) {}
119
62
 
120
63
  private async executeRequest<T>(
121
64
  url: string,
@@ -132,7 +75,7 @@ export class HaloInfiniteClient {
132
75
  if (useSpartanToken) {
133
76
  headers.set(
134
77
  "x-343-authorization-spartan",
135
- await this.haloAuthClient.getSpartanToken()
78
+ await this.spartanTokenProvider.getSpartanToken()
136
79
  );
137
80
  }
138
81
 
@@ -0,0 +1,55 @@
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
+
9
+ /**
10
+ * A SpartanTokenProvider that fetches both the Xbox and Halo tokens in the same
11
+ * process. This is useful for applications that do not need to contend with
12
+ * CORS restrictions.
13
+ */
14
+ export class AutoXstsSpartanTokenProvider implements SpartanTokenProvider {
15
+ public readonly getSpartanToken: () => Promise<string>;
16
+
17
+ constructor(
18
+ clientId: string,
19
+ redirectUri: string,
20
+ getAuthCode: (authorizeUrl: string) => Promise<string>,
21
+ tokenPersister?: TokenPersister
22
+ ) {
23
+ const xboxAuthClient = new XboxAuthenticationClient(
24
+ clientId,
25
+ redirectUri,
26
+ getAuthCode,
27
+ tokenPersister
28
+ );
29
+ const haloAuthClient = new HaloAuthenticationClient(
30
+ async () => {
31
+ const accessToken = await xboxAuthClient.getAccessToken();
32
+ const userToken = await xboxAuthClient.getUserToken(accessToken);
33
+ const xstsTicket = await xboxAuthClient.getXstsTicket(
34
+ userToken,
35
+ RelyingParty.Halo
36
+ );
37
+ return xstsTicket.Token;
38
+ },
39
+ async () => {
40
+ if (tokenPersister) {
41
+ return await tokenPersister.load("halo.authToken");
42
+ } else {
43
+ return null;
44
+ }
45
+ },
46
+ async (token) => {
47
+ if (tokenPersister) {
48
+ await tokenPersister.save("halo.authToken", token);
49
+ }
50
+ }
51
+ );
52
+
53
+ this.getSpartanToken = () => haloAuthClient.getSpartanToken();
54
+ }
55
+ }
@@ -0,0 +1,3 @@
1
+ export interface SpartanTokenProvider {
2
+ getSpartanToken(): Promise<string>;
3
+ }
@@ -0,0 +1,36 @@
1
+ import { TokenPersister } from "../token-persisters";
2
+ import { HaloAuthenticationClient } from "../../authentication/halo-authentication-client";
3
+ import { SpartanTokenProvider } from ".";
4
+
5
+ /**
6
+ * A SpartanTokenProvider that fetches uses a pre-fetched XSTS ticket token.
7
+ * Since requests to the Halo API are subject to CORS restrictions a
8
+ * HaloAuthenticationClient can be instantitated with a pre-fetched XSTS ticket
9
+ * and run on a server (such as one provided by the user).
10
+ */
11
+
12
+ export class StaticXstsTicketTokenSpartanTokenProvider
13
+ implements SpartanTokenProvider
14
+ {
15
+ public readonly getSpartanToken: () => Promise<string>;
16
+
17
+ constructor(xstsTicketToken: string, tokenPersister?: TokenPersister) {
18
+ const haloAuthClient = new HaloAuthenticationClient(
19
+ () => xstsTicketToken,
20
+ async () => {
21
+ if (tokenPersister) {
22
+ return await tokenPersister.load("halo.authToken");
23
+ } else {
24
+ return null;
25
+ }
26
+ },
27
+ async (token) => {
28
+ if (tokenPersister) {
29
+ await tokenPersister.save("halo.authToken", token);
30
+ }
31
+ }
32
+ );
33
+
34
+ this.getSpartanToken = () => haloAuthClient.getSpartanToken();
35
+ }
36
+ }
@@ -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
+ }
@@ -0,0 +1,32 @@
1
+ export class ResolvablePromise<TReturn> extends Promise<TReturn> {
2
+ isCompleted = false;
3
+ readonly resolve: (value: TReturn | PromiseLike<TReturn>) => void;
4
+ readonly reject: (reason?: unknown) => void;
5
+ constructor() {
6
+ let resolve!: (value: TReturn | PromiseLike<TReturn>) => void;
7
+ let reject!: (reason?: unknown) => void;
8
+ super((res, rej) => {
9
+ resolve = res;
10
+ reject = rej;
11
+ });
12
+ this.resolve = (v) => {
13
+ this.isCompleted = true;
14
+ return resolve(v);
15
+ };
16
+ this.reject = (r) => {
17
+ this.isCompleted = true;
18
+ return reject(r);
19
+ };
20
+ }
21
+
22
+ // you can also use Symbol.species in order to
23
+ // return a Promise for then/catch/finally
24
+ static get [Symbol.species]() {
25
+ return Promise;
26
+ }
27
+
28
+ // Promise overrides his Symbol.toStringTag
29
+ get [Symbol.toStringTag]() {
30
+ return "ResolvablePromise";
31
+ }
32
+ }