Skip to content

Commit 26794b2

Browse files
authored
Merge pull request #5 from Daniel-Ric/feature/2026-01-21/find-out-what-he-means
Add PlayFab e9d1 inventory test endpoint
2 parents 8068bb9 + 1bbd2b1 commit 26794b2

4 files changed

Lines changed: 112 additions & 9 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,17 @@ curl "http://localhost:3000/inventory/minecraft?includeReceipt=false" -H "Auth
190190
curl -X POST http://localhost:3000/inventory/playfab -H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" -d '{"sessionTicket":"<PLAYFAB_SESSION_TICKET>","count":50}'
191191
```
192192

193-
### 9) Captures (screenshots) → `/captures/screenshots`
193+
### 9) PlayFab inventory test (title id e9d1) → `/inventory/playfab/test`
194+
```bash
195+
curl -X POST http://localhost:3000/inventory/playfab/test -H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" -d '{"playfabToken":"XBL3.0 x=<uhs>;<xstsToken>","entityType":"title_player_account","count":50}'
196+
```
197+
198+
### 10) Captures (screenshots) → `/captures/screenshots`
194199
```bash
195200
curl "http://localhost:3000/captures/screenshots?max=24" -H "Authorization: Bearer <JWT>" -H "x-xbl-token: XBL3.0 x=<uhs>;<xstsToken>"
196201
```
197202

198-
### 10) Debug: decode token → `/debug/decode-token` (non-production)
203+
### 11) Debug: decode token → `/debug/decode-token` (non-production)
199204
```bash
200205
curl -X POST http://localhost:3000/debug/decode-token -H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" -d '{"token":"XBL3.0 x=<uhs>;<xstsToken>","type":"xsts"}'
201206
```
@@ -255,6 +260,7 @@ curl -X POST http://localhost:3000/debug/decode-token -H "Authorization: Beare
255260
| Method | Endpoint | Description | Headers |
256261
|-------:|------------------------------------|-------------------------------------------------------|--------------|
257262
| POST | `/inventory/playfab` | PlayFab inventory via SessionTicket/EntityToken ||
263+
| POST | `/inventory/playfab/test` | PlayFab inventory test via XSTS (title id e9d1) ||
258264
| GET | `/inventory/minecraft` | Minecraft entitlements (optional `includeReceipt`) | `x-mc-token` |
259265
| GET | `/inventory/minecraft/balances` | Minecraft Marketplace currency balances | `x-mc-token` |
260266
| GET | `/inventory/minecraft/creators/top`| Top creators from entitlements (by item count) | `x-mc-token` |

src/routes/inventory.routes.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import Joi from "joi";
33
import jwtLib from "jsonwebtoken";
44
import {jwtMiddleware} from "../utils/jwt.js";
55
import {asyncHandler} from "../utils/async.js";
6-
import {getEntityToken, getPlayFabInventory} from "../services/playfab.service.js";
6+
import {getEntityToken, getPlayFabInventory, loginWithXbox} from "../services/playfab.service.js";
77
import {getMCBalances, getMCInventory} from "../services/minecraft.service.js";
88
import {badRequest} from "../utils/httpError.js";
99

1010
const router = express.Router();
11+
const PLAYFAB_TEST_TITLE_ID = "e9d1";
1112

1213
/**
1314
* @swagger
@@ -66,6 +67,77 @@ router.post("/playfab", jwtMiddleware, asyncHandler(async (req, res) => {
6667
res.json({entity: entityData.Entity, items: inv.Items || []});
6768
}));
6869

70+
/**
71+
* @swagger
72+
* /inventory/playfab/test:
73+
* post:
74+
* summary: Test PlayFab inventory for title id e9d1
75+
* description: >
76+
* Logs in with a PlayFab XSTS token, exchanges for an EntityToken, and returns inventory
77+
* items for the selected entity type using title id e9d1.
78+
* tags: [Inventory]
79+
* security:
80+
* - BearerAuth: []
81+
* requestBody:
82+
* required: true
83+
* content:
84+
* application/json:
85+
* schema:
86+
* type: object
87+
* required: [playfabToken]
88+
* properties:
89+
* playfabToken:
90+
* type: string
91+
* description: PlayFab XSTS token in the form XBL3.0 x={uhs};{token}
92+
* entityType:
93+
* type: string
94+
* enum: [title_player_account, master_player_account]
95+
* default: title_player_account
96+
* entityId:
97+
* type: string
98+
* description: Optional entity id override for the chosen entity type
99+
* collectionId:
100+
* type: string
101+
* default: "default"
102+
* count:
103+
* type: integer
104+
* default: 50
105+
* minimum: 1
106+
* maximum: 200
107+
* responses:
108+
* 200:
109+
* description: PlayFab inventory items for the chosen entity
110+
*/
111+
router.post("/playfab/test", jwtMiddleware, asyncHandler(async (req, res) => {
112+
const schema = Joi.object({
113+
playfabToken: Joi.string().required(),
114+
entityType: Joi.string().valid("title_player_account", "master_player_account").default("title_player_account"),
115+
entityId: Joi.string().optional(),
116+
collectionId: Joi.string().default("default"),
117+
count: Joi.number().integer().min(1).max(200).default(50)
118+
});
119+
const {value, error} = schema.validate(req.body);
120+
if (error) throw badRequest(error.message);
121+
122+
const loginData = await loginWithXbox(value.playfabToken, PLAYFAB_TEST_TITLE_ID);
123+
const sessionTicket = loginData.SessionTicket;
124+
const playFabId = loginData.PlayFabId;
125+
if (value.entityType === "master_player_account" && !value.entityId && !playFabId) {
126+
throw badRequest("PlayFabId is required for master_player_account");
127+
}
128+
129+
const entityOverride = value.entityId ? {
130+
Type: value.entityType, Id: value.entityId
131+
} : value.entityType === "master_player_account" ? {
132+
Type: value.entityType, Id: playFabId
133+
} : null;
134+
135+
const entityData = await getEntityToken(sessionTicket, entityOverride || undefined, PLAYFAB_TEST_TITLE_ID);
136+
const inv = await getPlayFabInventory(entityData.EntityToken, entityData.Entity.Id, entityData.Entity.Type, value.collectionId, value.count, PLAYFAB_TEST_TITLE_ID);
137+
138+
res.json({entity: entityData.Entity, items: inv.Items || []});
139+
}));
140+
69141
/**
70142
* @swagger
71143
* /inventory/minecraft:

src/services/playfab.service.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ import {createHttp} from "../utils/http.js";
44

55
const http = createHttp(env.HTTP_TIMEOUT_MS);
66

7-
export async function loginWithXbox(xstsToken, titleId = env.PLAYFAB_TITLE_ID) {
7+
export function resolvePlayFabTitleId(titleId = env.PLAYFAB_TITLE_ID) {
88
if (!titleId) throw badRequest("PLAYFAB_TITLE_ID missing. Set it in .env");
9-
const baseUrl = `https://${titleId}.playfabapi.com/Client/LoginWithXbox`;
9+
return titleId;
10+
}
11+
12+
export async function loginWithXbox(xstsToken, titleId = env.PLAYFAB_TITLE_ID) {
13+
const resolvedTitleId = resolvePlayFabTitleId(titleId);
14+
const baseUrl = `https://${resolvedTitleId}.playfabapi.com/Client/LoginWithXbox`;
1015
try {
1116
const {data} = await http.post(baseUrl, {
1217
TitleId: titleId,
@@ -31,9 +36,10 @@ export async function loginWithXbox(xstsToken, titleId = env.PLAYFAB_TITLE_ID) {
3136
}
3237
}
3338

34-
export async function getEntityToken(sessionTicket, entity) {
39+
export async function getEntityToken(sessionTicket, entity, titleId = env.PLAYFAB_TITLE_ID) {
3540
if (!sessionTicket) throw badRequest("sessionTicket is required");
36-
const url = `https://${env.PLAYFAB_TITLE_ID}.playfabapi.com/Authentication/GetEntityToken`;
41+
const resolvedTitleId = resolvePlayFabTitleId(titleId);
42+
const url = `https://${resolvedTitleId}.playfabapi.com/Authentication/GetEntityToken`;
3743
try {
3844
const {data} = await http.post(url, entity ? {Entity: entity} : {}, {
3945
headers: {
@@ -57,9 +63,10 @@ export async function getEntityToken(sessionTicket, entity) {
5763
}
5864
}
5965

60-
export async function getPlayFabInventory(entityToken, entityId, entityType = "title_player_account", collectionId = "default", count = 50) {
66+
export async function getPlayFabInventory(entityToken, entityId, entityType = "title_player_account", collectionId = "default", count = 50, titleId = env.PLAYFAB_TITLE_ID) {
6167
if (!entityToken || !entityId) throw badRequest("entityToken and entityId are required");
62-
const url = `https://${env.PLAYFAB_TITLE_ID}.playfabapi.com/Inventory/GetInventoryItems`;
68+
const resolvedTitleId = resolvePlayFabTitleId(titleId);
69+
const url = `https://${resolvedTitleId}.playfabapi.com/Inventory/GetInventoryItems`;
6370
try {
6471
const {data} = await http.post(url, {
6572
Entity: {Type: entityType, Id: entityId}, CollectionId: collectionId, Count: count

tests/playfab.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
4+
process.env.NODE_ENV = "test";
5+
process.env.JWT_SECRET = process.env.JWT_SECRET || "test-secret-1234567890";
6+
process.env.CLIENT_ID = process.env.CLIENT_ID || "test-client";
7+
8+
const {resolvePlayFabTitleId} = await import("../src/services/playfab.service.js");
9+
10+
test("resolvePlayFabTitleId uses explicit title id", () => {
11+
assert.equal(resolvePlayFabTitleId("e9d1"), "e9d1");
12+
});
13+
14+
test("resolvePlayFabTitleId rejects empty title id", () => {
15+
assert.throws(() => resolvePlayFabTitleId(""), {
16+
message: "PLAYFAB_TITLE_ID missing. Set it in .env"
17+
});
18+
});

0 commit comments

Comments
 (0)