@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.
- package/.github/workflows/tests.yml +28 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/index.js +7 -0
- package/package.json +19 -0
- package/src/InspectLink.js +320 -0
- package/src/ItemPreviewData.js +84 -0
- package/src/Sticker.js +48 -0
- package/src/proto/reader.js +128 -0
- package/src/proto/writer.js +137 -0
- package/tests/inspect_link.test.js +361 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test, describe } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const InspectLink = require('../src/InspectLink');
|
|
7
|
+
const ItemPreviewData = require('../src/ItemPreviewData');
|
|
8
|
+
const Sticker = require('../src/Sticker');
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Known test vectors
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
// A real CS2 item encoded with XOR key 0xE3
|
|
15
|
+
const NATIVE_HEX = (
|
|
16
|
+
'E3F3367440334DE2FBE4C345E0CBE0D3E7DB6943400AE0A379E481ECEBE2F36F' +
|
|
17
|
+
'D9DE2BDB515EA6E30D74D981ECEBE3F37BCBDE640D475DA6E35EFCD881ECEBE3' +
|
|
18
|
+
'F359D5DE37E9D75DA6436DD3DD81ECEBE3F366DCDE3F8F9BDDA69B43B6DE81EC' +
|
|
19
|
+
'EBE3F33BC8DEBB1CA3DFA623F7DDDF8B71E293EBFD43382B'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// A tool-generated link with key 0x00
|
|
23
|
+
const TOOL_HEX = '00183C20B803280538E9A3C5DD0340E102C246A0D1';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helper
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function roundtrip(data) {
|
|
29
|
+
return InspectLink.deserialize(InspectLink.serialize(data));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Deserialize tests — native XOR key 0xE3
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
describe('deserialize — native XOR link (key 0xE3)', () => {
|
|
37
|
+
test('itemid', () => {
|
|
38
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
39
|
+
assert.equal(item.itemId, 46876117973);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('defindex (AK-47)', () => {
|
|
43
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
44
|
+
assert.equal(item.defIndex, 7);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('paintindex', () => {
|
|
48
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
49
|
+
assert.equal(item.paintIndex, 422);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('paintseed', () => {
|
|
53
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
54
|
+
assert.equal(item.paintSeed, 922);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('paintwear approximately 0.04121', () => {
|
|
58
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
59
|
+
assert.ok(Math.abs(item.paintWear - 0.04121) < 0.0001, `Expected ~0.04121, got ${item.paintWear}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('rarity', () => {
|
|
63
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
64
|
+
assert.equal(item.rarity, 3);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('quality', () => {
|
|
68
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
69
|
+
assert.equal(item.quality, 4);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('sticker count = 5', () => {
|
|
73
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
74
|
+
assert.equal(item.stickers.length, 5);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('sticker IDs [7436, 5144, 6970, 8069, 5592]', () => {
|
|
78
|
+
const item = InspectLink.deserialize(NATIVE_HEX);
|
|
79
|
+
const ids = item.stickers.map(s => s.stickerId);
|
|
80
|
+
assert.deepEqual(ids, [7436, 5144, 6970, 8069, 5592]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Deserialize tests — tool hex (key 0x00)
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
describe('deserialize — tool-generated link (key 0x00)', () => {
|
|
89
|
+
test('defindex', () => {
|
|
90
|
+
const item = InspectLink.deserialize(TOOL_HEX);
|
|
91
|
+
assert.equal(item.defIndex, 60);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('paintindex', () => {
|
|
95
|
+
const item = InspectLink.deserialize(TOOL_HEX);
|
|
96
|
+
assert.equal(item.paintIndex, 440);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('paintseed', () => {
|
|
100
|
+
const item = InspectLink.deserialize(TOOL_HEX);
|
|
101
|
+
assert.equal(item.paintSeed, 353);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('paintwear', () => {
|
|
105
|
+
const item = InspectLink.deserialize(TOOL_HEX);
|
|
106
|
+
assert.ok(
|
|
107
|
+
Math.abs(item.paintWear - 0.005411375779658556) < 1e-7,
|
|
108
|
+
`Expected ~0.005411375779658556, got ${item.paintWear}`,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('rarity', () => {
|
|
113
|
+
const item = InspectLink.deserialize(TOOL_HEX);
|
|
114
|
+
assert.equal(item.rarity, 5);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('lowercase hex accepted', () => {
|
|
118
|
+
const item = InspectLink.deserialize(TOOL_HEX.toLowerCase());
|
|
119
|
+
assert.equal(item.defIndex, 60);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('full steam:// URL accepted', () => {
|
|
123
|
+
const url = `steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20A${TOOL_HEX}`;
|
|
124
|
+
const item = InspectLink.deserialize(url);
|
|
125
|
+
assert.equal(item.defIndex, 60);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('csgo:// style URL with literal space accepted', () => {
|
|
129
|
+
const url = `csgo://rungame/730/76561202255233023/+csgo_econ_action_preview A${TOOL_HEX}`;
|
|
130
|
+
const item = InspectLink.deserialize(url);
|
|
131
|
+
assert.equal(item.defIndex, 60);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('payload too short throws TypeError', () => {
|
|
135
|
+
assert.throws(() => InspectLink.deserialize('0000'), TypeError);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Serialize tests
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
describe('serialize', () => {
|
|
144
|
+
test('known hex output matches TOOL_HEX', () => {
|
|
145
|
+
const data = new ItemPreviewData({
|
|
146
|
+
defIndex: 60,
|
|
147
|
+
paintIndex: 440,
|
|
148
|
+
paintSeed: 353,
|
|
149
|
+
paintWear: 0.005411375779658556,
|
|
150
|
+
rarity: 5,
|
|
151
|
+
});
|
|
152
|
+
assert.equal(InspectLink.serialize(data), TOOL_HEX);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('returns uppercase hex', () => {
|
|
156
|
+
const data = new ItemPreviewData({ defIndex: 1 });
|
|
157
|
+
const result = InspectLink.serialize(data);
|
|
158
|
+
assert.equal(result, result.toUpperCase());
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('starts with "00" (key_byte = 0x00)', () => {
|
|
162
|
+
const data = new ItemPreviewData({ defIndex: 1 });
|
|
163
|
+
assert.ok(InspectLink.serialize(data).startsWith('00'));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('minimum length >= 12 hex chars (6 bytes)', () => {
|
|
167
|
+
const data = new ItemPreviewData({ defIndex: 1 });
|
|
168
|
+
assert.ok(InspectLink.serialize(data).length >= 12);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Round-trip tests
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
describe('round-trip', () => {
|
|
177
|
+
test('defindex', () => {
|
|
178
|
+
assert.equal(roundtrip(new ItemPreviewData({ defIndex: 7 })).defIndex, 7);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('paintindex', () => {
|
|
182
|
+
assert.equal(roundtrip(new ItemPreviewData({ paintIndex: 422 })).paintIndex, 422);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('paintseed', () => {
|
|
186
|
+
assert.equal(roundtrip(new ItemPreviewData({ paintSeed: 999 })).paintSeed, 999);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('paintwear float32 precision', () => {
|
|
190
|
+
const original = 0.123456789;
|
|
191
|
+
// Compute expected float32 round-trip value
|
|
192
|
+
const dv = new DataView(new ArrayBuffer(4));
|
|
193
|
+
dv.setFloat32(0, original, true);
|
|
194
|
+
const expected = dv.getFloat32(0, true);
|
|
195
|
+
const result = roundtrip(new ItemPreviewData({ paintWear: original }));
|
|
196
|
+
assert.ok(Math.abs(result.paintWear - expected) < 1e-7);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('large itemid (46876117973)', () => {
|
|
200
|
+
const result = roundtrip(new ItemPreviewData({ itemId: 46876117973 }));
|
|
201
|
+
assert.equal(result.itemId, 46876117973);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('stickers — count and ids', () => {
|
|
205
|
+
const data = new ItemPreviewData({
|
|
206
|
+
defIndex: 7,
|
|
207
|
+
stickers: [
|
|
208
|
+
new Sticker({ slot: 0, stickerId: 7436 }),
|
|
209
|
+
new Sticker({ slot: 1, stickerId: 5144 }),
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
const result = roundtrip(data);
|
|
213
|
+
assert.equal(result.stickers.length, 2);
|
|
214
|
+
assert.equal(result.stickers[0].stickerId, 7436);
|
|
215
|
+
assert.equal(result.stickers[1].stickerId, 5144);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('sticker slot', () => {
|
|
219
|
+
const data = new ItemPreviewData({
|
|
220
|
+
stickers: [new Sticker({ slot: 3, stickerId: 123 })],
|
|
221
|
+
});
|
|
222
|
+
assert.equal(roundtrip(data).stickers[0].slot, 3);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('sticker wear (fixed32 float)', () => {
|
|
226
|
+
const data = new ItemPreviewData({
|
|
227
|
+
stickers: [new Sticker({ stickerId: 1, wear: 0.5 })],
|
|
228
|
+
});
|
|
229
|
+
const result = roundtrip(data);
|
|
230
|
+
assert.ok(result.stickers[0].wear !== null);
|
|
231
|
+
assert.ok(Math.abs(result.stickers[0].wear - 0.5) < 1e-6);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('keychains — stickerId and pattern', () => {
|
|
235
|
+
const data = new ItemPreviewData({
|
|
236
|
+
keychains: [new Sticker({ slot: 0, stickerId: 999, pattern: 42 })],
|
|
237
|
+
});
|
|
238
|
+
const result = roundtrip(data);
|
|
239
|
+
assert.equal(result.keychains.length, 1);
|
|
240
|
+
assert.equal(result.keychains[0].stickerId, 999);
|
|
241
|
+
assert.equal(result.keychains[0].pattern, 42);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('customName string', () => {
|
|
245
|
+
const data = new ItemPreviewData({ defIndex: 7, customName: 'My Knife' });
|
|
246
|
+
assert.equal(roundtrip(data).customName, 'My Knife');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('rarity and quality', () => {
|
|
250
|
+
const data = new ItemPreviewData({ rarity: 6, quality: 9 });
|
|
251
|
+
const result = roundtrip(data);
|
|
252
|
+
assert.equal(result.rarity, 6);
|
|
253
|
+
assert.equal(result.quality, 9);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('full item with 5 stickers', () => {
|
|
257
|
+
const data = new ItemPreviewData({
|
|
258
|
+
itemId: 46876117973,
|
|
259
|
+
defIndex: 7,
|
|
260
|
+
paintIndex: 422,
|
|
261
|
+
rarity: 3,
|
|
262
|
+
quality: 4,
|
|
263
|
+
paintWear: 0.04121,
|
|
264
|
+
paintSeed: 922,
|
|
265
|
+
stickers: [
|
|
266
|
+
new Sticker({ slot: 0, stickerId: 7436 }),
|
|
267
|
+
new Sticker({ slot: 1, stickerId: 5144 }),
|
|
268
|
+
new Sticker({ slot: 2, stickerId: 6970 }),
|
|
269
|
+
new Sticker({ slot: 3, stickerId: 8069 }),
|
|
270
|
+
new Sticker({ slot: 4, stickerId: 5592 }),
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
const result = roundtrip(data);
|
|
274
|
+
assert.equal(result.defIndex, 7);
|
|
275
|
+
assert.equal(result.paintIndex, 422);
|
|
276
|
+
assert.equal(result.paintSeed, 922);
|
|
277
|
+
assert.equal(result.stickers.length, 5);
|
|
278
|
+
assert.deepEqual(result.stickers.map(s => s.stickerId), [7436, 5144, 6970, 8069, 5592]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('empty stickers array', () => {
|
|
282
|
+
const data = new ItemPreviewData({ defIndex: 7, stickers: [] });
|
|
283
|
+
const result = roundtrip(data);
|
|
284
|
+
assert.equal(result.stickers.length, 0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Validation and hybrid URL tests
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
const HYBRID_URL = (
|
|
293
|
+
'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20' +
|
|
294
|
+
'S76561199323320483A50075495125D1101C4C4FCD4AB10092D31B8143914211829A1FAE3FD125119591141117308191301' +
|
|
295
|
+
'EA550C1111912E3C111151D12C413E6BAC54D1D29BAD731E191501B92C2C9B6BF92F5411C25B2A731E191501B92C2C' +
|
|
296
|
+
'EA2B182E5411F7212A731E191501B92C2C4F89C12F549164592A799713611956F4339F'
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const CLASSIC_URL = (
|
|
300
|
+
'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20' +
|
|
301
|
+
'S76561199842063946A49749521570D2751293026650298712'
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
describe('isMasked', () => {
|
|
305
|
+
test('returns true for pure hex payload URL', () => {
|
|
306
|
+
const url = `steam://run/730//+csgo_econ_action_preview%20${TOOL_HEX}`;
|
|
307
|
+
assert.equal(InspectLink.isMasked(url), true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('returns true for full native masked URL', () => {
|
|
311
|
+
const url = `steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20${NATIVE_HEX}`;
|
|
312
|
+
assert.equal(InspectLink.isMasked(url), true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('returns true for hybrid URL', () => {
|
|
316
|
+
assert.equal(InspectLink.isMasked(HYBRID_URL), true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('returns false for classic URL', () => {
|
|
320
|
+
assert.equal(InspectLink.isMasked(CLASSIC_URL), false);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('isClassic', () => {
|
|
325
|
+
test('returns true for classic URL', () => {
|
|
326
|
+
assert.equal(InspectLink.isClassic(CLASSIC_URL), true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('returns false for masked URL', () => {
|
|
330
|
+
const url = `steam://run/730//+csgo_econ_action_preview%20${TOOL_HEX}`;
|
|
331
|
+
assert.equal(InspectLink.isClassic(url), false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('returns false for hybrid URL', () => {
|
|
335
|
+
assert.equal(InspectLink.isClassic(HYBRID_URL), false);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('deserialize — hybrid URL', () => {
|
|
340
|
+
test('itemId equals assetId from URL (50075495125)', () => {
|
|
341
|
+
const item = InspectLink.deserialize(HYBRID_URL);
|
|
342
|
+
assert.equal(item.itemId, 50075495125);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Checksum test
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
describe('checksum', () => {
|
|
351
|
+
test('known hex checksum matches', () => {
|
|
352
|
+
const data = new ItemPreviewData({
|
|
353
|
+
defIndex: 60,
|
|
354
|
+
paintIndex: 440,
|
|
355
|
+
paintSeed: 353,
|
|
356
|
+
paintWear: 0.005411375779658556,
|
|
357
|
+
rarity: 5,
|
|
358
|
+
});
|
|
359
|
+
assert.equal(InspectLink.serialize(data), TOOL_HEX);
|
|
360
|
+
});
|
|
361
|
+
});
|