@dendotdev/grunt 1.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/LICENSE +21 -0
- package/README.md +359 -0
- package/dist/index.d.mts +5177 -0
- package/dist/index.d.ts +5177 -0
- package/dist/index.js +2899 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2873 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Den Delimarsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# @dendotdev/grunt
|
|
2
|
+
|
|
3
|
+
Unofficial TypeScript client library for the Halo Infinite API.
|
|
4
|
+
|
|
5
|
+
This is the TypeScript implementation of the Grunt library, providing type-safe access to Halo Infinite and Halo Waypoint APIs. For the .NET version, see the [dotnet folder](../dotnet/).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @dendotdev/grunt
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For authenticated API access, you'll also need the Xbox authentication library:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @dendotdev/conch
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import {
|
|
23
|
+
HaloInfiniteClient,
|
|
24
|
+
MatchType,
|
|
25
|
+
LifecycleMode,
|
|
26
|
+
isSuccess,
|
|
27
|
+
} from '@dendotdev/grunt';
|
|
28
|
+
|
|
29
|
+
// Create a client with your Spartan token
|
|
30
|
+
const client = new HaloInfiniteClient({
|
|
31
|
+
spartanToken: 'your-spartan-token',
|
|
32
|
+
xuid: '2533274855333605', // Your Xbox User ID
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Get match history
|
|
36
|
+
const history = await client.stats.getMatchHistory(
|
|
37
|
+
'2533274855333605',
|
|
38
|
+
0, // start index
|
|
39
|
+
25, // count (max 25)
|
|
40
|
+
MatchType.All
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (isSuccess(history)) {
|
|
44
|
+
console.log(`Found ${history.result.resultCount} matches`);
|
|
45
|
+
for (const match of history.result.results ?? []) {
|
|
46
|
+
console.log(`Match: ${match.matchId}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
To use authenticated endpoints, you need a Spartan token. The complete authentication flow is:
|
|
54
|
+
|
|
55
|
+
1. Authenticate with Xbox Live using OAuth to get an access token
|
|
56
|
+
2. Exchange the access token for an Xbox Live user token
|
|
57
|
+
3. Exchange the user token for an XSTS token (using the Halo Waypoint relying party)
|
|
58
|
+
4. Exchange the XSTS token for a Spartan token using `HaloAuthenticationClient`
|
|
59
|
+
|
|
60
|
+
The Xbox authentication steps (1-3) are handled by [`@dendotdev/conch`](https://github.com/dend/conch). Here's a complete example:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { XboxAuthenticationClient } from '@dendotdev/conch';
|
|
64
|
+
import { HaloAuthenticationClient, HaloInfiniteClient, isSuccess } from '@dendotdev/grunt';
|
|
65
|
+
|
|
66
|
+
// Step 1: Set up Xbox authentication
|
|
67
|
+
const xboxClient = new XboxAuthenticationClient();
|
|
68
|
+
|
|
69
|
+
// Generate the OAuth URL for the user to authorize
|
|
70
|
+
const clientId = 'your-azure-ad-client-id';
|
|
71
|
+
const redirectUrl = 'https://localhost:3000/callback';
|
|
72
|
+
const authUrl = xboxClient.generateAuthUrl(clientId, redirectUrl);
|
|
73
|
+
|
|
74
|
+
// User visits authUrl and authorizes your app, then gets redirected with a code
|
|
75
|
+
// ... handle the OAuth redirect and extract the authorization code ...
|
|
76
|
+
|
|
77
|
+
// Step 2: Exchange the authorization code for OAuth tokens
|
|
78
|
+
const oauthToken = await xboxClient.requestOAuthToken(clientId, authorizationCode, redirectUrl);
|
|
79
|
+
|
|
80
|
+
if (!oauthToken?.access_token) {
|
|
81
|
+
throw new Error('Failed to get OAuth token');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step 3: Get Xbox Live user token
|
|
85
|
+
const userToken = await xboxClient.requestUserToken(oauthToken.access_token);
|
|
86
|
+
|
|
87
|
+
if (!userToken?.Token) {
|
|
88
|
+
throw new Error('Failed to get user token');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Step 4: Get XSTS token with Halo Waypoint relying party
|
|
92
|
+
const relyingParty = HaloAuthenticationClient.getRelyingParty();
|
|
93
|
+
const xstsToken = await xboxClient.requestXstsToken(userToken.Token, relyingParty);
|
|
94
|
+
|
|
95
|
+
if (!xstsToken?.Token) {
|
|
96
|
+
throw new Error('Failed to get XSTS token');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Step 5: Exchange XSTS token for Spartan token
|
|
100
|
+
const haloAuthClient = new HaloAuthenticationClient();
|
|
101
|
+
const spartanToken = await haloAuthClient.getSpartanToken(xstsToken.Token);
|
|
102
|
+
|
|
103
|
+
if (!spartanToken?.token) {
|
|
104
|
+
throw new Error('Failed to get Spartan token');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 6: Create the Halo Infinite API client
|
|
108
|
+
const xuid = xstsToken.DisplayClaims?.xui?.[0]?.xid;
|
|
109
|
+
const client = new HaloInfiniteClient({
|
|
110
|
+
spartanToken: spartanToken.token,
|
|
111
|
+
xuid: xuid,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Now you can make authenticated API calls
|
|
115
|
+
const history = await client.stats.getMatchHistory(xuid, 0, 25);
|
|
116
|
+
|
|
117
|
+
if (isSuccess(history)) {
|
|
118
|
+
console.log(`Found ${history.result.resultCount} matches`);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### OAuth Setup
|
|
123
|
+
|
|
124
|
+
To use this authentication flow, you'll need to register an application in Azure AD:
|
|
125
|
+
|
|
126
|
+
1. Go to the [Azure Portal](https://portal.azure.com) and navigate to Azure Active Directory
|
|
127
|
+
2. Register a new application with a redirect URI
|
|
128
|
+
3. Note your Application (client) ID - this is your `clientId`
|
|
129
|
+
|
|
130
|
+
For more details on Xbox authentication, see the [@dendotdev/conch documentation](https://github.com/dend/conch).
|
|
131
|
+
|
|
132
|
+
## API Overview
|
|
133
|
+
|
|
134
|
+
### HaloInfiniteClient
|
|
135
|
+
|
|
136
|
+
The main client for Halo Infinite APIs, with 12 specialized modules:
|
|
137
|
+
|
|
138
|
+
| Module | Description |
|
|
139
|
+
|--------|-------------|
|
|
140
|
+
| `stats` | Match history, service records, match stats |
|
|
141
|
+
| `skill` | CSR (Competitive Skill Rank) queries |
|
|
142
|
+
| `economy` | Inventory, stores, customization, currency |
|
|
143
|
+
| `gameCms` | Item definitions, challenges, medals, career ranks |
|
|
144
|
+
| `ugc` | User-generated content authoring |
|
|
145
|
+
| `ugcDiscovery` | Search and browse user content |
|
|
146
|
+
| `academy` | Bot customization, drills |
|
|
147
|
+
| `lobby` | QoS servers, lobby presence |
|
|
148
|
+
| `settings` | Clearance levels, feature flags |
|
|
149
|
+
| `configuration` | API endpoint discovery |
|
|
150
|
+
| `banProcessor` | Ban status queries |
|
|
151
|
+
| `textModeration` | Text moderation keys |
|
|
152
|
+
|
|
153
|
+
### WaypointClient
|
|
154
|
+
|
|
155
|
+
Client for Halo Waypoint APIs:
|
|
156
|
+
|
|
157
|
+
| Module | Description |
|
|
158
|
+
|--------|-------------|
|
|
159
|
+
| `profile` | User profiles and settings |
|
|
160
|
+
| `redemption` | Code redemption |
|
|
161
|
+
| `content` | News articles |
|
|
162
|
+
| `comms` | Notifications |
|
|
163
|
+
|
|
164
|
+
## Usage Examples
|
|
165
|
+
|
|
166
|
+
### Get Player Service Record
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const record = await client.stats.getPlayerServiceRecordByXuid(
|
|
170
|
+
'2533274855333605',
|
|
171
|
+
LifecycleMode.Matchmade
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (isSuccess(record)) {
|
|
175
|
+
const stats = record.result.stats?.coreStats;
|
|
176
|
+
console.log(`K/D: ${stats?.kills}/${stats?.deaths}`);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Get Match Details
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const match = await client.stats.getMatchStats('match-guid-here');
|
|
184
|
+
|
|
185
|
+
if (isSuccess(match)) {
|
|
186
|
+
console.log(`Map: ${match.result.matchInfo?.mapVariant?.publicName}`);
|
|
187
|
+
console.log(`Players: ${match.result.players?.length}`);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Get Player CSR
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const csr = await client.skill.getPlaylistCsr(
|
|
195
|
+
'playlist-guid',
|
|
196
|
+
['2533274855333605']
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (isSuccess(csr)) {
|
|
200
|
+
const playerCsr = csr.result.value?.[0];
|
|
201
|
+
console.log(`CSR: ${playerCsr?.csr?.value} (${playerCsr?.csr?.tier})`);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Get Player Inventory
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const inventory = await client.economy.getInventoryItems('2533274855333605');
|
|
209
|
+
|
|
210
|
+
if (isSuccess(inventory)) {
|
|
211
|
+
console.log(`Items owned: ${inventory.result.items?.length}`);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Search UGC Maps
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { AssetKind } from '@dendotdev/grunt';
|
|
219
|
+
|
|
220
|
+
const maps = await client.ugcDiscovery.search({
|
|
221
|
+
assetKinds: [AssetKind.Map],
|
|
222
|
+
term: 'blood gulch',
|
|
223
|
+
count: 10,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (isSuccess(maps)) {
|
|
227
|
+
for (const map of maps.result.results ?? []) {
|
|
228
|
+
console.log(`${map.publicName} by ${map.admin}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Get News Articles (No Auth Required)
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { WaypointClient, isSuccess } from '@dendotdev/grunt';
|
|
237
|
+
|
|
238
|
+
const client = new WaypointClient(); // No auth needed
|
|
239
|
+
|
|
240
|
+
const articles = await client.content.getArticles(1, 10);
|
|
241
|
+
|
|
242
|
+
if (isSuccess(articles)) {
|
|
243
|
+
for (const article of articles.result.articles ?? []) {
|
|
244
|
+
console.log(article.title);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Result Handling
|
|
250
|
+
|
|
251
|
+
All API methods return `HaloApiResult<T>` which contains:
|
|
252
|
+
- `result`: The response data (or `null` on failure)
|
|
253
|
+
- `response`: Raw response info (status code, headers, etc.)
|
|
254
|
+
|
|
255
|
+
Use the helper functions to check results:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import {
|
|
259
|
+
isSuccess, // 2xx status with data
|
|
260
|
+
isNotModified, // 304 (cached response valid)
|
|
261
|
+
isClientError, // 4xx errors
|
|
262
|
+
isServerError, // 5xx errors
|
|
263
|
+
} from '@dendotdev/grunt';
|
|
264
|
+
|
|
265
|
+
const result = await client.stats.getMatchStats('match-id');
|
|
266
|
+
|
|
267
|
+
if (isSuccess(result)) {
|
|
268
|
+
// result.result is guaranteed non-null here
|
|
269
|
+
console.log(result.result.matchId);
|
|
270
|
+
} else if (isClientError(result)) {
|
|
271
|
+
console.error(`Client error: ${result.response.code}`);
|
|
272
|
+
} else if (isServerError(result)) {
|
|
273
|
+
console.error(`Server error: ${result.response.code}`);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Configuration Options
|
|
278
|
+
|
|
279
|
+
### HaloInfiniteClient Options
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const client = new HaloInfiniteClient({
|
|
283
|
+
// Required
|
|
284
|
+
spartanToken: 'your-spartan-token',
|
|
285
|
+
|
|
286
|
+
// Optional
|
|
287
|
+
xuid: '2533274855333605', // Your Xbox User ID
|
|
288
|
+
clearanceToken: 'flight-id', // For flighted/preview content
|
|
289
|
+
includeRawResponses: true, // Include full request/response in results
|
|
290
|
+
userAgent: 'MyApp/1.0', // Custom User-Agent header
|
|
291
|
+
cacheTtlMs: 3600000, // Cache TTL (default: 60 minutes)
|
|
292
|
+
maxRetries: 3, // Retry attempts (default: 3)
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Building from Source
|
|
297
|
+
|
|
298
|
+
### Prerequisites
|
|
299
|
+
|
|
300
|
+
- Node.js 18.0.0 or higher
|
|
301
|
+
- npm
|
|
302
|
+
|
|
303
|
+
### Install Dependencies
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm install
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Build
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npm run build
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
This creates the `dist/` folder with:
|
|
316
|
+
- `index.js` - CommonJS build
|
|
317
|
+
- `index.mjs` - ES Module build
|
|
318
|
+
- `index.d.ts` - TypeScript declarations
|
|
319
|
+
|
|
320
|
+
### Development
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
# Watch mode (rebuild on changes)
|
|
324
|
+
npm run dev
|
|
325
|
+
|
|
326
|
+
# Type check without emitting
|
|
327
|
+
npm run typecheck
|
|
328
|
+
|
|
329
|
+
# Run tests
|
|
330
|
+
npm run test
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Features
|
|
334
|
+
|
|
335
|
+
- **Type-safe**: Full TypeScript support with comprehensive type definitions
|
|
336
|
+
- **Caching**: Built-in ETag-based caching with configurable TTL
|
|
337
|
+
- **Retry Logic**: Automatic retry with exponential backoff for transient failures
|
|
338
|
+
- **Lazy Loading**: Modules are initialized on first access to minimize memory usage
|
|
339
|
+
- **Minimal Dependencies**: Only one runtime dependency (`lru-cache`)
|
|
340
|
+
- **Universal**: Works in Node.js and modern browsers (uses native `fetch`)
|
|
341
|
+
|
|
342
|
+
## API Reference
|
|
343
|
+
|
|
344
|
+
For detailed API documentation, refer to the TypeScript type definitions included with the package, or explore the source code in the `src/` directory.
|
|
345
|
+
|
|
346
|
+
The API mirrors the [.NET Grunt library](../dotnet/) structure, so its documentation can also serve as a reference.
|
|
347
|
+
|
|
348
|
+
## Disclaimer
|
|
349
|
+
|
|
350
|
+
This is an unofficial library and is not affiliated with Microsoft, 343 Industries, or Xbox Game Studios. Use at your own risk. The Halo Infinite API is not officially documented and may change without notice.
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
355
|
+
|
|
356
|
+
## Credits
|
|
357
|
+
|
|
358
|
+
- Original Grunt project by [Den Delimarsky](https://den.dev)
|
|
359
|
+
- TypeScript implementation maintains API compatibility with the .NET version
|