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

import browser from 'webextension-polyfill'
import isIPFS from 'is-ipfs'
import { browserActionFilesCpImportCurrentTab } from '../../lib/ipfs-import.js'
import { ipfsContentPath } from '../../lib/ipfs-path.js'
import { welcomePage, optionsPage } from '../../lib/constants.js'
import { contextMenuViewOnGateway, contextMenuCopyAddressAtPublicGw, contextMenuCopyPermalink, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuCopyCidAddress } from '../../lib/context-menus.js'

// The store contains and mutates the state for the app
export default (state, emitter) => {
  Object.assign(state, {
    // Global toggles
    active: true,
    redirect: true,
    // UI contexts
    isIpfsContext: false, // Active Tab represents IPFS resource
    isRedirectContext: false, // Active Tab or its subresources could be redirected
    // IPFS details
    ipfsNodeType: 'external',
    isIpfsOnline: false,
    ipfsApiUrl: null,
    publicGatewayUrl: null,
    publicSubdomainGatewayUrl: null,
    gatewayAddress: null,
    swarmPeers: null,
    gatewayVersion: null,
    isApiAvailable: false,
    // isRedirectContext
    currentTab: null,
    currentFqdn: null,
    currentDnslinkFqdn: null,
    enabledOn: [],
    disabledOn: []
  })

  let port

  emitter.on('DOMContentLoaded', async () => {
    browser.runtime.sendMessage({ telemetry: { trackView: 'browser-action' } })

    // initial render with status stub
    emitter.emit('render')
    // initialize connection to the background script which will trigger UI updates
    port = browser.runtime.connect({ name: 'browser-action-port' })
    port.onMessage.addListener(async (message) => {
      if (message.statusUpdate) {
        const status = message.statusUpdate
        console.log('In browser action, received message from background:', message)
        await updateBrowserActionState(status)
        emitter.emit('render')
      }
    })
    // fix for https://github.com/ipfs-shipyard/ipfs-companion/issues/318
    setTimeout(() => {
      document.body.style.height = window.innerHeight + 1 + 'px'
      setTimeout(() => document.body.style.removeProperty('height'), 50)
    }, 100)
  })

  emitter.on('viewOnGateway', async () => {
    port.postMessage({ event: contextMenuViewOnGateway })
    window.close()
  })

  emitter.on('copy', function (copyAction) {
    switch (copyAction) {
      case contextMenuCopyCanonicalAddress:
        port.postMessage({ event: contextMenuCopyCanonicalAddress })
        break
      case contextMenuCopyCidAddress:
        port.postMessage({ event: contextMenuCopyCidAddress })
        break
      case contextMenuCopyRawCid:
        port.postMessage({ event: contextMenuCopyRawCid })
        break
      case contextMenuCopyAddressAtPublicGw:
        port.postMessage({ event: contextMenuCopyAddressAtPublicGw })
        break
      case contextMenuCopyPermalink:
        port.postMessage({ event: contextMenuCopyPermalink })
        break
    }
    window.close()
  })

  emitter.on('filesCpImport', () => {
    port.postMessage({ event: browserActionFilesCpImportCurrentTab })
    window.close()
  })

  emitter.on('quickImport', () => {
    browser.tabs.create({ url: browser.runtime.getURL('dist/popup/quick-import.html') })
    window.close()
  })

  emitter.on('openWelcomePage', async () => {
    try {
      await browser.tabs.create({ url: welcomePage })
      window.close()
    } catch (error) {
      console.error(`Unable Open WelcomePage (${welcomePage})`, error)
    }
  })

  emitter.on('openWebUi', async (page = '/') => {
    const url = `${state.webuiRootUrl}#${page}`
    try {
      await browser.tabs.create({ url })
      window.close()
    } catch (error) {
      console.error(`Unable Open Web UI (${url})`, error)
    }
  })

  emitter.on('openReleaseNotes', async () => {
    const { version } = browser.runtime.getManifest()
    const stableChannel = version.match(/\./g).length === 2
    let url
    try {
      if (stableChannel) {
        url = `https://github.com/ipfs-shipyard/ipfs-companion/releases/tag/v${version}`
        await browser.storage.local.set({ dismissedUpdate: version })
      } else {
        // swap URL and do not dismiss
        url = 'https://github.com/ipfs-shipyard/ipfs-companion/issues/964'
      }
      // Note: opening tab needs to happen after storage.local.set because in Chromium 86
      // it triggers a premature window.close, which aborts storage update
      await browser.tabs.create({ url })
      window.close()
    } catch (error) {
      console.error(`Unable to open release notes (${url})`, error)
    }
  })

  emitter.on('openPrefs', () => {
    browser.runtime.openOptionsPage()
      .then(() => window.close())
      .catch((err) => {
        console.error('runtime.openOptionsPage() failed, opening options page in tab instead.', err)
        // brave: fallback to opening options page as a tab.
        browser.tabs.create({ url: browser.runtime.getURL(optionsPage) })
      })
  })

  emitter.on('toggleGlobalRedirect', async () => {
    const redirectEnabled = state.redirect
    state.redirect = !redirectEnabled
    state.gatewayAddress = state.redirect ? state.gwURLString : state.pubGwURLString
    emitter.emit('render')
    try {
      await browser.storage.local.set({ useCustomGateway: !redirectEnabled })
    } catch (error) {
      console.error(`Unable to update redirect state due to ${error}`)
      state.redirect = redirectEnabled
      emitter.emit('render')
    }
  })

  emitter.on('toggleSiteIntegrations', async () => {
    const wasOptedOut = state.currentTabIntegrationsOptOut
    state.currentTabIntegrationsOptOut = !wasOptedOut
    emitter.emit('render')

    try {
      let { enabledOn, disabledOn, currentTab, currentDnslinkFqdn, currentFqdn } = state
      // if we are on /ipns/fqdn.tld/ then use hostname from DNSLink
      const fqdn = currentDnslinkFqdn || currentFqdn
      if (wasOptedOut) {
        disabledOn = disabledOn.filter(host => host !== fqdn)
        enabledOn.push(fqdn)
      } else {
        enabledOn = enabledOn.filter(host => host !== fqdn)
        disabledOn.push(fqdn)
      }
      // console.dir('toggleSiteIntegrations', state)
      await browser.storage.local.set({ disabledOn, enabledOn })

      const path = ipfsContentPath(currentTab.url, { keepURIParams: true })
      // Reload the current tab to apply updated redirect preference
      if (!currentDnslinkFqdn || !isIPFS.ipnsPath(path)) {
        // No DNSLink, reload URL as-is
        await browser.tabs.reload(currentTab.id)
      } else {
        // DNSLinked websites require URL change
        // from  http?://gateway.tld/ipns/{fqdn}/some/path OR
        // from  http?://{fqdn}.ipns.gateway.tld/some/path
        // to    http://{fqdn}/some/path
        // (defaulting to http: https websites will have HSTS or a redirect)
        const originalUrl = path.replace(/^.*\/ipns\//, 'http://')
        await browser.tabs.update(currentTab.id, {
          // FF only: loadReplace: true,
          url: originalUrl
        })
      }
    } catch (error) {
      console.error(`Unable to update integrations state due to ${error}`)
      emitter.emit('render')
    }
  })

  emitter.on('toggleActive', async () => {
    const prev = state.active
    state.active = !prev
    if (!state.active) {
      state.gatewayAddress = state.pubGwURLString
      state.ipfsApiUrl = null
      state.gatewayVersion = null
      state.swarmPeers = null
      state.isIpfsOnline = false
    }
    try {
      await browser.storage.local.set({ active: state.active })
    } catch (error) {
      console.error(`Unable to update global Active flag due to ${error}`)
      state.active = prev
    }
    emitter.emit('render')
  })

  async function updateBrowserActionState (status) {
    if (status) {
      // Copy all attributes
      Object.assign(state, status)

      if (state.active && status.redirect && (status.ipfsNodeType !== 'embedded')) {
        state.gatewayAddress = status.gwURLString
      } else {
        state.gatewayAddress = status.pubGwURLString
      }
      // Import requires access to the background page (https://github.com/ipfs-shipyard/ipfs-companion/issues/477)
      state.isApiAvailable = state.active && !!(await getBackgroundPage()) && !browser.extension.inIncognitoContext // https://github.com/ipfs-shipyard/ipfs-companion/issues/243
      state.swarmPeers = !state.active || status.peerCount === -1 ? null : status.peerCount
      state.isIpfsOnline = state.active && status.peerCount > -1
      state.gatewayVersion = state.active && status.gatewayVersion ? status.gatewayVersion : null
      state.ipfsApiUrl = state.active ? status.apiURLString : null
    } else {
      state.ipfsNodeType = 'external'
      state.swarmPeers = null
      state.isIpfsOnline = false
      state.gatewayVersion = null
      state.isIpfsContext = false
      state.isRedirectContext = false
    }
  }
}

function getBackgroundPage () {
  return browser.runtime.getBackgroundPage()
}


repo_name: ipfs/ipfs-companion

repo_language: JavaScript

mime_type: text/x-diff


text:
'use strict'

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import logo from '../../popup/logo.js'
import { renderTranslatedLinks, renderTranslatedSpans } from '../../utils/i18n.js'

// Brave detection
import { brave } from '../../../src/lib/ipfs-client/brave.js'
import { optionsPage } from '../../../src/lib/constants.js'

// Assets
const libp2pLogo = '../../../images/libp2p.svg'
const multiformatsLogo = '../../../images/multiformats.svg'
const ipldLogo = '../../../images/ipld.svg'

// Colors
const colorIpfsLogo = '#57cbd0'
const colorWhite = '#ffffff'
const colorYellow = '#f39021'

export default function createWelcomePage (i18n) {
  return function welcomePage (state, emit) {
    const { isIpfsOnline, peerCount } = state
    const openWebUi = (page) => () => emit('openWebUi', page)

    // Set translated title
    document.title = i18n.getMessage('page_landingWelcome_title')

    return html`

          ${renderCompanionLogo(i18n, isIpfsOnline)}
          ${isIpfsOnline ? renderWelcome(i18n, peerCount, openWebUi) : renderInstallSteps(i18n, isIpfsOnline)}

          ${renderResources(i18n)}
          ${renderVideos(i18n)}
          ${renderProjects(i18n)}

    `
  }
}

/* ========================================================
   Render functions for the left side
   ======================================================== */
export const renderLogo = (isIpfsOnline, logoSize = 128) => {
  const logoPath = '../../../icons'

  return html`
    ${logo({ path: logoPath, size: logoSize, isIpfsOnline })}
  `
}

export const renderCompanionLogo = (i18n, isIpfsOnline, showTitle = true) => {
  const stateUnknown = isIpfsOnline === null

  return html`

      ${renderLogo(isIpfsOnline)}
      ${showTitle
        ? html`<p class="montserrat mt3 mb0 f2">${i18n.getMessage('page_landingWelcome_logo_title')}</p>`
        : ''
      }

  `
}

const renderWelcome = (i18n, peerCount, openWebUi) => {
  // const anchorClass = 'aqua hover-white'
  // const copyClass = 'mv0 tc lh-copy f5'
  const svgWidth = 130

  const nodeOnSvg = () => html`
    <svg x="0px" y="0px" viewBox="0 0 100 100" width="${svgWidth}">
      <path fill="${colorIpfsLogo}" d="M52.42 18.81A31.19 31.19 0 1083.6 50a31.22 31.22 0 00-31.18-31.19zm0 59.78A28.59 28.59 0 1181 50a28.62 28.62 0 01-28.58 28.59z"/>
      <path fill="${colorWhite}" d="M66.49 35.87a.75.75 0 00-1.06 0L46.6 54.7l-7.2-7.2a.75.75 0 00-1.06 0l-3.92 3.92a.75.75 0 000 1.06l11.65 11.65a.75.75 0 001.06 0l23.28-23.28a.74.74 0 000-1.06zM46.6 62.54L36 52l2.86-2.86 7.2 7.2a.75.75 0 001.06 0L66 37.46l2.86 2.86z"/>
    </svg>
  `

  return html`

        ${nodeOnSvg()}

${renderTranslatedSpans('page_landingWelcome_welcome_peers', [peerCount], 'class="aqua fw6"')}

        <button class="pv3 ph4 mh2 b navy br2 bn bg-white hover-bg-white-90 pointer" onclick=${openWebUi('/')}>${i18n.getMessage('page_landingWelcome_welcome_statusButton_text')}</button>
        <button class="pv3 ph4 mh2 b navy br2 bn bg-white hover-bg-white-90 pointer" onclick=${openWebUi('/files')}>${i18n.getMessage('page_landingWelcome_welcome_filesButton_text')}</button>
        <button class="pv3 ph4 mh2 b navy br2 bn bg-white hover-bg-white-90 pointer" onclick=${openWebUi('/peers')}>${i18n.getMessage('page_landingWelcome_welcome_peersButton_text')}</button>

  `
}

export const nodeOffSvg = (svgWidth = 130) => html`
<svg x="0px" y="0px" viewBox="0 0 100 100" width="${svgWidth}">
  <path fill="${colorYellow}" d="M82.84 71.14L55.06 23a5.84 5.84 0 00-10.12 0L17.16 71.14a5.85 5.85 0 005.06 8.77h55.56a5.85 5.85 0 005.06-8.77zm-30.1-.66h-5.48V65h5.48zm0-10.26h-5.48V38.46h5.48z"/>
</svg>
`

const renderInstallSteps = (i18n, isIpfsOnline) => {
  const copyClass = 'mv0 white f5 lh-copy'
  const anchorClass = 'aqua hover-white'
  const stateUnknown = isIpfsOnline === null

  const optionsUrl = browser.runtime.getURL(optionsPage)
  return html`

        ${nodeOffSvg()}

${i18n.getMessage('page_landingWelcome_installSteps_notRunning_title')}

      ${brave
        ? html`

${i18n.getMessage('page_landingWelcome_installSteps_brave_title')}

${renderTranslatedLinks('page_landingWelcome_installSteps_brave_install', [optionsUrl], `target="_blank" class="${anchorClass}"`)}
`
        : null}

${i18n.getMessage('page_landingWelcome_installSteps_desktop_title')}

${renderTranslatedLinks('page_landingWelcome_installSteps_desktop_install', ['https://github.com/ipfs-shipyard/ipfs-desktop#ipfs-desktop'], `target="_blank" class="${anchorClass}"`)}

${i18n.getMessage('page_landingWelcome_installSteps_cli_title')}

${renderTranslatedLinks('page_landingWelcome_installSteps_cli_install', ['https://docs.ipfs.tech/how-to/command-line-quick-start/'], `target="_blank" class="${anchorClass}"`)}

  `
}

/* ========================================================
   Render functions for the right side
   ======================================================== */

const renderResources = (i18n) => {
  const labelClass = 'ttu tracked f6 fw4 teal mt0 mb3'
  const copyClass = 'mt0 mb4 lh-copy'
  const anchorClass = 'link underline-under hover-aqua'

  return html`

${i18n.getMessage('page_landingWelcome_resources_title_new_ipfs')}

${renderTranslatedLinks('page_landingWelcome_resources_new_ipfs_companion_features', ['https://github.com/ipfs-shipyard/ipfs-companion#ipfs-companion-features'], `target="_blank" class="${anchorClass}"`)}

${renderTranslatedLinks('page_landingWelcome_resources_new_ipfs_concepts', ['https://docs.ipfs.tech/concepts/how-ipfs-works/'], `target="_blank" class="${anchorClass}"`)}

${renderTranslatedLinks('page_landingWelcome_resources_new_ipfs_docs', ['https://docs.ipfs.tech'], `target="_blank" class="${anchorClass}"`)}

${i18n.getMessage('page_landingWelcome_resources_title_build')}

${renderTranslatedLinks('page_landingWelcome_resources_build_tutorials', ['https://docs.ipfs.tech/how-to/'], `target="_blank" class="${anchorClass}"`)}

${renderTranslatedLinks('page_landingWelcome_resources_build_examples', ['https://awesome.ipfs.tech'], `target="_blank" class="${anchorClass}"`)}

${i18n.getMessage('page_landingWelcome_resources_title_get_help')}

${renderTranslatedLinks('page_landingWelcome_resources_get_help', ['https://discuss.ipfs.tech'], `target="_blank" class="${anchorClass}"`)}

${i18n.getMessage('page_landingWelcome_resources_title_community')}

${renderTranslatedLinks('page_landingWelcome_resources_community_contribute', ['https://docs.ipfs.tech/community/contribute/ways-to-contribute/'], `target="_blank" class="${anchorClass}"`)}

${renderTranslatedLinks('page_landingWelcome_resources_community_translate', ['https://www.transifex.com/ipfs/public/'], `target="_blank" class="${anchorClass}"`)}

${renderTranslatedLinks('page_landingWelcome_resources_community_resources', ['https://docs.ipfs.tech/community/'], `target="_blank" class="${anchorClass}"`)}

  `
}

const renderVideos = (i18n) => {
  const anchorClass = 'relative overflow-hidden br2 o-70 glow'
  const videoWidth = 240
  const videoHeight = 180

  const overlayDiv = () => html`

  `

  const playSvg = () => html`
    <svg class="aspect-ratio--object" x="0px" y="0px" viewBox="-90 -60 ${videoWidth} ${videoHeight}">
      <g fill="${colorWhite}">
        <polygon points="43,30 23,40 23,20" />
      </g>
    </svg>
  `

  return html`

${i18n.getMessage('page_landingWelcome_videos_why_ipfs')}

          ${overlayDiv()}
          ${playSvg()}

${i18n.getMessage('page_landingWelcome_videos_how_ipfs_works')}

          ${overlayDiv()}
          ${playSvg()}

  `
}

const renderProjects = (i18n) => {
  const anchorClass = 'flex flex-column items-center navy link underline-under hover-aqua'
  const logoWidth = 80

  return html`

${i18n.getMessage('page_landingWelcome_projects_title')}

Multiformats

IPLD

libp2p

  `
}


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

import './welcome.css'

import browser from 'webextension-polyfill'
import choo from 'choo'
import createWelcomePageStore from './store.js'
import createWelcomePage from './page.js'

const app = choo()

app.use(createWelcomePageStore(browser.i18n, browser.runtime))
app.route('*', createWelcomePage(browser.i18n))
app.mount('#root')


text:
@import url('~tachyons/css/tachyons.css');
@import url('~ipfs-css/ipfs.css');
@import url('../../popup/heartbeat.css');

#left-col {
  background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
  background-size: 100%;
  background-repeat: repeat;
}

