@vlydev/cs2-masked-inspect 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.
@@ -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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VlyDev
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,262 @@
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 @vlydev/cs2-masked-inspect
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Deserialize a CS2 inspect link
14
+
15
+ ```javascript
16
+ const { InspectLink } = require('@vlydev/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('@vlydev/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('@vlydev/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('@vlydev/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/vlydev/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
+ [VlyDev](https://github.com/vlydev) — vladdnepr1989@gmail.com
257
+
258
+ ---
259
+
260
+ ## License
261
+
262
+ MIT © [VlyDev](https://github.com/vlydev)
package/index.js ADDED
@@ -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/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@vlydev/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": "VlyDev",
12
+ "email": "vladdnepr1989@gmail.com",
13
+ "url": "https://github.com/vlydev"
14
+ },
15
+ "license": "MIT",
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ }
19
+ }
@@ -0,0 +1,320 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Encodes and decodes CS2 masked inspect links.
5
+ *
6
+ * Binary format:
7
+ * [key_byte] [proto_bytes XOR'd with key] [4-byte checksum XOR'd with key]
8
+ *
9
+ * For tool-generated links key_byte = 0x00 (no XOR needed).
10
+ * For native CS2 links key_byte != 0x00 — every byte must be XOR'd before parsing.
11
+ *
12
+ * Checksum:
13
+ * buffer = [0x00] + proto_bytes
14
+ * crc = crc32(buffer)
15
+ * xored = (crc & 0xffff) ^ (len(proto_bytes) * crc) [unsigned 32-bit]
16
+ * checksum = big-endian uint32 of (xored & 0xFFFFFFFF)
17
+ */
18
+
19
+ const ItemPreviewData = require('./ItemPreviewData');
20
+ const Sticker = require('./Sticker');
21
+ const { ProtoReader } = require('./proto/reader');
22
+ const { ProtoWriter } = require('./proto/writer');
23
+
24
+ // ------------------------------------------------------------------
25
+ // CRC32 (standard polynomial 0xEDB88320 — same as zlib/PHP crc32)
26
+ // ------------------------------------------------------------------
27
+
28
+ /** @type {Uint32Array} */
29
+ const CRC32_TABLE = (() => {
30
+ const table = new Uint32Array(256);
31
+ for (let i = 0; i < 256; i++) {
32
+ let c = i;
33
+ for (let j = 0; j < 8; j++) {
34
+ c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
35
+ }
36
+ table[i] = c;
37
+ }
38
+ return table;
39
+ })();
40
+
41
+ /**
42
+ * @param {Buffer} buf
43
+ * @returns {number} unsigned 32-bit CRC32
44
+ */
45
+ function crc32(buf) {
46
+ let crc = 0xFFFFFFFF;
47
+ for (let i = 0; i < buf.length; i++) {
48
+ crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ buf[i]) & 0xFF];
49
+ }
50
+ return (crc ^ 0xFFFFFFFF) >>> 0;
51
+ }
52
+
53
+ // ------------------------------------------------------------------
54
+ // Checksum helpers
55
+ // ------------------------------------------------------------------
56
+
57
+ /**
58
+ * @param {Buffer} buffer the [0x00] + proto_bytes buffer
59
+ * @param {number} protoLen
60
+ * @returns {Buffer} 4-byte big-endian checksum
61
+ */
62
+ function computeChecksum(buffer, protoLen) {
63
+ const crcVal = crc32(buffer);
64
+ const val = BigInt(((crcVal & 0xFFFF) ^ (protoLen * crcVal)) >>> 0) & 0xFFFFFFFFn;
65
+ const result = Buffer.alloc(4);
66
+ result.writeUInt32BE(Number(val), 0);
67
+ return result;
68
+ }
69
+
70
+ // ------------------------------------------------------------------
71
+ // float32 <-> uint32 reinterpretation
72
+ // ------------------------------------------------------------------
73
+
74
+ /** @param {number} f @returns {number} */
75
+ function float32ToUint32(f) {
76
+ const dv = new DataView(new ArrayBuffer(4));
77
+ dv.setFloat32(0, f, true); // little-endian
78
+ return dv.getUint32(0, true);
79
+ }
80
+
81
+ /** @param {number} u @returns {number} */
82
+ function uint32ToFloat32(u) {
83
+ const dv = new DataView(new ArrayBuffer(4));
84
+ dv.setUint32(0, u >>> 0, true);
85
+ return dv.getFloat32(0, true);
86
+ }
87
+
88
+ // ------------------------------------------------------------------
89
+ // URL extraction
90
+ // ------------------------------------------------------------------
91
+
92
+ const INSPECT_URL_RE = /(?:%20|\s|\+)A([0-9A-Fa-f]+)/i;
93
+ const HYBRID_URL_RE = /S\d+A\d+D([0-9A-Fa-f]+)$/i;
94
+ const CLASSIC_URL_RE = /csgo_econ_action_preview(?:%20|\s)[SM]\d+A\d+D\d+$/i;
95
+ const MASKED_URL_RE = /csgo_econ_action_preview(?:%20|\s)[0-9A-Fa-f]{10,}$/i;
96
+
97
+ /** @param {string} input @returns {string} */
98
+ function extractHex(input) {
99
+ const stripped = input.trim();
100
+
101
+ // Hybrid format: S\d+A\d+D<hexproto>
102
+ const mh = stripped.match(HYBRID_URL_RE);
103
+ if (mh && /[A-Fa-f]/.test(mh[1])) return mh[1];
104
+
105
+ // Classic/market URL: A<hex> preceded by %20, space, or + (A is a prefix marker, not hex)
106
+ const m = stripped.match(INSPECT_URL_RE);
107
+ if (m) return m[1];
108
+
109
+ // Pure masked format: csgo_econ_action_preview%20<hexblob> (no S/A/M prefix)
110
+ const mm = stripped.match(/csgo_econ_action_preview(?:%20|\s|\+)([0-9A-Fa-f]{10,})$/i);
111
+ if (mm) return mm[1];
112
+
113
+ // Bare hex — strip whitespace
114
+ return stripped.replace(/\s+/g, '');
115
+ }
116
+
117
+ // ------------------------------------------------------------------
118
+ // Sticker encode / decode
119
+ // ------------------------------------------------------------------
120
+
121
+ /** @param {Sticker} s @returns {Buffer} */
122
+ function encodeSticker(s) {
123
+ const w = new ProtoWriter();
124
+ w.writeUint32(1, s.slot);
125
+ w.writeUint32(2, s.stickerId);
126
+ if (s.wear !== null) w.writeFloat32Fixed(3, s.wear);
127
+ if (s.scale !== null) w.writeFloat32Fixed(4, s.scale);
128
+ if (s.rotation !== null) w.writeFloat32Fixed(5, s.rotation);
129
+ w.writeUint32(6, s.tintId);
130
+ if (s.offsetX !== null) w.writeFloat32Fixed(7, s.offsetX);
131
+ if (s.offsetY !== null) w.writeFloat32Fixed(8, s.offsetY);
132
+ if (s.offsetZ !== null) w.writeFloat32Fixed(9, s.offsetZ);
133
+ w.writeUint32(10, s.pattern);
134
+ return w.toBytes();
135
+ }
136
+
137
+ /** @param {Buffer} data @returns {Sticker} */
138
+ function decodeSticker(data) {
139
+ const reader = new ProtoReader(data);
140
+ const s = new Sticker();
141
+
142
+ for (const f of reader.readAllFields()) {
143
+ switch (f.field) {
144
+ case 1: s.slot = Number(f.value); break;
145
+ case 2: s.stickerId = Number(f.value); break;
146
+ case 3: s.wear = f.value.readFloatLE(0); break;
147
+ case 4: s.scale = f.value.readFloatLE(0); break;
148
+ case 5: s.rotation = f.value.readFloatLE(0); break;
149
+ case 6: s.tintId = Number(f.value); break;
150
+ case 7: s.offsetX = f.value.readFloatLE(0); break;
151
+ case 8: s.offsetY = f.value.readFloatLE(0); break;
152
+ case 9: s.offsetZ = f.value.readFloatLE(0); break;
153
+ case 10: s.pattern = Number(f.value); break;
154
+ default: break;
155
+ }
156
+ }
157
+
158
+ return s;
159
+ }
160
+
161
+ // ------------------------------------------------------------------
162
+ // ItemPreviewData encode / decode
163
+ // ------------------------------------------------------------------
164
+
165
+ /** @param {ItemPreviewData} item @returns {Buffer} */
166
+ function encodeItem(item) {
167
+ const w = new ProtoWriter();
168
+ w.writeUint32(1, item.accountId);
169
+ w.writeUint64(2, item.itemId);
170
+ w.writeUint32(3, item.defIndex);
171
+ w.writeUint32(4, item.paintIndex);
172
+ w.writeUint32(5, item.rarity);
173
+ w.writeUint32(6, item.quality);
174
+
175
+ // paintwear: float32 reinterpreted as uint32 varint
176
+ const pwUint32 = float32ToUint32(item.paintWear);
177
+ w.writeUint32(7, pwUint32);
178
+
179
+ w.writeUint32(8, item.paintSeed);
180
+ w.writeUint32(9, item.killEaterScoreType);
181
+ w.writeUint32(10, item.killEaterValue);
182
+ w.writeString(11, item.customName);
183
+
184
+ for (const sticker of item.stickers) {
185
+ w.writeRawBytes(12, encodeSticker(sticker));
186
+ }
187
+
188
+ w.writeUint32(13, item.inventory);
189
+ w.writeUint32(14, item.origin);
190
+ w.writeUint32(15, item.questId);
191
+ w.writeUint32(16, item.dropReason);
192
+ w.writeUint32(17, item.musicIndex);
193
+ w.writeInt32(18, item.entIndex);
194
+ w.writeUint32(19, item.petIndex);
195
+
196
+ for (const kc of item.keychains) {
197
+ w.writeRawBytes(20, encodeSticker(kc));
198
+ }
199
+
200
+ return w.toBytes();
201
+ }
202
+
203
+ /** @param {Buffer} data @returns {ItemPreviewData} */
204
+ function decodeItem(data) {
205
+ const reader = new ProtoReader(data);
206
+ const item = new ItemPreviewData();
207
+
208
+ for (const f of reader.readAllFields()) {
209
+ switch (f.field) {
210
+ case 1: item.accountId = Number(f.value); break;
211
+ case 2: item.itemId = Number(f.value); break;
212
+ case 3: item.defIndex = Number(f.value); break;
213
+ case 4: item.paintIndex = Number(f.value); break;
214
+ case 5: item.rarity = Number(f.value); break;
215
+ case 6: item.quality = Number(f.value); break;
216
+ case 7: item.paintWear = uint32ToFloat32(Number(f.value)); break;
217
+ case 8: item.paintSeed = Number(f.value); break;
218
+ case 9: item.killEaterScoreType = Number(f.value); break;
219
+ case 10: item.killEaterValue = Number(f.value); break;
220
+ case 11: item.customName = f.value.toString('utf8'); break;
221
+ case 12: item.stickers.push(decodeSticker(f.value)); break;
222
+ case 13: item.inventory = Number(f.value); break;
223
+ case 14: item.origin = Number(f.value); break;
224
+ case 15: item.questId = Number(f.value); break;
225
+ case 16: item.dropReason = Number(f.value); break;
226
+ case 17: item.musicIndex = Number(f.value); break;
227
+ case 18: item.entIndex = Number(f.value); break;
228
+ case 19: item.petIndex = Number(f.value); break;
229
+ case 20: item.keychains.push(decodeSticker(f.value)); break;
230
+ default: break;
231
+ }
232
+ }
233
+
234
+ return item;
235
+ }
236
+
237
+ // ------------------------------------------------------------------
238
+ // Public API
239
+ // ------------------------------------------------------------------
240
+
241
+ class InspectLink {
242
+ /**
243
+ * Encode an ItemPreviewData to an uppercase hex inspect-link payload.
244
+ *
245
+ * The returned string can be appended to a steam:// inspect URL or used
246
+ * standalone. The key_byte is always 0x00 (no XOR applied).
247
+ *
248
+ * @param {ItemPreviewData} data
249
+ * @returns {string} uppercase hex string
250
+ */
251
+ static serialize(data) {
252
+ const protoBytes = encodeItem(data);
253
+ const buffer = Buffer.concat([Buffer.from([0x00]), protoBytes]);
254
+ const checksum = computeChecksum(buffer, protoBytes.length);
255
+ return Buffer.concat([buffer, checksum]).toString('hex').toUpperCase();
256
+ }
257
+
258
+ /**
259
+ * Returns true if the link contains a decodable protobuf payload (can be decoded offline).
260
+ * @param {string} link
261
+ * @returns {boolean}
262
+ */
263
+ static isMasked(link) {
264
+ const s = link.trim();
265
+ if (MASKED_URL_RE.test(s)) return true;
266
+ const m = s.match(HYBRID_URL_RE);
267
+ return !!(m && /[A-Fa-f]/.test(m[1]));
268
+ }
269
+
270
+ /**
271
+ * Returns true if the link is a classic S/A/D inspect URL with decimal did.
272
+ * @param {string} link
273
+ * @returns {boolean}
274
+ */
275
+ static isClassic(link) {
276
+ return CLASSIC_URL_RE.test(link.trim());
277
+ }
278
+
279
+ /**
280
+ * Decode an inspect-link hex payload (or full URL) into an ItemPreviewData.
281
+ *
282
+ * Accepts:
283
+ * - A raw uppercase or lowercase hex string
284
+ * - A full steam://rungame/... inspect URL
285
+ * - A CS2-style csgo://rungame/... URL
286
+ *
287
+ * Handles the XOR obfuscation used in native CS2 links.
288
+ *
289
+ * @param {string} input
290
+ * @returns {ItemPreviewData}
291
+ */
292
+ static deserialize(input) {
293
+ const hex = extractHex(input);
294
+ const raw = Buffer.from(hex, 'hex');
295
+
296
+ if (raw.length < 6) {
297
+ throw new TypeError(
298
+ `Payload too short or invalid hex: "${input}"`,
299
+ );
300
+ }
301
+
302
+ const key = raw[0];
303
+ let decrypted;
304
+
305
+ if (key === 0) {
306
+ decrypted = raw;
307
+ } else {
308
+ decrypted = Buffer.alloc(raw.length);
309
+ for (let i = 0; i < raw.length; i++) {
310
+ decrypted[i] = raw[i] ^ key;
311
+ }
312
+ }
313
+
314
+ // Layout: [key_byte] [proto_bytes] [4-byte checksum]
315
+ const protoBytes = decrypted.slice(1, decrypted.length - 4);
316
+ return decodeItem(protoBytes);
317
+ }
318
+ }
319
+
320
+ module.exports = InspectLink;