@vlydev/cs2-masked-inspect 1.0.1 → 1.0.7
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/release.yml +30 -0
- package/README.md +18 -0
- package/package.json +12 -2
- package/src/InspectLink.js +21 -3
- package/src/ItemPreviewData.js +2 -2
- package/src/Sticker.js +3 -0
- package/src/proto/reader.js +4 -0
- package/tests/inspect_link.test.js +88 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish-npm:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: npm
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # required for npm trusted publishing (OIDC)
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Setup Node.js
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: "24"
|
|
22
|
+
registry-url: "https://registry.npmjs.org"
|
|
23
|
+
|
|
24
|
+
- name: Set version from tag
|
|
25
|
+
run: |
|
|
26
|
+
VERSION="${GITHUB_REF_NAME#v}"
|
|
27
|
+
npm version "$VERSION" --no-git-tag-version
|
|
28
|
+
|
|
29
|
+
- name: Publish to npm
|
|
30
|
+
run: npm publish --access public --provenance
|
package/README.md
CHANGED
|
@@ -98,6 +98,24 @@ InspectLink.isClassic(classicUrl); // true
|
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
101
|
+
## Validation rules
|
|
102
|
+
|
|
103
|
+
`deserialize()` enforces:
|
|
104
|
+
|
|
105
|
+
| Rule | Limit | Error |
|
|
106
|
+
|------|-------|-------|
|
|
107
|
+
| Hex payload length | max 4,096 characters | `RangeError` |
|
|
108
|
+
| Protobuf field count | max 100 per message | `RangeError` |
|
|
109
|
+
|
|
110
|
+
`serialize()` enforces:
|
|
111
|
+
|
|
112
|
+
| Field | Constraint | Error |
|
|
113
|
+
|-------|-----------|-------|
|
|
114
|
+
| `paintWear` | `[0.0, 1.0]` | `RangeError` |
|
|
115
|
+
| `customName` | max 100 characters | `RangeError` |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
101
119
|
## How the format works
|
|
102
120
|
|
|
103
121
|
Three URL formats are handled:
|
package/package.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vlydev/cs2-masked-inspect",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Offline decoder/encoder for CS2 masked inspect URLs — pure JavaScript, no dependencies",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node --test tests/inspect_link.test.js"
|
|
8
8
|
},
|
|
9
|
-
"keywords": [
|
|
9
|
+
"keywords": [
|
|
10
|
+
"cs2",
|
|
11
|
+
"csgo",
|
|
12
|
+
"inspect",
|
|
13
|
+
"protobuf",
|
|
14
|
+
"steam"
|
|
15
|
+
],
|
|
10
16
|
"author": {
|
|
11
17
|
"name": "VlyDev",
|
|
12
18
|
"email": "vladdnepr1989@gmail.com",
|
|
13
19
|
"url": "https://github.com/vlydev"
|
|
14
20
|
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/vlydev/cs2-masked-inspect-js"
|
|
24
|
+
},
|
|
15
25
|
"license": "MIT",
|
|
16
26
|
"engines": {
|
|
17
27
|
"node": ">=18.0.0"
|
package/src/InspectLink.js
CHANGED
|
@@ -134,6 +134,7 @@ function encodeSticker(s) {
|
|
|
134
134
|
if (s.offsetY !== null) w.writeFloat32Fixed(8, s.offsetY);
|
|
135
135
|
if (s.offsetZ !== null) w.writeFloat32Fixed(9, s.offsetZ);
|
|
136
136
|
w.writeUint32(10, s.pattern);
|
|
137
|
+
if (s.highlightReel != null) w.writeUint32(11, s.highlightReel);
|
|
137
138
|
return w.toBytes();
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -153,7 +154,8 @@ function decodeSticker(data) {
|
|
|
153
154
|
case 7: s.offsetX = f.value.readFloatLE(0); break;
|
|
154
155
|
case 8: s.offsetY = f.value.readFloatLE(0); break;
|
|
155
156
|
case 9: s.offsetZ = f.value.readFloatLE(0); break;
|
|
156
|
-
case 10: s.pattern
|
|
157
|
+
case 10: s.pattern = Number(f.value); break;
|
|
158
|
+
case 11: s.highlightReel = Number(f.value); break;
|
|
157
159
|
default: break;
|
|
158
160
|
}
|
|
159
161
|
}
|
|
@@ -176,8 +178,9 @@ function encodeItem(item) {
|
|
|
176
178
|
w.writeUint32(6, item.quality);
|
|
177
179
|
|
|
178
180
|
// paintwear: float32 reinterpreted as uint32 varint
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
if (item.paintWear != null) {
|
|
182
|
+
w.writeUint32(7, float32ToUint32(item.paintWear));
|
|
183
|
+
}
|
|
181
184
|
|
|
182
185
|
w.writeUint32(8, item.paintSeed);
|
|
183
186
|
w.writeUint32(9, item.killEaterScoreType);
|
|
@@ -252,6 +255,16 @@ class InspectLink {
|
|
|
252
255
|
* @returns {string} uppercase hex string
|
|
253
256
|
*/
|
|
254
257
|
static serialize(data) {
|
|
258
|
+
if (data.paintWear != null && (data.paintWear < 0.0 || data.paintWear > 1.0)) {
|
|
259
|
+
throw new RangeError(
|
|
260
|
+
`paintwear must be in [0.0, 1.0], got ${data.paintWear}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (data.customName != null && data.customName.length > 100) {
|
|
264
|
+
throw new RangeError(
|
|
265
|
+
`customname must not exceed 100 characters, got ${data.customName.length}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
255
268
|
const protoBytes = encodeItem(data);
|
|
256
269
|
const buffer = Buffer.concat([Buffer.from([0x00]), protoBytes]);
|
|
257
270
|
const checksum = computeChecksum(buffer, protoBytes.length);
|
|
@@ -294,6 +307,11 @@ class InspectLink {
|
|
|
294
307
|
*/
|
|
295
308
|
static deserialize(input) {
|
|
296
309
|
const hex = extractHex(input);
|
|
310
|
+
if (hex.length > 4096) {
|
|
311
|
+
throw new RangeError(
|
|
312
|
+
`Payload too long (max 4096 hex chars): "${input.slice(0, 64)}..."`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
297
315
|
const raw = Buffer.from(hex, 'hex');
|
|
298
316
|
|
|
299
317
|
if (raw.length < 6) {
|
package/src/ItemPreviewData.js
CHANGED
|
@@ -21,7 +21,7 @@ class ItemPreviewData {
|
|
|
21
21
|
* @param {number} [opts.paintIndex=0]
|
|
22
22
|
* @param {number} [opts.rarity=0]
|
|
23
23
|
* @param {number} [opts.quality=0]
|
|
24
|
-
* @param {number}
|
|
24
|
+
* @param {number|null} [opts.paintWear=null]
|
|
25
25
|
* @param {number} [opts.paintSeed=0]
|
|
26
26
|
* @param {number} [opts.killEaterScoreType=0]
|
|
27
27
|
* @param {number} [opts.killEaterValue=0]
|
|
@@ -43,7 +43,7 @@ class ItemPreviewData {
|
|
|
43
43
|
paintIndex = 0,
|
|
44
44
|
rarity = 0,
|
|
45
45
|
quality = 0,
|
|
46
|
-
paintWear =
|
|
46
|
+
paintWear = null,
|
|
47
47
|
paintSeed = 0,
|
|
48
48
|
killEaterScoreType = 0,
|
|
49
49
|
killEaterValue = 0,
|
package/src/Sticker.js
CHANGED
|
@@ -19,6 +19,7 @@ class Sticker {
|
|
|
19
19
|
* @param {number|null} [opts.offsetY=null]
|
|
20
20
|
* @param {number|null} [opts.offsetZ=null]
|
|
21
21
|
* @param {number} [opts.pattern=0]
|
|
22
|
+
* @param {number|null} [opts.highlightReel=null]
|
|
22
23
|
*/
|
|
23
24
|
constructor({
|
|
24
25
|
slot = 0,
|
|
@@ -31,6 +32,7 @@ class Sticker {
|
|
|
31
32
|
offsetY = null,
|
|
32
33
|
offsetZ = null,
|
|
33
34
|
pattern = 0,
|
|
35
|
+
highlightReel = null,
|
|
34
36
|
} = {}) {
|
|
35
37
|
this.slot = slot;
|
|
36
38
|
this.stickerId = stickerId;
|
|
@@ -42,6 +44,7 @@ class Sticker {
|
|
|
42
44
|
this.offsetY = offsetY;
|
|
43
45
|
this.offsetZ = offsetZ;
|
|
44
46
|
this.pattern = pattern;
|
|
47
|
+
this.highlightReel = highlightReel;
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
package/src/proto/reader.js
CHANGED
|
@@ -94,8 +94,12 @@ class ProtoReader {
|
|
|
94
94
|
*/
|
|
95
95
|
readAllFields() {
|
|
96
96
|
const fields = [];
|
|
97
|
+
let fieldCount = 0;
|
|
97
98
|
|
|
98
99
|
while (this.remaining() > 0) {
|
|
100
|
+
if (++fieldCount > 100) {
|
|
101
|
+
throw new RangeError('Protobuf field count exceeds limit of 100');
|
|
102
|
+
}
|
|
99
103
|
const [fieldNum, wireType] = this.readTag();
|
|
100
104
|
|
|
101
105
|
let value;
|
|
@@ -359,3 +359,91 @@ describe('checksum', () => {
|
|
|
359
359
|
assert.equal(InspectLink.serialize(data), TOOL_HEX);
|
|
360
360
|
});
|
|
361
361
|
});
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Defensive validation tests
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
describe('deserialize — payload too long', () => {
|
|
368
|
+
test('throws RangeError for hex payload exceeding 4096 chars (4098 chars)', () => {
|
|
369
|
+
const longHex = '00'.repeat(2049); // 4098 hex chars
|
|
370
|
+
assert.throws(() => InspectLink.deserialize(longHex), RangeError);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('serialize — paintwear validation', () => {
|
|
375
|
+
test('throws RangeError when paintwear > 1.0', () => {
|
|
376
|
+
const data = new ItemPreviewData({ paintWear: 1.1 });
|
|
377
|
+
assert.throws(() => InspectLink.serialize(data), RangeError);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('throws RangeError when paintwear < 0.0', () => {
|
|
381
|
+
const data = new ItemPreviewData({ paintWear: -0.1 });
|
|
382
|
+
assert.throws(() => InspectLink.serialize(data), RangeError);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('does not throw when paintwear = 0.0 (boundary)', () => {
|
|
386
|
+
const data = new ItemPreviewData({ paintWear: 0.0 });
|
|
387
|
+
assert.doesNotThrow(() => InspectLink.serialize(data));
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('does not throw when paintwear = 1.0 (boundary)', () => {
|
|
391
|
+
const data = new ItemPreviewData({ paintWear: 1.0 });
|
|
392
|
+
assert.doesNotThrow(() => InspectLink.serialize(data));
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('serialize — customname validation', () => {
|
|
397
|
+
test('throws RangeError when customName is 101 characters', () => {
|
|
398
|
+
const data = new ItemPreviewData({ customName: 'a'.repeat(101) });
|
|
399
|
+
assert.throws(() => InspectLink.serialize(data), RangeError);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('does not throw when customName is exactly 100 characters', () => {
|
|
403
|
+
const data = new ItemPreviewData({ customName: 'a'.repeat(100) });
|
|
404
|
+
assert.doesNotThrow(() => InspectLink.serialize(data));
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// CSFloat/gen.test.ts vectors
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
const CSFLOAT_A = '00180720DA03280638FBEE88F90340B2026BC03C96';
|
|
413
|
+
const CSFLOAT_B = '00180720C80A280638A4E1F5FB03409A0562040800104C62040801104C62040802104C62040803104C6D4F5E30';
|
|
414
|
+
const CSFLOAT_C = 'A2B2A2BA69A882A28AA192AECAA2D2B700A3A5AAA2B286FA7BA0D684BE72';
|
|
415
|
+
|
|
416
|
+
describe('CSFloat test vectors', () => {
|
|
417
|
+
test('VectorA: defindex', () => assert.equal(InspectLink.deserialize(CSFLOAT_A).defIndex, 7));
|
|
418
|
+
test('VectorA: paintindex', () => assert.equal(InspectLink.deserialize(CSFLOAT_A).paintIndex, 474));
|
|
419
|
+
test('VectorA: paintseed', () => assert.equal(InspectLink.deserialize(CSFLOAT_A).paintSeed, 306));
|
|
420
|
+
test('VectorA: rarity', () => assert.equal(InspectLink.deserialize(CSFLOAT_A).rarity, 6));
|
|
421
|
+
test('VectorA: paintwear not null', () => assert.notEqual(InspectLink.deserialize(CSFLOAT_A).paintWear, null));
|
|
422
|
+
test('VectorA: paintwear approx', () => assert.ok(Math.abs(InspectLink.deserialize(CSFLOAT_A).paintWear - 0.6337) < 0.001));
|
|
423
|
+
|
|
424
|
+
test('VectorB: 4 stickers', () => assert.equal(InspectLink.deserialize(CSFLOAT_B).stickers.length, 4));
|
|
425
|
+
test('VectorB: sticker ids all 76', () => {
|
|
426
|
+
InspectLink.deserialize(CSFLOAT_B).stickers.forEach(s => assert.equal(s.stickerId, 76));
|
|
427
|
+
});
|
|
428
|
+
test('VectorB: paintindex', () => assert.equal(InspectLink.deserialize(CSFLOAT_B).paintIndex, 1352));
|
|
429
|
+
test('VectorB: paintwear approx 0.99', () => assert.ok(Math.abs(InspectLink.deserialize(CSFLOAT_B).paintWear - 0.99) < 0.01));
|
|
430
|
+
|
|
431
|
+
test('VectorC: defindex', () => assert.equal(InspectLink.deserialize(CSFLOAT_C).defIndex, 1355));
|
|
432
|
+
test('VectorC: quality', () => assert.equal(InspectLink.deserialize(CSFLOAT_C).quality, 12));
|
|
433
|
+
test('VectorC: keychain count', () => assert.equal(InspectLink.deserialize(CSFLOAT_C).keychains.length, 1));
|
|
434
|
+
test('VectorC: keychain highlightReel', () => assert.equal(InspectLink.deserialize(CSFLOAT_C).keychains[0].highlightReel, 345));
|
|
435
|
+
test('VectorC: no paintwear', () => assert.equal(InspectLink.deserialize(CSFLOAT_C).paintWear, null));
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('Roundtrip: highlight_reel and nullable paintWear', () => {
|
|
439
|
+
test('highlightReel roundtrip', () => {
|
|
440
|
+
const data = new ItemPreviewData({ defIndex: 7, keychains: [new Sticker({ slot: 0, stickerId: 36, highlightReel: 345 })] });
|
|
441
|
+
const result = InspectLink.deserialize(InspectLink.serialize(data));
|
|
442
|
+
assert.equal(result.keychains[0].highlightReel, 345);
|
|
443
|
+
});
|
|
444
|
+
test('null paintWear roundtrip', () => {
|
|
445
|
+
const data = new ItemPreviewData({ defIndex: 7, paintWear: null });
|
|
446
|
+
const result = InspectLink.deserialize(InspectLink.serialize(data));
|
|
447
|
+
assert.equal(result.paintWear, null);
|
|
448
|
+
});
|
|
449
|
+
});
|