.underline-under {
  text-decoration: underline;
  text-underline-position: under;
}

a:hover {
  text-decoration: underline;
}

.state-unknown {
  opacity: 0;
  filter: blur( .15em );
}

/*
  https://github.com/tachyons-css/tachyons-queries
  Tachyons: $point == large
*/
@media (min-width: 64em) {
  #left-col {
    position: fixed;
    top: 0;
    right: 50%;
    width: 50%;
    background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
    background-size: 100%;
    background-repeat: repeat;
  }

  #right-col {
    margin-left: 50%;
  }
}

@media (max-height: 800px) {
  #left-col img {
    width: 98px !important;
    height: 98px !important;
  }

  #left-col svg {
    width: 60px;
  }
}


text:
'use strict'
/* eslint-env browser, webextensions */
import browser from 'webextension-polyfill'

export default function createWelcomePageStore (i18n, runtime) {
  return function welcomePageStore (state, emitter) {
    state.isIpfsOnline = null
    state.peerCount = null
    state.webuiRootUrl = null
    let port
    emitter.on('DOMContentLoaded', async () => {
      browser.runtime.sendMessage({ telemetry: { trackView: 'welcome' } })
      emitter.emit('render')
      port = runtime.connect({ name: 'browser-action-port' })
      port.onMessage.addListener(async (message) => {
        if (message.statusUpdate) {
          const webuiRootUrl = message.statusUpdate.webuiRootUrl
          const peerCount = message.statusUpdate.peerCount
          const isIpfsOnline = peerCount > -1
          if (isIpfsOnline !== state.isIpfsOnline || peerCount !== state.peerCount || webuiRootUrl !== state.webuiRootUrl) {
            state.webuiRootUrl = webuiRootUrl
            state.isIpfsOnline = isIpfsOnline
            state.peerCount = peerCount
            emitter.emit('render')
          }
        }
      })
    })

    emitter.on('openWebUi', async (page = '/') => {
      const url = `${state.webuiRootUrl}#${page}`
      try {
        await browser.tabs.create({ url })
      } catch (error) {
        console.error(`Unable Open Web UI (${url})`, error)
      }
    })
  }
}


