clean up test slop
This commit is contained in:
@@ -2,6 +2,7 @@ const net = require('net');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const dgram = require('dgram');
|
||||
const { WTVShared } = require('./WTVShared.js');
|
||||
|
||||
class WTVPNM {
|
||||
@@ -67,12 +68,6 @@ class WTVPNM {
|
||||
mediaPath: null,
|
||||
notFoundSent: false,
|
||||
pnaFields: null,
|
||||
tcpTimer: null,
|
||||
tcpSeq: 0,
|
||||
ipId: 0,
|
||||
tcpStartTimer: null,
|
||||
mediaData: null,
|
||||
mediaOffset: 0,
|
||||
bytesRx: 0,
|
||||
bytesTx: 0,
|
||||
// Control-stream command accumulator. TCP is byte-oriented so
|
||||
@@ -179,9 +174,12 @@ class WTVPNM {
|
||||
}
|
||||
|
||||
if (session.requestedMedia && !session.mediaPath) {
|
||||
console.log(' * PNM RealServer Warning: requested media not found', session.requestedMedia);
|
||||
this.sendNotFound(socket, session.requestedMedia);
|
||||
session.notFoundSent = true;
|
||||
return;
|
||||
} else {
|
||||
console.log(' * PNM RealServer Request from', session.id, 'for media', session.mediaPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +208,7 @@ class WTVPNM {
|
||||
} else {
|
||||
this.debugLog('client hash MISMATCH', session.id, 'expected', expectedResp);
|
||||
}
|
||||
if (session.clientUdpPort && this.service_config.auto_stream !== false) {
|
||||
if (session.clientUdpPort) {
|
||||
this.startUdpStream(socket, session);
|
||||
}
|
||||
} else {
|
||||
@@ -501,24 +499,6 @@ class WTVPNM {
|
||||
}
|
||||
}
|
||||
|
||||
scheduleDescriptorSequence(socket, session) {
|
||||
this.clearDescriptorTimer(session);
|
||||
let delayMs = null;
|
||||
let reason = 'webtv-sequence';
|
||||
|
||||
if (this.service_config.descriptor_on_first_pna === true) {
|
||||
delayMs = (typeof this.service_config.descriptor_after_hello_ms === 'number')
|
||||
? this.service_config.descriptor_after_hello_ms
|
||||
: (this.service_config.descriptor_fallback_ms || 20);
|
||||
}
|
||||
|
||||
if (delayMs === null) return;
|
||||
this.debugLog('descriptor scheduled', session.id, reason, `${delayMs}ms`);
|
||||
session.descriptorTimer = setTimeout(() => {
|
||||
this.sendDescriptorAndStartStream(socket, session, reason);
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
sendDescriptorAndStartStream(socket, session, reason) {
|
||||
if (!socket || !session || session.descriptorSent) return;
|
||||
this.clearDescriptorTimer(session);
|
||||
@@ -631,14 +611,22 @@ class WTVPNM {
|
||||
const startDelayMs = 72;
|
||||
const redundantSeqs = [0, 1];
|
||||
|
||||
// Pre-start burst: send the first N ms of audio at double rate to
|
||||
// pre-fill the client buffer before settling into normal pacing.
|
||||
const burstPrestartMs = typeof this.service_config.burst_prestart_ms === 'number'
|
||||
? this.service_config.burst_prestart_ms
|
||||
: 3000;
|
||||
const burstFrameCount = burstPrestartMs > 0 ? Math.ceil(burstPrestartMs / intervalMs) : 0;
|
||||
session.burstFramesSent = 0;
|
||||
|
||||
this.debugLog('udp stream start', session.id,
|
||||
`frames=${session.mediaFrames?.length || 0}`,
|
||||
`avgBitRate=${session.avgBitRate || 'unknown'}bps`,
|
||||
`bodyLen=${bodyLen}`,
|
||||
`interval=${intervalMs.toFixed(2)}ms`,
|
||||
`burstFrames=${burstFrameCount}`,
|
||||
`target=${socket.remoteAddress}:${session.clientUdpPort}`);
|
||||
|
||||
const dgram = require('dgram');
|
||||
session.udpSocket = dgram.createSocket('udp4');
|
||||
session.udpSocket.on('error', (err) => {
|
||||
this.debugLog('udp socket error', session.id, err.message);
|
||||
@@ -678,20 +666,18 @@ class WTVPNM {
|
||||
|
||||
session._startDataInterval = () => {
|
||||
if (session.udpTimer) return;
|
||||
session.udpTimer = setInterval(() => {
|
||||
const tick = () => {
|
||||
session.udpTimer = null;
|
||||
if (socket.destroyed || !session.udpSocket) {
|
||||
this.stopUdpStream(session);
|
||||
return;
|
||||
}
|
||||
// Don't re-arm while paused; resumeUdpStream calls _startDataInterval.
|
||||
if (session.paused) return;
|
||||
const frames = session.mediaFrames;
|
||||
if (!frames || session.mediaFrameIdx >= frames.length) {
|
||||
// End of media: stop sending once all RA frames are out.
|
||||
this.debugLog('udp stream complete', session.id, `sent=${seq}`);
|
||||
if (session.udpTimer) {
|
||||
clearInterval(session.udpTimer);
|
||||
session.udpTimer = null;
|
||||
}
|
||||
// Signal end-of-stream to the client on TCP. wtv2.pcap
|
||||
// shows the native RealServer sending a single 0x45 byte
|
||||
// ~0.5s after the last UDP packet; the client then FINs.
|
||||
@@ -716,7 +702,14 @@ class WTVPNM {
|
||||
sendPacket(seq, frame);
|
||||
seq++;
|
||||
session.mediaFrameIdx++;
|
||||
}, intervalMs);
|
||||
session.burstFramesSent++;
|
||||
// Use half the interval during the pre-start burst window, then
|
||||
// drop to normal pacing once burstFrameCount frames have been sent.
|
||||
const delay = session.burstFramesSent < burstFrameCount ? intervalMs / 2 : intervalMs;
|
||||
session.udpTimer = setTimeout(tick, delay);
|
||||
};
|
||||
const initialDelay = session.burstFramesSent < burstFrameCount ? intervalMs / 2 : intervalMs;
|
||||
session.udpTimer = setTimeout(tick, initialDelay);
|
||||
};
|
||||
|
||||
session.udpStartTimer = setTimeout(() => {
|
||||
@@ -760,18 +753,12 @@ class WTVPNM {
|
||||
return out;
|
||||
}
|
||||
|
||||
buildTunnelFrame(session) {
|
||||
// Send raw PNA media data directly on TCP (no PPP/IP/UDP wrapping).
|
||||
return this.buildMediaPayload(session);
|
||||
}
|
||||
|
||||
buildMediaPayload(session, pSeq, pFrame) {
|
||||
const seq = pSeq !== undefined ? pSeq : (session ? session.udpSeq || 0 : 0);
|
||||
if (session && pSeq === undefined) session.udpSeq = seq + 1;
|
||||
|
||||
// Pick the frame: caller can pass one explicitly (interval / burst /
|
||||
// seek path) or, for the legacy tunnel path, we fall back to indexing
|
||||
// by seq against mediaFrames as before.
|
||||
// seek path) or fall back to indexing by seq against mediaFrames.
|
||||
let frame = pFrame;
|
||||
if (frame === undefined) {
|
||||
frame = session?.mediaFrames?.[seq];
|
||||
@@ -826,83 +813,6 @@ class WTVPNM {
|
||||
return out;
|
||||
}
|
||||
|
||||
buildIPv4UdpPacket(session, udpPayload) {
|
||||
const srcIp = this.parseIPv4(this.service_config.stream_src_ip || '10.0.0.2');
|
||||
const dstIp = this.parseIPv4(this.service_config.stream_dst_ip || '10.0.0.3');
|
||||
const srcPort = this.service_config.stream_udp_src_port || 0xb385;
|
||||
const dstPort = this.service_config.stream_udp_dst_port || 6970;
|
||||
|
||||
const ipHeader = Buffer.alloc(20);
|
||||
const udpHeader = Buffer.alloc(8);
|
||||
const totalLen = 20 + 8 + udpPayload.length;
|
||||
const ipId = (session.ipId++ & 0xffff);
|
||||
|
||||
ipHeader[0] = 0x45;
|
||||
ipHeader[1] = 0x00;
|
||||
ipHeader.writeUInt16BE(totalLen, 2);
|
||||
ipHeader.writeUInt16BE(ipId, 4);
|
||||
ipHeader.writeUInt16BE(0x4000, 6);
|
||||
ipHeader[8] = 0x40;
|
||||
ipHeader[9] = 0x11;
|
||||
srcIp.copy(ipHeader, 12);
|
||||
dstIp.copy(ipHeader, 16);
|
||||
ipHeader.writeUInt16BE(this.ipv4HeaderChecksum(ipHeader), 10);
|
||||
|
||||
udpHeader.writeUInt16BE(srcPort & 0xffff, 0);
|
||||
udpHeader.writeUInt16BE(dstPort & 0xffff, 2);
|
||||
udpHeader.writeUInt16BE(8 + udpPayload.length, 4);
|
||||
udpHeader.writeUInt16BE(0, 6);
|
||||
|
||||
return Buffer.concat([ipHeader, udpHeader, udpPayload]);
|
||||
}
|
||||
|
||||
buildPppIpFrame(ipPacket) {
|
||||
const protocol = Buffer.from([0x21]);
|
||||
const raw = Buffer.concat([protocol, ipPacket]);
|
||||
return this.pppEscape(raw);
|
||||
}
|
||||
|
||||
pppEscape(buffer) {
|
||||
const escaped = [];
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
const b = buffer[i];
|
||||
if (b === 0x7d || b === 0x7e || b < 0x20) {
|
||||
escaped.push(0x7d, b ^ 0x20);
|
||||
} else {
|
||||
escaped.push(b);
|
||||
}
|
||||
}
|
||||
return Buffer.from(escaped);
|
||||
}
|
||||
|
||||
parseIPv4(ipStr) {
|
||||
const parts = String(ipStr).split('.').map((v) => parseInt(v, 10));
|
||||
if (parts.length !== 4 || parts.some((v) => Number.isNaN(v) || v < 0 || v > 255)) {
|
||||
return Buffer.from([10, 0, 0, 2]);
|
||||
}
|
||||
return Buffer.from(parts);
|
||||
}
|
||||
|
||||
ipv4HeaderChecksum(header) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 20; i += 2) {
|
||||
if (i === 10) continue;
|
||||
sum += header.readUInt16BE(i);
|
||||
while (sum > 0xffff) sum = (sum & 0xffff) + (sum >>> 16);
|
||||
}
|
||||
return (~sum) & 0xffff;
|
||||
}
|
||||
|
||||
// The server-challenge wire format has two observed variants:
|
||||
// - 16-bit: small values like 0x03f1, 0x047d, 0x011e..0x0138. Seen in
|
||||
// every WebTV capture (wtv.pcap, wtv2.pcap, wtv_multi.pcap). WebTV's
|
||||
// PNM client REFUSES to send its hash response when the upper 16 bits
|
||||
// are non-zero.
|
||||
// - 32-bit Unix timestamp: seen in multi_auth.pcap (a newer RealServer
|
||||
// build talking to modern RealPlayer).
|
||||
// Detection is by User-Agent (set in handleData from the raw request data)
|
||||
// since WebTV clients advertise the same cook/sipr caps as modern RP.
|
||||
|
||||
buildPnaHello(session = null) {
|
||||
// The client advertises its local `time()` value in tag 0 of the
|
||||
// PNA request, XORed with 0x67E32B93. The hello-parser in
|
||||
@@ -960,7 +870,6 @@ class WTVPNM {
|
||||
}
|
||||
|
||||
buildDescriptorPacket(session = null) {
|
||||
const fs = require('fs');
|
||||
const outChunks = [];
|
||||
|
||||
// 4F headers: Rule Tags / Properties (based on capture to appease client parser)
|
||||
@@ -1120,20 +1029,6 @@ class WTVPNM {
|
||||
}
|
||||
}
|
||||
|
||||
// Optional codec adaptation
|
||||
if (tag === 'MDPR' && this.service_config.adapt_codec_from_caps === true) {
|
||||
const caps = (session && Array.isArray(session.capabilities)) ? session.capabilities : [];
|
||||
if (caps.includes('cook')) {
|
||||
// Replace 'slae' codec with 'cook' if requested
|
||||
const at = chunkData.indexOf(Buffer.from('slae', 'ascii'));
|
||||
if (at >= 0) {
|
||||
const newChunk = Buffer.from(chunkData);
|
||||
Buffer.from('cook', 'ascii').copy(newChunk, at);
|
||||
chunkData = newChunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap in [0x72] [size_16]
|
||||
const wrap = Buffer.alloc(3);
|
||||
wrap[0] = 0x72;
|
||||
@@ -1156,9 +1051,7 @@ class WTVPNM {
|
||||
}
|
||||
|
||||
// Include the session token as tag 0x23 [size_16 = 64]
|
||||
const token = (this.service_config.dynamic_session_token === true)
|
||||
? this.buildSessionToken(session)
|
||||
: '8e475de1df1ddc5c58c5ecef20e64d26073fe6f98fc0077dd0eb4429e0d8c375';
|
||||
const token = this.buildSessionToken(session);
|
||||
|
||||
const tokenBuf = Buffer.alloc(3 + 64);
|
||||
tokenBuf[0] = 0x23;
|
||||
|
||||
@@ -399,20 +399,9 @@
|
||||
"flags": "0x00000001",
|
||||
"allow_double_slash": true,
|
||||
"protocol_handler": "pnm",
|
||||
"send_keepalive": false,
|
||||
"keepalive_zero_ack": true,
|
||||
"hello_split_ms": 8,
|
||||
"descriptor_on_first_pna": true,
|
||||
"descriptor_after_hello_ms": 85,
|
||||
"descriptor_fallback_ms": 85,
|
||||
"descriptor_idle_fallback_ms": 85,
|
||||
"tcp_start_delay_ms": 72,
|
||||
"tcp_initial_burst": 1,
|
||||
"tcp_interval_ms": 232,
|
||||
"dynamic_session_token": true,
|
||||
"adapt_codec_from_caps": false,
|
||||
"auto_stream": true,
|
||||
"debug": true
|
||||
"burst_prestart_ms": 5000,
|
||||
"debug": false
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
|
||||
62
zefie_wtvp_minisrv/package-lock.json
generated
62
zefie_wtvp_minisrv/package-lock.json
generated
@@ -44,9 +44,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -952,9 +952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
|
||||
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
|
||||
"integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
@@ -1051,9 +1051,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
|
||||
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.0.tgz",
|
||||
"integrity": "sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -1111,9 +1111,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1949,9 +1949,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -2158,9 +2158,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -2602,9 +2602,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/netmask": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
|
||||
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
|
||||
"integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
@@ -2880,9 +2880,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3144,9 +3144,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sanitize-html": {
|
||||
"version": "2.17.2",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.2.tgz",
|
||||
"integrity": "sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==",
|
||||
"version": "2.17.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz",
|
||||
"integrity": "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
@@ -3374,13 +3374,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
||||
Reference in New Issue
Block a user