Skip to content

Instantly share code, notes, and snippets.

@rksm
Created August 22, 2025 21:12
Show Gist options
  • Select an option

  • Save rksm/737b167d43b6fdd36126acebe9efc7d2 to your computer and use it in GitHub Desktop.

Select an option

Save rksm/737b167d43b6fdd36126acebe9efc7d2 to your computer and use it in GitHub Desktop.

Revisions

  1. rksm created this gist Aug 22, 2025.
    238 changes: 238 additions & 0 deletions tampermonkey-permission-debug.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,238 @@
    // ==UserScript==
    // @name Navigator Permissions Debug Logger
    // @namespace http://tampermonkey.net/
    // @version 1.0
    // @description Debug navigator.permissions.query and navigator.mediaDevices.getUserMedia calls
    // @author You
    // @match http://localhost:*/*
    // @match https://localhost:*/*
    // @match http://127.0.0.1:*/*
    // @match https://127.0.0.1:*/*
    // @match https://*.hyper.video/*
    // @match https://latest.dev.hyper.video/*
    // @match https://hyper-lite.dev/*
    // @grant GM_log
    // @run-at document-start
    // ==/UserScript==

    ;(function () {
    'use strict'

    let callSequence = 0
    const logPrefix = '[Navigator Debug]'
    const logStyle = 'color: #007ACC; font-weight: bold;'

    // Store original functions
    const originalPermissionsQuery = navigator.permissions?.query?.bind(
    navigator.permissions
    )
    const originalGetUserMedia = navigator.mediaDevices?.getUserMedia?.bind(
    navigator.mediaDevices
    )

    function formatTimestamp() {
    const now = new Date()
    return `${now.toISOString().split('T')[1]}`
    }

    function getCallStack() {
    const stack = new Error().stack
    // Remove the first few lines which are from this wrapper
    const lines = stack.split('\n').slice(3, 8)
    return lines.join('\n')
    }

    // Wrap navigator.permissions.query
    if (navigator.permissions && navigator.permissions.query) {
    navigator.permissions.query = async function (permissionDesc) {
    const sequence = ++callSequence
    const timestamp = formatTimestamp()

    console.group(
    `%c${logPrefix} [${sequence}] permissions.query CALLED @ ${timestamp}`,
    logStyle
    )
    console.log('Permission:', permissionDesc)
    console.log('Call Stack:\n', getCallStack())
    console.groupEnd()

    try {
    const result = await originalPermissionsQuery(permissionDesc)

    console.group(
    `%c${logPrefix} [${sequence}] permissions.query RESOLVED @ ${formatTimestamp()}`,
    'color: #28a745; font-weight: bold;'
    )
    console.log('Permission:', permissionDesc)
    console.log('State:', result.state)
    console.log('Full Result:', result)
    console.groupEnd()

    return result
    } catch (error) {
    console.group(
    `%c${logPrefix} [${sequence}] permissions.query REJECTED @ ${formatTimestamp()}`,
    'color: #dc3545; font-weight: bold;'
    )
    console.log('Permission:', permissionDesc)
    console.error('Error:', error)
    console.groupEnd()
    throw error
    }
    }
    }

    // Wrap navigator.mediaDevices.getUserMedia
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    navigator.mediaDevices.getUserMedia = async function (constraints) {
    const sequence = ++callSequence
    const timestamp = formatTimestamp()

    console.group(
    `%c${logPrefix} [${sequence}] getUserMedia CALLED @ ${timestamp}`,
    logStyle
    )
    console.log('Constraints:', JSON.parse(JSON.stringify(constraints || {})))
    console.log('Call Stack:\n', getCallStack())
    console.groupEnd()

    try {
    const stream = await originalGetUserMedia(constraints)

    const audioTracks = stream.getAudioTracks().map(t => ({
    id: t.id,
    label: t.label,
    kind: t.kind,
    enabled: t.enabled,
    muted: t.muted,
    readyState: t.readyState,
    settings: t.getSettings ? t.getSettings() : {},
    }))

    const videoTracks = stream.getVideoTracks().map(t => ({
    id: t.id,
    label: t.label,
    kind: t.kind,
    enabled: t.enabled,
    muted: t.muted,
    readyState: t.readyState,
    settings: t.getSettings ? t.getSettings() : {},
    }))

    console.group(
    `%c${logPrefix} [${sequence}] getUserMedia RESOLVED @ ${formatTimestamp()}`,
    'color: #28a745; font-weight: bold;'
    )
    console.log(
    'Constraints:',
    JSON.parse(JSON.stringify(constraints || {}))
    )
    console.log('Stream ID:', stream.id)
    console.log('Audio Tracks:', audioTracks)
    console.log('Video Tracks:', videoTracks)
    console.groupEnd()

    return stream
    } catch (error) {
    console.group(
    `%c${logPrefix} [${sequence}] getUserMedia REJECTED @ ${formatTimestamp()}`,
    'color: #dc3545; font-weight: bold;'
    )
    console.log(
    'Constraints:',
    JSON.parse(JSON.stringify(constraints || {}))
    )
    console.error('Error:', {
    name: error.name,
    message: error.message,
    constraintName: error.constraintName || null,
    })
    console.groupEnd()
    throw error
    }
    }
    }

    // Also wrap enumerateDevices for completeness
    if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
    const originalEnumerateDevices =
    navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices)

    navigator.mediaDevices.enumerateDevices = async function () {
    const sequence = ++callSequence
    const timestamp = formatTimestamp()

    console.group(
    `%c${logPrefix} [${sequence}] enumerateDevices CALLED @ ${timestamp}`,
    logStyle
    )
    console.log('Call Stack:\n', getCallStack())
    console.groupEnd()

    try {
    const devices = await originalEnumerateDevices()

    const deviceSummary = devices.map(d => ({
    kind: d.kind,
    label: d.label || '(no label)',
    deviceId: d.deviceId ? `${d.deviceId.substring(0, 8)}...` : '(no id)',
    groupId: d.groupId ? `${d.groupId.substring(0, 8)}...` : '(no group)',
    }))

    console.group(
    `%c${logPrefix} [${sequence}] enumerateDevices RESOLVED @ ${formatTimestamp()}`,
    'color: #28a745; font-weight: bold;'
    )
    console.table(deviceSummary)
    console.groupEnd()

    return devices
    } catch (error) {
    console.group(
    `%c${logPrefix} [${sequence}] enumerateDevices REJECTED @ ${formatTimestamp()}`,
    'color: #dc3545; font-weight: bold;'
    )
    console.error('Error:', error)
    console.groupEnd()
    throw error
    }
    }
    }

    console.log(
    `%c${logPrefix} Script loaded! Monitoring navigator.permissions.query and navigator.mediaDevices.getUserMedia`,
    'color: #6f42c1; font-weight: bold;'
    )

    // Add a global function to check status
    window.navigatorDebugStatus = function () {
    console.log(`%c${logPrefix} Status:`, 'color: #6f42c1; font-weight: bold;')
    console.log(`- Total calls intercepted: ${callSequence}`)
    console.log('- Wrapped APIs:')
    console.log(
    ' • navigator.permissions.query:',
    !!navigator.permissions?.query
    )
    console.log(
    ' • navigator.mediaDevices.getUserMedia:',
    !!navigator.mediaDevices?.getUserMedia
    )
    console.log(
    ' • navigator.mediaDevices.enumerateDevices:',
    !!navigator.mediaDevices?.enumerateDevices
    )
    }

    // Add helper to reset counter
    window.navigatorDebugReset = function () {
    callSequence = 0
    console.log(
    `%c${logPrefix} Call sequence counter reset`,
    'color: #6f42c1; font-weight: bold;'
    )
    }

    console.log(`%c${logPrefix} Helper functions available:`, 'color: #6f42c1;')
    console.log(' - window.navigatorDebugStatus() : Check wrapper status')
    console.log(' - window.navigatorDebugReset() : Reset call counter')
    })()