text:
<!DOCTYPE html>

    <title>IPFS Node is Offline</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
    <link rel="stylesheet" href="/dist/bundles/uiCommons.css">
    <link rel="stylesheet" href="/dist/bundles/recoveryPage.css">

    <app class="flex flex-column transition-all vh-100">

    </app>


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

#left-col {
  background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
  background-size: 100%;
  background-repeat: repeat;
}

a:hover {
  text-decoration: none;
}

a:visited {
  color: inherit;
}

/*
  https://github.com/tachyons-css/tachyons-queries
  Tachyons: $point == large
*/
@media (min-width: 60em) {
  #left-col {
    position: fixed;
    top: 0;
    right: 55%;
    width: 45%;
    background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
    background-size: 100%;
    background-repeat: repeat;
  }

  #right-col {
    margin-left: 54%;
    margin-right: 6%;
  }
}

@media (max-height: 800px) {
  #left-col img {
    width: 98px !important;
    height: 98px !important;
  }

  #left-col svg {
    width: 60px;
  }
}

.recovery-root {
  width: 100%;
  height: 100%;
  text-align: left;
}


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

import choo from 'choo'
import html from 'choo/html/index.js'
import browser, { i18n, runtime } from 'webextension-polyfill'
import { nodeOffSvg } from '../landing-pages/welcome/page.js'
import createWelcomePageStore from '../landing-pages/welcome/store.js'
import { optionsPage } from '../lib/constants.js'
import './recovery.css'

const app = choo()

const learnMoreLink = html`<a class="navy link underline-under hover-aqua" href="https://docs.ipfs.tech/how-to/companion-node-types/" target="_blank" rel="noopener noreferrer">${i18n.getMessage('recovery_page_learn_more')}</a>`

const optionsPageLink = html`<a class="navy link underline-under hover-aqua" id="learn-more" href="${optionsPage}" target="_blank" rel="noopener noreferrer">${i18n.getMessage('recovery_page_update_preferences')}</a>`

// TODO (whizzzkid): refactor base store to be more generic.
app.use(createWelcomePageStore(i18n, runtime))
// Register our single route
app.route('*', (state) => {
  browser.runtime.sendMessage({ telemetry: { trackView: 'recovery' } })
  const { hash } = window.location
  const { href: publicURI } = new URL(decodeURIComponent(hash.slice(1)))

  if (!publicURI) {
    return
  }

  const openURLFromHash = () => {
    try {
      console.log('Opening URL from hash:', publicURI)
      window.location.replace(publicURI)
    } catch (err) {
      console.error('Failed to open URL from hash:', err)
    }
  }

  // if the IPFS node is online, open the URL from the hash, this will redirect to the local node.
  if (state.isIpfsOnline) {
    openURLFromHash()
    return
  }

  return html`

        ${nodeOffSvg(200)}

${i18n.getMessage('recovery_page_sub_header')}

${i18n.getMessage('recovery_page_message_p1')}

${i18n.getMessage('recovery_page_message_p2')}

Public URL: ${publicURI}

      <button
        class="fade-in ba bw1 b--teal bg-teal snow f7 ph2 pv3 br2 ma4 pointer"
        onclick=${openURLFromHash}
        href="${publicURI}"
      >
        ${i18n.getMessage('recovery_page_button')}
      </button>

        ${learnMoreLink} | ${optionsPageLink}

`
})

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

// Set page title and header translation
document.title = i18n.getMessage('recovery_page_title')


text:
<!DOCTYPE html>
<meta charset="utf-8">
<script src="/dist/bundles/ipfs.bundle.js"></script>
<script src="/dist/bundles/backgroundPage.bundle.js"></script>
<body></body>


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

import browser from 'webextension-polyfill'
import { onInstalled } from '../lib/on-installed.js'
import { getUninstallURL } from '../lib/on-uninstalled.js'
import { optionDefaults } from '../lib/options.js'
import createIpfsCompanion from '../lib/ipfs-companion.js'

// register lifecycle hooks early, otherwise we miss first install event
browser.runtime.onInstalled.addListener(onInstalled)
browser.runtime.setUninstallURL(getUninstallURL(browser))

// init add-on after all libs are loaded
document.addEventListener('DOMContentLoaded', async () => {
  browser.runtime.sendMessage({ telemetry: { trackView: 'background' } })
  // setting debug namespaces require page reload to get applied
  const debugNs = (await browser.storage.local.get({ logNamespaces: optionDefaults.logNamespaces })).logNamespaces
  if (debugNs !== localStorage.debug) {
    localStorage.debug = debugNs
    window.location.reload()
  }
  // init inlined to read updated localStorage.debug
  // @ts-expect-error - TS does not know about window.ipfsCompanion
  window.ipfsCompanion = await createIpfsCompanion()
})


text:
import MetricsProvider from '@ipfs-shipyard/ignite-metrics/vanilla'
import debug from 'debug'

const log = debug('ipfs-companion:telemetry')

const metricsProvider = new MetricsProvider({
  appKey: '393f72eb264c28a1b59973da1e0a3938d60dc38a',
  autoTrack: false,
  storageProvider: null
})

/**
 *
 * @param {import('../types.js').CompanionState} state
 * @returns {void}
 */
export function handleConsentFromState (state) {
  const telemetryGroups = {
    minimal: state?.telemetryGroupMinimal || false,
    performance: state?.telemetryGroupPerformance || false,
    ux: state?.telemetryGroupUx || false,
    feedback: state?.telemetryGroupFeedback || false,
    location: state?.telemetryGroupLocation || false
  }
  for (const [groupName, isEnabled] of Object.entries(telemetryGroups)) {
    if (isEnabled) {
      log(`Adding consent for '${groupName}'`)
      metricsProvider.addConsent(groupName)
    } else {
      log(`Removing consent for '${groupName}'`)
      metricsProvider.removeConsent(groupName)
    }
  }
}

const ignoredViewsRegex = []
export function trackView (view, segments) {
  log('trackView called for view: ', view)
  metricsProvider.trackView(view, ignoredViewsRegex, segments)
}

export const startSession = (...args) => metricsProvider.startSession(...args)
export const endSession = (...args) => metricsProvider.endSession(...args)


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

import Pqueue from 'p-queue'

import debug from 'debug'
import IsIpfs from 'is-ipfs'
import LRU from 'lru-cache'
import { offlinePeerCount } from './state.js'
import { ipfsContentPath, sameGateway, pathAtHttpGateway } from './ipfs-path.js'

const log = debug('ipfs-companion:dnslink')
log.error = debug('ipfs-companion:dnslink:error')

