Skip to content

Commit bd79b16

Browse files
committed
feat: CS2 masked inspect URL decoder — pure JavaScript implementation
0 parents  commit bd79b16

11 files changed

Lines changed: 1386 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
node: ["18", "20", "22"]
16+
17+
name: Node.js ${{ matrix.node }}
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: ${{ matrix.node }}
26+
27+
- name: Run tests
28+
run: npm test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
.DS_Store
3+
*.log
4+
.idea/

README.md

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# cs2-masked-inspect (JavaScript)
2+
3+
Pure JavaScript library for encoding and decoding CS2 masked inspect links — no external dependencies.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @vladdnepr/cs2-masked-inspect
9+
```
10+
11+
## Usage
12+
13+
### Deserialize a CS2 inspect link
14+
15+
```javascript
16+
const { InspectLink } = require('@vladdnepr/cs2-masked-inspect');
17+
18+
// Accepts a full steam:// URL or a raw hex string
19+
const item = InspectLink.deserialize(
20+
'steam://run/730//+csgo_econ_action_preview%20E3F3367440334DE2FBE4C345E0CBE0D3...'
21+
);
22+
23+
console.log(item.defIndex); // 7 (AK-47)
24+
console.log(item.paintIndex); // 422
25+
console.log(item.paintSeed); // 922
26+
console.log(item.paintWear); // ~0.04121
27+
console.log(item.itemId); // 46876117973
28+
29+
item.stickers.forEach(s => console.log(s.stickerId));
30+
// 7436, 5144, 6970, 8069, 5592
31+
```
32+
33+
### Serialize an item to a hex payload
34+
35+
```javascript
36+
const { InspectLink, ItemPreviewData } = require('@vladdnepr/cs2-masked-inspect');
37+
38+
const data = new ItemPreviewData({
39+
defIndex: 60,
40+
paintIndex: 440,
41+
paintSeed: 353,
42+
paintWear: 0.005411375779658556,
43+
rarity: 5,
44+
});
45+
46+
const hex = InspectLink.serialize(data);
47+
// 00183C20B803280538E9A3C5DD0340E102C246A0D1
48+
49+
const url = `steam://run/730//+csgo_econ_action_preview%20${hex}`;
50+
```
51+
52+
### Item with stickers and keychains
53+
54+
```javascript
55+
const { InspectLink, ItemPreviewData, Sticker } = require('@vladdnepr/cs2-masked-inspect');
56+
57+
const data = new ItemPreviewData({
58+
defIndex: 7,
59+
paintIndex: 422,
60+
paintSeed: 922,
61+
paintWear: 0.04121,
62+
rarity: 3,
63+
quality: 4,
64+
stickers: [
65+
new Sticker({ slot: 0, stickerId: 7436 }),
66+
new Sticker({ slot: 1, stickerId: 5144, wear: 0.1 }),
67+
],
68+
});
69+
70+
const hex = InspectLink.serialize(data);
71+
const decoded = InspectLink.deserialize(hex); // round-trip
72+
```
73+
74+
---
75+
76+
## Validation
77+
78+
Use `InspectLink.isMasked()` and `InspectLink.isClassic()` to detect the link type without decoding it.
79+
80+
```javascript
81+
const { InspectLink } = require('@vladdnepr/cs2-masked-inspect');
82+
83+
// New masked format (pure hex blob) — can be decoded offline
84+
const maskedUrl = 'steam://run/730//+csgo_econ_action_preview%20E3F3...';
85+
InspectLink.isMasked(maskedUrl); // true
86+
InspectLink.isClassic(maskedUrl); // false
87+
88+
// Hybrid format (S/A/D prefix with hex proto after D) — also decodable offline
89+
const hybridUrl = 'steam://rungame/730/.../+csgo_econ_action_preview%20S76561199323320483A50075495125D1101C4C4FCD4AB10...';
90+
InspectLink.isMasked(hybridUrl); // true
91+
InspectLink.isClassic(hybridUrl); // false
92+
93+
// Classic format — requires Steam Game Coordinator to fetch item info
94+
const classicUrl = 'steam://rungame/730/.../+csgo_econ_action_preview%20S76561199842063946A49749521570D2751293026650298712';
95+
InspectLink.isMasked(classicUrl); // false
96+
InspectLink.isClassic(classicUrl); // true
97+
```
98+
99+
---
100+
101+
## How the format works
102+
103+
Three URL formats are handled:
104+
105+
1. **New masked format** — pure hex blob after `csgo_econ_action_preview`:
106+
```
107+
steam://run/730//+csgo_econ_action_preview%20<hexbytes>
108+
```
109+
110+
2. **Hybrid format** — old-style `S/A/D` prefix, but with a hex proto appended after `D` (instead of a decimal did):
111+
```
112+
steam://rungame/730/.../+csgo_econ_action_preview%20S<steamid>A<assetid>D<hexproto>
113+
```
114+
115+
3. **Classic format** — old-style `S/A/D` with a decimal did; requires Steam GC to resolve item details.
116+
117+
For formats 1 and 2 the library decodes the item offline. For format 3 only URL parsing is possible.
118+
119+
The hex blob (formats 1 and 2) has the following binary layout:
120+
121+
```
122+
[key_byte] [proto_bytes XOR'd with key] [4-byte checksum XOR'd with key]
123+
```
124+
125+
| Section | Size | Description |
126+
|---------|------|-------------|
127+
| `key_byte` | 1 byte | XOR key. `0x00` = no obfuscation (tool links). Other values = native CS2 links. |
128+
| `proto_bytes` | variable | `CEconItemPreviewDataBlock` protobuf, each byte XOR'd with `key_byte`. |
129+
| `checksum` | 4 bytes | Big-endian uint32, XOR'd with `key_byte`. |
130+
131+
### Checksum algorithm
132+
133+
```javascript
134+
const buffer = Buffer.concat([Buffer.from([0x00]), protoBytes]);
135+
const crc = crc32(buffer); // standard CRC32
136+
const xored = ((crc & 0xFFFF) ^ (protoBytes.length * crc)) >>> 0;
137+
const checksum = Buffer.alloc(4);
138+
checksum.writeUInt32BE(xored, 0); // big-endian uint32
139+
```
140+
141+
### `paintWear` encoding
142+
143+
`paintWear` is stored as a `uint32` varint whose bit pattern is the IEEE 754 representation
144+
of a `float32`. The library handles this transparently — callers always work with regular
145+
JavaScript `number` values.
146+
147+
---
148+
149+
## Proto field reference
150+
151+
### CEconItemPreviewDataBlock
152+
153+
| Field | Number | Type | JS property |
154+
|-------|--------|------|-------------|
155+
| `accountid` | 1 | uint32 | `accountId` |
156+
| `itemid` | 2 | uint64 | `itemId` |
157+
| `defindex` | 3 | uint32 | `defIndex` |
158+
| `paintindex` | 4 | uint32 | `paintIndex` |
159+
| `rarity` | 5 | uint32 | `rarity` |
160+
| `quality` | 6 | uint32 | `quality` |
161+
| `paintwear` | 7 | uint32* | `paintWear` (float32 reinterpreted as uint32) |
162+
| `paintseed` | 8 | uint32 | `paintSeed` |
163+
| `killeaterscoretype` | 9 | uint32 | `killEaterScoreType` |
164+
| `killeatervalue` | 10 | uint32 | `killEaterValue` |
165+
| `customname` | 11 | string | `customName` |
166+
| `stickers` | 12 | repeated Sticker | `stickers` |
167+
| `inventory` | 13 | uint32 | `inventory` |
168+
| `origin` | 14 | uint32 | `origin` |
169+
| `questid` | 15 | uint32 | `questId` |
170+
| `dropreason` | 16 | uint32 | `dropReason` |
171+
| `musicindex` | 17 | uint32 | `musicIndex` |
172+
| `entindex` | 18 | int32 | `entIndex` |
173+
| `petindex` | 19 | uint32 | `petIndex` |
174+
| `keychains` | 20 | repeated Sticker | `keychains` |
175+
176+
### Sticker
177+
178+
| Field | Number | Type | JS property |
179+
|-------|--------|------|-------------|
180+
| `slot` | 1 | uint32 | `slot` |
181+
| `sticker_id` | 2 | uint32 | `stickerId` |
182+
| `wear` | 3 | float32 | `wear` |
183+
| `scale` | 4 | float32 | `scale` |
184+
| `rotation` | 5 | float32 | `rotation` |
185+
| `tint_id` | 6 | uint32 | `tintId` |
186+
| `offset_x` | 7 | float32 | `offsetX` |
187+
| `offset_y` | 8 | float32 | `offsetY` |
188+
| `offset_z` | 9 | float32 | `offsetZ` |
189+
| `pattern` | 10 | uint32 | `pattern` |
190+
191+
---
192+
193+
## Known test vectors
194+
195+
### Vector 1 — Native CS2 link (XOR key 0xE3)
196+
197+
```
198+
E3F3367440334DE2FBE4C345E0CBE0D3E7DB6943400AE0A379E481ECEBE2F36F
199+
D9DE2BDB515EA6E30D74D981ECEBE3F37BCBDE640D475DA6E35EFCD881ECEBE3
200+
F359D5DE37E9D75DA6436DD3DD81ECEBE3F366DCDE3F8F9BDDA69B43B6DE81EC
201+
EBE3F33BC8DEBB1CA3DFA623F7DDDF8B71E293EBFD43382B
202+
```
203+
204+
| Field | Value |
205+
|-------|-------|
206+
| `itemId` | `46876117973` |
207+
| `defIndex` | `7` (AK-47) |
208+
| `paintIndex` | `422` |
209+
| `paintSeed` | `922` |
210+
| `paintWear` | `≈ 0.04121` |
211+
| `rarity` | `3` |
212+
| `quality` | `4` |
213+
| sticker IDs | `[7436, 5144, 6970, 8069, 5592]` |
214+
215+
### Vector 2 — Tool-generated link (key 0x00)
216+
217+
```javascript
218+
new ItemPreviewData({ defIndex: 60, paintIndex: 440, paintSeed: 353,
219+
paintWear: 0.005411375779658556, rarity: 5 })
220+
```
221+
222+
Expected hex:
223+
224+
```
225+
00183C20B803280538E9A3C5DD0340E102C246A0D1
226+
```
227+
228+
---
229+
230+
## Running tests
231+
232+
```bash
233+
npm test
234+
```
235+
236+
36 tests using the Node.js built-in `node:test` runner — no external test framework required.
237+
238+
---
239+
240+
## Contributing
241+
242+
Bug reports and pull requests are welcome on [GitHub](https://github.com/vladdnepr/cs2-masked-inspect-js).
243+
244+
1. Fork the repository
245+
2. Create a branch: `git checkout -b my-fix`
246+
3. Make your changes and add tests
247+
4. Ensure all tests pass: `npm test`
248+
5. Open a Pull Request
249+
250+
All PRs require the CI checks to pass before merging.
251+
252+
---
253+
254+
## Author
255+
256+
[Vladyslav Lyshenko](https://github.com/vladdnepr)vladdnepr1989@gmail.com

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
const InspectLink = require('./src/InspectLink');
4+
const ItemPreviewData = require('./src/ItemPreviewData');
5+
const Sticker = require('./src/Sticker');
6+
7+
module.exports = { InspectLink, ItemPreviewData, Sticker };

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@vladdnepr/cs2-masked-inspect",
3+
"version": "1.0.0",
4+
"description": "Offline decoder/encoder for CS2 masked inspect URLs — pure JavaScript, no dependencies",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "node --test tests/inspect_link.test.js"
8+
},
9+
"keywords": ["cs2", "csgo", "inspect", "protobuf", "steam"],
10+
"author": {
11+
"name": "Vladyslav Lyshenko",
12+
"email": "vladdnepr1989@gmail.com",
13+
"url": "https://github.com/vladdnepr"
14+
},
15+
"license": "MIT",
16+
"engines": {
17+
"node": ">=18.0.0"
18+
}
19+
}

0 commit comments

Comments
 (0)