@vlydev/cs2-masked-inspect-ts 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.
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/dist/cjs/InspectLink.d.ts +45 -0
- package/dist/cjs/InspectLink.d.ts.map +1 -0
- package/dist/cjs/InspectLink.js +368 -0
- package/dist/cjs/ItemPreviewData.d.ts +59 -0
- package/dist/cjs/ItemPreviewData.d.ts.map +1 -0
- package/dist/cjs/ItemPreviewData.js +28 -0
- package/dist/cjs/Sticker.d.ts +34 -0
- package/dist/cjs/Sticker.d.ts.map +1 -0
- package/dist/cjs/Sticker.js +19 -0
- package/dist/cjs/crc32.d.ts +9 -0
- package/dist/cjs/crc32.d.ts.map +1 -0
- package/dist/cjs/crc32.js +28 -0
- package/dist/cjs/index.d.ts +10 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +15 -0
- package/dist/cjs/proto/reader.d.ts +42 -0
- package/dist/cjs/proto/reader.d.ts.map +1 -0
- package/dist/cjs/proto/reader.js +105 -0
- package/dist/cjs/proto/writer.d.ts +31 -0
- package/dist/cjs/proto/writer.d.ts.map +1 -0
- package/dist/cjs/proto/writer.js +105 -0
- package/dist/esm/InspectLink.d.ts +45 -0
- package/dist/esm/InspectLink.d.ts.map +1 -0
- package/dist/esm/InspectLink.js +364 -0
- package/dist/esm/ItemPreviewData.d.ts +59 -0
- package/dist/esm/ItemPreviewData.d.ts.map +1 -0
- package/dist/esm/ItemPreviewData.js +24 -0
- package/dist/esm/Sticker.d.ts +34 -0
- package/dist/esm/Sticker.d.ts.map +1 -0
- package/dist/esm/Sticker.js +15 -0
- package/dist/esm/crc32.d.ts +9 -0
- package/dist/esm/crc32.d.ts.map +1 -0
- package/dist/esm/crc32.js +25 -0
- package/dist/esm/index.d.ts +10 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/proto/reader.d.ts +42 -0
- package/dist/esm/proto/reader.d.ts.map +1 -0
- package/dist/esm/proto/reader.js +101 -0
- package/dist/esm/proto/writer.d.ts +31 -0
- package/dist/esm/proto/writer.d.ts.map +1 -0
- package/dist/esm/proto/writer.js +101 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 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,221 @@
|
|
|
1
|
+
# @vlydev/cs2-masked-inspect-ts
|
|
2
|
+
|
|
3
|
+
TypeScript library for encoding and decoding CS2 masked inspect links — fully typed, no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vlydev/cs2-masked-inspect-ts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { InspectLink, ItemPreviewData, Sticker } from '@vlydev/cs2-masked-inspect-ts';
|
|
15
|
+
|
|
16
|
+
// Decode a masked inspect link payload or full URL
|
|
17
|
+
const item = InspectLink.deserialize('00183C20B803280538E9A3C5DD0340E102C246A0D1');
|
|
18
|
+
console.log(item.defIndex); // 60
|
|
19
|
+
console.log(item.paintIndex); // 440
|
|
20
|
+
console.log(item.paintWear); // 0.005411375779658556
|
|
21
|
+
|
|
22
|
+
// Also accepts full steam:// URLs
|
|
23
|
+
const item2 = InspectLink.deserialize(
|
|
24
|
+
'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20A00183C20B803280538...'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Encode an item back to a hex payload
|
|
28
|
+
const data = new ItemPreviewData({
|
|
29
|
+
defIndex: 7,
|
|
30
|
+
paintIndex: 422,
|
|
31
|
+
paintSeed: 922,
|
|
32
|
+
paintWear: 0.04121,
|
|
33
|
+
rarity: 3,
|
|
34
|
+
quality: 4,
|
|
35
|
+
stickers: [
|
|
36
|
+
new Sticker({ slot: 0, stickerId: 7436 }),
|
|
37
|
+
new Sticker({ slot: 1, stickerId: 5144 }),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
const hex = InspectLink.serialize(data);
|
|
41
|
+
console.log(hex); // uppercase hex string starting with '00'
|
|
42
|
+
|
|
43
|
+
// Check link type
|
|
44
|
+
InspectLink.isMasked('steam://...csgo_econ_action_preview%20A0011...'); // true — decodable offline
|
|
45
|
+
InspectLink.isClassic('steam://...csgo_econ_action_preview%20S123A456D789'); // true — classic S/A/D format
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### `InspectLink`
|
|
51
|
+
|
|
52
|
+
#### `InspectLink.serialize(data: ItemPreviewData): string`
|
|
53
|
+
|
|
54
|
+
Encodes an `ItemPreviewData` to an uppercase hex payload (key_byte = 0x00, no XOR).
|
|
55
|
+
|
|
56
|
+
Throws:
|
|
57
|
+
- `RangeError` if `paintWear` is outside `[0.0, 1.0]`
|
|
58
|
+
- `RangeError` if `customName` exceeds 100 characters
|
|
59
|
+
|
|
60
|
+
#### `InspectLink.deserialize(input: string): ItemPreviewData`
|
|
61
|
+
|
|
62
|
+
Decodes a hex payload or full inspect URL into an `ItemPreviewData`.
|
|
63
|
+
|
|
64
|
+
Accepts:
|
|
65
|
+
- Raw hex strings (uppercase or lowercase)
|
|
66
|
+
- `steam://rungame/...` URLs
|
|
67
|
+
- `csgo://rungame/...` URLs
|
|
68
|
+
- Hybrid format: `S\d+A\d+D<hexproto>`
|
|
69
|
+
|
|
70
|
+
Handles the XOR obfuscation used in native CS2 links (key_byte != 0x00).
|
|
71
|
+
|
|
72
|
+
Throws:
|
|
73
|
+
- `RangeError` if payload exceeds 4096 hex chars
|
|
74
|
+
- `TypeError` if payload is too short (< 6 bytes) or invalid hex
|
|
75
|
+
|
|
76
|
+
#### `InspectLink.isMasked(link: string): boolean`
|
|
77
|
+
|
|
78
|
+
Returns `true` if the link contains a decodable protobuf payload (can be decoded offline without Steam API).
|
|
79
|
+
|
|
80
|
+
#### `InspectLink.isClassic(link: string): boolean`
|
|
81
|
+
|
|
82
|
+
Returns `true` if the link is a classic `S/A/D` inspect URL with decimal D value (requires Steam API to decode).
|
|
83
|
+
|
|
84
|
+
### `ItemPreviewData`
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
new ItemPreviewData({
|
|
88
|
+
accountId?: number | bigint; // Steam account ID
|
|
89
|
+
itemId?: number | bigint; // Item asset ID (may exceed 2^32)
|
|
90
|
+
defIndex?: number; // Weapon/item definition index
|
|
91
|
+
paintIndex?: number; // Skin paint index
|
|
92
|
+
rarity?: number; // Item rarity
|
|
93
|
+
quality?: number; // Item quality
|
|
94
|
+
paintWear?: number | null; // Float value [0.0, 1.0], null if not applicable
|
|
95
|
+
paintSeed?: number; // Pattern seed [0, 1000]
|
|
96
|
+
killEaterScoreType?: number; // StatTrak type
|
|
97
|
+
killEaterValue?: number; // StatTrak value
|
|
98
|
+
customName?: string; // Custom name tag (max 100 chars)
|
|
99
|
+
stickers?: Sticker[]; // Applied stickers (fields 1-4)
|
|
100
|
+
inventory?: number;
|
|
101
|
+
origin?: number;
|
|
102
|
+
questId?: number;
|
|
103
|
+
dropReason?: number;
|
|
104
|
+
musicIndex?: number;
|
|
105
|
+
entIndex?: number;
|
|
106
|
+
petIndex?: number;
|
|
107
|
+
keychains?: Sticker[]; // Applied keychains (field 5 slot)
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `Sticker`
|
|
112
|
+
|
|
113
|
+
Used for both stickers and keychains.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
new Sticker({
|
|
117
|
+
slot?: number; // Slot position on the item
|
|
118
|
+
stickerId?: number; // Sticker/keychain definition ID
|
|
119
|
+
wear?: number | null; // Sticker wear (float32, wire type 5)
|
|
120
|
+
scale?: number | null; // Sticker scale (float32, wire type 5)
|
|
121
|
+
rotation?: number | null; // Sticker rotation (float32, wire type 5)
|
|
122
|
+
tintId?: number; // Tint/color ID
|
|
123
|
+
offsetX?: number | null; // X offset (float32, wire type 5)
|
|
124
|
+
offsetY?: number | null; // Y offset (float32, wire type 5)
|
|
125
|
+
offsetZ?: number | null; // Z offset (float32, wire type 5)
|
|
126
|
+
pattern?: number; // Pattern index
|
|
127
|
+
highlightReel?: number | null; // Highlight reel frame (keychains)
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Binary Format
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
[key_byte] [proto_bytes XOR'd with key] [4-byte checksum XOR'd with key]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `key_byte = 0x00` — tool-generated links (no XOR)
|
|
138
|
+
- `key_byte != 0x00` — native CS2 links (every byte XOR'd with key_byte)
|
|
139
|
+
|
|
140
|
+
### Checksum Algorithm
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
buffer = [0x00] + proto_bytes
|
|
144
|
+
crc = crc32(buffer) // polynomial 0xEDB88320
|
|
145
|
+
xored = (crc & 0xffff) ^ (len(proto_bytes) * crc) [unsigned 32-bit]
|
|
146
|
+
checksum = big-endian uint32
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### PaintWear Encoding
|
|
150
|
+
|
|
151
|
+
`paintWear` is stored on the wire as a `uint32` varint whose bit pattern is the IEEE 754 float32 representation of the float value. This library handles the conversion transparently using `DataView`.
|
|
152
|
+
|
|
153
|
+
## Protobuf Schema Reference
|
|
154
|
+
|
|
155
|
+
`CEconItemPreviewDataBlock` fields:
|
|
156
|
+
|
|
157
|
+
| Field | Name | Type |
|
|
158
|
+
|-------|------|------|
|
|
159
|
+
| 1 | accountId | varint |
|
|
160
|
+
| 2 | itemId | varint (uint64) |
|
|
161
|
+
| 3 | defIndex | varint |
|
|
162
|
+
| 4 | paintIndex | varint |
|
|
163
|
+
| 5 | rarity | varint |
|
|
164
|
+
| 6 | quality | varint |
|
|
165
|
+
| 7 | paintWear | varint (float32 bits) |
|
|
166
|
+
| 8 | paintSeed | varint |
|
|
167
|
+
| 9 | killEaterScoreType | varint |
|
|
168
|
+
| 10 | killEaterValue | varint |
|
|
169
|
+
| 11 | customName | length-delimited (string) |
|
|
170
|
+
| 12 | stickers | length-delimited (repeated) |
|
|
171
|
+
| 13 | inventory | varint |
|
|
172
|
+
| 14 | origin | varint |
|
|
173
|
+
| 15 | questId | varint |
|
|
174
|
+
| 16 | dropReason | varint |
|
|
175
|
+
| 17 | musicIndex | varint |
|
|
176
|
+
| 18 | entIndex | varint |
|
|
177
|
+
| 19 | petIndex | varint |
|
|
178
|
+
| 20 | keychains | length-delimited (repeated) |
|
|
179
|
+
|
|
180
|
+
`Sticker` fields:
|
|
181
|
+
|
|
182
|
+
| Field | Name | Type |
|
|
183
|
+
|-------|------|------|
|
|
184
|
+
| 1 | slot | varint |
|
|
185
|
+
| 2 | stickerId | varint |
|
|
186
|
+
| 3 | wear | fixed32 LE (float32) |
|
|
187
|
+
| 4 | scale | fixed32 LE (float32) |
|
|
188
|
+
| 5 | rotation | fixed32 LE (float32) |
|
|
189
|
+
| 6 | tintId | varint |
|
|
190
|
+
| 7 | offsetX | fixed32 LE (float32) |
|
|
191
|
+
| 8 | offsetY | fixed32 LE (float32) |
|
|
192
|
+
| 9 | offsetZ | fixed32 LE (float32) |
|
|
193
|
+
| 10 | pattern | varint |
|
|
194
|
+
| 11 | highlightReel | varint (omit if null) |
|
|
195
|
+
|
|
196
|
+
## Test Vectors
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Tool-generated (key 0x00): DefIndex=60, PaintIndex=440, PaintSeed=353, PaintWear≈0.00541, Rarity=5
|
|
200
|
+
'00183C20B803280538E9A3C5DD0340E102C246A0D1'
|
|
201
|
+
|
|
202
|
+
// Native CS2 (key 0xE3): ItemId=46876117973, DefIndex=7, PaintIndex=422, PaintSeed=922, PaintWear≈0.04121
|
|
203
|
+
// Stickers: [7436, 5144, 6970, 8069, 5592]
|
|
204
|
+
'E3F3367440334DE2FBE4C345E0CBE0D3...'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Running Tests
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npm test
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Tests use Node.js built-in test runner (`node:test`) with `--experimental-strip-types` (Node 22+) to run TypeScript directly without compilation.
|
|
214
|
+
|
|
215
|
+
## Contributing
|
|
216
|
+
|
|
217
|
+
Pull requests welcome. Please ensure `npm test` passes before submitting.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes and decodes CS2 masked inspect links.
|
|
3
|
+
*
|
|
4
|
+
* Binary format:
|
|
5
|
+
* [key_byte] [proto_bytes XOR'd with key] [4-byte checksum XOR'd with key]
|
|
6
|
+
*
|
|
7
|
+
* For tool-generated links key_byte = 0x00 (no XOR needed).
|
|
8
|
+
* For native CS2 links key_byte != 0x00 — every byte must be XOR'd before parsing.
|
|
9
|
+
*
|
|
10
|
+
* Checksum:
|
|
11
|
+
* buffer = [0x00] + proto_bytes
|
|
12
|
+
* crc = crc32(buffer)
|
|
13
|
+
* xored = (crc & 0xffff) ^ (len(proto_bytes) * crc) [unsigned 32-bit]
|
|
14
|
+
* checksum = big-endian uint32 of (xored & 0xFFFFFFFF)
|
|
15
|
+
*/
|
|
16
|
+
import { ItemPreviewData } from './ItemPreviewData.ts';
|
|
17
|
+
export declare class InspectLink {
|
|
18
|
+
/**
|
|
19
|
+
* Encode an ItemPreviewData to an uppercase hex inspect-link payload.
|
|
20
|
+
*
|
|
21
|
+
* The returned string can be appended to a steam:// inspect URL or used
|
|
22
|
+
* standalone. The key_byte is always 0x00 (no XOR applied).
|
|
23
|
+
*/
|
|
24
|
+
static serialize(data: ItemPreviewData): string;
|
|
25
|
+
/**
|
|
26
|
+
* Returns true if the link contains a decodable protobuf payload (can be decoded offline).
|
|
27
|
+
*/
|
|
28
|
+
static isMasked(link: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if the link is a classic S/A/D inspect URL with decimal did.
|
|
31
|
+
*/
|
|
32
|
+
static isClassic(link: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Decode an inspect-link hex payload (or full URL) into an ItemPreviewData.
|
|
35
|
+
*
|
|
36
|
+
* Accepts:
|
|
37
|
+
* - A raw uppercase or lowercase hex string
|
|
38
|
+
* - A full steam://rungame/... inspect URL
|
|
39
|
+
* - A CS2-style csgo://rungame/... URL
|
|
40
|
+
*
|
|
41
|
+
* Handles the XOR obfuscation used in native CS2 links.
|
|
42
|
+
*/
|
|
43
|
+
static deserialize(input: string): ItemPreviewData;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=InspectLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InspectLink.d.ts","sourceRoot":"","sources":["../../src/InspectLink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA6PvD,qBAAa,WAAW;IACtB;;;;;OAKG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM;IAiB/C;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAOtC;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIvC;;;;;;;;;OASG;IACH,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe;CA+BnD"}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Encodes and decodes CS2 masked inspect links.
|
|
4
|
+
*
|
|
5
|
+
* Binary format:
|
|
6
|
+
* [key_byte] [proto_bytes XOR'd with key] [4-byte checksum XOR'd with key]
|
|
7
|
+
*
|
|
8
|
+
* For tool-generated links key_byte = 0x00 (no XOR needed).
|
|
9
|
+
* For native CS2 links key_byte != 0x00 — every byte must be XOR'd before parsing.
|
|
10
|
+
*
|
|
11
|
+
* Checksum:
|
|
12
|
+
* buffer = [0x00] + proto_bytes
|
|
13
|
+
* crc = crc32(buffer)
|
|
14
|
+
* xored = (crc & 0xffff) ^ (len(proto_bytes) * crc) [unsigned 32-bit]
|
|
15
|
+
* checksum = big-endian uint32 of (xored & 0xFFFFFFFF)
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.InspectLink = void 0;
|
|
19
|
+
const ItemPreviewData_ts_1 = require("./ItemPreviewData.js");
|
|
20
|
+
const Sticker_ts_1 = require("./Sticker.js");
|
|
21
|
+
const reader_ts_1 = require("./proto/reader.js");
|
|
22
|
+
const writer_ts_1 = require("./proto/writer.js");
|
|
23
|
+
const crc32_ts_1 = require("./crc32.js");
|
|
24
|
+
// ------------------------------------------------------------------
|
|
25
|
+
// Checksum helpers
|
|
26
|
+
// ------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Compute checksum over [0x00] + proto_bytes.
|
|
29
|
+
* Returns a 4-byte big-endian Uint8Array.
|
|
30
|
+
*/
|
|
31
|
+
function computeChecksum(buffer, protoLen) {
|
|
32
|
+
const crcVal = (0, crc32_ts_1.crc32)(buffer);
|
|
33
|
+
const val = BigInt(((crcVal & 0xFFFF) ^ (protoLen * crcVal)) >>> 0) & 0xffffffffn;
|
|
34
|
+
const result = new Uint8Array(4);
|
|
35
|
+
const dv = new DataView(result.buffer);
|
|
36
|
+
dv.setUint32(0, Number(val), false); // big-endian
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
// ------------------------------------------------------------------
|
|
40
|
+
// float32 <-> uint32 reinterpretation
|
|
41
|
+
// ------------------------------------------------------------------
|
|
42
|
+
function float32ToUint32(f) {
|
|
43
|
+
const dv = new DataView(new ArrayBuffer(4));
|
|
44
|
+
dv.setFloat32(0, f, true); // little-endian
|
|
45
|
+
return dv.getUint32(0, true);
|
|
46
|
+
}
|
|
47
|
+
function uint32ToFloat32(u) {
|
|
48
|
+
const dv = new DataView(new ArrayBuffer(4));
|
|
49
|
+
dv.setUint32(0, u >>> 0, true);
|
|
50
|
+
return dv.getFloat32(0, true);
|
|
51
|
+
}
|
|
52
|
+
// ------------------------------------------------------------------
|
|
53
|
+
// Hex encoding / decoding helpers
|
|
54
|
+
// ------------------------------------------------------------------
|
|
55
|
+
function hexToBytes(hex) {
|
|
56
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
57
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
58
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
59
|
+
}
|
|
60
|
+
return bytes;
|
|
61
|
+
}
|
|
62
|
+
function bytesToHex(bytes) {
|
|
63
|
+
return Array.from(bytes)
|
|
64
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
65
|
+
.join('')
|
|
66
|
+
.toUpperCase();
|
|
67
|
+
}
|
|
68
|
+
function concatBytes(...arrays) {
|
|
69
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
70
|
+
const result = new Uint8Array(totalLength);
|
|
71
|
+
let offset = 0;
|
|
72
|
+
for (const arr of arrays) {
|
|
73
|
+
result.set(arr, offset);
|
|
74
|
+
offset += arr.length;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
// ------------------------------------------------------------------
|
|
79
|
+
// URL extraction
|
|
80
|
+
// ------------------------------------------------------------------
|
|
81
|
+
const INSPECT_URL_RE = /(?:%20|\s|\+)A([0-9A-Fa-f]+)/i;
|
|
82
|
+
const HYBRID_URL_RE = /S\d+A\d+D([0-9A-Fa-f]+)$/i;
|
|
83
|
+
const CLASSIC_URL_RE = /csgo_econ_action_preview(?:%20|\s)[SM]\d+A\d+D\d+$/i;
|
|
84
|
+
const MASKED_URL_RE = /csgo_econ_action_preview(?:%20|\s)[0-9A-Fa-f]{10,}$/i;
|
|
85
|
+
function extractHex(input) {
|
|
86
|
+
const stripped = input.trim();
|
|
87
|
+
// Hybrid format: S\d+A\d+D<hexproto>
|
|
88
|
+
const mh = stripped.match(HYBRID_URL_RE);
|
|
89
|
+
if (mh && /[A-Fa-f]/.test(mh[1]))
|
|
90
|
+
return mh[1];
|
|
91
|
+
// Classic/market URL: A<hex> preceded by %20, space, or + (A is a prefix marker, not hex).
|
|
92
|
+
// If stripping A yields odd-length hex, A is actually the first byte of the payload —
|
|
93
|
+
// fall through to the pure-masked check below which captures it with A included.
|
|
94
|
+
const m = stripped.match(INSPECT_URL_RE);
|
|
95
|
+
if (m && m[1].length % 2 === 0)
|
|
96
|
+
return m[1];
|
|
97
|
+
// Pure masked format: csgo_econ_action_preview%20<hexblob> (no S/A/M prefix).
|
|
98
|
+
// Also handles payloads whose first hex character happens to be A.
|
|
99
|
+
const mm = stripped.match(/csgo_econ_action_preview(?:%20|\s|\+)([0-9A-Fa-f]{10,})$/i);
|
|
100
|
+
if (mm)
|
|
101
|
+
return mm[1];
|
|
102
|
+
// Bare hex — strip whitespace
|
|
103
|
+
return stripped.replace(/\s+/g, '');
|
|
104
|
+
}
|
|
105
|
+
// ------------------------------------------------------------------
|
|
106
|
+
// Sticker encode / decode
|
|
107
|
+
// ------------------------------------------------------------------
|
|
108
|
+
function encodeSticker(s) {
|
|
109
|
+
const w = new writer_ts_1.ProtoWriter();
|
|
110
|
+
w.writeUint32(1, s.slot);
|
|
111
|
+
w.writeUint32(2, s.stickerId);
|
|
112
|
+
if (s.wear !== null)
|
|
113
|
+
w.writeFloat32Fixed(3, s.wear);
|
|
114
|
+
if (s.scale !== null)
|
|
115
|
+
w.writeFloat32Fixed(4, s.scale);
|
|
116
|
+
if (s.rotation !== null)
|
|
117
|
+
w.writeFloat32Fixed(5, s.rotation);
|
|
118
|
+
w.writeUint32(6, s.tintId);
|
|
119
|
+
if (s.offsetX !== null)
|
|
120
|
+
w.writeFloat32Fixed(7, s.offsetX);
|
|
121
|
+
if (s.offsetY !== null)
|
|
122
|
+
w.writeFloat32Fixed(8, s.offsetY);
|
|
123
|
+
if (s.offsetZ !== null)
|
|
124
|
+
w.writeFloat32Fixed(9, s.offsetZ);
|
|
125
|
+
w.writeUint32(10, s.pattern);
|
|
126
|
+
if (s.highlightReel != null)
|
|
127
|
+
w.writeUint32(11, s.highlightReel);
|
|
128
|
+
return w.toBytes();
|
|
129
|
+
}
|
|
130
|
+
function decodeSticker(data) {
|
|
131
|
+
const reader = new reader_ts_1.ProtoReader(data);
|
|
132
|
+
const s = new Sticker_ts_1.Sticker();
|
|
133
|
+
for (const f of reader.readAllFields()) {
|
|
134
|
+
switch (f.field) {
|
|
135
|
+
case 1:
|
|
136
|
+
s.slot = Number(f.value);
|
|
137
|
+
break;
|
|
138
|
+
case 2:
|
|
139
|
+
s.stickerId = Number(f.value);
|
|
140
|
+
break;
|
|
141
|
+
case 3: {
|
|
142
|
+
const bytes = f.value;
|
|
143
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
144
|
+
s.wear = dv.getFloat32(0, true);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 4: {
|
|
148
|
+
const bytes = f.value;
|
|
149
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
150
|
+
s.scale = dv.getFloat32(0, true);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 5: {
|
|
154
|
+
const bytes = f.value;
|
|
155
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
156
|
+
s.rotation = dv.getFloat32(0, true);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 6:
|
|
160
|
+
s.tintId = Number(f.value);
|
|
161
|
+
break;
|
|
162
|
+
case 7: {
|
|
163
|
+
const bytes = f.value;
|
|
164
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
165
|
+
s.offsetX = dv.getFloat32(0, true);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 8: {
|
|
169
|
+
const bytes = f.value;
|
|
170
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
171
|
+
s.offsetY = dv.getFloat32(0, true);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 9: {
|
|
175
|
+
const bytes = f.value;
|
|
176
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
177
|
+
s.offsetZ = dv.getFloat32(0, true);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case 10:
|
|
181
|
+
s.pattern = Number(f.value);
|
|
182
|
+
break;
|
|
183
|
+
case 11:
|
|
184
|
+
s.highlightReel = Number(f.value);
|
|
185
|
+
break;
|
|
186
|
+
default: break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return s;
|
|
190
|
+
}
|
|
191
|
+
// ------------------------------------------------------------------
|
|
192
|
+
// ItemPreviewData encode / decode
|
|
193
|
+
// ------------------------------------------------------------------
|
|
194
|
+
function encodeItem(item) {
|
|
195
|
+
const w = new writer_ts_1.ProtoWriter();
|
|
196
|
+
w.writeUint32(1, item.accountId);
|
|
197
|
+
w.writeUint64(2, item.itemId);
|
|
198
|
+
w.writeUint32(3, item.defIndex);
|
|
199
|
+
w.writeUint32(4, item.paintIndex);
|
|
200
|
+
w.writeUint32(5, item.rarity);
|
|
201
|
+
w.writeUint32(6, item.quality);
|
|
202
|
+
// paintWear: float32 reinterpreted as uint32 varint
|
|
203
|
+
if (item.paintWear != null) {
|
|
204
|
+
w.writeUint32(7, float32ToUint32(item.paintWear));
|
|
205
|
+
}
|
|
206
|
+
w.writeUint32(8, item.paintSeed);
|
|
207
|
+
w.writeUint32(9, item.killEaterScoreType);
|
|
208
|
+
w.writeUint32(10, item.killEaterValue);
|
|
209
|
+
w.writeString(11, item.customName);
|
|
210
|
+
for (const sticker of item.stickers) {
|
|
211
|
+
w.writeRawBytes(12, encodeSticker(sticker));
|
|
212
|
+
}
|
|
213
|
+
w.writeUint32(13, item.inventory);
|
|
214
|
+
w.writeUint32(14, item.origin);
|
|
215
|
+
w.writeUint32(15, item.questId);
|
|
216
|
+
w.writeUint32(16, item.dropReason);
|
|
217
|
+
w.writeUint32(17, item.musicIndex);
|
|
218
|
+
w.writeInt32(18, item.entIndex);
|
|
219
|
+
w.writeUint32(19, item.petIndex);
|
|
220
|
+
for (const kc of item.keychains) {
|
|
221
|
+
w.writeRawBytes(20, encodeSticker(kc));
|
|
222
|
+
}
|
|
223
|
+
return w.toBytes();
|
|
224
|
+
}
|
|
225
|
+
function decodeItem(data) {
|
|
226
|
+
const reader = new reader_ts_1.ProtoReader(data);
|
|
227
|
+
const item = new ItemPreviewData_ts_1.ItemPreviewData();
|
|
228
|
+
for (const f of reader.readAllFields()) {
|
|
229
|
+
switch (f.field) {
|
|
230
|
+
case 1:
|
|
231
|
+
item.accountId = Number(f.value);
|
|
232
|
+
break;
|
|
233
|
+
case 2:
|
|
234
|
+
item.itemId = Number(f.value);
|
|
235
|
+
break;
|
|
236
|
+
case 3:
|
|
237
|
+
item.defIndex = Number(f.value);
|
|
238
|
+
break;
|
|
239
|
+
case 4:
|
|
240
|
+
item.paintIndex = Number(f.value);
|
|
241
|
+
break;
|
|
242
|
+
case 5:
|
|
243
|
+
item.rarity = Number(f.value);
|
|
244
|
+
break;
|
|
245
|
+
case 6:
|
|
246
|
+
item.quality = Number(f.value);
|
|
247
|
+
break;
|
|
248
|
+
case 7:
|
|
249
|
+
item.paintWear = uint32ToFloat32(Number(f.value));
|
|
250
|
+
break;
|
|
251
|
+
case 8:
|
|
252
|
+
item.paintSeed = Number(f.value);
|
|
253
|
+
break;
|
|
254
|
+
case 9:
|
|
255
|
+
item.killEaterScoreType = Number(f.value);
|
|
256
|
+
break;
|
|
257
|
+
case 10:
|
|
258
|
+
item.killEaterValue = Number(f.value);
|
|
259
|
+
break;
|
|
260
|
+
case 11:
|
|
261
|
+
item.customName = new TextDecoder().decode(f.value);
|
|
262
|
+
break;
|
|
263
|
+
case 12:
|
|
264
|
+
item.stickers.push(decodeSticker(f.value));
|
|
265
|
+
break;
|
|
266
|
+
case 13:
|
|
267
|
+
item.inventory = Number(f.value);
|
|
268
|
+
break;
|
|
269
|
+
case 14:
|
|
270
|
+
item.origin = Number(f.value);
|
|
271
|
+
break;
|
|
272
|
+
case 15:
|
|
273
|
+
item.questId = Number(f.value);
|
|
274
|
+
break;
|
|
275
|
+
case 16:
|
|
276
|
+
item.dropReason = Number(f.value);
|
|
277
|
+
break;
|
|
278
|
+
case 17:
|
|
279
|
+
item.musicIndex = Number(f.value);
|
|
280
|
+
break;
|
|
281
|
+
case 18:
|
|
282
|
+
item.entIndex = Number(f.value);
|
|
283
|
+
break;
|
|
284
|
+
case 19:
|
|
285
|
+
item.petIndex = Number(f.value);
|
|
286
|
+
break;
|
|
287
|
+
case 20:
|
|
288
|
+
item.keychains.push(decodeSticker(f.value));
|
|
289
|
+
break;
|
|
290
|
+
default: break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return item;
|
|
294
|
+
}
|
|
295
|
+
// ------------------------------------------------------------------
|
|
296
|
+
// Public API
|
|
297
|
+
// ------------------------------------------------------------------
|
|
298
|
+
class InspectLink {
|
|
299
|
+
/**
|
|
300
|
+
* Encode an ItemPreviewData to an uppercase hex inspect-link payload.
|
|
301
|
+
*
|
|
302
|
+
* The returned string can be appended to a steam:// inspect URL or used
|
|
303
|
+
* standalone. The key_byte is always 0x00 (no XOR applied).
|
|
304
|
+
*/
|
|
305
|
+
static serialize(data) {
|
|
306
|
+
if (data.paintWear != null && (data.paintWear < 0.0 || data.paintWear > 1.0)) {
|
|
307
|
+
throw new RangeError(`paintwear must be in [0.0, 1.0], got ${data.paintWear}`);
|
|
308
|
+
}
|
|
309
|
+
if (data.customName != null && data.customName.length > 100) {
|
|
310
|
+
throw new RangeError(`customname must not exceed 100 characters, got ${data.customName.length}`);
|
|
311
|
+
}
|
|
312
|
+
const protoBytes = encodeItem(data);
|
|
313
|
+
const buffer = concatBytes(new Uint8Array([0x00]), protoBytes);
|
|
314
|
+
const checksum = computeChecksum(buffer, protoBytes.length);
|
|
315
|
+
return bytesToHex(concatBytes(buffer, checksum));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Returns true if the link contains a decodable protobuf payload (can be decoded offline).
|
|
319
|
+
*/
|
|
320
|
+
static isMasked(link) {
|
|
321
|
+
const s = link.trim();
|
|
322
|
+
if (MASKED_URL_RE.test(s))
|
|
323
|
+
return true;
|
|
324
|
+
const m = s.match(HYBRID_URL_RE);
|
|
325
|
+
return !!(m && /[A-Fa-f]/.test(m[1]));
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Returns true if the link is a classic S/A/D inspect URL with decimal did.
|
|
329
|
+
*/
|
|
330
|
+
static isClassic(link) {
|
|
331
|
+
return CLASSIC_URL_RE.test(link.trim());
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Decode an inspect-link hex payload (or full URL) into an ItemPreviewData.
|
|
335
|
+
*
|
|
336
|
+
* Accepts:
|
|
337
|
+
* - A raw uppercase or lowercase hex string
|
|
338
|
+
* - A full steam://rungame/... inspect URL
|
|
339
|
+
* - A CS2-style csgo://rungame/... URL
|
|
340
|
+
*
|
|
341
|
+
* Handles the XOR obfuscation used in native CS2 links.
|
|
342
|
+
*/
|
|
343
|
+
static deserialize(input) {
|
|
344
|
+
const hex = extractHex(input);
|
|
345
|
+
if (hex.length > 4096) {
|
|
346
|
+
throw new RangeError(`Payload too long (max 4096 hex chars): "${input.slice(0, 64)}..."`);
|
|
347
|
+
}
|
|
348
|
+
const raw = hexToBytes(hex);
|
|
349
|
+
if (raw.length < 6) {
|
|
350
|
+
throw new TypeError(`Payload too short or invalid hex: "${input}"`);
|
|
351
|
+
}
|
|
352
|
+
const key = raw[0];
|
|
353
|
+
let decrypted;
|
|
354
|
+
if (key === 0) {
|
|
355
|
+
decrypted = raw;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
decrypted = new Uint8Array(raw.length);
|
|
359
|
+
for (let i = 0; i < raw.length; i++) {
|
|
360
|
+
decrypted[i] = raw[i] ^ key;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Layout: [key_byte] [proto_bytes] [4-byte checksum]
|
|
364
|
+
const protoBytes = decrypted.slice(1, decrypted.length - 4);
|
|
365
|
+
return decodeItem(protoBytes);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exports.InspectLink = InspectLink;
|