export default function createDnslinkResolver (getState) {
  // DNSLink lookup result cache
  const cacheOptions = { max: 1000, ttl: 1000 * 60 * 60 * 12 }
  const cache = new LRU(cacheOptions)
  // upper bound for concurrent background lookups done by resolve(url)
  const lookupQueue = new Pqueue({ concurrency: 4 })
  // preload of DNSLink data
  const preloadUrlCache = new LRU(cacheOptions)
  const preloadQueue = new Pqueue({ concurrency: 4 })

  const dnslinkResolver = {

    get _cache () {
      return cache
    },

    setDnslink (fqdn, value) {
      cache.set(fqdn, value)
    },

    clearCache () {
      cache.clear()
    },

    cachedDnslink (fqdn) {
      return cache.get(fqdn)
    },

    canLookupURL (requestUrl) {
      // skip URLs that could produce infinite recursion or weird loops
      const state = getState()
      return state.dnslinkPolicy &&
        requestUrl.startsWith('http') &&
        !IsIpfs.url(requestUrl) &&
        !sameGateway(requestUrl, state.apiURL) &&
        !sameGateway(requestUrl, state.gwURL)
    },

    dnslinkAtGateway (url, dnslink) {
      if (typeof url === 'string') {
        url = new URL(url)
      }
      if (dnslinkResolver.canRedirectToIpns(url, dnslink)) {
        const state = getState()
        // redirect to IPNS and leave it up to the gateway
        // to load the correct path from IPFS
        // - https://github.com/ipfs/ipfs-companion/issues/298
        const ipnsPath = dnslinkResolver.convertToIpnsPath(url)
        const gateway = state.redirect && state.localGwAvailable ? state.gwURLString : state.pubGwURLString
        return pathAtHttpGateway(ipnsPath, gateway)
      }
    },

    readAndCacheDnslink (fqdn) {
      let dnslink = dnslinkResolver.cachedDnslink(fqdn)
      if (typeof dnslink === 'undefined') {
        try {
          log(`dnslink cache miss for '${fqdn}', running DNS TXT lookup`)
          dnslink = dnslinkResolver.readDnslinkFromTxtRecord(fqdn)
          if (dnslink) {
            // TODO: set TTL as maxAge: setDnslink(fqdn, dnslink, maxAge)
            dnslinkResolver.setDnslink(fqdn, dnslink)
            log(`found dnslink: '${fqdn}' -> '${dnslink}'`)
          } else {
            dnslinkResolver.setDnslink(fqdn, false)
            log(`found NO dnslink for '${fqdn}'`)
          }
        } catch (error) {
          log.error(`error in readAndCacheDnslink for '${fqdn}'`, error)
        }
      } else {
        // Most of the time we will hit cache, which makes below line is too noisy
        // console.info(`[ipfs-companion] using cached dnslink: '${fqdn}' -> '${dnslink}'`)
      }
      return dnslink
    },

    // runs async lookup in a queue in the background and returns the record
    async resolve (url) {
      if (!dnslinkResolver.canLookupURL(url)) return
      const fqdn = new URL(url).hostname
      const cachedResult = dnslinkResolver.cachedDnslink(fqdn)
      if (cachedResult) return cachedResult
      return lookupQueue.add(() => {
        return dnslinkResolver.readAndCacheDnslink(fqdn)
      })
    },

    // preloads data behind the url to local node
    async preloadData (url) {
      const state = getState()
      if (!state.dnslinkDataPreload || state.dnslinkRedirect) return
      if (preloadUrlCache.get(url)) return
      preloadUrlCache.set(url, true)
      const dnslink = await dnslinkResolver.resolve(url)
      if (!dnslink) return
      if (!state.localGwAvailable) return
      if (state.peerCount < 1) return
      return preloadQueue.add(async () => {
        const { pathname } = new URL(url)
        const preloadUrl = new URL(state.gwURLString)
        preloadUrl.pathname = `${dnslink}${pathname}`
        await fetch(preloadUrl.toString(), { method: 'HEAD' })
        return preloadUrl
      })
    },

    // low level lookup without cache
    readDnslinkFromTxtRecord (fqdn) {
      const state = getState()
      let apiProvider
      if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) {
        // Use gw port so it can be a GET:
        // Chromium does not execute onBeforeSendHeaders for synchronous calls
        // made from the same extension context as onBeforeSendHeaders
        // which means we are unable to fixup Origin on the fly for this
        // This will no longer be needed when we switch
        // to async lookup via ipfs.dns everywhere
        apiProvider = state.gwURLString
      } else {
        // fallback to resolver at public gateway
        apiProvider = 'https://ipfs.io/'
      }
      // js-ipfs-api does not provide method for fetching this
      // TODO: revisit after https://github.com/ipfs/js-ipfs-api/issues/501 is addressed
      // TODO: consider worst-case-scenario fallback to https://developers.google.com/speed/public-dns/docs/dns-over-https
      const apiCall = `${apiProvider}api/v0/name/resolve/${fqdn}?r=false`
      const xhr = new XMLHttpRequest() // older XHR API us used because window.fetch appends Origin which causes error 403 in go-ipfs
      // synchronous mode with small timeout
      // (it is okay, because we do it only once, then it is cached and read via readAndCacheDnslink)
      xhr.open('GET', apiCall, false)
      xhr.setRequestHeader('Accept', 'application/json')
      xhr.send(null)
      if (xhr.status === 200) {
        const dnslink = JSON.parse(xhr.responseText).Path
        // console.log('readDnslinkFromTxtRecord', readDnslinkFromTxtRecord)
        if (!IsIpfs.path(dnslink)) {
          throw new Error(`dnslink for '${fqdn}' is not a valid IPFS path: '${dnslink}'`)
        }
        return dnslink
      } else if (xhr.status === 500) {
        // go-ipfs returns 500 if host has no dnslink or an error occurred
        // TODO: find/fill an upstream bug to make this more intuitive
        return false
      } else {
        throw new Error(xhr.statusText)
      }
    },

    canRedirectToIpns (url, dnslink) {
      if (typeof url === 'string') {
        url = new URL(url)
      }
      // Safety check: detect and skip gateway paths
      // Public gateways such as ipfs.io are often exposed under the same domain name.
      // We don't want dnslink to interfere with content-addressing redirects,
      // or things like /api/v0 paths exposed by the writable gateway
      // so we ignore known namespaces exposed by HTTP2IPFS gateways
      // and ignore them even if things like CID are invalid
      // -- we don't want to skew errors from gateway
      const path = url.pathname
      const httpGatewayPath = path.startsWith('/ipfs/') || path.startsWith('/ipns/') || path.startsWith('/api/v')
      if (!httpGatewayPath) {
        const fqdn = url.hostname
        // If dnslink policy is 'enabled' then lookups will be
        // executed for every unique hostname on every visited website.
        // Until we get efficient DNS TXT Lookup API there will be an overhead,
        // so 'enabled' policy is an opt-in for now.  By default we use
        // 'best-effort' policy which does async lookups to populate dnslink cache
        // in the background and do blocking lookup only when X-Ipfs-Path header
        // is found in initial response.
        // More: https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/dnslink.md
        const foundDnslink = dnslink ||
          (getState().dnslinkPolicy === 'enabled'
            ? dnslinkResolver.readAndCacheDnslink(fqdn)
            : dnslinkResolver.cachedDnslink(fqdn))
        if (foundDnslink) {
          return true
        }
      }
      return false
    },

    convertToIpnsPath (url) {
      if (typeof url === 'string') {
        url = new URL(url)
      }
      return `/ipns/${url.hostname}${url.pathname}${url.search}${url.hash}`
    },

    // Test if URL contains a valid DNSLink FQDN
    // in url.hostname OR in url.pathname (/ipns/<fqdn>)
    // and return matching FQDN if present
    findDNSLinkHostname (url) {
      if (!url) return
      // Normalize subdomain and path gateways to to /ipns/<fqdn>
      const contentPath = ipfsContentPath(url)
      if (IsIpfs.ipnsPath(contentPath)) {
        // we may have false-positives here, so we do additional checks below
        const ipnsRoot = contentPath.match(/^\/ipns\/([^/]+)/)[1]
        // console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
        // Ignore PeerIDs, match DNSLink only
        if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
          // console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
          return ipnsRoot
        }
      }
      // Check main hostname
      const { hostname } = new URL(url)
      if (dnslinkResolver.readAndCacheDnslink(hostname)) {
        // console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
        return hostname
      }
    }

  }

  return dnslinkResolver
}


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

export const welcomePage = '/dist/landing-pages/welcome/index.html'
export const optionsPage = '/dist/options/options.html'
export const recoveryPagePath = '/dist/recovery/recovery.html'
export const tickMs = 250 // no CPU spike, but still responsive enough


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

import pMemoize from 'p-memoize'
import isIPFS from 'is-ipfs'
import isFQDN from 'is-fqdn'

// For how long more expensive lookups (DAG traversal etc) should be cached
const RESULT_TTL_MS = 300000 // 5 minutes

export const dropSlash = url => url.replace(/\/$/, '')

