@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.
@@ -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.1",
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": ["cs2", "csgo", "inspect", "protobuf", "steam"],
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"
@@ -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 = Number(f.value); break;
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
- const pwUint32 = float32ToUint32(item.paintWear);
180
- w.writeUint32(7, pwUint32);
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) {
@@ -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} [opts.paintWear=0.0]
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 = 0.0,
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
 
@@ -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
+ });