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.
- 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 +9 -9
- package/dist/authentication/xbox-authentication-client.js +80 -63
- package/dist/authentication/xbox-authentication-client.js.map +1 -1
- package/dist/core/halo-infinite-client.d.ts +5 -8
- package/dist/core/halo-infinite-client.js +5 -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 +32 -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 +26 -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/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 +107 -81
- package/src/core/halo-infinite-client.ts +9 -66
- package/src/core/spartan-token-providers/auto-xsts-spartan-token-provider.ts +55 -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 +36 -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,22 +21,90 @@ 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: 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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
70
|
-
await this.
|
|
134
|
+
this.accessTokenPromise.resolve(newToken);
|
|
135
|
+
await this.tokenPersister?.save("xbox.accessToken", newToken);
|
|
71
136
|
return newToken.token;
|
|
72
137
|
} catch (e) {
|
|
73
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
await this.
|
|
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
|
-
|
|
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 } =
|
|
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:
|
|
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
|
|
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;
|
|
257
|
+
const { Token } = await this.userTokenCache.getToken(accessToken);
|
|
258
|
+
return Token;
|
|
214
259
|
}
|
|
215
260
|
|
|
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;
|
|
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
|
|
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.
|
|
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,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,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
|
+
}
|