// Turns URL or URIencoded path into a content path
export function ipfsContentPath (urlOrPath, opts) {
  opts = opts || {}

  // ipfs:// → /ipfs/
  if (urlOrPath && urlOrPath.toString().startsWith('ip')) {
    urlOrPath = urlOrPath.replace(/^(ip[n|f]s):\/\//, '/$1/')
  }

  // Fail fast if no content path can be extracted from input
  if (!isIPFS.urlOrPath(urlOrPath)) return null

  // Turn path to URL (hostname does not matter, let's use localhost)
  if (isIPFS.path(urlOrPath)) urlOrPath = `https://localhost${urlOrPath}`

  // Create URL
  let url = typeof urlOrPath === 'string' ? new URL(urlOrPath) : urlOrPath

  if (isIPFS.subdomain(urlOrPath)) {
    // Move CID-in-subdomain to URL pathname
    let { id, ns } = subdomainPatternMatch(url)
    id = dnsLabelToFqdn(id)
    url = new URL(`https://localhost/${ns}/${id}${url.pathname}${url.search}${url.hash}`)
  }

  // To get IPFS content path we need to reverse URI encoding of special
  // characters (https://github.com/ipfs/ipfs-companion/issues/303)
  const contentPath = decodeURI(url.pathname)

  // End if not a content path
  if (!isIPFS.path(contentPath)) return null

  // Attach suffix with query parameters or hash if explicitly asked to do so
  if (opts.keepURIParams) return `${contentPath}${url.search}${url.hash}`

  // Else, return content path as-is
  return contentPath
}

// Turns URL or URIencoded path into a ipfs:// or ipns:// URI
export function ipfsUri (urlOrPath) {
  const contentPath = ipfsContentPath(urlOrPath, { keepURIParams: true })
  if (!contentPath) return null
  return contentPath.replace(/^\/(ip[f|n]s)\//, '$1://')
}

function subdomainPatternMatch (url) {
  if (typeof url === 'string') {
    url = new URL(url)
  }
  const match = url.toString().match(isIPFS.subdomainGatewayPattern)
  if (!match || match.length < 3) return false
  const id = match[1]
  const ns = match[2]
  return { id, ns }
}

function dnsLabelToFqdn (label) {
  if (label && !label.includes('.') && label.includes('-') && !isIPFS.cid(label)) {
    // no '.' means the subdomain name is most likely an inlined DNSLink into single DNS label
    // en-wikipedia--on--ipfs-org → en.wikipedia-on-ipfs.org
    // (https://github.com/ipfs/in-web-browsers/issues/169)
    label = label.replace(/--/g, '@').replace(/-/g, '.').replace(/@/g, '-')
  }
  return label
}

export function pathAtHttpGateway (path, gatewayUrl) {
  // return URL without duplicated slashes
  return trimDoubleSlashes(new URL(`${gatewayUrl}${path}`).toString())
}

function swapSubdomainGateway (url, subdomainGwURL) {
  if (typeof url === 'string') {
    url = new URL(url)
  }
  const { id, ns } = subdomainPatternMatch(url)
  if (!id || !ns) throw new Error('no matching isIPFS.*subdomainPattern')
  return new URL(trimDoubleSlashes(
    `${subdomainGwURL.protocol}//${id}.${ns}.${subdomainGwURL.hostname}${url.pathname}${url.search}${url.hash}`
  )).toString()
}

export function trimDoubleSlashes (urlString) {
  return urlString.replace(/([^:]\/)\/+/g, '$1')
}

export function trimHashAndSearch (urlString) {
  // https://github.com/ipfs-shipyard/ipfs-companion/issues/567
  return urlString.split('#')[0].split('?')[0]
}

// Returns true if URL belongs to the gateway.
// The check includes subdomain gateways and quirks of ipfs.io
export function sameGateway (url, gwUrl) {
  if (typeof url === 'string') {
    url = new URL(url)
  }
  if (typeof gwUrl === 'string') {
    gwUrl = new URL(gwUrl)
  }

  if (url.hostname === 'ipfs.io') {
    // canonical gateway uses the same hostname as various DNSLink websites
    // related to IPFS project. To avoid false-positive we reduce it to only
    // match as path-based gateway, and not subdomains.
    return url.hostname === gwUrl.hostname
  }
  if (url.hostname === '0.0.0.0') {
    // normalize 0.0.0.0 (used by go-ipfs in the console)
    // to 127.0.0.1 to minimize the number of edge cases we need to handle later
    // https://github.com/ipfs-shipyard/ipfs-companion/issues/867
    url = new URL(url.toString())
    url.hostname = '127.0.0.1'
  }

  // Additional check to avoid false-positives when user has some unrelated HTTP server running on localhost:8080
  // It is not "sameGateway" if "localhost" URL does not look like Gateway or RPC URL.
  // This removes surface for bugs like https://github.com/ipfs/ipfs-companion/issues/1162
  if (!(isIPFS.url(url.toString()) || isIPFS.subdomain(url.toString()) || url.pathname.startsWith('/api/v0/') || url.pathname.startsWith('/webui'))) return false

  const gws = [gwUrl.host]

  // localhost gateway has more than one hostname
  if (gwUrl.hostname === 'localhost') {
    gws.push(`127.0.0.1:${gwUrl.port}`)
  }
  if (gwUrl.hostname === '127.0.0.1' || gwUrl.hostname === '[::1]') {
    gws.push(`localhost:${gwUrl.port}`)
  }

  for (const gwName of gws) {
    // match against the end to include subdomain gateways
    if (url.host.endsWith(gwName)) return true
  }
  return false
}

export const safeHostname = (url) => {
  // In case vendor-specific thing like brave://settings/extensions
  // cause errors, we don't throw, just return null
  try {
    return new URL(url).hostname
  } catch (e) {
    console.error(`[ipfs-companion] safeHostname(url) error for url='${url}'`, e)
  }
  return null
}

export function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
  const ipfsPathValidator = {
    // Test if URL is a Public IPFS resource
    // (pass validIpfsOrIpns(url) and not at the local gateway or API)
    publicIpfsOrIpnsResource (url) {
      // exclude custom gateway and api, otherwise we have infinite loops
      const { gwURL, apiURL } = getState()
      if (!sameGateway(url, gwURL) && !sameGateway(url, apiURL)) {
        return this.validIpfsOrIpns(url)
      }
      return false
    },

    // Test if URL or a path is a valid IPFS resource
    // (IPFS needs to be a CID, IPNS can be PeerId or have dnslink entry)
    validIpfsOrIpns (urlOrPath) {
      // normalize input to a content path
      const path = ipfsContentPath(urlOrPath)
      if (!path) return false

      // `/ipfs/` is easy to validate, we just check if CID is correct
      if (isIPFS.ipfsPath(path)) {
        return true
      }

      // `/ipns/` requires multiple stages/branches (can be FQDN with dnslink or CID)
      if (isIPFS.ipnsPath(path)) {
        // we may have false-positives here, so we do additional checks below
        const ipnsRoot = path.match(/^\/ipns\/([^/?#]+)/)[1]
        // console.log('==> IPNS root', ipnsRoot)
        // first check if root is a regular CID
        // TODO: support all peerIds, not just cids?
        if (isIPFS.cid(ipnsRoot)) {
          // console.log('==> IPNS is a valid CID', ipnsRoot)
          return true
        }
        // then see if there is an DNSLink entry for 'ipnsRoot' hostname
        // TODO: use dnslink cache only
        if (dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
          // console.log('==> IPNS for FQDN with valid dnslink: ', ipnsRoot)
          return true
        }
      }
      // everything else is not IPFS-related
      return false
    },

    // Test if actions such as 'copy URL', 'pin/unpin' should be enabled for the URL
    isIpfsPageActionsContext (url) {
      if (!url) return false
      const { apiURLString } = getState()
      const { hostname } = new URL(url)
      return Boolean(url && !url.startsWith(apiURLString) && (
        !!ipfsContentPath(url) ||
        dnslinkResolver.cachedDnslink(hostname)
      ))
    },

    // Test if actions such as 'per site redirect toggle' should be enabled for the URL
    isRedirectPageActionsContext (url) {
      const { localGwAvailable, gwURL, apiURL } = getState()
      return localGwAvailable && // show only when redirect is possible
      (isIPFS.ipnsUrl(url) || // show on /ipns/<fqdn>
        ((url.startsWith('http') || url.startsWith('ip')) && // hide on non-HTTP/native pages
         !sameGateway(url, gwURL) && // hide on /ipfs/* and *.ipfs.
          !sameGateway(url, apiURL))) // hide on api port
    },

    // Resolve URL or path to HTTP URL:
    // - IPFS paths are attached to HTTP Path Gateway root
    // - IPFS subdomains are attached to HTTP Subdomain Gateway root
    // - URL of DNSLinked websites are returned as-is
    //
    // The purpose of this resolver is to always return a meaningful, publicly
    // accessible URL that can be accessed without the need of IPFS client.
    // TODO: add Local version
    resolveToPublicUrl (urlOrPath) {
      const { pubSubdomainGwURL, pubGwURLString } = getState()
      const input = urlOrPath

      // NATIVE ipns:// with DNSLink requires simple protocol swap
      if (input.startsWith('ipns://')) {
        const dnslinkUrl = new URL(input)
        dnslinkUrl.protocol = 'https:'
        const dnslink = dnslinkResolver.readAndCacheDnslink(dnslinkUrl.hostname)
        if (dnslink) {
          return dnslinkUrl.toString()
        }
      }

      // SUBDOMAINS
      // Detect *.dweb.link and other subdomain gateways
      if (isIPFS.subdomain(input)) {
        // Switch Origin to prefered public subdomain gateway (default: dweb.link)
        const subdomainUrl = swapSubdomainGateway(input, pubSubdomainGwURL)

        // *.ipfs namespace is easy, just return
        if (isIPFS.ipfsSubdomain(subdomainUrl)) return subdomainUrl

        // DNSLink in subdomains does not make sense outside of *.localhost
        // and is usually not supported due to limitation of TLS wildcart certs.
        // Instead, we resolve it to the canonical FQDN Origin
        //
        // Remove gateway suffix to get potential FQDN
        const url = new URL(subdomainUrl)
        const { id: ipnsId } = subdomainPatternMatch(url)
        if (!isIPFS.cid(ipnsId)) {
          // Confirm DNSLink record is present and its not a false-positive
          const dnslink = dnslinkResolver.readAndCacheDnslink(ipnsId)
          if (dnslink) {
            // return URL to DNSLink hostname (FQDN without any suffix)
            url.hostname = ipnsId
            return url.toString()
          }
        }

        // Return *.ipns with libp2p-key
        if (isIPFS.ipnsSubdomain(subdomainUrl)) return subdomainUrl
      }

      // PATHS
      const ipfsPath = ipfsContentPath(input, { keepURIParams: true })

      // IPFS Paths should be attached to the public gateway
      if (isIPFS.path(ipfsPath)) return pathAtHttpGateway(ipfsPath, pubGwURLString)

      // Return original URL as-is (eg. DNSLink domains) or null if not an URL
      return input && input.startsWith('http') ? input : null
    },

    // Version of resolveToPublicUrl that always resolves to URL representing
    // path gateway at local machine (This is ok, as localhost gw will redirect
    // to correct subdomain)
    resolveToLocalUrl (urlOrPath) {
      const { gwURLString } = getState()
      const ipfsPath = ipfsContentPath(urlOrPath, { keepURIParams: true })
      if (isIPFS.path(ipfsPath)) return pathAtHttpGateway(ipfsPath, gwURLString)
      return null
    },

    // Resolve URL or path to HTTP URL with CID:
    // - IPFS paths are attached to HTTP Gateway root
    // - URL of DNSLinked websties are resolved to CIDs
    // The purpose of this resolver is to always return a meaningful, publicly
    // accessible URL that can be accessed without the need of an IPFS client
    // and that never changes.
    async resolveToPermalink (urlOrPath, optionalGatewayUrl) {
      const input = urlOrPath
      const ipfsPath = await this.resolveToImmutableIpfsPath(input)
      const gateway = optionalGatewayUrl || getState().pubGwURLString
      if (ipfsPath) return pathAtHttpGateway(ipfsPath, gateway)
      return input.startsWith('http') ? input : null
    },

    // Resolve URL or path to IPFS Path:
    // - The path can be /ipfs/ or /ipns/
    // - Keeps pathname + ?search + #hash from original URL
    // - Returns null if no valid path can be produced
    // The purpose of this resolver is to return a valid IPFS path
    // that can be accessed with IPFS client.
    resolveToIpfsPath (urlOrPath) {
      const input = urlOrPath
      // Try to normalize to IPFS path (gateway path or CID-in-subdomain)
      const ipfsPath = ipfsContentPath(input, { keepURIParams: true })
      if (ipfsPath) return ipfsPath
      // Check URL for DNSLink
      if (!input.startsWith('http')) return null
      const { hostname } = new URL(input)
      const dnslink = dnslinkResolver.cachedDnslink(hostname)
      if (dnslink) {
        // Return full IPNS path (keeps pathname + ?search + #hash)
        return dnslinkResolver.convertToIpnsPath(input)
      }
      // No IPFS path by this point
      return null
    },

    // Resolve URL or path to Immutable IPFS Path:
    // - Same as resolveToIpfsPath, but the path is always immutable /ipfs/
    // - Keeps pathname + ?search + #hash from original URL
    // - Returns null if no valid path can be produced
    // The purpose of this resolver is to return immutable /ipfs/ address
    // even if /ipns/ is present in its input.
    resolveToImmutableIpfsPath: pMemoize(async function (urlOrPath) {
      const path = ipfsPathValidator.resolveToIpfsPath(urlOrPath)
      // Fail fast if no IPFS Path
      if (!path) return null
      // Resolve /ipns/ → /ipfs/
      if (isIPFS.ipnsPath(path)) {
        // We resolve /ipns/<fqdn> as value in DNSLink cache may be out of date
        const ipnsRoot = `/ipns/${path.split('/')[2]}`
        const result = await getIpfs().resolve(ipnsRoot, { recursive: true })

        // Old API returned object, latest one returns string ¯\_(ツ)_/¯
        const ipfsRoot = result.Path ? result.Path : result
        // Return original path with swapped root (keeps pathname + ?search + #hash)
        return path.replace(ipnsRoot, ipfsRoot)
      }
      // Return /ipfs/ path
      return path
    }, { maxAge: RESULT_TTL_MS }),

    // Resolve URL or path to a raw CID:
    // - Result is the direct CID
    // - Ignores ?search and #hash from original URL
    // - Returns null if no CID can be produced
    // The purpose of this resolver is to return direct CID without anything else.
    resolveToCid: pMemoize(async function (urlOrPath) {
      const path = ipfsPathValidator.resolveToIpfsPath(urlOrPath)
      // Fail fast if no IPFS Path
      if (!path) return null
      // Drop unused parts
      const rawPath = trimHashAndSearch(path)

      // js-ipfs v0.34 does not support DNSLinks in ipfs.resolve: https://github.com/ipfs/js-ipfs/issues/1918
      // TODO: remove ipfsResolveWithDnslinkFallback when js-ipfs implements DNSLink support in ipfs.resolve
      const ipfsResolveWithDnslinkFallback = async (resolve) => {
        try {
          return await resolve()
        } catch (err) {
          const fqdn = rawPath.replace(/^.*\/ipns\/([^/]+).*/, '$1')
          if (err.message === 'resolve non-IPFS names is not implemented' && isFQDN(fqdn)) {
            // js-ipfs without dnslink support, fallback to the value read from DNSLink
            const dnslink = dnslinkResolver.readAndCacheDnslink(fqdn)
            if (dnslink) {
              // swap problematic /ipns/{fqdn} with /ipfs/{cid} and retry lookup
              const safePath = trimDoubleSlashes(rawPath.replace(/^.*(\/ipns\/[^/]+)/, dnslink))
              if (rawPath !== safePath) {
                const result = await ipfsPathValidator.resolveToCid(safePath)
                // return in format of ipfs.resolve()
                return isIPFS.cid(result) ? `/ipfs/${result}` : result
              }
            }
          }
          throw err
        }
      }
      const result = await ipfsResolveWithDnslinkFallback(async () =>
        // dhtt/dhtrc optimize for lookup time
        getIpfs().resolve(rawPath, { recursive: true, dhtt: '5s', dhtrc: 1 })
      )

      const directCid = isIPFS.ipfsPath(result) ? result.split('/')[2] : result
      return directCid
    }, { maxAge: RESULT_TTL_MS })
  }

  return ipfsPathValidator
}


text:
'use strict'
/* eslint-env browser, webextensions */
// @ts-check
import { isHostname, safeURL } from './options.js'

export const offlinePeerCount = -1

/**
 *
 * @param {import('../types.js').CompanionOptions} options
 * @param {Partial<import('../types.js').CompanionOptions>} [overrides]
 * @returns {import('../types.js').CompanionState}
 */
export function initState (options, overrides) {
  // we store options and some pregenerated values to avoid async storage
  // reads and minimize performance impact on overall browsing experience
  /**
   * @type {Partial<import('../types.js').CompanionState & import('../types.js').CompanionOptions>}
   */
  const state = Object.assign({}, options)
  // generate some additional values
  state.peerCount = offlinePeerCount
  state.pubGwURL = safeURL(options.publicGatewayUrl)
  state.pubGwURLString = state.pubGwURL?.toString()
  delete state.publicGatewayUrl
  state.pubSubdomainGwURL = safeURL(options.publicSubdomainGatewayUrl)
  state.pubSubdomainGwURLString = state.pubSubdomainGwURL?.toString()
  delete state.publicSubdomainGatewayUrl
  state.redirect = options.useCustomGateway
  delete state.useCustomGateway
  state.apiURL = safeURL(options.ipfsApiUrl, { useLocalhostName: false }) // go-ipfs returns 403 if IP is beautified to 'localhost'
  state.apiURLString = state.apiURL?.toString()
  delete state.ipfsApiUrl
  state.gwURL = safeURL(options.customGatewayUrl, { useLocalhostName: state.useSubdomains })
  state.gwURLString = state.gwURL?.toString()
  delete state.customGatewayUrl
  state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy

  // attach helper functions
  state.activeIntegrations = (url) => {
    if (!state.active) return false
    try {
      const hostname = isHostname(url) ? url : new URL(url).hostname
      // opt-out has more weight, we also match parent domains
      const disabledDirectlyOrIndirectly = state.disabledOn?.some(optout => hostname.endsWith(optout))
      // ..however direct opt-in should overwrite parent's opt-out
      const enabledDirectly = state.enabledOn?.some(optin => optin === hostname)
      return !(disabledDirectlyOrIndirectly && !enabledDirectly)
    } catch (_) {
      return false
    }
  }
  // TODO refactor this into a class. It's getting too big and messy.
  Object.defineProperty(state, 'nodeActive', {
    // TODO: make quick fetch to confirm it works?
    get: function () { return this.peerCount !== offlinePeerCount }
  })
  Object.defineProperty(state, 'localGwAvailable', {
    // TODO: make quick fetch to confirm it works?
    get: function () { return this.ipfsNodeType !== 'embedded' }
  })
  Object.defineProperty(state, 'webuiRootUrl', {
    get: function () {
      // Did user opt-in for rolling release published on DNSLink?
      if (state.useLatestWebUI) return `${state.gwURLString}ipns/webui.ipfs.io/`
      return `${state.apiURLString}webui`
    }
  })
  // apply optional overrides
  if (overrides) Object.assign(state, overrides)
  return /** @type {import('../types.js').CompanionState} */(state)
}


text:
'use strict'

import browser from 'webextension-polyfill'

import debug from 'debug'
const log = debug('ipfs-companion:context-menus')
log.error = debug('ipfs-companion:context-menus:error')

// mapping between context name and field with data for it
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/menus/ContextType
const contextSources = {
  selection: 'selectionText',
  image: 'srcUrl',
  video: 'srcUrl',
  audio: 'srcUrl',
  link: 'linkUrl',
  page: 'pageUrl'
}

export async function findValueForContext (context, contextType) {
  if (context) {
    if (contextType) {
      const field = contextSources[contextType]
      return context[field]
    }
    if (context.srcUrl) {
      // present when clicked on page element such as image or video
      return context.srcUrl
    }
    if (context.linkUrl) {
      // present when clicked on a link
      return context.linkUrl
    }
    if (context.pageUrl) {
      // pageUrl is the root frame
      return context.pageUrl
    }
  }
  // falback to the url of current tab
  const currentTab = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
  return currentTab.url
}

// Context Roots
const menuParentImage = 'contextMenu_parentImage'
const menuParentVideo = 'contextMenu_parentVideo'
const menuParentAudio = 'contextMenu_parentAudio'
const menuParentLink = 'contextMenu_parentLink'
const menuParentPage = 'contextMenu_parentPage'
// const menuParentText = 'contextMenu_parentText'
// Generic Add to IPFS
const contextMenuImportToIpfs = 'contextMenu_importToIpfs'
// Add X to IPFS
const contextMenuImportToIpfsSelection = 'contextMenu_importToIpfsSelection'
// Copy X
export const contextMenuCopyCidAddress = 'panelCopy_currentIpfsAddress'
export const contextMenuCopyCanonicalAddress = 'panelCopy_currentIpnsAddress'
export const contextMenuCopyRawCid = 'panelCopy_copyRawCid'
export const contextMenuCopyAddressAtPublicGw = 'panel_copyCurrentPublicGwUrl'
export const contextMenuViewOnGateway = 'panel_contextMenuViewOnGateway'
export const contextMenuCopyPermalink = 'panel_copyCurrentPermalink'

// menu item ids for things that are enabled only when API is online (static)
const apiMenuItemIds = new Set([contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuImportToIpfs])
// menu items that are enabled only when API is online (dynamic)
const apiMenuItems = new Set()
// menu items enabled only in IPFS context (dynamic)
const ipfsContextItems = new Set()

export function createContextMenus (
  getState, _runtime, ipfsPathValidator, { onAddFromContext, onCopyRawCid, onCopyAddressAtPublicGw }) {
  try {
    const createSubmenu = (id, contextType, menuBuilder) => {
      browser.contextMenus.create({
        id,
        title: browser.i18n.getMessage(id),
        documentUrlPatterns: ['<all_urls>'],
        contexts: [contextType]
      })
    }
    const createImportToIpfsMenuItem = (parentId, id, contextType, ipfsAddOptions) => {
      const itemId = `${parentId}_${id}`
      apiMenuItems.add(itemId)
      return browser.contextMenus.create({
        id: itemId,
        parentId,
        title: browser.i18n.getMessage(id),
        contexts: [contextType],
        documentUrlPatterns: ['<all_urls>'],
        enabled: false,
        /* no support for 'icons' in Chrome
        icons: {
          '48': '/ui-kit/icons/stroke_cube.svg'
        }, */
        onclick: (context) => onAddFromContext(context, contextType, ipfsAddOptions)
      })
    }
    const createCopierMenuItem = (parentId, id, contextType, handler) => {
      const itemId = `${parentId}_${id}`
      ipfsContextItems.add(itemId)
      // some items also require API access
      if (apiMenuItemIds.has(id)) {
        apiMenuItems.add(itemId)
      }
      return browser.contextMenus.create({
        id: itemId,
        parentId,
        title: browser.i18n.getMessage(id),
        contexts: [contextType],
        documentUrlPatterns: [
          '*://*/ipfs/*', '*://*/ipns/*',
          '*://*.ipfs.dweb.link/*', '*://*.ipns.dweb.link/*', // TODO: add any custom public gateway from Preferences
          '*://*.ipfs.localhost/*', '*://*.ipns.localhost/*'
        ],
        /* no support for 'icons' in Chrome
        icons: {
          '48': '/ui-kit/icons/stroke_copy.svg'
        }, */
        onclick: (context) => handler(context, contextType)
      })
    }
    const buildSubmenu = (parentId, contextType) => {
      createSubmenu(parentId, contextType)
      createImportToIpfsMenuItem(parentId, contextMenuImportToIpfs, contextType, { wrapWithDirectory: true, pin: false })
      createCopierMenuItem(parentId, contextMenuCopyAddressAtPublicGw, contextType, onCopyAddressAtPublicGw)
      // TODO: below needs refactor to support for both IPFS and IPNS, like one added to browser action in https://github.com/ipfs-shipyard/ipfs-companion/pull/937
      // createCopierMenuItem(parentId, contextMenuCopyCanonicalAddress, contextType, onCopyCanonicalAddress)
      createCopierMenuItem(parentId, contextMenuCopyRawCid, contextType, onCopyRawCid)
    }

    /*
    createSubmenu(menuParentText, 'selection')
    createImportToIpfsMenuItem(menuParentText, contextMenuImportToIpfsSelection, 'selection')
    */
    createImportToIpfsMenuItem(null, contextMenuImportToIpfsSelection, 'selection', { pin: false })
    buildSubmenu(menuParentImage, 'image')
    buildSubmenu(menuParentVideo, 'video')
    buildSubmenu(menuParentAudio, 'audio')
    buildSubmenu(menuParentLink, 'link')
    buildSubmenu(menuParentPage, 'page')
  } catch (err) {
    // documentUrlPatterns is not supported in Muon-Brave
    if (err.message.indexOf('createProperties.documentUrlPatterns of contextMenus.create is not supported yet') > -1) {
      log('context menus disabled - createProperties.documentUrlPatterns of contextMenus.create is not supported yet')
      return { update: () => Promise.resolve() }
    }
    // contextMenus are not supported in Firefox for Android
    if (err.message === 'browser.contextMenus is undefined' || typeof browser.contextMenus === 'undefined') {
      log('context menus disabled - browser.contextMenus is undefined')
      return { update: () => Promise.resolve() }
    }
    throw err
  }

  // enabled only when ipfsContext is shown when API is up
  const apiAndIpfsContextItems = new Set([...apiMenuItems].filter(i => ipfsContextItems.has(i)))
  // state to avoid async tab lookups
  let ipfsContext = false

  return {
    async update (changedTabId) {
      try {
        if (changedTabId) {
          // recalculate tab-dependant menu items
          const currentTab = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
          if (currentTab && currentTab.id === changedTabId) {
            ipfsContext = ipfsPathValidator.isIpfsPageActionsContext(currentTab.url)
          }
        }
        const ifApi = getState().peerCount >= 0
        for (const item of apiMenuItems) {
          await browser.contextMenus.update(item, { enabled: ifApi })
        }
        for (const item of ipfsContextItems) {
          await browser.contextMenus.update(item, { enabled: ipfsContext })
        }
        for (const item of apiAndIpfsContextItems) {
          await browser.contextMenus.update(item, { enabled: (ifApi && ipfsContext) })
        }
      } catch (err) {
        log.error('Error updating context menus', err)
      }
    }

    // TODO: destroy?
  }
}


text:
'use strict'

import browser from 'webextension-polyfill'
import { findValueForContext } from './context-menus.js'
import { pathAtHttpGateway } from './ipfs-path.js'

export default function createInspector (notify, ipfsPathValidator, getState) {
  return {
    async viewOnGateway (context, contextType) {
      const url = await findValueForContext(context, contextType)
      const ipfsPath = ipfsPathValidator.resolveToIpfsPath(url)
      const gateway = getState().pubGwURLString
      const gatewayUrl = pathAtHttpGateway(ipfsPath, gateway)
      await browser.tabs.create({ url: gatewayUrl })
    }
    // TODO: view in WebUI's Files
    // TODO: view in WebUI's IPLD Explorer
  }
}


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

import debug from 'debug'
const log = debug('ipfs-companion:precache')
log.error = debug('ipfs-companion:precache:error')

/**
 * Adds important assets such as Web UI to the local js-ipfs-repo.
 * This ensures they load instantly, even in offline environments.
 */
export async function precache (ipfs, state) {
  const roots = []
  // find out the content path of webui, and add it to precache list
  try {
    let cid, name
    if (state.useLatestWebUI) { // resolve DNSLink
      cid = await ipfs.dns('webui.ipfs.io', { recursive: true })
      name = 'latest webui from DNSLink at webui.ipfs.io'
    } else { // find out safelisted path behind <api-port>/webui
      cid = new URL((await fetch(`${state.apiURLString}webui`)).url).pathname
      name = `stable webui hardcoded at ${state.apiURLString}webui`
    }
    roots.push({
      nodeType: 'external',
      name,
      cid
    })
  } catch (e) {
    log.error('unable to find webui content path for precache', e)
  }

  // precache each root
  for (const { name, cid, nodeType } of roots) {
    if (state.ipfsNodeType !== nodeType) continue
    if (await inRepo(ipfs, cid)) {
      log(`${name} (${cid}) already in local repo, skipping import`)
      continue
    }
    log(`importing ${name} (${cid}) to local ipfs repo`)

    // prefetch over IPFS
    try {
      for await (const ref of ipfs.refs(cid, { recursive: true })) {
        if (ref.err) {
          log.error(`error while preloading ${name} (${cid})`, ref.err)
          continue
        }
      }
      log(`${name} successfully cached under CID ${cid}`)
    } catch (err) {
      log.error(`error while processing ${name}`, err)
    }
  }
}

async function inRepo (ipfs, cid) {
  // dag.get in offline mode will throw block is not present in local repo
  // (we also have timeout as a failsafe)
  try {
    await ipfs.dag.get(cid, { offline: true, timeout: 5000 })
    return true
  } catch (_) {
    return false
  }
}


text:
'use strict'

import { findValueForContext } from './context-menus.js'

async function copyTextToClipboard (text, notify) {
  try {
    try {
      // Modern API (spotty support, but works in Firefox)
      await navigator.clipboard.writeText(text)
      // FUN FACT:
      // Before this API existed we had no access to cliboard from
      // the background page in Firefox and had to inject content script
      // into current page to copy there:
      // https://github.com/ipfs-shipyard/ipfs-companion/blob/b4a168880df95718e15e57dace6d5006d58e7f30/add-on/src/lib/copier.js#L10-L35
      // :-))
    } catch (e) {
      // Fallback to old API (works only in Chromium)
      function oncopy (event) { // eslint-disable-line no-inner-declarations
        document.removeEventListener('copy', oncopy, true)
        event.stopImmediatePropagation()
        event.preventDefault()
        event.clipboardData.setData('text/plain', text)
      }
      document.addEventListener('copy', oncopy, true)
      document.execCommand('copy')
    }
    notify('notify_copiedTitle', text)
  } catch (error) {
    console.error('[ipfs-companion] Failed to copy text', error)
    notify('notify_addonIssueTitle', 'Unable to copy')
  }
}

export default function createCopier (notify, ipfsPathValidator) {
  return {
    async copyTextToClipboard (text) {
      await copyTextToClipboard(text, notify)
    },

    async copyCanonicalAddress (context, contextType) {
      const url = await findValueForContext(context, contextType)
      const ipfsPath = ipfsPathValidator.resolveToIpfsPath(url)
      await copyTextToClipboard(ipfsPath, notify)
    },

    async copyCidAddress (context, contextType) {
      const url = await findValueForContext(context, contextType)
      const ipfsPath = await ipfsPathValidator.resolveToImmutableIpfsPath(url)
      await copyTextToClipboard(ipfsPath, notify)
    },

    async copyRawCid (context, contextType) {
      const url = await findValueForContext(context, contextType)
      try {
        const cid = await ipfsPathValidator.resolveToCid(url)
        await copyTextToClipboard(cid, notify)
      } catch (error) {
        console.error('Unable to resolve/copy direct CID:', error.message)
        if (notify) {
          const errMsg = error.toString()
          if (errMsg.startsWith('Error: no link')) {
            // Sharding support is limited:
            // - https://github.com/ipfs/js-ipfs/issues/1279
            // - https://github.com/ipfs/go-ipfs/issues/5270
            notify('notify_addonIssueTitle', 'Unable to resolve CID within HAMT-sharded directory, sorry! Will be fixed soon.')
          } else {
            notify('notify_addonIssueTitle', 'notify_inlineErrorMsg', error.message)
          }
        }
      }
    },

    async copyAddressAtPublicGw (context, contextType) {
      const url = await findValueForContext(context, contextType)
      const publicUrl = ipfsPathValidator.resolveToPublicUrl(url)
      await copyTextToClipboard(publicUrl, notify)
    },

    async copyPermalink (context, contextType) {
      const url = await findValueForContext(context, contextType)
      const permalink = await ipfsPathValidator.resolveToPermalink(url)
      await copyTextToClipboard(permalink, notify)
    }
  }
}


text:
'use strict'

import isFQDN from 'is-fqdn'
import { isIPv4, isIPv6 } from 'is-ip'

/**
 * @type {Readonly<import('../types.js').CompanionOptions>}
 */
export const optionDefaults = Object.freeze({
  active: true, // global ON/OFF switch, overrides everything else
  ipfsNodeType: 'external',
  ipfsNodeConfig: buildDefaultIpfsNodeConfig(),
  publicGatewayUrl: 'https://ipfs.io',
  publicSubdomainGatewayUrl: 'https://dweb.link',
  useCustomGateway: true,
  useSubdomains: true,
  enabledOn: [], // hostnames with explicit integration opt-in
  disabledOn: [], // hostnames with explicit integration opt-out
  automaticMode: true,
  linkify: false,
  dnslinkPolicy: 'best-effort',
  dnslinkDataPreload: true,
  dnslinkRedirect: true,
  recoverFailedHttpRequests: true,
  detectIpfsPathHeader: true,
  preloadAtPublicGateway: true,
  catchUnhandledProtocols: true,
  displayNotifications: true,
  displayReleaseNotes: false,
  customGatewayUrl: 'http://localhost:8080',
  ipfsApiUrl: 'http://127.0.0.1:5001',
  ipfsApiPollMs: 3000,
  logNamespaces: 'jsipfs*,ipfs*,libp2p:mdns*,libp2p-delegated*,-*:ipns*,-ipfs:preload*,-ipfs-http-client:request*,-ipfs:http-api*',
  importDir: '/ipfs-companion-imports/%Y-%M-%D_%h%m%s/',
  useLatestWebUI: false,
  dismissedUpdate: null,
  openViaWebUI: true,
  telemetryGroupMinimal: true,
  telemetryGroupPerformance: false,
  telemetryGroupUx: false,
  telemetryGroupFeedback: false,
  telemetryGroupLocation: false
})

function buildDefaultIpfsNodeConfig () {
  return JSON.stringify({
    config: {
      Addresses: {
        Swarm: []
      }
    }
  }, null, 2)
}

// `storage` should be a browser.storage.local or similar
export async function storeMissingOptions (read, defaults, storage) {
  const requiredKeys = Object.keys(defaults)
  const changes = {}
  const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
  for (const key of requiredKeys) {
    // limit work to defaults and missing values, skip values other than defaults
    if (!has(read, key) || read[key] === defaults[key]) {
      const data = await storage.get(key)
      if (!has(data, key)) { // detect and fix key without value in storage
        changes[key] = defaults[key]
      }
    }
  }
  // save all in bulk
  await storage.set(changes)
  return changes
}

// safeURL produces URL object with optional normalizations
export function safeURL (url, opts) {
  opts = opts || { useLocalhostName: true }
  if (typeof url === 'string') {
    url = new URL(url)
  }
  if (url.hostname === '0.0.0.0') {
    // normalize 0.0.0.0 (used by go-ipfs in the console)
    // to 127.0.0.1 to minimize the number of edge cases we need to handle later
    // https://github.com/ipfs-shipyard/ipfs-companion/issues/867
    url = new URL(url.toString())
    url.hostname = '127.0.0.1'
  }
  // "localhost" gateway normalization matters because:
  // - 127.0.0.1 is a path gateway
  // - localhost is a subdomain gateway
  // https://github.com/ipfs-shipyard/ipfs-companion/issues/328#issuecomment-537383212
  if (opts.useLocalhostName && localhostIpUrl(url)) {
    url.hostname = 'localhost'
  }
  if (!opts.useLocalhostName && localhostNameUrl(url)) {
    url.hostname = '127.0.0.1'
  }
  return url
}

// Return string without trailing slash
export function guiURLString (url, opts) {
  return safeURL(url, opts).toString().replace(/\/$/, '')
}

// ensure value is a valid URL.hostname (FQDN || ipv4 || ipv6 WITH brackets)
export function isHostname (x) {
  if (isFQDN(x) || isIPv4(x)) {
    return true
  }

  const match = x.match(/^\[(.*)\]$/)

  if (match == null) {
    return false
  }

  return isIPv6(match[1])
}

// convert JS array to multiline textarea
function hostArrayCleanup (array) {
  array = array.map(host => host.trim().toLowerCase())
  // normalize/extract hostnames (just domain/ip, drop ports etc), if provided
  array = array.map(x => {
    try {
      if (isIPv6(x)) x = `[${x}]`
      return new URL(`http://${x}`).hostname
    } catch (_) {
      return undefined
    }
  })
  array = array.filter(Boolean).filter(isHostname)
  array = [...new Set(array)] // dedup
  array.sort()
  return array
}
export function hostArrayToText (array) {
  return hostArrayCleanup(array).join('\n')
}
export function hostTextToArray (text) {
  return hostArrayCleanup(text.split('\n'))
}

function localhostIpUrl (url) {
  if (typeof url === 'string') {
    url = new URL(url)
  }
  return url.hostname === '127.0.0.1' || url.hostname === '[::1]'
}
function localhostNameUrl (url) {
  if (typeof url === 'string') {
    url = new URL(url)
  }
  return url.hostname.toLowerCase() === 'localhost'
}

export async function migrateOptions (storage, debug) {
  const log = debug('ipfs-companion:migrations')
  log.error = debug('ipfs-companion:migrations:error')

  // <= v2.4.4
  // DNSLINK: convert old on/off 'dnslink' flag to text-based 'dnslinkPolicy'
  const { dnslink } = await storage.get('dnslink')
  if (dnslink) {
    // migrating old dnslink policy to 'best-effort'
    await storage.set({
      dnslinkPolicy: 'best-effort',
      detectIpfsPathHeader: true
    })
    await storage.remove('dnslink')
  }

  // ~ v2.9.x: migrating noRedirectHostnames → noIntegrationsHostnames
  // https://github.com/ipfs-shipyard/ipfs-companion/pull/830
  const { noRedirectHostnames } = await storage.get('noRedirectHostnames')
  if (noRedirectHostnames) {
    await storage.set({ noIntegrationsHostnames: noRedirectHostnames })
    await storage.remove('noRedirectHostnames')
  }

  // ~v2.11: subdomain proxy at *.ipfs.localhost
  // migrate old default 127.0.0.1 to localhost hostname
  const { customGatewayUrl: gwUrl } = await storage.get('customGatewayUrl')
  if (gwUrl && (localhostIpUrl(gwUrl) || localhostNameUrl(gwUrl))) {
    const { useSubdomains } = await storage.get('useSubdomains')
    const newUrl = guiURLString(gwUrl, { useLocalhostName: useSubdomains })
    if (gwUrl !== newUrl) {
      await storage.set({ customGatewayUrl: newUrl })
    }
  }

  { // ~v2.15.x: migrating noIntregrationsHostnames → disabledOn
    const { disabledOn, noIntegrationsHostnames } = await storage.get(['disabledOn', 'noIntegrationsHostnames'])
    if (noIntegrationsHostnames) {
      log('migrating noIntregrationsHostnames → disabledOn')
      await storage.set({ disabledOn: disabledOn.concat(noIntegrationsHostnames) })
      await storage.remove('noIntegrationsHostnames')
    }
  }

  { // ~v2.15.x: opt-out some hostnames if user does not have excplicit rule already
    const { enabledOn, disabledOn } = await storage.get(['enabledOn', 'disabledOn'])
    for (const fqdn of [
      'proto.school', //  https://github.com/ipfs-shipyard/ipfs-companion/issues/921
      'app.fleek.co' // https://github.com/ipfs-shipyard/ipfs-companion/pull/929#pullrequestreview-509501401
    ]) {
      if (enabledOn.includes(fqdn) || disabledOn.includes(fqdn)) continue
      log(`adding '${fqdn}' to 'disabledOn' list`)
      disabledOn.push(fqdn)
      await storage.set({ disabledOn })
    }
  }

  { // ~v2.15.1: change displayReleaseNotes opt-out flag to opt-in
    const { displayReleaseNotes, dismissedUpdate } = await storage.get(['displayReleaseNotes', 'dismissedUpdate'])
    if (!dismissedUpdate && displayReleaseNotes) {
      log('converting displayReleaseNotes from out-out to opt-in')
      await storage.set({ displayReleaseNotes: false })
    }
  }

  // TODO: refactor this, so migrations only run once (like https://github.com/sindresorhus/electron-store#migrations)
}


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

import browser from 'webextension-polyfill'
import debug from 'debug'
import { welcomePage } from './constants.js'
import { brave, braveNodeType } from './ipfs-client/brave.js'

const { version } = browser.runtime.getManifest()
export const updatePage = 'https://github.com/ipfs-shipyard/ipfs-companion/releases/tag/v'

export async function onInstalled (details) {
  // details.temporary === run via `npm run firefox`
  if (details.reason === 'install' || details.temporary) {
    await browser.storage.local.set({ onInstallTasks: 'onFirstInstall' })
  } else if (details.reason === 'update' || details.temporary) {
    await browser.storage.local.set({ onInstallTasks: 'onVersionUpdate' })
  }
}

export async function runPendingOnInstallTasks () {
  const { onInstallTasks, displayReleaseNotes } = await browser.storage.local.get(['onInstallTasks', 'displayReleaseNotes'])
  await browser.storage.local.remove('onInstallTasks')
  switch (onInstallTasks) {
    case 'onFirstInstall':
      await useNativeNodeIfFeasible(browser)
      return browser.tabs.create({
        url: welcomePage
      })
    case 'onVersionUpdate':
      if (!displayReleaseNotes) return
      await browser.storage.local.set({ dismissedUpdate: version })
      return browser.tabs.create({ url: updatePage + version })
  }
}

async function useNativeNodeIfFeasible (browser) {
  // lazy-loaded dependencies due to debug package
  // depending on the value of localStorage.debug, which is set later
  const log = debug('ipfs-companion:on-installed')
  log.error = debug('ipfs-companion:on-installed:error')
  const { ipfsNodeType, ipfsApiUrl } = await browser.storage.local.get(['ipfsNodeType', 'ipfsApiUrl'])

  // Brave >= v1.19 (https://brave.com/ipfs-support/)
  if (typeof brave !== 'undefined' && ipfsNodeType !== braveNodeType) {
    try {
      log(`brave detected, but node type is ${ipfsNodeType}. testing external endpoint at ${ipfsApiUrl}`)
      const response = await (await fetch(`${ipfsApiUrl}/api/v0/id`, { method: 'post' })).json()
      if (typeof response.ID === 'undefined') throw new Error(`unable to read PeerID from API at ${ipfsApiUrl}`)
      log(`endpoint is online, PeerID is ${response.ID}, nothing to do`)
    } catch (e) {
      log.error(`endpoint ${ipfsApiUrl} does not work`, e)
      log('switching node type to one provided by brave')
      await browser.storage.local.set({ ipfsNodeType: braveNodeType })
    }
  }
}