Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ Optional Marketplace headers (when enabled):
| ------ | -------------------------------------------- | -------------------------------- | ---------------------------------------------- |
| GET | `/inventory/balances` | Player virtual currency balances | |
| GET | `/inventory/entitlements?includeReceipt=true | false` | Player entitlements (optionally with receipts) |
| GET | `/inventory/playfab/items?filter=<query>` | PlayFab inventory items with official filters |

#### Purchase

Expand Down Expand Up @@ -269,6 +270,10 @@ Headers: `authorization` (JWT), `x-mc-token` (required). Returns Minecoin and ot

Headers: `authorization` (JWT), `x-mc-token` (required). Returns `{ count, entitlements: [] }` when called via `/inventory`; purchase routes return the raw upstream payload.

#### `GET /inventory/playfab/items?filter=<query>`

Headers: `authorization` (JWT), `x-playfab-session` (required), `x-playfab-id` (required). Returns the PlayFab Economy inventory payload with official PlayFab filter queries.

#### `POST /purchase/quote`

Headers: `authorization` (JWT) and **either** `x-mc-token` **or** `x-playfab-session`.
Expand Down Expand Up @@ -335,6 +340,10 @@ curl -sS "$BASE/inventory/balances" \
curl -sS "$BASE/inventory/entitlements?includeReceipt=true" \
-H "Authorization: Bearer $TOKEN" -H "x-mc-token: $MC"

# PlayFab inventory items with filter query
curl -sS "$BASE/inventory/playfab/items?filter=InventoryItem.ItemClass%20eq%20%27Subscription%27" \
-H "Authorization: Bearer $TOKEN" -H "x-playfab-session: $ST" -H "x-playfab-id: <playfabId>"

# Debug: decode multiple tokens
curl -sS -X POST "$BASE/debug/decode-token" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
Expand Down
17 changes: 17 additions & 0 deletions src/routes/inventory.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {jwtMiddleware} from "../utils/jwt.js";
import {asyncHandler} from "../utils/async.js";
import {badRequest} from "../utils/httpError.js";
import {getBalances, getInventory} from "../services/minecraft.service.js";
import {getEntityTokenForPlayer, getInventoryItems} from "../services/playfab.service.js";

const router = express.Router();

Expand All @@ -21,4 +22,20 @@ router.get("/entitlements", jwtMiddleware, asyncHandler(async (req, res) => {
res.json({count: data.length, entitlements: data});
}));

router.get("/playfab/items", jwtMiddleware, asyncHandler(async (req, res) => {
const sessionTicket = req.headers["x-playfab-session"];
const playfabId = req.headers["x-playfab-id"];
if (!sessionTicket || !playfabId) throw badRequest("x-playfab-session and x-playfab-id are required");
const filter = String(req.query.filter || "").trim();
const countRaw = req.query.count;
const count = countRaw === undefined ? null : Number(countRaw);
if (count !== null && (!Number.isInteger(count) || count < 1 || count > 200)) {
throw badRequest("count must be an integer between 1 and 200");
}
const continuationToken = req.query.continuationToken ? String(req.query.continuationToken) : null;
const entityToken = await getEntityTokenForPlayer(sessionTicket, playfabId);
const data = await getInventoryItems(entityToken, {filter, count, continuationToken});
res.json(data);
}));

export default router;
24 changes: 24 additions & 0 deletions src/services/playfab.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,27 @@ export async function redeemOnestore(entityToken, redeemToken, xuid) {
throw internal("Failed to redeem", err.response?.data || err.message);
}
}

export async function getEntityTokenForPlayer(sessionTicket, playfabId) {
const token = await getEntityToken(sessionTicket, {Id: playfabId, Type: "master_player_account"});
if (!token) throw internal("Failed to resolve EntityToken");
return token;
}

export async function getInventoryItems(entityToken, {filter, count, continuationToken} = {}) {
const url = `https://${env.PLAYFAB_TITLE_ID}.playfabapi.com/Inventory/GetInventoryItems`;
try {
const body = {};
if (filter) body.Filter = filter;
if (count !== null && count !== undefined) body.Count = count;
if (continuationToken) body.ContinuationToken = continuationToken;
const {data} = await http.post(url, body, {
headers: {
"Content-Type": "application/json", "X-EntityToken": entityToken, Accept: "application/json"
}
});
return data?.data || {};
} catch (err) {
throw internal("Failed to get PlayFab inventory items", err.response?.data || err.message);
}
}
40 changes: 40 additions & 0 deletions src/utils/swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,46 @@ const options = {
200: {description: "List of entitlements owned by the player."}
}
}
}, "/inventory/playfab/items": {
get: {
tags: ["Inventory"],
summary: "Get PlayFab inventory items",
description: "Returns PlayFab Economy inventory items using official PlayFab filter queries.",
parameters: [{
in: "header",
name: "x-playfab-session",
required: true,
schema: {type: "string"},
description: "PlayFab SessionTicket used to mint an EntityToken."
}, {
in: "header",
name: "x-playfab-id",
required: true,
schema: {type: "string"},
description: "PlayFab master_player_account id for the user."
}, {
in: "query",
name: "filter",
required: false,
schema: {type: "string"},
description: "PlayFab Economy filter query string."
}, {
in: "query",
name: "count",
required: false,
schema: {type: "integer", minimum: 1, maximum: 200},
description: "Optional page size (1-200)."
}, {
in: "query",
name: "continuationToken",
required: false,
schema: {type: "string"},
description: "Continuation token from a previous response."
}],
responses: {
200: {description: "Inventory items payload from PlayFab."}
}
}
}, "/purchase/quote": {
post: {
tags: ["Purchase"],
Expand Down