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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/node_modules
/dist/public/vaks.json
2 changes: 0 additions & 2 deletions .prettierignore

This file was deleted.

5 changes: 0 additions & 5 deletions .prettierrc

This file was deleted.

31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# 2.0.0
## Breaking changes
- `search()` now returns a clean, denormalised structure: `{ offers, lowestPrices }`.
Each offer already has its `merchant`, `edition` and `region` resolved to a
readable name (instead of numeric ids), plus `currentPrice`, `minDiscountPrice`,
`couponCode` and `lastUpdate`. `lowestPrices.official` and `lowestPrices.keyshops`
expose the cheapest official-store and key-reseller price (either may be `null`).
This replaces the previous `{ success, offers, merchants, editions, regions }` shape.

## Fixes
- Fix search returning no results: migrated to the `price_history_api` endpoint,
as the old `admin-ajax` offers endpoint had stopped returning data.
- Cache the game catalog in the OS temp directory instead of inside the package,
so it keeps working when installed as a read-only dependency.
- Only cache successful catalog responses (previously an error payload could be
written to disk and read back as valid for a day).

## Improvements
- Deduplicate concurrent catalog downloads onto a single in-flight request.
- Reuse the fuzzy-search index across searches instead of rebuilding it every call.
- Behavioural test suite covering the transform, store/name filtering, currency
handling and the error/empty paths.
- The build no longer emits test files into `dist`.

