text:
'use strict'
/* eslint-env browser, webextensions */

import debug from 'debug'

import browser from 'webextension-polyfill'
import toMultiaddr from 'uri-to-multiaddr'
import pMemoize from 'p-memoize'
import LRU from 'lru-cache'
import all from 'it-all'
import { optionDefaults, storeMissingOptions, migrateOptions, guiURLString, safeURL } from './options.js'
import { initState, offlinePeerCount } from './state.js'
import { createIpfsPathValidator, dropSlash, sameGateway, safeHostname } from './ipfs-path.js'
import createDnslinkResolver from './dnslink.js'
import { createRequestModifier } from './ipfs-request.js'
import { initIpfsClient, destroyIpfsClient, reloadIpfsClientOfflinePages } from './ipfs-client/index.js'
import { braveNodeType, useBraveEndpoint, releaseBraveEndpoint } from './ipfs-client/brave.js'
import { createIpfsImportHandler, formatImportDirectory, browserActionFilesCpImportCurrentTab } from './ipfs-import.js'
import createNotifier from './notifier.js'
import createCopier from './copier.js'
import createInspector from './inspector.js'
import createRuntimeChecks from './runtime-checks.js'
import { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway, contextMenuCopyPermalink, contextMenuCopyCidAddress } from './context-menus.js'
import { registerSubdomainProxy } from './http-proxy.js'
import { runPendingOnInstallTasks } from './on-installed.js'
import { handleConsentFromState, startSession, endSession, trackView } from './telemetry.js'
const log = debug('ipfs-companion:main')
log.error = debug('ipfs-companion:main:error')

let browserActionPort // reuse instance for status updates between on/off toggles

