@signalr-devtools/analysis 0.1.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 +18 -0
- package/README.md +89 -0
- package/msgpackDecoder.js +371 -0
- package/package.json +45 -0
- package/sessionFormat.js +254 -0
- package/signalrAnalysis.js +798 -0
- package/signalrProtocol.js +324 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Jakub Grzywaczewski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
7
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
8
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
12
|
+
substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
|
15
|
+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
|
18
|
+
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @signalr-devtools/analysis
|
|
2
|
+
|
|
3
|
+
Decode and analyze captured ASP.NET Core SignalR traffic in Node using the same protocol and
|
|
4
|
+
analysis modules shipped by the SignalR Inspector browser extension.
|
|
5
|
+
|
|
6
|
+
This package is the headless analysis core. It does not install or expose the Chrome/Edge DevTools
|
|
7
|
+
panel, capture browser traffic, connect to Chrome DevTools Protocol, or run an MCP server.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @signalr-devtools/analysis
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package is CommonJS and can also be imported from an ES module. The root export is the
|
|
16
|
+
conversation analysis API; protocol parsing, MessagePack decoding, and session handling are
|
|
17
|
+
available through explicit subpaths.
|
|
18
|
+
|
|
19
|
+
ES modules can use named imports from both the root and the explicit subpaths:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { analyze } from '@signalr-devtools/analysis';
|
|
23
|
+
import { parsePayload } from '@signalr-devtools/analysis/signalrProtocol';
|
|
24
|
+
|
|
25
|
+
const result = analyze(messages, parsePayload);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The same APIs are available to CommonJS consumers:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
const analysis = require('@signalr-devtools/analysis');
|
|
32
|
+
const protocol = require('@signalr-devtools/analysis/signalrProtocol');
|
|
33
|
+
|
|
34
|
+
const separator = '\u001e';
|
|
35
|
+
const invocationPayload =
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
type: 1,
|
|
38
|
+
invocationId: '1',
|
|
39
|
+
target: 'SendMessage',
|
|
40
|
+
arguments: ['Ada', 'Hello'],
|
|
41
|
+
}) + separator;
|
|
42
|
+
const completionPayload = JSON.stringify({ type: 3, invocationId: '1' }) + separator;
|
|
43
|
+
const messages = [
|
|
44
|
+
{
|
|
45
|
+
id: 1,
|
|
46
|
+
direction: 'outgoing',
|
|
47
|
+
endpoint: 'https://localhost/chatHub',
|
|
48
|
+
transport: 'websocket',
|
|
49
|
+
timestamp: 100,
|
|
50
|
+
encoding: 'text',
|
|
51
|
+
size: invocationPayload.length,
|
|
52
|
+
textPayload: invocationPayload,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 2,
|
|
56
|
+
direction: 'incoming',
|
|
57
|
+
endpoint: 'https://localhost/chatHub',
|
|
58
|
+
transport: 'websocket',
|
|
59
|
+
timestamp: 145,
|
|
60
|
+
encoding: 'text',
|
|
61
|
+
size: completionPayload.length,
|
|
62
|
+
textPayload: completionPayload,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const result = analysis.analyze(messages, protocol.parsePayload);
|
|
67
|
+
console.log(result.insights.summary.hubMessages); // 2
|
|
68
|
+
console.log(result.insights.methods[0].target); // SendMessage
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Public modules
|
|
72
|
+
|
|
73
|
+
- `@signalr-devtools/analysis` or `/signalrAnalysis`: `analyze`, `formatDuration`
|
|
74
|
+
- `/signalrProtocol`: `parsePayload`, `formatPayload`
|
|
75
|
+
- `/msgpackDecoder`: `decode`, `decodeVarIntFrames`
|
|
76
|
+
- `/sessionFormat`: `create`, `parse`, `serialize`, `FORMAT`, `VERSION`,
|
|
77
|
+
`MAX_FILE_CHARACTERS`
|
|
78
|
+
|
|
79
|
+
Load `/msgpackDecoder` before parsing binary payloads with `/signalrProtocol`, matching the script
|
|
80
|
+
order used by the extension. Importing it registers the decoder used by the shared protocol module.
|
|
81
|
+
|
|
82
|
+
The exported SignalR Inspector session format is currently version `1`. Package versions and
|
|
83
|
+
session-format versions are independent.
|
|
84
|
+
|
|
85
|
+
This project is not affiliated with or endorsed by Microsoft.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
(function exposeSignalRMsgPack(root) {
|
|
4
|
+
const MAX_DEPTH = 32;
|
|
5
|
+
const MAX_ELEMENTS = 100_000;
|
|
6
|
+
const MAX_VARINT_BYTES = 5;
|
|
7
|
+
const BINARY_PREVIEW_BYTES = 32;
|
|
8
|
+
const textDecoder = new TextDecoder();
|
|
9
|
+
|
|
10
|
+
function asBytes(input) {
|
|
11
|
+
if (input instanceof Uint8Array) {
|
|
12
|
+
return input;
|
|
13
|
+
}
|
|
14
|
+
if (input instanceof ArrayBuffer) {
|
|
15
|
+
return new Uint8Array(input);
|
|
16
|
+
}
|
|
17
|
+
if (ArrayBuffer.isView(input)) {
|
|
18
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
19
|
+
}
|
|
20
|
+
throw new TypeError('MessagePack input must be binary data.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeInteger(value) {
|
|
24
|
+
const minimum = BigInt(Number.MIN_SAFE_INTEGER);
|
|
25
|
+
const maximum = BigInt(Number.MAX_SAFE_INTEGER);
|
|
26
|
+
return value >= minimum && value <= maximum ? Number(value) : value.toString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function bytesPreview(bytes) {
|
|
30
|
+
const shown = bytes.subarray(0, BINARY_PREVIEW_BYTES);
|
|
31
|
+
const hex = Array.from(shown, (byte) => byte.toString(16).padStart(2, '0')).join(' ');
|
|
32
|
+
return bytes.length > shown.length ? `${hex} …` : hex;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function binaryValue(bytes) {
|
|
36
|
+
return {
|
|
37
|
+
type: 'binary',
|
|
38
|
+
length: bytes.length,
|
|
39
|
+
preview: bytesPreview(bytes),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function timestampValue(bytes) {
|
|
44
|
+
let seconds;
|
|
45
|
+
let nanoseconds;
|
|
46
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
47
|
+
|
|
48
|
+
if (bytes.length === 4) {
|
|
49
|
+
seconds = BigInt(view.getUint32(0));
|
|
50
|
+
nanoseconds = 0;
|
|
51
|
+
} else if (bytes.length === 8) {
|
|
52
|
+
const packed = view.getBigUint64(0);
|
|
53
|
+
nanoseconds = Number(packed >> 34n);
|
|
54
|
+
seconds = packed & 0x3ffffffffn;
|
|
55
|
+
} else if (bytes.length === 12) {
|
|
56
|
+
nanoseconds = view.getUint32(0);
|
|
57
|
+
seconds = view.getBigInt64(4);
|
|
58
|
+
} else {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const milliseconds = Number(seconds) * 1000 + Math.floor(nanoseconds / 1_000_000);
|
|
63
|
+
const date = new Date(milliseconds);
|
|
64
|
+
if (Number.isFinite(milliseconds) && !Number.isNaN(date.getTime())) {
|
|
65
|
+
return date.toISOString();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
type: 'timestamp',
|
|
70
|
+
seconds: seconds.toString(),
|
|
71
|
+
nanoseconds,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class Decoder {
|
|
76
|
+
constructor(bytes) {
|
|
77
|
+
this.bytes = bytes;
|
|
78
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
79
|
+
this.offset = 0;
|
|
80
|
+
this.elements = 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
require(length) {
|
|
84
|
+
if (!Number.isSafeInteger(length) || length < 0 || this.offset + length > this.bytes.length) {
|
|
85
|
+
throw new RangeError('Truncated MessagePack value.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
countElement() {
|
|
90
|
+
this.elements += 1;
|
|
91
|
+
if (this.elements > MAX_ELEMENTS) {
|
|
92
|
+
throw new RangeError(`MessagePack value exceeds ${MAX_ELEMENTS} elements.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
readByte() {
|
|
97
|
+
this.require(1);
|
|
98
|
+
const value = this.bytes[this.offset];
|
|
99
|
+
this.offset += 1;
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
readUnsigned(length) {
|
|
104
|
+
this.require(length);
|
|
105
|
+
let value;
|
|
106
|
+
if (length === 1) {
|
|
107
|
+
value = this.view.getUint8(this.offset);
|
|
108
|
+
} else if (length === 2) {
|
|
109
|
+
value = this.view.getUint16(this.offset);
|
|
110
|
+
} else if (length === 4) {
|
|
111
|
+
value = this.view.getUint32(this.offset);
|
|
112
|
+
} else {
|
|
113
|
+
value = safeInteger(this.view.getBigUint64(this.offset));
|
|
114
|
+
}
|
|
115
|
+
this.offset += length;
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
readSigned(length) {
|
|
120
|
+
this.require(length);
|
|
121
|
+
let value;
|
|
122
|
+
if (length === 1) {
|
|
123
|
+
value = this.view.getInt8(this.offset);
|
|
124
|
+
} else if (length === 2) {
|
|
125
|
+
value = this.view.getInt16(this.offset);
|
|
126
|
+
} else if (length === 4) {
|
|
127
|
+
value = this.view.getInt32(this.offset);
|
|
128
|
+
} else {
|
|
129
|
+
value = safeInteger(this.view.getBigInt64(this.offset));
|
|
130
|
+
}
|
|
131
|
+
this.offset += length;
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
readFloat(length) {
|
|
136
|
+
this.require(length);
|
|
137
|
+
const value =
|
|
138
|
+
length === 4 ? this.view.getFloat32(this.offset) : this.view.getFloat64(this.offset);
|
|
139
|
+
this.offset += length;
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
readBytes(length) {
|
|
144
|
+
this.require(length);
|
|
145
|
+
const value = this.bytes.subarray(this.offset, this.offset + length);
|
|
146
|
+
this.offset += length;
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
readString(length) {
|
|
151
|
+
return textDecoder.decode(this.readBytes(length));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
readArray(length, depth) {
|
|
155
|
+
if (length > MAX_ELEMENTS) {
|
|
156
|
+
throw new RangeError(`MessagePack array exceeds ${MAX_ELEMENTS} elements.`);
|
|
157
|
+
}
|
|
158
|
+
const value = [];
|
|
159
|
+
for (let index = 0; index < length; index += 1) {
|
|
160
|
+
value.push(this.readValue(depth + 1));
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
readMap(length, depth) {
|
|
166
|
+
if (length > MAX_ELEMENTS) {
|
|
167
|
+
throw new RangeError(`MessagePack map exceeds ${MAX_ELEMENTS} entries.`);
|
|
168
|
+
}
|
|
169
|
+
const value = Object.create(null);
|
|
170
|
+
for (let index = 0; index < length; index += 1) {
|
|
171
|
+
const key = this.readValue(depth + 1);
|
|
172
|
+
const mapKey = typeof key === 'string' ? key : JSON.stringify(key);
|
|
173
|
+
value[mapKey ?? String(key)] = this.readValue(depth + 1);
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
readExtension(length) {
|
|
179
|
+
const extensionType = this.readSigned(1);
|
|
180
|
+
const bytes = this.readBytes(length);
|
|
181
|
+
if (extensionType === -1) {
|
|
182
|
+
const timestamp = timestampValue(bytes);
|
|
183
|
+
if (timestamp !== null) {
|
|
184
|
+
return timestamp;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
type: 'extension',
|
|
189
|
+
extensionType,
|
|
190
|
+
length,
|
|
191
|
+
preview: bytesPreview(bytes),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
readValue(depth = 0) {
|
|
196
|
+
if (depth > MAX_DEPTH) {
|
|
197
|
+
throw new RangeError(`MessagePack nesting exceeds ${MAX_DEPTH} levels.`);
|
|
198
|
+
}
|
|
199
|
+
this.countElement();
|
|
200
|
+
const prefix = this.readByte();
|
|
201
|
+
|
|
202
|
+
if (prefix <= 0x7f) {
|
|
203
|
+
return prefix;
|
|
204
|
+
}
|
|
205
|
+
if (prefix >= 0xe0) {
|
|
206
|
+
return prefix - 0x100;
|
|
207
|
+
}
|
|
208
|
+
if (prefix >= 0xa0 && prefix <= 0xbf) {
|
|
209
|
+
return this.readString(prefix & 0x1f);
|
|
210
|
+
}
|
|
211
|
+
if (prefix >= 0x90 && prefix <= 0x9f) {
|
|
212
|
+
return this.readArray(prefix & 0x0f, depth);
|
|
213
|
+
}
|
|
214
|
+
if (prefix >= 0x80 && prefix <= 0x8f) {
|
|
215
|
+
return this.readMap(prefix & 0x0f, depth);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
switch (prefix) {
|
|
219
|
+
case 0xc0:
|
|
220
|
+
return null;
|
|
221
|
+
case 0xc2:
|
|
222
|
+
return false;
|
|
223
|
+
case 0xc3:
|
|
224
|
+
return true;
|
|
225
|
+
case 0xc4:
|
|
226
|
+
return binaryValue(this.readBytes(this.readUnsigned(1)));
|
|
227
|
+
case 0xc5:
|
|
228
|
+
return binaryValue(this.readBytes(this.readUnsigned(2)));
|
|
229
|
+
case 0xc6:
|
|
230
|
+
return binaryValue(this.readBytes(this.readUnsigned(4)));
|
|
231
|
+
case 0xc7:
|
|
232
|
+
return this.readExtension(this.readUnsigned(1));
|
|
233
|
+
case 0xc8:
|
|
234
|
+
return this.readExtension(this.readUnsigned(2));
|
|
235
|
+
case 0xc9:
|
|
236
|
+
return this.readExtension(this.readUnsigned(4));
|
|
237
|
+
case 0xca:
|
|
238
|
+
return this.readFloat(4);
|
|
239
|
+
case 0xcb:
|
|
240
|
+
return this.readFloat(8);
|
|
241
|
+
case 0xcc:
|
|
242
|
+
return this.readUnsigned(1);
|
|
243
|
+
case 0xcd:
|
|
244
|
+
return this.readUnsigned(2);
|
|
245
|
+
case 0xce:
|
|
246
|
+
return this.readUnsigned(4);
|
|
247
|
+
case 0xcf:
|
|
248
|
+
return this.readUnsigned(8);
|
|
249
|
+
case 0xd0:
|
|
250
|
+
return this.readSigned(1);
|
|
251
|
+
case 0xd1:
|
|
252
|
+
return this.readSigned(2);
|
|
253
|
+
case 0xd2:
|
|
254
|
+
return this.readSigned(4);
|
|
255
|
+
case 0xd3:
|
|
256
|
+
return this.readSigned(8);
|
|
257
|
+
case 0xd4:
|
|
258
|
+
return this.readExtension(1);
|
|
259
|
+
case 0xd5:
|
|
260
|
+
return this.readExtension(2);
|
|
261
|
+
case 0xd6:
|
|
262
|
+
return this.readExtension(4);
|
|
263
|
+
case 0xd7:
|
|
264
|
+
return this.readExtension(8);
|
|
265
|
+
case 0xd8:
|
|
266
|
+
return this.readExtension(16);
|
|
267
|
+
case 0xd9:
|
|
268
|
+
return this.readString(this.readUnsigned(1));
|
|
269
|
+
case 0xda:
|
|
270
|
+
return this.readString(this.readUnsigned(2));
|
|
271
|
+
case 0xdb:
|
|
272
|
+
return this.readString(this.readUnsigned(4));
|
|
273
|
+
case 0xdc:
|
|
274
|
+
return this.readArray(this.readUnsigned(2), depth);
|
|
275
|
+
case 0xdd:
|
|
276
|
+
return this.readArray(this.readUnsigned(4), depth);
|
|
277
|
+
case 0xde:
|
|
278
|
+
return this.readMap(this.readUnsigned(2), depth);
|
|
279
|
+
case 0xdf:
|
|
280
|
+
return this.readMap(this.readUnsigned(4), depth);
|
|
281
|
+
default:
|
|
282
|
+
throw new RangeError(`Unsupported MessagePack prefix 0x${prefix.toString(16)}.`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function decode(input) {
|
|
288
|
+
try {
|
|
289
|
+
const decoder = new Decoder(asBytes(input));
|
|
290
|
+
const value = decoder.readValue();
|
|
291
|
+
return { value, bytesRead: decoder.offset };
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return {
|
|
294
|
+
value: undefined,
|
|
295
|
+
bytesRead: 0,
|
|
296
|
+
error: error instanceof Error ? error.message : String(error),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function frameResult(frames, incomplete = false, error = '') {
|
|
302
|
+
Object.defineProperties(frames, {
|
|
303
|
+
incomplete: { value: incomplete, enumerable: false },
|
|
304
|
+
error: { value: error, enumerable: false },
|
|
305
|
+
});
|
|
306
|
+
return frames;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function decodeVarIntFrames(input) {
|
|
310
|
+
const frames = [];
|
|
311
|
+
let bytes;
|
|
312
|
+
try {
|
|
313
|
+
bytes = asBytes(input);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return frameResult(frames, false, error instanceof Error ? error.message : String(error));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let offset = 0;
|
|
319
|
+
while (offset < bytes.length) {
|
|
320
|
+
const prefixStart = offset;
|
|
321
|
+
let length = 0;
|
|
322
|
+
let prefixComplete = false;
|
|
323
|
+
|
|
324
|
+
for (let index = 0; index < MAX_VARINT_BYTES; index += 1) {
|
|
325
|
+
if (offset >= bytes.length) {
|
|
326
|
+
return frameResult(frames, true);
|
|
327
|
+
}
|
|
328
|
+
const byte = bytes[offset];
|
|
329
|
+
offset += 1;
|
|
330
|
+
if (index === MAX_VARINT_BYTES - 1) {
|
|
331
|
+
if ((byte & 0x80) !== 0) {
|
|
332
|
+
return frameResult(frames, false, 'SignalR frame length prefix is too long.');
|
|
333
|
+
}
|
|
334
|
+
if (byte > 0x07) {
|
|
335
|
+
return frameResult(frames, false, 'SignalR frame length exceeds 2 GiB.');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
length += (byte & 0x7f) * 2 ** (index * 7);
|
|
339
|
+
if ((byte & 0x80) === 0) {
|
|
340
|
+
prefixComplete = true;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!prefixComplete) {
|
|
346
|
+
return frameResult(frames, false, 'SignalR frame length prefix is too long.');
|
|
347
|
+
}
|
|
348
|
+
if (bytes.length - offset < length) {
|
|
349
|
+
return frameResult(frames, true);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
frames.push(bytes.subarray(offset, offset + length));
|
|
353
|
+
offset += length;
|
|
354
|
+
if (offset <= prefixStart) {
|
|
355
|
+
return frameResult(frames, false, 'SignalR frame parser made no progress.');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return frameResult(frames);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const api = { decode, decodeVarIntFrames };
|
|
363
|
+
root.SignalRMsgPack = api;
|
|
364
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
365
|
+
// biome-ignore lint/style/noCommonJs: The browser IIFE also exposes a CommonJS test API.
|
|
366
|
+
module.exports = api;
|
|
367
|
+
// Keep these explicit assignments detectable as synthetic named exports for Node ESM.
|
|
368
|
+
module.exports.decode = decode;
|
|
369
|
+
module.exports.decodeVarIntFrames = decodeVarIntFrames;
|
|
370
|
+
}
|
|
371
|
+
})(globalThis);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@signalr-devtools/analysis",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Decode and analyze captured ASP.NET Core SignalR traffic in Node.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./signalrAnalysis.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./signalrAnalysis.js",
|
|
9
|
+
"./signalrAnalysis": "./signalrAnalysis.js",
|
|
10
|
+
"./signalrProtocol": "./signalrProtocol.js",
|
|
11
|
+
"./msgpackDecoder": "./msgpackDecoder.js",
|
|
12
|
+
"./sessionFormat": "./sessionFormat.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"msgpackDecoder.js",
|
|
16
|
+
"sessionFormat.js",
|
|
17
|
+
"signalrAnalysis.js",
|
|
18
|
+
"signalrProtocol.js"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepack": "node ../../signalr-inspector/scripts/analysis-package.mjs prepare",
|
|
22
|
+
"postpack": "node ../../signalr-inspector/scripts/analysis-package.mjs cleanup"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"registry": "https://registry.npmjs.org/"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/jakubgrzywaczewski/signalr-devtools.git",
|
|
34
|
+
"directory": "packages/analysis"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"signalr",
|
|
38
|
+
"aspnetcore",
|
|
39
|
+
"protocol",
|
|
40
|
+
"messagepack",
|
|
41
|
+
"analysis"
|
|
42
|
+
],
|
|
43
|
+
"author": "Jakub Grzywaczewski",
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|