@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/dist/cjs/InspectLink.d.ts +45 -0
  4. package/dist/cjs/InspectLink.d.ts.map +1 -0
  5. package/dist/cjs/InspectLink.js +368 -0
  6. package/dist/cjs/ItemPreviewData.d.ts +59 -0
  7. package/dist/cjs/ItemPreviewData.d.ts.map +1 -0
  8. package/dist/cjs/ItemPreviewData.js +28 -0
  9. package/dist/cjs/Sticker.d.ts +34 -0
  10. package/dist/cjs/Sticker.d.ts.map +1 -0
  11. package/dist/cjs/Sticker.js +19 -0
  12. package/dist/cjs/crc32.d.ts +9 -0
  13. package/dist/cjs/crc32.d.ts.map +1 -0
  14. package/dist/cjs/crc32.js +28 -0
  15. package/dist/cjs/index.d.ts +10 -0
  16. package/dist/cjs/index.d.ts.map +1 -0
  17. package/dist/cjs/index.js +15 -0
  18. package/dist/cjs/proto/reader.d.ts +42 -0
  19. package/dist/cjs/proto/reader.d.ts.map +1 -0
  20. package/dist/cjs/proto/reader.js +105 -0
  21. package/dist/cjs/proto/writer.d.ts +31 -0
  22. package/dist/cjs/proto/writer.d.ts.map +1 -0
  23. package/dist/cjs/proto/writer.js +105 -0
  24. package/dist/esm/InspectLink.d.ts +45 -0
  25. package/dist/esm/InspectLink.d.ts.map +1 -0
  26. package/dist/esm/InspectLink.js +364 -0
  27. package/dist/esm/ItemPreviewData.d.ts +59 -0
  28. package/dist/esm/ItemPreviewData.d.ts.map +1 -0
  29. package/dist/esm/ItemPreviewData.js +24 -0
  30. package/dist/esm/Sticker.d.ts +34 -0
  31. package/dist/esm/Sticker.d.ts.map +1 -0
  32. package/dist/esm/Sticker.js +15 -0
  33. package/dist/esm/crc32.d.ts +9 -0
  34. package/dist/esm/crc32.d.ts.map +1 -0
  35. package/dist/esm/crc32.js +25 -0
  36. package/dist/esm/index.d.ts +10 -0
  37. package/dist/esm/index.d.ts.map +1 -0
  38. package/dist/esm/index.js +6 -0
  39. package/dist/esm/proto/reader.d.ts +42 -0
  40. package/dist/esm/proto/reader.d.ts.map +1 -0
  41. package/dist/esm/proto/reader.js +101 -0
  42. package/dist/esm/proto/writer.d.ts +31 -0
  43. package/dist/esm/proto/writer.d.ts.map +1 -0
  44. package/dist/esm/proto/writer.js +101 -0
  45. 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;