// init happens on addon load in background/background.js
export default async function init () {
  // INIT
  // ===================================================================
  let ipfs // ipfs-api instance
  /** @type {import('../types.js').CompanionState} */
  let state // avoid redundant API reads by utilizing local cache of various states
  let dnslinkResolver
  let ipfsPathValidator
  let modifyRequest
  let notify
  let copier
  let inspector
  let runtime
  let contextMenus
  let apiStatusUpdateInterval
  let ipfsImportHandler
  const idleInSecs = 5 * 60
  const browserActionPortName = 'browser-action-port'

  try {
    log('init')
    await storeMissingOptions(await browser.storage.local.get(), optionDefaults, browser.storage.local)
    await migrateOptions(browser.storage.local, debug)
    const options = await browser.storage.local.get(optionDefaults)
    runtime = await createRuntimeChecks(browser)
    state = initState(options)
    notify = createNotifier(getState)
    // ensure consent is set properly on app init
    handleConsentFromState(state)

    if (state.active) {
      startSession()
      // It's ok for this to fail, node might be unavailable or mis-configured
      try {
        ipfs = await initIpfsClient(browser, state)
      } catch (err) {
        console.error('[ipfs-companion] Failed to init IPFS client', err)
        notify(
          'notify_startIpfsNodeErrorTitle',
          err.name === 'ValidationError' ? err.details[0].message : err.message
        )
      }
    }

    dnslinkResolver = createDnslinkResolver(getState)
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    copier = createCopier(notify, ipfsPathValidator)
    ipfsImportHandler = createIpfsImportHandler(getState, getIpfs, ipfsPathValidator, runtime, copier)
    inspector = createInspector(notify, ipfsPathValidator, getState)
    contextMenus = createContextMenus(getState, runtime, ipfsPathValidator, {
      onAddFromContext,
      onCopyCanonicalAddress: copier.copyCanonicalAddress,
      onCopyRawCid: copier.copyRawCid,
      onCopyAddressAtPublicGw: copier.copyAddressAtPublicGw
    })
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
    log('register all listeners')
    registerListeners()
    await registerSubdomainProxy(getState, runtime, notify)
    log('init done')
    setApiStatusUpdateInterval(options.ipfsApiPollMs)
    await runPendingOnInstallTasks()
  } catch (error) {
    log.error('Unable to initialize addon due to error', error)
    if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
    throw error
  }

  function getState () {
    return state
  }

  function getIpfs () {
    if (state.active && ipfs) return ipfs
    log.error('refused access to IPFS API client, check if extension is enabled')
    throw new Error('IPFS Companion: API client is disabled')
  }

  function registerListeners () {
    const onBeforeSendInfoSpec = ['blocking', 'requestHeaders']
    if (browser.webRequest.OnBeforeSendHeadersOptions && 'EXTRA_HEADERS' in browser.webRequest.OnBeforeSendHeadersOptions) {
      // Chrome 72+  requires 'extraHeaders' for accessing all headers
      // Note: we need this for code ensuring kubo-rpc-client can talk to API without setting CORS
      onBeforeSendInfoSpec.push('extraHeaders')
    }
    browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: ['<all_urls>'] }, onBeforeSendInfoSpec)
    browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { urls: ['<all_urls>'] }, ['blocking'])
    browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: ['<all_urls>'] }, ['blocking', 'responseHeaders'])
    browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: ['<all_urls>'], types: ['main_frame'] })
    browser.webRequest.onCompleted.addListener(onCompleted, { urls: ['<all_urls>'], types: ['main_frame'] })
    browser.storage.onChanged.addListener(onStorageChange)
    browser.webNavigation.onCommitted.addListener(onNavigationCommitted)
    browser.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded)
    browser.tabs.onActivated.addListener(onActivatedTab)
    if (browser.windows) {
      browser.windows.onFocusChanged.addListener(onWindowFocusChanged)
    }
    browser.runtime.onMessage.addListener(onRuntimeMessage)
    browser.runtime.onConnect.addListener(onRuntimeConnect)

    if (runtime.hasNativeProtocolHandler) {
      log('native protocol handler support detected, but IPFS handler is not implemented yet :-(')
    } else {
      log('no browser.protocol API, native protocol will not be registered')
    }
  }

  // HTTP Request Hooks
  // ===================================================================

  function onBeforeSendHeaders (request) {
    return modifyRequest.onBeforeSendHeaders(request)
  }

  function onBeforeRequest (request) {
    return modifyRequest.onBeforeRequest(request)
  }

  function onHeadersReceived (request) {
    return modifyRequest.onHeadersReceived(request)
  }

  function onErrorOccurred (request) {
    return modifyRequest.onErrorOccurred(request)
  }

  function onCompleted (request) {
    return modifyRequest.onCompleted(request)
  }

  // RUNTIME MESSAGES (one-off messaging)
  // ===================================================================
  // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage

  function onRuntimeMessage (request, sender) {
    // console.log((sender.tab ? 'Message from a content script:' + sender.tab.url : 'Message from the extension'), request)
    if (request.pubGwUrlForIpfsOrIpnsPath) {
      const path = request.pubGwUrlForIpfsOrIpnsPath
      const { validIpfsOrIpns, resolveToPublicUrl } = ipfsPathValidator
      const result = validIpfsOrIpns(path) ? resolveToPublicUrl(path) : null
      return Promise.resolve({ pubGwUrlForIpfsOrIpnsPath: result })
    }
    if (request.telemetry) {
      return Promise.resolve(onTelemetryMessage(request.telemetry, sender))
    }
  }

  function onTelemetryMessage (request, sender) {
    if (request.trackView) {
      const { version } = browser.runtime.getManifest()
      return trackView(request.trackView, { version })
    }
  }

  // PORTS (connection-based messaging)
  // ===================================================================
  // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/connect
  // Make a connection between different contexts inside the add-on,
  // e.g. signalling between browser action popup and background page that works
  // in everywhere, even in private contexts (https://github.com/ipfs/ipfs-companion/issues/243)

  // Cache for async URL2CID resolution used by browser action
  // (resolution happens off-band so UI render is not blocked with sometimes expensive DHT traversal)
  const resolveCache = new LRU({ max: 10, ttl: 1000 * 30 })

  function onRuntimeConnect (port) {
    // console.log('onConnect', port)
    if (port.name === browserActionPortName) {
      browserActionPort = port
      browserActionPort.onMessage.addListener(handleMessageFromBrowserAction)
      browserActionPort.onDisconnect.addListener(() => { browserActionPort = null })
      sendStatusUpdateToBrowserAction()
    }
  }

  const BrowserActionMessageHandlers = {
    notification: (message) => notify(message.title, message.message),
    [browserActionFilesCpImportCurrentTab]: () => ipfsImportHandler.filesCpImportCurrentTab(browser),
    [contextMenuViewOnGateway]: inspector.viewOnGateway,
    [contextMenuCopyCanonicalAddress]: copier.copyCanonicalAddress,
    [contextMenuCopyCidAddress]: copier.copyCidAddress,
    [contextMenuCopyRawCid]: copier.copyRawCid,
    [contextMenuCopyAddressAtPublicGw]: copier.copyAddressAtPublicGw,
    [contextMenuCopyPermalink]: copier.copyPermalink
  }

  function handleMessageFromBrowserAction (message) {
    const handler = BrowserActionMessageHandlers[message && message.event]
    if (!handler) return console.warn('Unknown browser action message event', message)
    handler(message)
  }

  async function sendStatusUpdateToBrowserAction () {
    if (!browserActionPort) return
    const currentTab = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
    const { version } = browser.runtime.getManifest()
    const info = {
      active: state.active,
      ipfsNodeType: state.ipfsNodeType,
      peerCount: state.peerCount,
      gwURLString: dropSlash(state.gwURLString),
      pubGwURLString: dropSlash(state.pubGwURLString),
      webuiRootUrl: dropSlash(state.webuiRootUrl), // TODO: fix js-ipfs - it fails with trailing slash
      importDir: state.importDir,
      openViaWebUI: state.openViaWebUI,
      apiURLString: dropSlash(state.apiURLString),
      redirect: state.redirect,
      enabledOn: state.enabledOn,
      disabledOn: state.disabledOn,
      newVersion: state.dismissedUpdate !== version ? version : null,
      currentTab
    }
    try {
      const v = await ipfs.version()
      if (v) {
        info.gatewayVersion = v.commit ? v.version + '/' + v.commit : v.version
      }
    } catch (error) {
      info.gatewayVersion = null
    }
    if (state.active && info.currentTab) {
      const url = info.currentTab.url
      info.isIpfsContext = ipfsPathValidator.isIpfsPageActionsContext(url)
      if (info.isIpfsContext) {
        info.currentTabPublicUrl = ipfsPathValidator.resolveToPublicUrl(url)
        info.currentTabContentPath = ipfsPathValidator.resolveToIpfsPath(url)
        if (resolveCache.has(url)) {
          const [immutableIpfsPath, permalink, cid] = resolveCache.get(url)
          info.currentTabImmutablePath = immutableIpfsPath
          info.currentTabPermalink = permalink
          info.currentTabCid = cid
        } else {
          // run async resolution in the next event loop so it does not block the UI
          setTimeout(async () => {
            resolveCache.set(url, [
              await ipfsPathValidator.resolveToImmutableIpfsPath(url),
              await ipfsPathValidator.resolveToPermalink(url),
              await ipfsPathValidator.resolveToCid(url)
            ])
            await sendStatusUpdateToBrowserAction()
          }, 0)
        }
      }
      info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url)
      info.currentFqdn = info.currentDnslinkFqdn || safeHostname(url)
      info.currentTabIntegrationsOptOut = !state.activeIntegrations(info.currentFqdn)
      info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url)
    }
    // Still here?
    if (browserActionPort) {
      browserActionPort.postMessage({ statusUpdate: info })
    }
  }

  // Context Menu Importer
  // -------------------------------------------------------------------

  async function onAddFromContext (context, contextType, options) {
    const {
      copyImportResultsToFiles,
      copyShareLink,
      preloadFilesAtPublicGateway,
      openFilesAtGateway,
      openFilesAtWebUI
    } = ipfsImportHandler
    const importDir = formatImportDirectory(state.importDir)
    let data
    let results
    try {
      const dataSrc = await findValueForContext(context, contextType)
      if (contextType === 'selection') {
        // TODO: persist full pageUrl somewhere (eg. append at the end of the content but add toggle to disable it)
        data = {
          path: `${new URL(context.pageUrl).hostname}.txt`,
          content: dataSrc
        }
      } else {
        // Enchanced addFromURL
        // --------------------
        // Initially, this was a workaround due to https://github.com/ipfs/ipfs-companion/issues/227
        // but now we have additional rules about keeping file name, so we can't use valilla ipfs.addFromURL
        const fetchOptions = {
          cache: 'force-cache',
          referrer: context.pageUrl
        }
        // console.log('onAddFromContext.context', context)
        // console.log('onAddFromContext.fetchOptions', fetchOptions)
        const response = await fetch(dataSrc, fetchOptions)
        const blob = await response.blob()
        const url = new URL(response.url)
        // https://github.com/ipfs-shipyard/ipfs-companion/issues/599
        const filename = url.pathname === '/'
          ? url.hostname
          : url.pathname.replace(/[\\/]+$/, '').split('/').pop()
        data = {
          path: decodeURIComponent(filename),
          content: blob
        }
      }

      results = await all(ipfs.addAll([data], options))
      copyShareLink(results)
      preloadFilesAtPublicGateway(results)

      await copyImportResultsToFiles(results, importDir)
      if (!state.openViaWebUI || state.ipfsNodeType.startsWith('embedded')) {
        await openFilesAtGateway(importDir)
      } else {
        await openFilesAtWebUI(importDir)
      }
    } catch (error) {
      console.error('Error in import to IPFS context menu', error)
      if (error.message === 'NetworkError when attempting to fetch resource.') {
        notify('notify_importErrorTitle', 'notify_importTrackingProtectionErrorMsg')
        console.warn('IPFS import often fails because remote file can not be downloaded due to Tracking Protection. See details at: https://github.com/ipfs/ipfs-companion/issues/227')
        browser.tabs.create({
          url: 'https://github.com/ipfs/ipfs-companion/issues/227'
        })
      } else {
        notify('notify_importErrorTitle', 'notify_inlineErrorMsg', `${error.message}`)
      }
    }
  }

  // Page-specific Actions
  // -------------------------------------------------------------------

  async function onWindowFocusChanged (windowId) {
    // Note: On some Linux window managers, WINDOW_ID_NONE will always be sent
    // immediately preceding a switch from one browser window to another.
    if (windowId !== browser.windows.WINDOW_ID_NONE) {
      const currentTab = await browser.tabs.query({ active: true, windowId }).then(tabs => tabs[0])
      await contextMenus.update(currentTab.id)
    }
  }

  async function onActivatedTab (activeInfo) {
    await contextMenus.update(activeInfo.tabId)
  }

  async function onNavigationCommitted (details) {
    await contextMenus.update(details.tabId)
    await updatePageActionIndicator(details.tabId, details.url)
  }

  async function updatePageActionIndicator (tabId, url) {
    // Chrome does not permit for both pageAction and browserAction to be enabled at the same time
    // https://github.com/ipfs-shipyard/ipfs-companion/issues/398
    if (runtime.isFirefox && ipfsPathValidator.isIpfsPageActionsContext(url)) {
      if (sameGateway(url, state.gwURL) || sameGateway(url, state.apiURL)) {
        await browser.pageAction.setIcon({ tabId, path: '/icons/ipfs-logo-on.svg' })
        await browser.pageAction.setTitle({ tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtCustomGateway') })
      } else {
        await browser.pageAction.setIcon({ tabId, path: '/icons/ipfs-logo-off.svg' })
        await browser.pageAction.setTitle({ tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtPublicGateway') })
      }
      await browser.pageAction.show(tabId)
    }
  }

  async function onDOMContentLoaded (details) {
    if (!state.active) return // skip content script injection when off
    if (!details.url || !details.url.startsWith('http')) return // skip empty and special pages
    if (!state.activeIntegrations(details.url)) return // skip if opt-out exists
    // console.info(`[ipfs-companion] onDOMContentLoaded`, details)
    if (state.linkify) {
      log(`running linkfy experiment on ${details.url}`)
      try {
        await browser.tabs.executeScript(details.tabId, {
          file: '/dist/bundles/linkifyContentScript.bundle.js',
          matchAboutBlank: false,
          allFrames: true,
          runAt: 'document_idle'
        })
      } catch (error) {
        log.error(`Unable to linkify DOM at '${details.url}' due to`, error)
      }
    }
    // Ensure embedded js-ipfs in Brave uses correct API
    if (details.url.startsWith(state.webuiRootUrl)) {
      const apiMultiaddr = toMultiaddr(state.apiURLString)
      await browser.tabs.executeScript(details.tabId, {
        runAt: 'document_start',
        code: `if (!localStorage.getItem('ipfsApi')) {
        console.log('[ipfs-companion] Setting API to ${apiMultiaddr}');
        localStorage.setItem('ipfsApi', '${apiMultiaddr}');
        window.location.reload();
      }`
      })
    }
  }

  // API STATUS UPDATES
  // -------------------------------------------------------------------
  // API is polled for peer count every ipfsApiPollMs

  async function setApiStatusUpdateInterval (ipfsApiPollMs) {
    if (apiStatusUpdateInterval) {
      clearInterval(apiStatusUpdateInterval)
    }
    apiStatusUpdateInterval = setInterval(() => runIfNotIdle(apiStatusUpdate), ipfsApiPollMs)
    await apiStatusUpdate()
  }

  async function apiStatusUpdate () {
    // update peer count
    const oldPeerCount = state.peerCount
    state.peerCount = await getSwarmPeerCount()
    await Promise.all([
      updateAutomaticModeRedirectState(oldPeerCount, state.peerCount),
      updateBrowserActionBadge(),
      contextMenus.update(),
      sendStatusUpdateToBrowserAction()
    ])
  }

  async function getSwarmPeerCount () {
    if (!ipfs) return offlinePeerCount
    try {
      const peerInfos = await ipfs.swarm.peers({ timeout: 2500 })
      return peerInfos.length
    } catch (error) {
      console.error(`Error while ipfs.swarm.peers: ${error}`)
      return offlinePeerCount
    }
  }

  async function runIfNotIdle (action) {
    try {
      const state = await browser.idle.queryState(idleInSecs)
      if (state === 'active') {
        return action()
      }
    } catch (error) {
      console.error('Unable to read idle state, executing action without idle check', error)
      return action()
    }
  }

  // browserAction
  // -------------------------------------------------------------------

  async function updateBrowserActionBadge () {
    if (typeof browser.browserAction.setBadgeBackgroundColor === 'undefined') {
      // Firefox for Android does not have this UI, so we just skip it
      return
    }

    let badgeText, badgeColor, badgeIcon

    badgeText = ''
    if (state.peerCount > 0) {
      // All is good (online with peers)
      badgeColor = '#418B8E'
      badgeIcon = '/icons/ipfs-logo-on.svg'

      // prevent text overflow when peer count has more than 3 digits
      badgeText = (state.peerCount > 999) ? (Math.floor(state.peerCount / 1000).toString() + 'k') : state.peerCount.toString()
    } else if (state.peerCount === 0) {
      // API is online but no peers
      badgeColor = 'red'
      badgeIcon = '/icons/ipfs-logo-on.svg'
    } else {
      // API is offline
      badgeColor = '#8C8C8C'
      badgeIcon = '/icons/ipfs-logo-off.svg'
    }
    try {
      const oldColor = colorArraytoHex(await browser.browserAction.getBadgeBackgroundColor({}))
      if (badgeColor !== oldColor) {
        await browser.browserAction.setBadgeBackgroundColor({ color: badgeColor })
        await setBrowserActionIcon(badgeIcon)
      }
      const oldText = await browser.browserAction.getBadgeText({})
      if (oldText !== badgeText) await browser.browserAction.setBadgeText({ text: badgeText })
    } catch (error) {
      console.error('Unable to update browserAction badge due to error', error)
    }
  }

  // ColorArray [0,0,0,0] → Hex #000000
  // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/ColorArray
  function colorArraytoHex (colorArray) {
    if (!colorArray) return ''
    const colorAt = i => colorArray[i].toString(16).padStart(2, '0')
    const r = colorAt(0)
    const g = colorAt(1)
    const b = colorAt(2)
    return ('#' + r + g + b).toUpperCase()
  }

  async function setBrowserActionIcon (iconPath) {
    let iconDefinition = { path: iconPath }
    try {
      // Try SVG first -- Firefox supports it natively
      await browser.browserAction.setIcon(iconDefinition)
    } catch (error) {
      // Fallback!
      // Chromium does not support SVG [ticket below is 8 years old, I can't even..]
      // https://bugs.chromium.org/p/chromium/issues/detail?id=29683
      // Still, we want icon, so we precompute rasters of popular sizes and use them instead
      iconDefinition = await rasterIconDefinition(iconPath)
      await browser.browserAction.setIcon(iconDefinition)
    }
  }

  // OPTIONS
  // ===================================================================

  async function updateAutomaticModeRedirectState (oldPeerCount, newPeerCount) {
    // enable/disable gw redirect based on API going online or offline
    if (state.automaticMode && state.localGwAvailable) {
      if (oldPeerCount === offlinePeerCount && newPeerCount > offlinePeerCount && !state.redirect) {
        await browser.storage.local.set({ useCustomGateway: true })
        reloadIpfsClientOfflinePages(browser, ipfs, state)
      } else if (newPeerCount === offlinePeerCount && state.redirect) {
        await browser.storage.local.set({ useCustomGateway: false })
      }
    }
  }

  async function onStorageChange (changes, area) {
    let shouldReloadExtension = false
    let shouldRestartIpfsClient = false
    let shouldStopIpfsClient = false

    for (const key in changes) {
      const change = changes[key]
      if (change.oldValue === change.newValue) continue

      // debug info
      log(`storage key "${key}" changed: "${change.oldValue}" → "${change.newValue}"`)
      switch (key) {
        case 'active':
          state[key] = change.newValue
          await registerSubdomainProxy(getState, runtime)
          shouldRestartIpfsClient = true
          shouldStopIpfsClient = !state.active
          // Any time the extension switches active state, start or stop the current session.
          state.active ? startSession() : endSession()
          break
        case 'ipfsNodeType':
          if (change.oldValue !== braveNodeType && change.newValue === braveNodeType) {
            useBraveEndpoint(browser)
          } else if (change.oldValue === braveNodeType && change.newValue !== braveNodeType) {
            releaseBraveEndpoint(browser)
          }
          shouldRestartIpfsClient = true
          state[key] = change.newValue
          break
        case 'ipfsNodeConfig':
          shouldRestartIpfsClient = true
          state[key] = change.newValue
          break
        case 'ipfsApiUrl':
          state.apiURL = safeURL(change.newValue, { useLocalhostName: false }) // go-ipfs returns 403 if IP is beautified to 'localhost'
          state.apiURLString = state.apiURL.toString()
          shouldRestartIpfsClient = true
          break
        case 'ipfsApiPollMs':
          setApiStatusUpdateInterval(change.newValue)
          break
        case 'customGatewayUrl':
          state.gwURL = safeURL(change.newValue, { useLocalhostName: state.useSubdomains })
          state.gwURLString = state.gwURL.toString()
          break
        case 'publicGatewayUrl':
          state.pubGwURL = new URL(change.newValue)
          state.pubGwURLString = state.pubGwURL.toString()
          break
        case 'publicSubdomainGatewayUrl':
          state.pubSubdomainGwURL = new URL(change.newValue)
          state.pubSubdomainGwURLString = state.pubSubdomainGwURL.toString()
          break
        case 'useCustomGateway':
          state.redirect = change.newValue
          break
        case 'useSubdomains':
          state[key] = change.newValue
          await browser.storage.local.set({
            // We need to update the hostname in customGatewayUrl because:
            // 127.0.0.1 - path gateway
            // localhost - subdomain gateway
            // and we need to use the latter
            customGatewayUrl: guiURLString(state.gwURLString, {
              useLocalhostName: state.useSubdomains
            })
          })
          // Finally, update proxy settings based on the state
          await registerSubdomainProxy(getState, runtime)
          break
        case 'dnslinkPolicy':
          state.dnslinkPolicy = String(change.newValue) === 'false' ? false : change.newValue
          if (state.dnslinkPolicy === 'best-effort' && !state.detectIpfsPathHeader) {
            await browser.storage.local.set({ detectIpfsPathHeader: true })
          }
          break
        case 'logNamespaces':
          shouldReloadExtension = true
          state[key] = localStorage.debug = change.newValue
          break
        default:
          state[key] = change.newValue
          break
      }
    }
    // ensure consent is set properly on state changes
    handleConsentFromState(state)

    if ((state.active && shouldRestartIpfsClient) || shouldStopIpfsClient) {
      try {
        log('stoping ipfs client due to config changes', changes)
        await destroyIpfsClient(browser)
      } catch (err) {
        console.error('[ipfs-companion] Failed to destroy IPFS client', err)
        notify('notify_stopIpfsNodeErrorTitle', err.message)
      } finally {
        ipfs = null
      }

      if (!state.active) return

      try {
        log('starting ipfs client with the new config')
        ipfs = await initIpfsClient(browser, state)
      } catch (err) {
        console.error('[ipfs-companion] Failed to init IPFS client', err)
        notify(
          'notify_startIpfsNodeErrorTitle',
          err.name === 'ValidationError' ? err.details[0].message : err.message
        )
      }

      apiStatusUpdate()
    }
    if (shouldReloadExtension) {
      log('reloading extension due to config change')
      browser.tabs.reload() // async reload of options page to keep it alive
      await browser.runtime.reload()
    }
    // Post update to Browser Action (if exists) -- this gives UX a snappy feel
    await sendStatusUpdateToBrowserAction()
  }

  // Public API
  // (typically attached to a window variable to interact with this companion instance)
  const api = {
    get ipfs () {
      return ipfs
    },

    get state () {
      return state
    },

    get runtime () {
      return runtime
    },

    get dnslinkResolver () {
      return dnslinkResolver
    },

    get ipfsPathValidator () {
      return ipfsPathValidator
    },

    get notify () {
      return notify
    },

    get ipfsImportHandler () {
      return ipfsImportHandler
    },

    async destroy () {
      if (state) {
        state.active = false
        state.peerCount = -1 // api down
      }

      if (apiStatusUpdateInterval) {
        clearInterval(apiStatusUpdateInterval)
        apiStatusUpdateInterval = null
      }

      if (ipfs) {
        await destroyIpfsClient(browser)
        ipfs = null
      }
    }
  }

  return api
}

const rasterIconDefinition = pMemoize((svgPath) => {
  const pngPath = (size) => {
    // point at precomputed PNG file
    const baseName = /\/icons\/(.+)\.svg/.exec(svgPath)[1]
    return `/icons/png/${baseName}_${size}.png`
  }
  // icon sizes to cover ranges from:
  // - https://bugs.chromium.org/p/chromium/issues/detail?id=647182
  // - https://developer.chrome.com/extensions/manifest/icons
  const r19 = pngPath(19)
  const r38 = pngPath(38)
  const r128 = pngPath(128)
  // return computed values to be cached by p-memoize
  return { path: { 19: r19, 38: r38, 128: r128 } }
})


text:
'use strict'
/* eslint-env browser */

const stableChannels = new Set([
  'ipfs-firefox-addon@lidel.org', // firefox (for legacy reasons)
  'nibjojkomfdiaoajekhjakgkdhaomnch' // chromium (chrome web store)
])

const stableChannelFormUrl = 'https://docs.google.com/forms/d/e/1FAIpQLSfLF7uzaxRKiF4XpPL9_DvkdaQHoRnDihRTZ1uVL6ceQwIrtg/viewform'

export function getUninstallURL (browser) {
  // on uninstall feedback form shown only on stable channel
  return stableChannels.has(browser.runtime.id) ? stableChannelFormUrl : ''
}

repo_name: ipfs/ipfs-companion

repo_language: JavaScript

mime_type: text/x-diff


text:
'use strict'
/* eslint-env browser, webextensions */
import pWaitFor from 'p-wait-for'

import debug from 'debug'

import * as external from './external.js'
import toUri from 'multiaddr-to-uri'
import { welcomePage, optionsPage, tickMs } from '../constants.js'
const log = debug('ipfs-companion:client:brave')
log.error = debug('ipfs-companion:client:brave:error')

// increased interval to decrease impact of IPFS service process spawns
const waitFor = (f, t) => pWaitFor(f, { interval: tickMs, timeout: t || Infinity })

// wrapper for chrome.ipfs.* that gets us closer to ergonomics of promise-based browser.*
export const brave = hasBraveChromeIpfs()
  ? Object.freeze({
    // This is the main check - returns true only in Brave and only when
    // feature flag is enabled brave://flags and can be used for high level UI
    // decisions such as showing custom node type on Preferences
    getIPFSEnabled: async () =>
      Boolean(await promisifyBraveCheck(chrome.ipfs.getIPFSEnabled)),

    // Obtains a string representation of the resolve method
    // method is one of the following strings:
    // "ask" uses a gateway but also prompts them to install a local node
    // "gateway" uses a gateway but also prompts them to install a local node
    // "local" uses a gateway but also prompts them to install a local node
    // "disabled" disabled by the user
    // "undefined" everything else (IPFS feature flag is not enabled, error etc)
    getResolveMethodType: async () =>
      String(await promisifyBraveCheck(chrome.ipfs.getResolveMethodType)),

    // Obtains the config contents of the local IPFS node
    // Returns undefined if missing for any reason
    getConfig: async () =>
      await promisifyBraveCheck(chrome.ipfs.getConfig),

    // Returns true if binary is present
    getExecutableAvailable: async () =>
      Boolean(await promisifyBraveCheck(chrome.ipfs.getExecutableAvailable)),

    // Attempts to start the daemon and returns true if finished
    launch: async () =>
      Boolean(await promisifyBraveCheck(chrome.ipfs.launch)),

    // Attempts to stop the daemon and returns true if finished
    shutdown: async () =>
      Boolean(await promisifyBraveCheck(chrome.ipfs.shutdown))
  })
  : undefined

export async function init (browser, opts) {
  log('ensuring Brave Settings are correct')
  await initBraveSettings(browser, brave)
  log('delegating API client init to "external" backend pointed at node managed by Brave')
  return external.init(browser, opts)
}

export async function destroy (browser) {
  log('shuting down node managed by Brave')
  const method = await brave.getResolveMethodType()
  if (method === 'local') {
    // shut down local node when this backend is not active
    log('waiting for brave.shutdown() to finish')
    await waitFor(() => brave.shutdown())
    log('brave.shutdown() done')
  }
  log('delegating API client destroy to "external" backend pointed at node managed by Brave')
  return external.destroy(browser)
}

// ---------------- Brave-specifics -------------------

// ipfs:// URI that will be used for triggering the "Enable IPFS" dropbar in Brave
// Here we use inlined empty byte array, which resolves instantly and does not
// introduce any delay in UI.
const braveIpfsUriTrigger = 'ipfs://bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss/'
const braveGatewayUrlTrigger = 'https://bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss.ipfs.dweb.link/'

// Settings screen in Brave where user can manage IPFS support
const braveSettingsPage = 'brave://settings/extensions'
// TODO: replace with brave://settings/ipfs after https://github.com/brave/brave-browser/issues/13655 lands in Brave Stable

// Diagnostic page for manually starting/stopping Brave's node
// const braveIpfsDiagnosticPage = 'brave://ipfs' // TODO: https://github.com/brave/brave-browser/issues/14500

// ipfsNodeType for this backend
export const braveNodeType = 'external:brave'

// Detect chrome.ipfs.* APIs provided by Brave to IPFS Companion
function hasBraveChromeIpfs () {
  return typeof chrome === 'object' &&
    typeof chrome.ipfs === 'object' &&
    typeof chrome.ipfs.getIPFSEnabled === 'function' &&
    typeof chrome.ipfs.getResolveMethodType === 'function' &&
    typeof chrome.ipfs.launch === 'function' &&
    typeof chrome.ipfs.shutdown === 'function' &&
    typeof chrome.ipfs.getExecutableAvailable === 'function' &&
    typeof chrome.ipfs.getConfig === 'function'
}

// Reads value via chrome.ipfs and returns it.
// Never throws: missing/error is returned as undefined.
const promisifyBraveCheck = (fn) => {
  return new Promise((resolve, reject) => {
    try {
      if (fn === chrome.ipfs.getConfig) {
        fn((ok, config) => {
          if (ok && config) return resolve(JSON.parse(config))
          return resolve(undefined)
        })
      }
      fn(val => resolve(val))
    } catch (e) {
      log.error('unexpected error during promisifyBraveCheck', e)
      reject(e)
    }
  })
}

// We preserve original "external" config, so user can switch between
// nodes provided by Brave and IPFS Desktop without the need for
// manually editing the address of IPFS API endpoint.

export async function useBraveEndpoint (browser) {
  const braveConfig = await brave.getConfig()
  if (typeof braveConfig === 'undefined') {
    log.error('useBraveEndpoint: IPFS_PATH/config is missing, unable to use endpoint from Brave at this time, will try later')
    return
  }

  const {
    externalNodeConfig: oldExternalNodeConfig,
    customGatewayUrl: oldGatewayUrl,
    ipfsApiUrl: oldApiUrl
  } = (await browser.storage.local.get(['customGatewayUrl', 'ipfsApiUrl', 'externalNodeConfig']))
  const braveApiUrl = addrs2url(braveConfig.Addresses.API)
  const braveGatewayUrl = addrs2url(braveConfig.Addresses.Gateway)

  if (braveApiUrl === oldApiUrl && braveGatewayUrl === oldGatewayUrl) {
    log('useBraveEndpoint: ok')
    return
  }

  log(`useBraveEndpoint: setting api=${braveApiUrl}, gw=${braveGatewayUrl} (before: api=${oldApiUrl}, gw=${oldGatewayUrl})`)
  await browser.storage.local.set({
    ipfsApiUrl: braveApiUrl,
    customGatewayUrl: braveGatewayUrl,
    externalNodeConfig: oldExternalNodeConfig || [oldGatewayUrl, oldApiUrl]
  })
}

export async function releaseBraveEndpoint (browser) {
  const [oldGatewayUrl, oldApiUrl] = (await browser.storage.local.get('externalNodeConfig')).externalNodeConfig
  log(`releaseBraveEndpoint: restoring api=${oldApiUrl}, gw=${oldGatewayUrl}`)
  await browser.storage.local.set({
    ipfsApiUrl: oldApiUrl,
    customGatewayUrl: oldGatewayUrl,
    externalNodeConfig: null
  })
}

// Addresses in go-ipfs config can be a String or array of strings with multiaddrs
function addrs2url (addr) {
  if (Array.isArray(addr)) {
    addr = addr[0]
  }
  return toUri(addr, { assumeHttp: true })
}

async function initBraveSettings (browser, brave) {
  let method = await brave.getResolveMethodType()
  log(`brave.resolveMethodType is '${method}'`)

  if (method === 'ask') {
    // Trigger the dropbar with "Enable IPFS" button by opening ipfs:// URI in a new tab.
    await browser.tabs.create({ url: braveIpfsUriTrigger })

    // IPFS Companion is unable to change Brave settings,
    // all we can do is to poll chrome.ipfs.* and detect when user made a decision
    log('waiting for user to make a decision how IPFS resources should be resolved')
    await waitFor(async () => {
      method = await brave.getResolveMethodType()
      return method && method !== 'ask'
    })
    log(`user set resolveMethodType to '${method}'`)

    if (method === 'local') {
      log('waiting while Brave downloads IPFS executable..')
      await waitFor(() => brave.getExecutableAvailable())

      log('waiting while Brave creates repo and config via ipfs init..')
      await waitFor(async () => typeof (await brave.getConfig()) !== 'undefined')
    }
  }

  if (method !== 'local') {
    // user picked remote gateway mode, turn Companion off for now
    // (this way clicking power button in browser action menu will re-run activation flow)
    await browser.storage.local.set({ active: false })
    // close tab with temporary trigger URI
    await closeIpfsTab(browser, braveIpfsUriTrigger)
    await closeIpfsTab(browser, braveGatewayUrlTrigger)
    // open settings
    await browser.tabs.create({ url: braveSettingsPage })
    throw new Error('"Method to resolve IPFS resources" in Brave settings should be "Local node"')
  }

  // ensure local node is started
  log('waiting while brave.launch() starts ipfs daemon..')
  await waitFor(() => brave.launch())
  log('brave.launch() finished')

  // ensure Companion uses the endpoint provided by Brave
  await useBraveEndpoint(browser)

  // async UI cleanup, after other stuff
  setTimeout(() => activationUiCleanup(browser), tickMs)
}

// close tab in a way that works with ipfs://
async function closeIpfsTab (browser, tabUrl) {
  // fun bug: querying for { url: 'ipfs://..' } does not work,
  // but if we query for { } ipfs:// tabs are returned just fine,
  // so we do that and discard unwanted ones  ¯\_(ツ)_/¯
  // TODO: fix chrome.tabs.query when we care about upstreaming things to Chromium
  for (const tab of await browser.tabs.query({})) {
    if (tab.url === tabUrl) {
      await browser.tabs.remove(tab.id)
    }
  }
}

// Various tedious tasks that need to happen for nice UX:
// - wait for gateway to be up (indicates node finished booting)
// - close ephemeral activation tab
// - re-activate entry point (options or welcome page)
// - ignore unexpected failures (user could do something weird, close tab before time etc)
async function activationUiCleanup (browser) {
  try {
    // after useBraveEndpoint we can start polling for gateway to become online
    const { customGatewayUrl: braveGwUrl } = await browser.storage.local.get('customGatewayUrl')
    // wait 1m for gateway to be online (bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss)
    await waitFor(async () => {
      try {
        return await fetch(`${braveGwUrl}/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss`).then(response => response.ok)
      } catch (_) {
        return false
      }
    })
    log('[activation ui cleanup] Brave gateway is up, cleaning up')

    const welcomePageUrl = browser.runtime.getURL(welcomePage)
    const optionsPageUrl = browser.runtime.getURL(optionsPage)
    // we are unable to query ipfs:// directly due to reasons mentioned in 'closeIpfsTab'
    // so we make quick pass over all tabs and check welcome and options while at it.
    for (const tab of await browser.tabs.query({})) {
      try {
        // close tab with temporary trigger
        if (tab.url === braveIpfsUriTrigger || tab.url === braveGatewayUrlTrigger) {
          await browser.tabs.remove(tab.id)
        }
        // switch to welcome page if present (onboarding via fresh install)
        if (tab.url === welcomePageUrl) {
          await browser.tabs.reload(tab.id)
          await browser.tabs.update(tab.id, { active: true })
        }
        // switch to options page if present (onboarding via Preferences)
        if (tab.url === optionsPageUrl) {
          await browser.tabs.update(tab.id, { active: true })
        }
      } catch (e) {
        log.error('[activation ui cleanup] unexpected error, but safe to ignore', e)
        continue
      }
    }
    log('[activation ui cleanup] done')

    // (if ok or not, close temporary tab and switch to welcome page or open it if not existing
    // if ((await browser.tabs.get(tabId)).url.startsWith(braveIpfsUriTrigger)) {
  } catch (e) {
    // most likely tab is gone (closed by user, etc)
    log.error('[activation ui cleanup] failed to cleanup ephemeral UI tab', e)
  }
}


text:
'use strict'

import debug from 'debug'

import mergeOptions from 'merge-options'
import { create } from 'ipfs-core'
import { optionDefaults } from '../options.js'
const log = debug('ipfs-companion:client:embedded')
log.error = debug('ipfs-companion:client:embedded:error')

let node = null

export async function init (browser, opts) {
  log('init')
  const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
  const userOpts = JSON.parse(opts.ipfsNodeConfig)
  const ipfsOpts = mergeOptions(defaultOpts, userOpts, { start: true })
  const missing = (array) => (!Array.isArray(array) || !array.length)
  const { Addresses } = ipfsOpts.config
  if (missing(Addresses.Swarm)) {
    Addresses.Swarm = [
      '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
      '/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
    ]
  }
  if (missing(ipfsOpts.Delegates)) {
    Addresses.Delegates = [
      '/dns4/node0.delegate.ipfs.io/tcp/443/https',
      '/dns4/node1.delegate.ipfs.io/tcp/443/https',
      '/dns4/node2.delegate.ipfs.io/tcp/443/https',
      '/dns4/node3.delegate.ipfs.io/tcp/443/https'
    ]
  }
  node = await create(ipfsOpts)
  return node
}

export async function destroy (browser) {
  log('destroy')
  if (!node) return

  await node.stop()
  node = null
}


text:
'use strict'

/* eslint-env browser, webextensions */

import debug from 'debug'

import * as external from './external.js'
import * as embedded from './embedded.js'
import * as brave from './brave.js'
import { precache } from '../precache.js'
import {
  prepareReloadExtensions, WebUiReloader, LocalGatewayReloader, InternalTabReloader
} from './reloaders/index.js'
const log = debug('ipfs-companion:client')
log.error = debug('ipfs-companion:client:error')

// ensure single client at all times, and no overlap between init and destroy
let client

export async function initIpfsClient (browser, opts) {
  log('init ipfs client')
  if (client) return // await destroyIpfsClient()
  let backend
  switch (opts.ipfsNodeType) {
    case 'embedded:chromesockets':
      // TODO: remove this one-time migration after in second half of 2021
      setTimeout(async () => {
        log('converting embedded:chromesockets to native external:brave')
        opts.ipfsNodeType = 'external:brave'
        await browser.storage.local.set({
          ipfsNodeType: 'external:brave',
          ipfsNodeConfig: '{}' // remove chrome-apps config
        })
        await browser.tabs.create({ url: 'https://docs.ipfs.tech/how-to/companion-node-types/#native' })
      }, 0)
      // Halt client init
      throw new Error('Embedded + chrome.sockets is deprecated. Switching to Native IPFS in Brave.')
    case 'embedded':
      backend = embedded
      break
    case 'external:brave':
      backend = brave
      break
    case 'external':
      backend = external
      break
    default:
      throw new Error(`Unsupported ipfsNodeType: ${opts.ipfsNodeType}`)
  }
  const instance = await backend.init(browser, opts)
  _reloadIpfsClientDependents(browser, instance, opts) // async (API is present)
  client = backend
  return instance
}

export async function destroyIpfsClient (browser) {
  log('destroy ipfs client')
  if (!client) return
  try {
    await client.destroy(browser)
    await _reloadIpfsClientDependents(browser) // sync (API stopped working)
  } finally {
    client = null
  }
}

/**
 * Reloads pages dependant on ipfs to be online
 *
 * @typedef {embedded|brave|external} Browser
 * @param {Browser} browser
 * @param {import('kubo-rpc-client').default} instance
 * @param {Object} opts
 * @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]=} reloadExtensions
 * @returns {void}
 */
async function _reloadIpfsClientDependents (
  browser, instance, opts, reloadExtensions = [WebUiReloader, LocalGatewayReloader, InternalTabReloader]) {
  // online || offline
  if (browser.tabs && browser.tabs.query) {
    const tabs = await browser.tabs.query({})
    if (tabs) {
      try {
        const reloadExtensionInstances = await prepareReloadExtensions(reloadExtensions, browser, log)
        // the reload process is async, fire and forget.
        reloadExtensionInstances.forEach(ext => ext.reload(tabs))
      } catch (e) {
        log('Failed to trigger reloaders')
      }
    }
  }

  // online only
  if (client && instance && opts) {
    // add important data to local ipfs repo for instant load
    setTimeout(() => precache(instance, opts), 5000)
  }
}

/**
 * Reloads local gateway pages dependant on ipfs to be online
 *
 * @typedef {embedded|brave|external} Browser
 * @param {Browser} browser
 * @param {import('kubo-rpc-client').default} instance
 * @param {Object} opts
 * @returns {void}
 */
export function reloadIpfsClientOfflinePages (browser, instance, opts) {
  _reloadIpfsClientDependents(browser, instance, opts, [LocalGatewayReloader])
}


text:
'use strict'
/* eslint-env browser */

import debug from 'debug'

import { create } from 'kubo-rpc-client'
const log = debug('ipfs-companion:client:external')
log.error = debug('ipfs-companion:client:external:error')

export async function init (browser, opts) {
  log(`init with IPFS API at ${opts.apiURLString}`)
  const clientConfig = opts.apiURLString
  const api = await create(clientConfig)
  return api
}

export async function destroy (browser) {
  log('destroy')
}


text:
import ReloaderBase from './reloaderBase.js'

export default class WebUiReloader extends ReloaderBase {
  /**
   * Performs url validation for the tab. If tab is a WebUI tab.
   *
   * @param {Object} tab
   * @returns {boolean}
   */
  validation ({ url }) {
    const bundled = !url.startsWith('http') && url.includes('/webui/index.html#/')
    const ipns = url.includes('/webui.ipfs.io/#/')
    return bundled || ipns
  }

  /**
   * Returns message when reloading the tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {string} message.
   */
  message ({ url }) {
    return `reloading webui at ${url}`
  }
}


text:
import InternalTabReloader from './internalTabReloader.js'
import LocalGatewayReloader from './localGatewayReloader.js'
import WebUiReloader from './webUiReloader.js'

/**
 * Prepares extension by creating an instance and awaiting for init.
 *
 * @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]} extensions
 * @param {Browser} browserInstance
 * @param {Logger} loggerInstance
 * @returns {Promise<Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]>}
 */
function prepareReloadExtensions (extensions, browserInstance, loggerInstance) {
  const reloadExtensions = Array.isArray(extensions) ? extensions : [extensions]
  return Promise.all(reloadExtensions
    .map(async Ext => {
      try {
        const ext = new Ext(browserInstance, loggerInstance)
        await ext.init()
        return ext
      } catch (e) {
        loggerInstance(`Extension Instance Failed to Initialize with error: ${e}. Extension: ${Ext}`)
      }
    })
  )
}

export {
  InternalTabReloader,
  LocalGatewayReloader,
  WebUiReloader,
  prepareReloadExtensions
}


text:
export default class ReloaderBase {
  /**
   * Constructor for reloader base class.
   *
   * @param {Browser} browser
   * @param {Logger} log
   */
  constructor (browser, log) {
    if (!browser || !log) {
      throw new Error('Instances of browser and logger are needed!')
    }
    this._browserInstance = browser
    this._log = log
  };

  /**
   * Initializes the instance.
   */
  init () {
    this._log('Initialized without additional config.')
  }

  /**
   * To be implemented in child class.
   */
  validation () {
    throw new Error('Validation: Method Not Implemented')
  }

  /**
   * To be implemented in child class.
   */
  message () {
    throw new Error('Message: Method Not Implemented')
  }

  /**
   * Handles reload for all tabs.
   * params {Array<tabs>}
   */
  reload (tabs) {
    tabs
      .filter(tab => this.validation(tab))
      .forEach(tab => {
        this._log(this.message(tab))
        this._browserInstance.tabs.reload(tab.id)
      })
  }
}


text:
import ReloaderBase from './reloaderBase.js'
import isIPFS from 'is-ipfs'

export default class LocalGatewayReloader extends ReloaderBase {
  /**
   * Performs url validation for the tab. If tab is loaded via local gateway.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @param {string} tab.url
   * @returns {boolean}
   */
  validation ({ url, title }) {
    /**
     * Check if the url is the local gateway url and if the title is contained within the url then it was not loaded.
     * - This assumes that the title of most pages on the web will be set and hence when not reachable, the browser
     *   will set title to the url/host (both chrome and brave) and 'problem loading page' for firefox.
     * - There is probability that this might be true in case the <title> tag is omitted, but worst case it only reloads
     *   those pages.
     * - The benefit we get from this approach is the static nature of just observing the tabs in their current state
     *   which reduces the overhead of injecting content scripts to track urls that were loaded after the connection
     *   was offline, it may also need extra permissions to inject code on error pages.
     */
    return (isIPFS.url(url) || isIPFS.subdomain(url)) &&
      (url.includes(title) || title.toLowerCase() === 'problem loading page')
  }

  /**
   * Returns message when reloading the tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {string} message.
   */
  message ({ url }) {
    return `reloading local gateway at ${url}`
  }
}


text:
import ReloaderBase from './reloaderBase.js'

export default class InternalTabReloader extends ReloaderBase {
  /**
   * Setting up the extension origin.
   */
  init () {
    this.extensionOrigin = this._browserInstance.runtime.getURL('/')
    this._log('InternalTabReloader Ready for use.')
  }

  /**
   * Performs url validation for the tab. If tab is a WebUI tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {boolean}
   */
  validation ({ url }) {
    return url.startsWith(this.extensionOrigin)
  }

  /**
   * Returns message when reloading the tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {string} message.
   */
  message ({ url }) {
    return `reloading internal extension page at ${url}`
  }
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'

/**
 * Renders a translated string with html anchors.
 * @param {String} message The message to translate.
 * @param {Array} links The array of hrefs.
 * @param {Object} attributes HTML attributes to put in the anchor.
 * @return {html} An HTML node with the translated string with anchors.
 */
export const renderTranslatedLinks = (message, links, attributes) => {
  const regexLink = /<\d+>(.+?)<\/\d+>/mg
  const regexIndex = /<(\d+)>/mg
  const str = browser.i18n.getMessage(message)
  let output = str

  let matchLink = regexLink.exec(str)
  while (matchLink !== null) {
    let matchIndex = regexIndex.exec(matchLink[0])
    while (matchIndex !== null) {
      output = output.replace(matchLink[0], `<a href="${links[parseInt(matchIndex[1])]}" ${attributes}>${matchLink[1]}</a>`)
      matchIndex = regexIndex.exec(str)
    }
    matchLink = regexLink.exec(str)
  }

  const template = document.createElement('template')
  template.innerHTML = output

  return template.content
}

/**
 * Renders a translated string with html spans.
 * @param {String} message - The message to translate.
 * @param {Array} values -  The array of dynamic values ($1, $2 etc) in the translation.
 * @param {Object} attributes - HTML attributes to put in the span around each of them.
 * @return {html} An HTML node with the translated string with spans.
 */
export const renderTranslatedSpans = (message, values, attributes) => {
  const regexSpan = /<\d+>(.+?)<\/\d+>/mg
  const regexIndex = /<(\d+)>/mg
  const str = browser.i18n.getMessage(message, values)
  let output = str

  let matchSpan = regexSpan.exec(str)
  while (matchSpan !== null) {
    let matchIndex = regexIndex.exec(matchSpan[0])
    while (matchIndex !== null) {
      output = output.replace(matchSpan[0], `<span ${attributes}>${matchSpan[1]}</span>`)
      matchIndex = regexIndex.exec(str)
    }
    matchSpan = regexSpan.exec(str)
  }

  const template = document.createElement('template')
  template.innerHTML = output

  return template.content
}


text:
@import url('~tachyons/css/tachyons.css');
@import url('~ipfs-css/ipfs.css');
@import url('../pages/components/switch-toggle.css');

header {
  border-color: #69c4cd;
  text-transform: uppercase;
}
#header-logo {
  height: 50px;
  width: 117.5px;
}
form * {
  font-family: Inter,system-ui,sans-serif;
  font-size: 0.875rem;
}
fieldset {
  border: none;
  display: flex;
  flex-direction: column;
}
fieldset > div {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding-bottom: 1.5em;
}
fieldset > div + div {
  border-top: 1px solid #ccc;
}
label {
  flex: 1;
}
select:hover,
button:hover,
label:hover {
  cursor: pointer;
}
label > dl {
  margin-right: 2em;
}
label > dl > dt {
  font-weight: 600;
}
label > dl > dd {
  margin: .5em 0 .5em 0;
}
label a[href^="http"]:after {
  content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="%2334373f" viewBox="20 12 60 70"><path d="M75.4 49.9c-.9 0-1.6.7-1.6 1.6v21.8c0 1.5-.6 2.9-1.7 4s-2.5 1.7-4 1.7h-40c-1.5 0-2.9-.6-4-1.7s-1.7-2.5-1.7-4v-40c0-1.5.6-2.9 1.7-4s2.5-1.7 4-1.7h21.8c.9 0 1.6-.7 1.6-1.6s-.7-1.6-1.6-1.6H28.1c-2.4 0-4.6.9-6.3 2.6-1.7 1.7-2.6 3.9-2.6 6.3v40c0 2.4.9 4.6 2.6 6.3 1.7 1.7 3.9 2.6 6.3 2.6h40c2.4 0 4.6-.9 6.3-2.6 1.7-1.7 2.6-3.9 2.6-6.3V51.5c0-.9-.7-1.6-1.6-1.6z"/><path d="M83.8 19.3V19v-.2-.2c0-.1-.1-.1-.1-.2 0 0 0-.1-.1-.1-.1-.1-.1-.2-.2-.2l-.2-.2s-.1 0-.1-.1l-.1-.1c-.1 0-.1 0-.2-.1H60.3c-.9 0-1.6.7-1.6 1.6s.7 1.6 1.6 1.6h17.9l-25.8 26c-.6.6-.6 1.7 0 2.3.3.3.7.5 1.1.5s.8-.2 1.1-.5l25.8-25.8v17.9c0 .9.7 1.6 1.6 1.6s1.6-.7 1.6-1.6l.2-21.9z"/></svg>');
  padding-left: 0.25em;
}
input, textarea, select {
  flex: 1;
  padding: .75em;
  font-family: Consolas, monaco, monospace;
}
select {
	appearance: none;
}

fieldset > div > div,
#resetAllOptions {
  flex: 1;
}
input[type=text],
input[type=url],
input[type=number],
select,
textarea
 {
  transition: all 0.30s ease-in-out;
  border-radius: 0.25rem;
  border: 1px solid #b7bbc8;
}

input:invalid {
  border: 1px solid #ea5037;
  background-color: #f36149;
}
.mdc-switch {
  display: block;
}

/* https://brave.com/brave-branding-assets/ */
.brave {
  background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg version='1.1' viewBox='0 0 218 256' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3ClinearGradient id='c' y1='50.706%25' y2='50.706%25'%3E%3Cstop stop-color='%23f50' offset='0'/%3E%3Cstop stop-color='%23f50' offset='.40988'/%3E%3Cstop stop-color='%23FF2000' offset='.58198'/%3E%3Cstop stop-color='%23FF2000' offset='1'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' x1='2.1484%25' y1='50.706%25' y2='50.706%25'%3E%3Cstop stop-color='%23FF452A' offset='0'/%3E%3Cstop stop-color='%23FF2000' offset='1'/%3E%3C/linearGradient%3E%3Cpath id='a' d='m170.27 25.336-22.304-25.336h-39.168-39.168l-22.304 25.336s-19.584-5.4468-28.832 3.8128c0 0 26.112-2.3601 35.088 12.255 0 0 24.208 4.6298 27.472 4.6298s10.336-2.7234 16.864-4.9021c6.528-2.1787 10.88-2.1945 10.88-2.1945s4.352 0.015796 10.88 2.1945 13.6 4.9021 16.864 4.9021 27.472-4.6298 27.472-4.6298c8.976-14.615 35.088-12.255 35.088-12.255-9.248-9.2596-28.832-3.8128-28.832-3.8128'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg transform='translate(-70 -350)'%3E%3Cg transform='translate(50 350)'%3E%3Cg transform='translate(20.114)'%3E%3Cpath d='m209.98 61.285 5.984-14.706s-7.616-8.1702-16.864-17.43c-9.248-9.2596-28.832-3.8128-28.832-3.8128l-22.304-25.336h-39.168-39.168l-22.304 25.336s-19.584-5.4468-28.832 3.8128c-9.248 9.2596-16.864 17.43-16.864 17.43l5.984 14.706-7.616 21.787s22.399 84.946 25.024 95.319c5.168 20.426 8.704 28.323 23.392 38.672s41.344 28.323 45.696 31.047 9.792 7.3625 14.688 7.3625 10.336-4.639 14.688-7.3625 31.008-20.698 45.696-31.047 18.224-18.247 23.392-38.672c2.6243-10.373 25.024-95.319 25.024-95.319l-7.616-21.787z' fill='url(%23c)'/%3E%3Cpath d='m164.02 41.404s28.688 34.723 28.688 42.145-3.6089 9.3799-7.2374 13.238c-3.6285 3.858-19.476 20.707-21.511 22.871-2.0351 2.164-6.2718 5.4446-3.7797 11.35s6.1684 13.419 2.0803 21.04c-4.0887 7.6212-11.093 12.708-15.581 11.867-4.488-0.84153-15.028-6.3564-18.904-8.8756s-16.161-12.663-16.161-16.545c0-3.8809 12.699-10.849 15.044-12.433 2.3468-1.5828 13.047-7.711 13.267-10.116 0.21869-2.4059 0.13546-3.1112-3.0225-9.0548-3.1579-5.9441-8.8449-13.876-7.8983-19.153 0.94656-5.2769 10.118-8.021 16.664-10.496 6.5443-2.4745 19.146-7.1484 20.72-7.875 1.5749-0.72715 1.168-1.4194-3.6013-1.8721-4.7682-0.45263-18.301-2.2512-24.402-0.54849-6.101 1.7027-16.524 4.2932-17.368 5.6669s-1.589 1.42-0.72189 6.1587c0.86659 4.7387 5.3307 27.477 5.7637 31.516 0.43357 4.0388 1.2811 6.7088-3.0682 7.7045-4.3498 0.99568-11.672 2.7245-14.188 2.7245s-9.8382-1.7288-14.188-2.7245c-4.3498-0.99568-3.5023-3.6657-3.0687-7.7045 0.43357-4.0388 4.8971-26.777 5.7642-31.516 0.86659-4.7387 0.12186-4.785-0.72189-6.1587-0.84429-1.3737-11.268-3.9642-17.369-5.6669-6.101-1.7027-19.633 0.095864-24.402 0.54849s-5.1756 1.1449-3.6013 1.8721c1.5743 0.7266 14.176 5.4005 20.72 7.875 6.5449 2.475 15.717 5.2191 16.664 10.496 0.94656 5.2774-4.741 13.209-7.8983 19.153-3.1579 5.9436-3.2417 6.6489-3.0225 9.0548 0.21869 2.4053 10.92 8.5335 13.266 10.116 2.3463 1.5834 15.044 8.552 15.044 12.433 0 3.8814-12.284 14.026-16.16 16.545s-14.416 8.034-18.904 8.8756c-4.488 0.84099-11.492-4.2458-15.581-11.867-4.0882-7.6212-0.41181-15.135 2.0797-21.04 2.4921-5.9049-1.7441-9.1855-3.7797-11.35-2.0346-2.1635-17.882-19.013-21.511-22.871-3.6285-3.858-7.2368-5.8166-7.2368-13.238s28.688-42.145 28.688-42.145 24.208 4.6298 27.472 4.6298 10.336-2.7234 16.864-4.9021c6.528-2.1787 10.88-2.1945 10.88-2.1945s4.352 0.015796 10.88 2.1945 13.6 4.9021 16.864 4.9021 27.472-4.6298 27.472-4.6298zm-21.507 132.82c1.7756 1.1133 0.69251 3.212-0.9248 4.3569-1.6179 1.1449-23.357 18-25.466 19.862-2.1102 1.8634-5.2104 4.9403-7.3184 4.9403s-5.2088-3.0769-7.3184-4.9403c-2.1102-1.8628-23.848-18.717-25.466-19.862-1.6179-1.1449-2.7004-3.2436-0.9248-4.3569 1.7762-1.1128 7.332-3.9217 14.999-7.8941 7.6655-3.9718 17.22-7.3488 18.711-7.3488s11.045 3.377 18.711 7.3488c7.666 3.9724 13.222 6.7813 14.998 7.8941z' fill='%23fff'/%3E%3Cmask fill='white'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cuse fill='url(%23b)' xlink:href='%23a'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");
  background-size: 1.5em;
  background-position: left 0.5em top 50%;
  background-repeat: no-repeat;
  padding-left: 2.5em;
}
input.brave {
  background-color: #f7f8fa;
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import globalToggleForm from './forms/global-toggle-form.js'
import ipfsNodeForm from './forms/ipfs-node-form.js'
import fileImportForm from './forms/file-import-form.js'
import dnslinkForm from './forms/dnslink-form.js'
import gatewaysForm from './forms/gateways-form.js'
import apiForm from './forms/api-form.js'
import experimentsForm from './forms/experiments-form.js'
import telemetryForm from './forms/telemetry-form.js'
import resetForm from './forms/reset-form.js'

// Render the options page:
// Passed current app `state` from the store and `emit`, a function to create
// events, allowing views to signal back to the store that something happened.
export default function optionsPage (state, emit) {
  const onOptionChange = (key, modifyValue) => (e) => {
    e.preventDefault()

    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value

    if (!e.target.reportValidity()) {
      return console.warn(`[ipfs-companion] Invalid value for ${key}: ${value}`)
    }

    emit('optionChange', { key, value: modifyValue ? modifyValue(value) : value })
    if (modifyValue) {
      emit('render')
    }
  }

  const onOptionsReset = (e) => {
    e.preventDefault()
    emit('optionsReset')
  }

  if (!state.options.active) {
    // we don't want to confuse users by showing "active" checkboxes
    // when global toggle is in "suspended" state
    return html`

  ${globalToggleForm({
    active: state.options.active,
    onOptionChange
  })}

    `
  }
  return html`

  ${globalToggleForm({
    active: state.options.active,
    onOptionChange
  })}
  ${ipfsNodeForm({
    ipfsNodeType: state.options.ipfsNodeType,
    ipfsNodeConfig: state.options.ipfsNodeConfig,
    withNodeFromBrave: state.withNodeFromBrave,
    onOptionChange
  })}
  ${state.options.ipfsNodeType.startsWith('external')
  ? apiForm({
    ipfsNodeType: state.options.ipfsNodeType,
    ipfsApiUrl: state.options.ipfsApiUrl,
    ipfsApiPollMs: state.options.ipfsApiPollMs,
    automaticMode: state.options.automaticMode,
    onOptionChange
  })
  : null}
  ${gatewaysForm({
    ipfsNodeType: state.options.ipfsNodeType,
    customGatewayUrl: state.options.customGatewayUrl,
    useCustomGateway: state.options.useCustomGateway,
    useSubdomains: state.options.useSubdomains,
    publicGatewayUrl: state.options.publicGatewayUrl,
    publicSubdomainGatewayUrl: state.options.publicSubdomainGatewayUrl,
    disabledOn: state.options.disabledOn,
    enabledOn: state.options.enabledOn,
    onOptionChange
  })}
  ${fileImportForm({
    importDir: state.options.importDir,
    openViaWebUI: state.options.openViaWebUI,
    preloadAtPublicGateway: state.options.preloadAtPublicGateway,
    onOptionChange
  })}
  ${dnslinkForm({
    dnslinkPolicy: state.options.dnslinkPolicy,
    dnslinkDataPreload: state.options.dnslinkDataPreload,
    dnslinkRedirect: state.options.dnslinkRedirect,
    onOptionChange
  })}
  ${experimentsForm({
    useLatestWebUI: state.options.useLatestWebUI,
    displayNotifications: state.options.displayNotifications,
    displayReleaseNotes: state.options.displayReleaseNotes,
    catchUnhandledProtocols: state.options.catchUnhandledProtocols,
    linkify: state.options.linkify,
    recoverFailedHttpRequests: state.options.recoverFailedHttpRequests,
    detectIpfsPathHeader: state.options.detectIpfsPathHeader,
    logNamespaces: state.options.logNamespaces,
    onOptionChange
  })}
  ${telemetryForm({
    telemetryGroupMinimal: state.options.telemetryGroupMinimal,
    telemetryGroupMarketing: state.options.telemetryGroupMarketing,
    telemetryGroupPerformance: state.options.telemetryGroupPerformance,
    telemetryGroupTracking: state.options.telemetryGroupTracking,
    onOptionChange
  })}
  ${resetForm({
    onOptionsReset
  })}

  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import './options.css'

import { i18n } from 'webextension-polyfill'
import choo from 'choo'
import optionsPage from './page.js'
import optionsStore from './store.js'

const app = choo()

// Use the store to setup state defaults and event listeners for mutations
app.use(optionsStore)

// Register our single route
app.route('*', optionsPage)

// Start the application and render it to the given querySelector
app.mount('#root')

// Set page title and header translation
document.getElementById('header-text').innerText = i18n.getMessage('option_page_header')
document.title = i18n.getMessage('option_page_title')


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import { optionDefaults } from '../lib/options.js'
import createRuntimeChecks from '../lib/runtime-checks.js'

// The store contains and mutates the state for the app
export default function optionStore (state, emitter) {
  state.options = optionDefaults

  const updateStateOptions = async () => {
    const runtime = await createRuntimeChecks(browser)
    state.withNodeFromBrave = runtime.brave && await runtime.brave.getIPFSEnabled()
    /**
     * FIXME: Why are we setting `state.options` when state is supposed to extend options?
     */
    state.options = await getOptions()
    emitter.emit('render')
  }

  emitter.on('DOMContentLoaded', async () => {
    browser.runtime.sendMessage({ telemetry: { trackView: 'options' } })
    updateStateOptions()
    browser.storage.onChanged.addListener(updateStateOptions)
  })

  emitter.on('optionChange', ({ key, value }) => (
    browser.storage.local.set({ [key]: value })
  ))

  emitter.on('optionsReset', () => (
    browser.storage.local.set(optionDefaults)
  ))
}

async function getOptions () {
  const storedOpts = await browser.storage.local.get()
  return Object.keys(optionDefaults).reduce((opts, key) => {
    opts[key] = storedOpts[key] == null ? optionDefaults[key] : storedOpts[key]
    return opts
  }, {})
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import switchToggle from '../../pages/components/switch-toggle.js'

export default function experimentsForm ({
  useLatestWebUI,
  displayNotifications,
  displayReleaseNotes,
  catchUnhandledProtocols,
  linkify,
  recoverFailedHttpRequests,
  detectIpfsPathHeader,
  logNamespaces,
  onOptionChange
}) {
  const onDisplayNotificationsChange = onOptionChange('displayNotifications')
  const onDisplayReleaseNotesChange = onOptionChange('displayReleaseNotes')
  const onUseLatestWebUIChange = onOptionChange('useLatestWebUI')
  const onCatchUnhandledProtocolsChange = onOptionChange('catchUnhandledProtocols')
  const onLinkifyChange = onOptionChange('linkify')
  const onrecoverFailedHttpRequestsChange = onOptionChange('recoverFailedHttpRequests')
  const onDetectIpfsPathHeaderChange = onOptionChange('detectIpfsPathHeader')

  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_experiments')}

${browser.i18n.getMessage('option_experiments_warning')}

${browser.i18n.getMessage('option_useLatestWebUI_title')}

${browser.i18n.getMessage('option_useLatestWebUI_description')}

${switchToggle({ id: 'useLatestWebUI', checked: useLatestWebUI, onchange: onUseLatestWebUIChange })}

${browser.i18n.getMessage('option_displayNotifications_title')}

${browser.i18n.getMessage('option_displayNotifications_description')}

${switchToggle({ id: 'displayNotifications', checked: displayNotifications, onchange: onDisplayNotificationsChange })}

${browser.i18n.getMessage('option_displayReleaseNotes_title')}

${browser.i18n.getMessage('option_displayReleaseNotes_description')}

${switchToggle({ id: 'displayReleaseNotes', checked: displayReleaseNotes, onchange: onDisplayReleaseNotesChange })}

${browser.i18n.getMessage('option_catchUnhandledProtocols_title')}

${browser.i18n.getMessage('option_catchUnhandledProtocols_description')}

${switchToggle({ id: 'catchUnhandledProtocols', checked: catchUnhandledProtocols, onchange: onCatchUnhandledProtocolsChange })}

${browser.i18n.getMessage('option_recoverFailedHttpRequests_title')}

${browser.i18n.getMessage('option_recoverFailedHttpRequests_description')}

${switchToggle({ id: 'recoverFailedHttpRequests', checked: recoverFailedHttpRequests, onchange: onrecoverFailedHttpRequestsChange })}

${browser.i18n.getMessage('option_linkify_title')}

${browser.i18n.getMessage('option_linkify_description')}

${switchToggle({ id: 'linkify', checked: linkify, onchange: onLinkifyChange })}

${browser.i18n.getMessage('option_detectIpfsPathHeader_title')}

${browser.i18n.getMessage('option_detectIpfsPathHeader_description')}

                  ${browser.i18n.getMessage('option_legend_readMore')}

${switchToggle({ id: 'detectIpfsPathHeader', checked: detectIpfsPathHeader, onchange: onDetectIpfsPathHeaderChange })}

${browser.i18n.getMessage('option_logNamespaces_title')}

${browser.i18n.getMessage('option_logNamespaces_description')}

          <input
            class="bg-white navy self-center-ns"
            id="logNamespaces"
            type="text"
            required
            onchange=${onOptionChange('logNamespaces')}
            value=${logNamespaces} />

      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import browser from 'webextension-polyfill'
import switchToggle from '../../pages/components/switch-toggle.js'

export default function telemetryForm ({
  onOptionChange,
  ...stateOptions
}) {
  // @ts-expect-error - TS doesn't like the `html` template tag
  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_telemetry')}

${browser.i18n.getMessage('option_telemetry_disclaimer')}

              ${browser.i18n.getMessage('option_legend_readMore')}

${browser.i18n.getMessage('option_telemetryGroupMinimal_title')}

${browser.i18n.getMessage('option_telemetryGroupMinimal_description')}

${switchToggle({
            id: 'telemetryGroupMinimal',
            checked: stateOptions.telemetryGroupMinimal,
            onchange: onOptionChange('telemetryGroupMinimal')
          })}

      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import switchToggle from '../../pages/components/switch-toggle.js'
import { guiURLString, hostTextToArray, hostArrayToText } from '../../lib/options.js'
import { braveNodeType } from '../../lib/ipfs-client/brave.js'

// Warn about mixed content issues when changing the gateway
// to something other than HTTP or localhost
// https://github.com/ipfs-shipyard/ipfs-companion/issues/648
const secureContextUrl = /^https:\/\/|^http:\/\/localhost|^http:\/\/127.0.0.1|^http:\/\/\[::1\]/

export default function gatewaysForm ({
  ipfsNodeType,
  customGatewayUrl,
  useCustomGateway,
  useSubdomains,
  disabledOn,
  enabledOn,
  publicGatewayUrl,
  publicSubdomainGatewayUrl,
  onOptionChange
}) {
  const onCustomGatewayUrlChange = onOptionChange('customGatewayUrl', (url) => guiURLString(url, { useLocalhostName: useSubdomains }))
  const onUseCustomGatewayChange = onOptionChange('useCustomGateway')
  const onUseSubdomainProxyChange = onOptionChange('useSubdomains')
  const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', guiURLString)
  const onPublicSubdomainGatewayUrlChange = onOptionChange('publicSubdomainGatewayUrl', guiURLString)
  const onDisabledOnChange = onOptionChange('disabledOn', hostTextToArray)
  const onEnabledOnChange = onOptionChange('enabledOn', hostTextToArray)
  const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
  const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded'
  const allowChangeOfCustomGateway = ipfsNodeType === 'external'
  const braveClass = ipfsNodeType === braveNodeType ? 'brave' : ''

  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_gateways')}

${browser.i18n.getMessage('option_publicGatewayUrl_title')}

${browser.i18n.getMessage('option_publicGatewayUrl_description')}

            <input
              class="bg-white navy self-center-ns"
              id="publicGatewayUrl"
              type="url"
              inputmode="url"
              required
              pattern="^https?://[^/]+/?$"
              spellcheck="false"
              title="${browser.i18n.getMessage('option_hint_url')}"
              onchange=${onPublicGatewayUrlChange}
              value=${publicGatewayUrl} />

${browser.i18n.getMessage('option_publicSubdomainGatewayUrl_title')}

                  ${browser.i18n.getMessage('option_publicSubdomainGatewayUrl_description')}

                    ${browser.i18n.getMessage('option_legend_readMore')}

            <input
              class="bg-white navy self-center-ns"
              id="publicSubdomainGatewayUrl"
              type="url"
              inputmode="url"
              required
              pattern="^https?://[^/]+/?$"
              spellcheck="false"
              title="${browser.i18n.getMessage('option_hint_url')}"
              onchange=${onPublicSubdomainGatewayUrlChange}
              value=${publicSubdomainGatewayUrl} />

          ${supportRedirectToCustomGateway
            ? html`

${browser.i18n.getMessage('option_customGatewayUrl_title')}

${browser.i18n.getMessage('option_customGatewayUrl_description')}
                    ${mixedContentWarning ? html`<p class="red i">${browser.i18n.getMessage('option_customGatewayUrl_warning')}</p>` : null}

              <input
                class="bg-white navy self-center-ns ${braveClass}"
                id="customGatewayUrl"
                type="url"
                inputmode="url"
                required
                pattern="^https?://[^/]+/?$"
                spellcheck="false"
                title="${browser.i18n.getMessage(allowChangeOfCustomGateway ? 'option_hint_url' : 'option_hint_readonly')}"
                onchange=${onCustomGatewayUrlChange}
                ${allowChangeOfCustomGateway ? '' : 'disabled'}
                value=${customGatewayUrl} />

`
            : null}
          ${supportRedirectToCustomGateway
          ? html`

${browser.i18n.getMessage('option_useCustomGateway_title')}

${browser.i18n.getMessage('option_useCustomGateway_description')}

${switchToggle({ id: 'useCustomGateway', checked: useCustomGateway, onchange: onUseCustomGatewayChange })}

`
          : null}
          ${supportRedirectToCustomGateway
          ? html`

${browser.i18n.getMessage('option_useSubdomains_title')}

                    ${browser.i18n.getMessage('option_useSubdomains_description')}

                      ${browser.i18n.getMessage('option_legend_readMore')}

${switchToggle({ id: 'useSubdomains', checked: useSubdomains, onchange: onUseSubdomainProxyChange })}

`
            : null}
          ${supportRedirectToCustomGateway
          ? html`

${browser.i18n.getMessage('option_disabledOn_title')}

${browser.i18n.getMessage('option_disabledOn_description')}

              <textarea
                class="bg-white navy self-center-ns"
                id="disabledOn"
                spellcheck="false"
                onchange=${onDisabledOnChange}
                rows="${Math.min(disabledOn.length + 1, 10)}"
                >${hostArrayToText(disabledOn)}</textarea>

${browser.i18n.getMessage('option_enabledOn_title')}

${browser.i18n.getMessage('option_enabledOn_description')}

              <textarea
                class="bg-white navy self-center-ns"
                id="enabledOn"
                spellcheck="false"
                onchange=${onEnabledOnChange}
                rows="${Math.min(enabledOn.length + 1, 10)}"
                >${hostArrayToText(enabledOn)}</textarea>

`
            : null}

      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import { braveNodeType } from '../../lib/ipfs-client/brave.js'

export default function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange, withNodeFromBrave }) {
  const onIpfsNodeTypeChange = onOptionChange('ipfsNodeType')
  const onIpfsNodeConfigChange = onOptionChange('ipfsNodeConfig')
  const braveClass = ipfsNodeType === braveNodeType ? 'brave' : ''
  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_nodeType')}

${browser.i18n.getMessage('option_ipfsNodeType_title')}

${browser.i18n.getMessage('option_ipfsNodeType_external_description')}

                ${withNodeFromBrave ? html`<p>${browser.i18n.getMessage('option_ipfsNodeType_brave_description')}</p>` : null}

${browser.i18n.getMessage('option_ipfsNodeType_embedded_description')}

                  ${browser.i18n.getMessage('option_legend_readMore')}

          <select id="ipfsNodeType" name='ipfsNodeType' class="self-center-ns bg-white navy ${braveClass}" onchange=${onIpfsNodeTypeChange}>
            <option
              value='external'
              selected=${ipfsNodeType === 'external'}>
              ${browser.i18n.getMessage('option_ipfsNodeType_external')}
            </option>
            ${withNodeFromBrave
            ? html`<option
                  value='external:brave'
                  selected=${ipfsNodeType === 'external:brave'}>
                  ${browser.i18n.getMessage('option_ipfsNodeType_brave')}
                </option>`
              : null}
            <option
              value='embedded'
              selected=${ipfsNodeType === 'embedded'}>
              ${browser.i18n.getMessage('option_ipfsNodeType_embedded')} (${browser.i18n.getMessage('option_experimental')})
            </option>
          </select>

        ${ipfsNodeType.startsWith('embedded')
        ? html`

${browser.i18n.getMessage('option_ipfsNodeConfig_title')}

${browser.i18n.getMessage('option_ipfsNodeConfig_description')}

            <textarea
              class="bg-white navy self-center-ns"
              spellcheck="false"
              id="ipfsNodeConfig"
              rows="${Math.min((ipfsNodeConfig.match(/\n/g) || []).length + 1, 30)}"
              onchange=${onIpfsNodeConfigChange}>${ipfsNodeConfig}</textarea>

`
          : null}
      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'

export default function resetForm ({
  onOptionsReset
}) {
  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_reset')}

${browser.i18n.getMessage('option_resetAllOptions_title')}

${browser.i18n.getMessage('option_resetAllOptions_description')}

<button id="resetAllOptions" class="Button transition-all sans-serif v-mid fw5 nowrap lh-copy bn br1 pa2 pointer focus-outline white bg-red white" onclick=${onOptionsReset}>${browser.i18n.getMessage('option_resetAllOptions_title')}</button>

      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import switchToggle from '../../pages/components/switch-toggle.js'

export default function fileImportForm ({
  importDir, openViaWebUI, preloadAtPublicGateway, onOptionChange
}) {
  const onImportDirChange = onOptionChange('importDir')
  const onOpenViaWebUIChange = onOptionChange('openViaWebUI')
  const onPreloadAtPublicGatewayChange = onOptionChange('preloadAtPublicGateway')
  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_fileImport')}

${browser.i18n.getMessage('option_importDir_title')}

                ${browser.i18n.getMessage('option_importDir_description')}

                  ${browser.i18n.getMessage('option_legend_readMore')}

          <input
            class="bg-white navy self-center-ns"
            id="importDir"
            type="text"
            pattern="^\/(.*)"
            required
            onchange=${onImportDirChange}
            value=${importDir} />

${browser.i18n.getMessage('option_openViaWebUI_title')}

${browser.i18n.getMessage('option_openViaWebUI_description')}

${switchToggle({ id: 'openViaWebUI', checked: openViaWebUI, onchange: onOpenViaWebUIChange })}

${browser.i18n.getMessage('option_preloadAtPublicGateway_title')}

${browser.i18n.getMessage('option_preloadAtPublicGateway_description')}

${switchToggle({ id: 'preloadAtPublicGateway', checked: preloadAtPublicGateway, onchange: onPreloadAtPublicGatewayChange })}

      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import switchToggle from '../../pages/components/switch-toggle.js'

export default function globalToggleForm ({ active, onOptionChange }) {
  const toggle = onOptionChange('active')
  return html`
    <form class="db b mb3 bg-aqua-muted charcoal">
      <label for="active" class="dib pa3 flex items-center pointer ${!active ? 'charcoal bg-gray-muted br2' : ''}">
        ${switchToggle({ id: 'active', checked: active, onchange: toggle, style: 'mr3' })}
        ${browser.i18n.getMessage('panel_headerActiveToggleTitle')}
      </label>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import switchToggle from '../../pages/components/switch-toggle.js'

export default function dnslinkForm ({
  dnslinkPolicy,
  dnslinkDataPreload,
  dnslinkRedirect,
  onOptionChange
}) {
  const onDnslinkPolicyChange = onOptionChange('dnslinkPolicy')
  const onDnslinkRedirectChange = onOptionChange('dnslinkRedirect')
  const onDnslinkDataPreloadChange = onOptionChange('dnslinkDataPreload')

  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_dnslink')}

${browser.i18n.getMessage('option_dnslinkPolicy_title')}

                ${browser.i18n.getMessage('option_dnslinkPolicy_description')}

                  ${browser.i18n.getMessage('option_legend_readMore')}

          <select id="dnslinkPolicy" name='dnslinkPolicy' class="self-center-ns bg-white navy" onchange=${onDnslinkPolicyChange}>
            <option
              value='false'
              selected=${String(dnslinkPolicy) === 'false'}>
              ${browser.i18n.getMessage('option_dnslinkPolicy_disabled')}
            </option>
            <option
              value='best-effort'
              selected=${dnslinkPolicy === 'best-effort'}>
              ${browser.i18n.getMessage('option_dnslinkPolicy_bestEffort')}
            </option>
            <option
              value='enabled'
              selected=${dnslinkPolicy === 'enabled'}>
              ${browser.i18n.getMessage('option_dnslinkPolicy_enabled')}
            </option>
          </select>

${browser.i18n.getMessage('option_dnslinkDataPreload_title')}

${browser.i18n.getMessage('option_dnslinkDataPreload_description')}

${switchToggle({ id: 'dnslinkDataPreload', checked: dnslinkDataPreload, disabled: dnslinkRedirect, onchange: onDnslinkDataPreloadChange })}

${browser.i18n.getMessage('option_dnslinkRedirect_title')}

                ${browser.i18n.getMessage('option_dnslinkRedirect_description')}
                ${dnslinkRedirect ? html`<p class="red i">${browser.i18n.getMessage('option_dnslinkRedirect_warning')}</p>` : null}

                  ${browser.i18n.getMessage('option_legend_readMore')}

${switchToggle({ id: 'dnslinkRedirect', checked: dnslinkRedirect, onchange: onDnslinkRedirectChange })}

      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import { guiURLString } from '../../lib/options.js'
import { braveNodeType } from '../../lib/ipfs-client/brave.js'
import switchToggle from '../../pages/components/switch-toggle.js'

export default function apiForm ({ ipfsNodeType, ipfsApiUrl, ipfsApiPollMs, automaticMode, onOptionChange }) {
  const onIpfsApiUrlChange = onOptionChange('ipfsApiUrl', (url) => guiURLString(url, { useLocalhostName: false }))
  const onIpfsApiPollMsChange = onOptionChange('ipfsApiPollMs')
  const onAutomaticModeChange = onOptionChange('automaticMode')
  const apiAddresEditable = ipfsNodeType === 'external'
  const braveClass = ipfsNodeType === braveNodeType ? 'brave' : ''

  return html`
    <form>
      <fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">

${browser.i18n.getMessage('option_header_api')}

${browser.i18n.getMessage('option_ipfsApiUrl_title')}

${browser.i18n.getMessage('option_ipfsApiUrl_description')}

          <input
            class="bg-white navy self-center-ns ${braveClass}"
            id="ipfsApiUrl"
            type="url"
            inputmode="url"
            required
            pattern="^https?://[^/]+/?$"
            spellcheck="false"
            title="${browser.i18n.getMessage(apiAddresEditable ? 'option_hint_url' : 'option_hint_readonly')}"
            onchange=${onIpfsApiUrlChange}
            ${apiAddresEditable ? '' : 'disabled'}
            value=${ipfsApiUrl} />

${browser.i18n.getMessage('option_ipfsApiPollMs_title')}

${browser.i18n.getMessage('option_ipfsApiPollMs_description')}

          <input
            class="bg-white navy self-center-ns"
            id="ipfsApiPollMs"
            type="number"
            inputmode="numeric"
            min="1000"
            max="60000"
            step="1000"
            required
            onchange=${onIpfsApiPollMsChange}
            value=${ipfsApiPollMs} />

${browser.i18n.getMessage('option_automaticMode_title')}

${browser.i18n.getMessage('option_automaticMode_description')}

${switchToggle({ id: 'automaticMode', checked: automaticMode, onchange: onAutomaticModeChange })}

      </fieldset>
    </form>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import PQueue from 'p-queue'

/*
 * This content script is responsible for performing the logic of replacing
 * plain text with IPFS addresses with clickable links.
 * Loosely based on https://github.com/mdn/webextensions-examples/blob/master/emoji-substitution/substitute.js
 * Note that this is a quick&dirty PoC and may slow down browsing experience.
 * Test page: http://bit.ly/2yuknPI
 * TODO: measure & improve performance
 */

;(function (alreadyLoaded) {
  if (alreadyLoaded) {
    return
  }

  // Limit contentType to "text/plain" or "text/html"
  if (document.contentType !== undefined && document.contentType !== 'text/plain' && document.contentType !== 'text/html') {
    return
  }

  // linkify lock
  window.ipfsCompanionLinkifiedDOM = true
  window.ipfsCompanionLinkifyValidationCache = new Map()

  const urlRE = /(?:\s+|^)(\/ip(?:f|n)s\/|dweb:\/ip(?:f|n)s\/|ipns:\/\/|ipfs:\/\/)([^\s+"<>:]+)/g

  // tags we will scan looking for un-hyperlinked IPFS addresses
  const allowedParents = [
    'abbr', 'acronym', 'address', 'applet', 'b', 'bdo', 'big', 'blockquote', 'body',
    'caption', 'center', 'cite', 'code', 'dd', 'del', 'div', 'dfn', 'dt', 'em',
    'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'iframe',
    'ins', 'kdb', 'li', 'object', 'pre', 'p', 'q', 'samp', 'small', 'span', 'strike',
    's', 'strong', 'sub', 'sup', 'td', 'th', 'tt', 'u', 'var'
  ]

  const textNodeXpath = './/text()[' +
    "(contains(., '/ipfs/') or contains(., '/ipns/') or contains(., 'ipns:/') or contains(., 'ipfs:/')) and " +
    'not(ancestor::a) and not(ancestor::script) and not(ancestor::style) and ' +
    '(parent::' + allowedParents.join(' or parent::') + ') ' +
    ']'

  function init () {
    // Linkify jobs are executed one by one
    // (fixes race-conditions in huge DOMs, does not lock UI)
    const linkifyJobs = new PQueue({ concurrency: 1 })
    // console.log('[ipfs-companion] running Linkify experiment')
    linkifyContainer(document.body, linkifyJobs)
      .then(() => {
        // console.log('[ipfs-companion] registering MutationObserver for Linkify experiment')
        new MutationObserver(function (mutations) {
          mutations.forEach(async (mutation) => linkifyMutation(mutation, linkifyJobs))
        }).observe(document.body, {
          characterData: true,
          childList: true,
          subtree: true
        })
      })
  }

  async function linkifyMutation (mutation, linkifyJobs) {
    linkifyJobs = linkifyJobs || new PQueue({ concurrency: 1 })
    if (mutation.type === 'childList') {
      for (const addedNode of mutation.addedNodes) {
        if (addedNode.nodeType === Node.TEXT_NODE) {
          linkifyJobs.add(async () => linkifyTextNode(addedNode))
        } else {
          linkifyJobs.add(async () => linkifyContainer(addedNode))
        }
      }
    }
    if (mutation.type === 'characterData') {
      linkifyJobs.add(async () => linkifyTextNode(mutation.target))
    }
    await linkifyJobs.onIdle()
  }

  async function linkifyContainer (container, linkifyJobs) {
    if (!container || !container.nodeType || container.isContentEditable) {
      return
    }
    if (container.className && container.className.match && container.className.match(/\blinkifiedIpfsAddress\b/)) {
      // prevent infinite recursion
      return
    }
    const xpathResult = document.evaluate(textNodeXpath, container, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
    let i = 0
    let node = null

    linkifyJobs = linkifyJobs || new PQueue({ concurrency: 1 })
    while ((node = xpathResult.snapshotItem(i++))) {
      const parent = node.parentNode
      // Skip if no longer in visible DOM
      if (!parent || !container.contains(node)) continue
      const currentNode = node
      linkifyJobs.add(async () => linkifyTextNode(currentNode))
    }
    await linkifyJobs.onIdle()
  }

  function textToIpfsResource (match) {
    let root = match[1]
    let path = match[2]

    // skip trailing dots and commas
    path = path.replace(/[.,]*$/, '')

    // convert various protocol variants to regular URL at the public gateway
    if (root === 'ipfs://') {
      root = '/ipfs/'
    } else if (root === 'ipns://') {
      root = '/ipns/'
    } else if (root === 'dweb:/ipfs/') {
      root = '/ipfs/'
    } else if (root === 'dweb:/ipns/') {
      root = '/ipns/'
    }
    return validIpfsResource(root + path)
  }

  async function validIpfsResource (path) {
    // validation is expensive, caching result improved performance
    // on page that have multiple copies of the same path
    if (window.ipfsCompanionLinkifyValidationCache.has(path)) {
      return window.ipfsCompanionLinkifyValidationCache.get(path)
    }
    try {
      // Callback wrapped in promise -- Chrome compatibility
      const checkResult = await browser.runtime.sendMessage({ pubGwUrlForIpfsOrIpnsPath: path })
      window.ipfsCompanionLinkifyValidationCache.set(path, checkResult.pubGwUrlForIpfsOrIpnsPath)
    } catch (error) {
      window.ipfsCompanionLinkifyValidationCache.set(path, null)
      console.error('pubGwUrlForIpfsOrIpnsPath.error for ' + path, error)
    }
    return window.ipfsCompanionLinkifyValidationCache.get(path)
  }

  function isParentTreeSafe (node) {
    let parent = node.parentNode
    // Skip if no longer in visible DOM
    if (!parent) return false
    // Walk back over parent tree and check each of them
    while (parent) {
      // Skip forms, textareas
      if (parent.isContentEditable) return false
      // Skip already linkified nodes
      if (parent.className && parent.className.match(/\blinkifiedIpfsAddress\b/)) return false
      // Skip styled <pre> -- often highlighted by scripts
      if (parent.tagName === 'PRE' && parent.className) return false
      // Skip if no longer in visible DOM
      if (!(parent instanceof HTMLDocument) && !parent.parentNode) return false
      parent = parent.parentNode
    }
    return true
  }

  async function linkifyTextNode (node) {
    // Skip if node belongs to a parent from unsafe tree
    if (!isParentTreeSafe(node)) return

    let link
    let match
    const txt = node.textContent
    let span = null
    let point = 0

    while ((match = urlRE.exec(txt))) {
      link = await textToIpfsResource(match)
      if (link) {
        const textChunk = document.createTextNode(match[0])
        if (span == null) {
          // Create a span to hold the new text with links in it.
          span = document.createElement('span')
          span.className = 'linkifiedIpfsAddress'
        }
        // put in text up to the link
        span.appendChild(document.createTextNode(txt.substring(point, match.index)))
        // create a link and put it in the span
        const a = document.createElement('a')
        a.className = 'linkifiedIpfsAddress'
        a.setAttribute('href', link)
        a.appendChild(textChunk)
        span.appendChild(a)
        // track insertion point
        const replaceLength = match[0].length
        point = match.index + replaceLength
      }
    }
    if (span && node.parentNode) {
      try {
        // take the text after the last link
        span.appendChild(document.createTextNode(txt.substring(point, txt.length)))
        // replace the original text with the new span
        node.parentNode.replaceChild(span, node)
      } catch (e) {
        console.error(e)
      }
    }
  }

  init()
}(window.ipfsCompanionLinkifiedDOM))


text:
diff --git a/node_modules/multiaddr/src/convert.js b/node_modules/multiaddr/src/convert.js
index c315201..80170c7 100644
--- a/node_modules/multiaddr/src/convert.js
+++ b/node_modules/multiaddr/src/convert.js
@@ -51,6 +51,7 @@ Convert.toString = function convertToString (proto, buf) {
     case 55: // dns6
     case 56: // dnsaddr
     case 400: // unix
+    case 466: // certhash
     case 777: // memory
       return bytes2str(buf)

@@ -84,6 +85,7 @@ Convert.toBytes = function convertToBytes (/** @type {string | number } */ proto
     case 55: // dns6
     case 56: // dnsaddr
     case 400: // unix
+    case 466: // certhash
     case 777: // memory
       return str2bytes(str)

diff --git a/node_modules/multiaddr/src/protocols-table.js b/node_modules/multiaddr/src/protocols-table.js
index 3431af5..8939fb1 100644
--- a/node_modules/multiaddr/src/protocols-table.js
+++ b/node_modules/multiaddr/src/protocols-table.js
@@ -60,6 +60,9 @@ Protocols.table = [
   [445, 296, 'onion3'],
   [446, V, 'garlic64'],
   [460, 0, 'quic'],
+  [461, 0, 'quic-v1'],
+  [465, 0, 'webtransport'],
+  [466, V, 'certhash'],
   [477, 0, 'ws'],
   [478, 0, 'wss'],
   [479, 0, 'p2p-websocket-star'],


text:
diff --git a/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js b/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js
index 1ee89f9..ed10f8e 100644
--- a/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js
+++ b/node_modules/@multiformats/multiaddr/dist/src/protocols-table.js
@@ -32,6 +32,7 @@ export const table = [
     [445, 296, 'onion3'],
     [446, V, 'garlic64'],
     [460, 0, 'quic'],
+    [461, 0, 'quic-v1'],
     [465, 0, 'webtransport'],
     [466, V, 'certhash'],
     [477, 0, 'ws'],
diff --git a/node_modules/@multiformats/multiaddr/src/protocols-table.ts b/node_modules/@multiformats/multiaddr/src/protocols-table.ts
index 8d864cd..2e352ea 100644
--- a/node_modules/@multiformats/multiaddr/src/protocols-table.ts
+++ b/node_modules/@multiformats/multiaddr/src/protocols-table.ts
@@ -41,6 +41,7 @@ export const table: Array<[number, number, string, boolean?, boolean?]> = [
   [445, 296, 'onion3'],
   [446, V, 'garlic64'],
   [460, 0, 'quic'],
+  [461, 0, 'quic-v1'],
   [465, 0, 'webtransport'],
   [466, V, 'certhash'],
   [477, 0, 'ws'],