## Tooling
- Upgrade all dependencies to their latest versions (TypeScript 6.0, Jest 30,
`@types/node` 26, …).
- Modernise `tsconfig` for TypeScript 6.0: `moduleResolution: nodenext` and an
explicit `types` field (TS 6.0 no longer auto-includes `@types/*`).
- Replace ESLint + Prettier with [Biome](https://biomejs.dev) (`npm run check`).

# 1.3.0
- Remove axios dependency and use fetch instead.
- Upgrade dev dependencies.
Expand Down
53 changes: 53 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# CLAUDE.md

Guidance for working in this repository.

## What this is

`allkeyshop-api` — an unofficial TypeScript client for [AllKeyShop](https://www.allkeyshop.com).
Published to npm as a library (`main` → `dist/src/allkeyshop.js`). There is no server;
consumers import `AllkeyshopService` and call `search()` / `find()`.

## Commands

- `npm run build` — compile with `tsc` to `dist/` (emits `dist/src` + `dist/config` only).
- `npm test` — runs `build` then Jest (ts-jest type-checks the tests).
- `npm run check` — Biome: format + lint + organise imports, with `--write`. Use this before committing.
- `npm run lint` / `npm run format` — lint-only / format-only.

## Architecture

The public surface is one class with two methods:

- `AllkeyshopService.search(name)` → `GameOffers` — offers + lowest prices for the best-matching game.
- `AllkeyshopService.find(name)` → `ProductIdsResponse` — just the matching game names/ids, no pricing.

Constructor options: `currency` (default `eur`), `platform` (default PC; appended to the
search term, `pc` means none), `store` (default any; filters offers by merchant name).

Data flow for `search()`:

1. `gather.ts:getProductIds` → `fetch.ts:fetchAllGames` downloads the **full** game
catalog (`vaks.php`) once, caches it on disk (`os.tmpdir()`, 1-day TTL) and in memory,
then `filter.ts:filterByName` fuzzy-matches the name locally.
2. `gather.ts:getGameData` fetches pricing for the top match (`price_history_api.php`) and
**transforms** the raw response into the clean `GameOffers` shape (resolving merchant/
edition/region ids to names, parsing prices). `filterByStore` narrows offers when a
`store` is set.

Files: `allkeyshop.ts` (public class) · `gather.ts` (lookup + pricing transform + public
types) · `fetch.ts` (catalog download/cache) · `file.ts` (cache dir) · `filter.ts` (fuzzy
filters) · `config/constants.ts` (defaults).

## Conventions & gotchas

- **Two external endpoints**, both undocumented and unstable: `vaks.php` (catalog) and
`price_history_api.php` (pricing). If search breaks, suspect an endpoint change first —
that is exactly what 2.0.0 fixed.
- **Raw vs public types**: the raw API shapes (`Raw*`) are internal to `gather.ts`; only
the transformed `Offer` / `LowestPrice` / `GameOffers` types are exported. Keep that boundary.
- **`dist/` is committed** and shipped (`files: ["dist"]`). Rebuild it when source changes.
- **Tests are behavioural** — assert observable behaviour, not implementation/existence.
Mocks live in `tests/mock/`.
- Formatting is Biome-enforced: single quotes, no semicolons, 2-space indent. Non-null
assertions are allowed in `tests/` only.
65 changes: 29 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,57 +36,50 @@ allkeyshopService.search('Borderlands 3').then((data) => {

// Output:
// {
// success: true,
// offers: [
// {
// id: 133508130,
// affiliateUrl: 'https://www.g2a.com/borderlands-3-standard-edition-steam-key-europe-i10000186970058?aid=13344657&gtag=dccb1b16c9&utm_content=COM_GLOBAL_PB_PLUS_GAM_LISTING_NOR_allkeyshopPLUS',
// isActive: true,
// merchant: '61616',
// price: [Object],
// edition: '1',
// region: '9',
// stock: 'InStock',
// platform: 'steam'
// },
// ...
// ],
// merchants: {
// '1': {
// id: '1',
// name: 'Steam',
// aggregateRating: [Object],
// types: 'Official Store',
// searchable: 1,
// paymentMethods: [Object],
// logoSlug: 'steam',
// reviewUrl: 'https://www.allkeyshop.com/blog/review/steam/'
// },
// ...
// },
// editions: {
// '1': { id: '1', name: 'Standard' },
// ...
// },
// regions: {
// '1': { id: '1', name: 'GLOBAL', filterName: 'PUBLISHER GLOBAL' },
// {
// merchant: 'Kinguin',
// edition: 'Standard Edition',
// region: 'Steam',
// currentPrice: 38.66,
// minDiscountPrice: 37.37,
// couponCode: 'AKSGAME',
// lastUpdate: '2026-06-19 18:28:55'
// },
// ...
// ],
// lowestPrices: {
// official: {
// merchant: 'Kinguin',
// price: 38.66,
// lastUpdate: '2026-06-19 18:28:55'
// },
// keyshops: {
// merchant: 'G2A',
// price: 41.19,
// lastUpdate: '2026-06-19 03:02:53'
// }
// }
// }
```

Each offer already has its `merchant`, `edition` and `region` resolved to a
readable name. `lowestPrices.official` is the cheapest official-store price and
`lowestPrices.keyshops` the cheapest key-reseller price (either may be `null`
when no data is available).

### Get game names without data
```typescript
allkeyshopService.find('FIFA 22').then((data) => {
allkeyshopService.find('DARK SOULS III').then((data) => {
console.log(data)
})

// Output:
// {
// status: 'success',
// games: [
// { id: '83060', name: 'FIFA 22' },
// { id: '83063', name: 'FIFA 22 PS4' },
// { id: '83060', name: 'DARK SOULS' },
// { id: '83063', name: 'DARK SOULS REMASTERED' },
// ...
// ]
// }
Expand Down
41 changes: 41 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["src/**/*.ts", "tests/**/*.ts", "config/**/*.ts"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"quoteStyle": "single",
"trailingCommas": "es5"
}
},
"overrides": [
{
"includes": ["tests/**/*.ts"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off"
}
}
}
}
]
}
4 changes: 2 additions & 2 deletions dist/src/allkeyshop.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ProductIdsResponse, type ProductSellingDetails } from './gather';
import { type GameOffers, type ProductIdsResponse } from './gather';
export declare class AllkeyshopService {
private readonly currency;
private readonly platform;
Expand All @@ -8,7 +8,7 @@ export declare class AllkeyshopService {
platform?: string;
store?: string;
});
search(name: string): Promise<ProductSellingDetails>;
search(name: string): Promise<GameOffers>;
find(name: string): Promise<ProductIdsResponse>;
private appendPlatform;
private emptyData;
Expand Down
58 changes: 21 additions & 37 deletions dist/src/allkeyshop.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/src/allkeyshop.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 1 addition & 5 deletions dist/src/fetch.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { type BasicGameData } from './gather';
export interface ProductListResponse {
status: string;
games?: BasicGameData[];
}
import type { BasicGameData } from './gather';
declare const fetchAllGames: () => Promise<BasicGameData[] | undefined>;
export { fetchAllGames };
Loading
Loading