text:
'use strict'
import { describe, it, before, beforeEach, after } from 'mocha'
import sinon from 'sinon'
import { expect } from 'chai'
import { URL } from 'url'
import browser from 'sinon-chrome'
import { initState } from '../../../add-on/src/lib/state.js'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
import { createRequestModifier } from '../../../add-on/src/lib/ipfs-request.js'
import createDnslinkResolver from '../../../add-on/src/lib/dnslink.js'
import { createIpfsPathValidator } from '../../../add-on/src/lib/ipfs-path.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

const url2request = (string) => {
  return { url: string, type: 'main_frame' }
}

const fakeRequestId = () => {
  return Math.floor(Math.random() * 100000).toString()
}

// const nodeTypes = ['external', 'embedded']

describe('modifyRequest processing of DNSLinks', function () {
  let state, dnslinkResolver, ipfsPathValidator, modifyRequest, runtime

  before(function () {
    global.URL = URL
    global.browser = browser
    browser.runtime.id = 'testid'
  })

  beforeEach(async function () {
    state = Object.assign(initState(optionDefaults), {
      ipfsNodeType: 'external',
      peerCount: 1,
      redirect: true,
      catchUnhandledProtocols: true,
      gwURLString: 'http://127.0.0.1:8080',
      pubGwURLString: 'https://ipfs.io'
    })
    const getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDnslinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  describe('a request to FQDN with dnslinkPolicy=false', function () {
    let activeGateway
    beforeEach(function () {
      // Ensure the eager dnslinkPolicy is on (dns txt lookup for every request)
      state.dnslinkPolicy = false
      // API is online and has one peer
      state.peerCount = 1
      // embedded node (js-ipfs) defaults to public gw
      activeGateway = (state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString)
      // clear dnslink cache to ensure DNS TXT record lookup is triggered
      dnslinkResolver.clearCache()
    })
    it('should do nothing if DNS TXT record exists', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should do nothing if dnslink for FQDN is in cache', function () {
      // stub existence of a positive DNSLink cache hit
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should do nothing if DNS TXT record exists and dnslink is in cache', function () {
      // stub existence of a valid DNS record and cache
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should do nothing if DNS TXT record exists and x-ipfs-path is disabled', function () {
      // enable detection of x-ipfs-path to ensure it is not enough without dnslinkPolicy=detectIpfsPathHeader
      state.detectIpfsPathHeader = false
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipns/this-should-be-ignored.io')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // simulate presence of x-ipfs-path header returned by HTTP gateway
      request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipns/value-from-x-ipfs-path.io' }]
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should ignore DNS TXT record and use /ipfs/ path from x-ipfs-path if both are present', function () {
      // enable detection of x-ipfs-path to ensure it is not enough without dnslinkPolicy=detectIpfsPathHeader
      state.detectIpfsPathHeader = true
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipns/this-should-be-ignored.io')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // simulate presence of x-ipfs-path header returned by HTTP gateway
      request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
      // onHeadersReceived should redirect to value from X-Ipfs-Path
      expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(activeGateway + '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd?argTest#hashTest')
    })
    it('should ignore DNS TXT record and also ignore /ipns/ path from x-ipfs-path if both are present', function () {
      // enable detection of x-ipfs-path to ensure it is not enough without dnslinkPolicy=detectIpfsPathHeader
      state.detectIpfsPathHeader = true
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipns/this-should-be-ignored.io')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // simulate presence of x-ipfs-path header returned by HTTP gateway
      request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipns/value-from-x-ipfs-path-to-ignore.io' }]
      // onHeadersReceived should ignore /ipns/ from x-ipfs-path because dnslink is disabled in preferences
      // and redirect would confuse users
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
  })

  describe('a request to FQDN with dnslinkPolicy=enabled', function () {
    let activeGateway
    beforeEach(function () {
      // Ensure the eager dnslinkPolicy is on (dns txt lookup for every request)
      state.dnslinkPolicy = 'enabled'
      // disable detection of x-ipfs-path to ensure isolated test
      // TODO: create separate 'describe' section  for detectIpfsPathHeader==true
      state.detectIpfsPathHeader = false
      // API is online and has one peer
      state.peerCount = 1
      // embedded node (js-ipfs) defaults to public gw
      activeGateway = (state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString)
      // clear dnslink cache to ensure DNS TXT record lookup is triggered
      dnslinkResolver.clearCache()
    })
    it('should redirect in onBeforeRequest if dnslink exists', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should redirect in onBeforeRequest if DNS TXT record exists, XHR is cross-origin and runtime is Chromium', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      runtime.isFirefox = false
      // Chrome uses 'initiator' for origin
      const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should redirect in onBeforeRequest if dnslink exists, XHR is cross-origin and runtime is Firefox', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      runtime.isFirefox = true
      // Firefox uses 'originUrl' for origin
      const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should redirect later in onHeadersReceived if dnslink exists, XHR is cross-origin and runtime is Firefox <69', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      // Context for CORS XHR problems in Firefox <69: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
      runtime.requiresXHRCORSfix = true
      // Firefox uses 'originUrl' for origin
      const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      // onBeforeRequest should not change anything, as it will trigger false-positive CORS error
      expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
      // onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
      expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should do nothing if dnslink does not exist and XHR is cross-origin in Firefox', function () {
      // stub no dnslink
      const fqdn = 'youtube.com'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns(undefined)
      // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
      runtime.isFirefox = true
      // Firefox uses 'originUrl' for origin
      const xhrRequest = { url: 'https://youtube.com/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(xhrRequest)).to.equal(undefined)
    })
  })

  describe('a request to FQDN with dnslinkPolicy=detectIpfsPathHeader', function () {
    let activeGateway
    beforeEach(function () {
      // Enable the eager dnslinkPolicy (dns txt lookup for every request)
      state.dnslinkPolicy = 'detectIpfsPathHeader'
      // disable detection of x-ipfs-path to ensure isolated test
      // TODO: create separate 'describe' section  for detectIpfsPathHeader==true
      state.detectIpfsPathHeader = true
      // API is online and has one peer
      state.peerCount = 1
      // embedded node (js-ipfs) defaults to public gw
      activeGateway = (state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString)
    })
    describe('and dnslink cache miss', function () {
      beforeEach(function () {
        // force-clear dnslink cache to enable cache miss
        dnslinkResolver.clearCache()
      })
      it('should redirect subrequests in onHeadersReceived if DNS TXT record exists and x-ipfs-path header is present', function () {
        // clear dnslink cache to ensure miss
        dnslinkResolver.clearCache()
        // stub existence of a valid DNS record
        const fqdn = 'explore.ipld.io'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        request.type = 'sub_frame' // we test a subrequests because main_frame gets early DNSLink preload in onBeforeRequest
        expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        // simulate presence of x-ipfs-path header returned by HTTP gateway
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
        // DNSLink is present, so we ignore hash from X-Ipfs-Path header and redirect to nice /ipns/ address
        expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
      })
      it('should redirect in onHeadersReceived if DNS TXT record is missing but x-ipfs-path header is present', function () {
        // clear dnslink cache to ensure miss
        dnslinkResolver.clearCache()
        // stub lack of DNS record
        const fqdn = 'explore.ipld.io'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns(false)
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        request.type = 'sub_frame' // we test a subrequest here because main_frame gets early DNSLink preload in onBeforeRequest
        expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        // simulate presence of x-ipfs-path header returned by HTTP gateway
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
        // Note that DNSLink is missing, so a path from x-ipfs-path is used
        expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(activeGateway + '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd?argTest#hashTest')
      })
      it('should do nothing if DNS TXT record exists but there is no x-ipfs-path header', function () {
        // clear dnslink cache to ensure miss
        dnslinkResolver.clearCache()
        // stub existence of a valid DNS record
        const fqdn = 'explore.ipld.io'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        request.type = 'sub_frame' // we test a subrequest here because main_frame gets early DNSLink preload in onBeforeRequest
        expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
      })
      describe('(XHR CORS scenario)', function () {
        // Test makes more sense for dnslinkPolicy=enabled, but we keep it here for completeness
        it('should redirect in onHeadersReceived if XHR is cross-origin and runtime is not Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          runtime.isFirefox = false
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          xhrRequest.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
          expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        // Test makes more sense for dnslinkPolicy=enabled, but we keep it here for completeness
        it('should redirect in onHeadersReceived if XHR is cross-origin and runtime is Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything, as it will trigger false-positive CORS error
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
          xhrRequest.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
          expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        it('should redirect later in onHeadersReceived if XHR is cross-origin and runtime is Firefox <69', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          // Context for CORS XHR problems in Firefox <69: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.requiresXHRCORSfix = true
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything, as it will trigger false-positive CORS error
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
          expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        // Test makes more sense for dnslinkPolicy=enabled, but we keep it here for completeness
        it('should do nothing if DNS TXT record is missing and XHR is cross-origin in Firefox', function () {
          // stub no dnslink
          const fqdn = 'youtube.com'
          dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns(false)
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'https://youtube.com/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived should not change anything
          expect(modifyRequest.onHeadersReceived(xhrRequest)).to.equal(undefined)
        })
      })
    })
    describe('and dnslink cache hit', function () {
      it('should redirect in onBeforeRequest', function () {
        // stub existence of a valid DNS record
        const fqdn = 'explore.ipld.io'
        // manually add item to cache to ensure cache hit
        dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
      })
      describe('(XHR CORS scenario)', function () {
        it('should redirect in onBeforeRequest if XHR is cross-origin and runtime is not Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          runtime.isFirefox = false
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        it('should redirect in onBeforeRequest if XHR is cross-origin and runtime is Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        it('should do nothing if DNS TXT record is missing and XHR is cross-origin in Firefox', function () {
          // stub cached info about lack of dnslink
          const fqdn = 'youtube.com'
          dnslinkResolver.setDnslink(fqdn, false)
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'https://youtube.com/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived should not change anything
          expect(modifyRequest.onHeadersReceived(xhrRequest)).to.equal(undefined)
        })
      })
    })
  })

  after(function () {
    delete global.URL
    delete global.browser
    browser.flush()
  })
})


text:
'use strict'
import { describe, it, beforeEach } from 'mocha'
import { expect } from 'chai'
import { URL } from 'url'
import { initState } from '../../../add-on/src/lib/state.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

describe('state.js', function () {
  let state

  beforeEach(function () {
    global.URL = URL
    state = Object.assign(initState(optionDefaults), { peerCount: 1 })
  })

  describe('activeIntegrations(url)', function () {
    it('should return false if input is undefined', async function () {
      expect(state.activeIntegrations(undefined)).to.equal(false)
    })
    it('should return true if host is not on the opt-out list', async function () {
      state.disabledOn = ['pl.wikipedia.org']
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(true)
    })
    it('should return true if parent host is on the opt-out list, but a direct match exist on opt-in one', async function () {
      state.disabledOn = ['wikipedia.org']
      state.enabledOn = ['en.wikipedia.org']
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(true)
    })
    it('should return false if parent host is on the opt-in list, but a direct match exist on opt-out one', async function () {
      state.disabledOn = ['en.wikipedia.org']
      state.enabledOn = ['wikipedia.org']
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return true if direct match host is on both opt-in and opt-out lists', async function () {
      state.disabledOn = ['example.com']
      state.enabledOn = ['example.com']
      const url = 'https://example.com/path'
      expect(state.activeIntegrations(url)).to.equal(true)
    })
    it('should return false if parent host is on both opt-in and opt-out lists', async function () {
      state.disabledOn = ['example.com']
      state.enabledOn = ['example.com']
      const url = 'https://subdomain.example.com/path'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return false if host is not on the opt-out list but global toggle is off', async function () {
      state.disabledOn = ['pl.wikipedia.org']
      state.active = false
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return false if host is on the opt-out list', async function () {
      state.disabledOn = ['example.com', 'pl.wikipedia.org']
      const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return false if parent host of a subdomain is on the opt-out list', async function () {
      state.disabledOn = ['wikipedia.org']
      const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
  })
})

repo_name: ipfs/ipfs-companion

repo_language: JavaScript

mime_type: text/x-diff


text:
'use strict'
import { describe, it, before, beforeEach } from 'mocha'
import browser from 'sinon-chrome'
import sinon from 'sinon'

// Units to be tested.
import {
  prepareReloadExtensions,
  InternalTabReloader,
  LocalGatewayReloader,
  WebUiReloader
} from '../../../../../add-on/src/lib/ipfs-client/reloaders/index.js'

const CUSTOM_GATEWAY_URL = {
  customGatewayUrl: 'http://127.0.0.1:8080'
}

const logger = sinon.spy()

describe('Reloader Helper Method: prepareReloadExtensions', function () {
  it('Prepares the reloader extensions', async function () {
    browser.runtime.id = 'testid'
    browser.storage.local.get.resolves(CUSTOM_GATEWAY_URL)
    browser.runtime.getURL.returns('chrome-extension://<some_id>')
    await prepareReloadExtensions([WebUiReloader, InternalTabReloader, LocalGatewayReloader], browser, logger)
    sinon.assert.callCount(logger, 3)
  })
})

describe('Reloaders', function () {
  before(() => {
    global.browser = browser
    browser.runtime.id = 'testid'
    global.chrome = {}
  })

  beforeEach(function () {
    browser.flush()
    logger.resetHistory()
  })

  describe('WebUiReloader', function () {
    let webUiReloader
    beforeEach(async function () {
      webUiReloader = new WebUiReloader(browser, logger)
      await webUiReloader.init()
    })

    it('should initialize', function () {
      sinon.assert.calledWith(logger, 'Initialized without additional config.')
    })

    it('should handle webUi reloading', function () {
      const tabs = [{
        url: 'http://some-url.com',
        id: 1
      }, {
        url: 'ipfs://some-ipfs-server.tld/webui/index.html#/',
        id: 2
      }]

      webUiReloader.reload(tabs)
      sinon.assert.calledWith(browser.tabs.reload, 2)
      sinon.assert.calledWith(logger, 'reloading webui at ipfs://some-ipfs-server.tld/webui/index.html#/')
    })
  })

  describe('InternalTabReloader', function () {
    let internalTabReloader
    beforeEach(async function () {
      browser.runtime.getURL.returns('chrome-extension://<some_id>')
      internalTabReloader = new InternalTabReloader(browser, logger)
      await internalTabReloader.init()
    })

    it('should initialize', function () {
      sinon.assert.calledWith(logger, 'InternalTabReloader Ready for use.')
    })

    it('should handle internal tab reloading', function () {
      const tabs = [{
        url: 'http://127.0.0.1:8080/ipfs/cid/wiki1',
        id: 1
      }, {
        url: 'chrome-extension://<some_id>/index.html',
        id: 2
      }]

      internalTabReloader.reload(tabs)
      sinon.assert.calledWith(browser.tabs.reload, 2)
      sinon.assert.calledWith(logger, 'reloading internal extension page at chrome-extension://<some_id>/index.html')
    })
  })

  describe('LocalGatewayReloader', function () {
    let localGatewayReloader
    beforeEach(async function () {
      browser.storage.local.get.resolves(CUSTOM_GATEWAY_URL)
      localGatewayReloader = new LocalGatewayReloader(browser, logger)
      await localGatewayReloader.init()
    })

    it('should initialize', function () {
      sinon.assert.calledWith(logger, 'Initialized without additional config.')
    })

    it('should handle local gateway tab reloading', function () {
      const tabs = [{
        title: 'Main Page',
        url: 'http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki1',
        id: 1
      }, {
        title: '127.0.0.1:8080',
        url: 'http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki2',
        id: 2
      }]

      localGatewayReloader.reload(tabs)
      sinon.assert.calledWith(browser.tabs.reload, 2)
      sinon.assert.calledWith(logger, 'reloading local gateway at http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki2')
    })
  })
})


text:
export default () => ({
  getMessage (key, substititions) {
    if (!substititions) return key
    substititions = Array.isArray(substititions) ? substititions : [substititions]
    if (!substititions.length) return key
    return `${key}[${substititions}]`
  }
})


text:
<!doctype html>

    <meta charset="utf-8" />

    Make sure the Linkify experiment is enabled in IPFS Companion before you load this page:

 Should be linkified

        /ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

/ipfs/bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy

ipfs://QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w (cidv0b58)

ipfs://bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy (cidv1b32)

ipns://en.wikipedia-on-ipfs.org

dweb:/ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

 Should NOT be linkified

ipfs:/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

ipns:/en.wikipedia-on-ipfs.org

dweb://ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

    dweb:/ipfs/Qm.. link
    relative /ipfs/Qm link
    gateway link


permissions: <all_urls> idle tabs notifications proxy storage unlimitedStorage contextMenus clipboardWrite webNavigation webRequest webRequestBlocking

IPFS Companion: DWEB Protocol Handler

protocol: web+dweb

IPFS Companion: IPNS Protocol Handler

protocol: web+ipns

IPFS Companion: IPFS Protocol Handler

protocol: web+ipfs

IPFS Companion: DWEB Protocol Handler

protocol: dweb

IPFS Companion: IPNS Protocol Handler

protocol: ipns

IPFS Companion: IPFS Protocol Handler

protocol: ipfs


text:
export interface CompanionOptions {
  active: boolean
  ipfsNodeType: string
  ipfsNodeConfig: string
  publicGatewayUrl: string
  publicSubdomainGatewayUrl: string
  useCustomGateway: boolean
  useSubdomains: boolean
  enabledOn: string[]
  disabledOn: string[]
  automaticMode: boolean
  linkify: boolean
  dnslinkPolicy: boolean | string
  dnslinkDataPreload: boolean
  dnslinkRedirect: boolean
  recoverFailedHttpRequests: boolean
  detectIpfsPathHeader: boolean
  preloadAtPublicGateway: boolean
  catchUnhandledProtocols: boolean
  displayNotifications: boolean
  displayReleaseNotes: boolean
  customGatewayUrl: string
  ipfsApiUrl: string
  ipfsApiPollMs: number
  logNamespaces: string
  importDir: string
  useLatestWebUI: boolean
  dismissedUpdate: null | string
  openViaWebUI: boolean
  telemetryGroupMinimal: boolean
  telemetryGroupPerformance: boolean
  telemetryGroupUx: boolean
  telemetryGroupFeedback: boolean
  telemetryGroupLocation: boolean
}

export interface CompanionState extends Omit<CompanionOptions, 'publicGatewayUrl' | 'publicSubdomainGatewayUrl' | 'useCustomGateway' | 'ipfsApiUrl' | 'customGatewayUrl'> {
  peerCount: number
  pubGwURL: URL
  pubGwURLString: string
  pubSubdomainGwURL: URL
  pubSubdomainGwURLString: string
  redirect: boolean
  apiURL: URL
  apiURLString: string
  gwURL: URL
  gwURLString: string
  activeIntegrations: (url: string) => boolean
  localGwAvailable: boolean
  webuiRootUrl: string
}

interface SwitchToggleArguments {
  id: string
  onchange: () => void
  checked?: boolean
  disabled?: boolean
  style?: string
}
export function SwitchToggle (args: SwitchToggleArguments): undefined | HTMLElement


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

import html from 'choo/html/index.js'

/**
 * @type {import('../../types.js').SwitchToggle}
 */
export default function switchToggle ({
  checked,
  disabled,
  id,
  onchange,
  style
}) {
  if (typeof checked === 'undefined') return
  // @ts-expect-error - TS doesn't like the `html` template tag
  return html`

          <input type="checkbox" id="${id}" onchange=${onchange} class="mdc-switch__native-control" role="switch"
            ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''}>

  `
}


text:
@import url('~@material/switch/dist/mdc.switch.css');

.mdc-switch {
  --mdc-theme-secondary: #3e9096 /* teal */
}

.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb,
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
  background-color: #7f8491; /* charcoal-muted */
  border-color: #7f8491;
}


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

import './quick-import.css'

import browser from 'webextension-polyfill'
import choo from 'choo'
import html from 'choo/html/index.js'
import logo from './logo.js'
import * as externalApiClient from '../lib/ipfs-client/external.js'
import { formatImportDirectory } from '../lib/ipfs-import.js'
import all from 'it-all'
import drop from 'drag-and-drop-files'
import { filesize } from 'filesize'

document.title = browser.i18n.getMessage('quickImport_page_title')

const app = choo()

app.use(quickImportStore)
app.route('*', quickImportPage)
app.mount('#root')

function quickImportStore (state, emitter) {
  state.message = ''
  state.peerCount = ''
  state.ipfsNodeType = 'external'
  state.expandOptions = false
  state.openViaWebUI = true
  state.userChangedOpenViaWebUI = false
  state.importDir = ''
  state.userChangedImportDir = false

  function updateState ({ ipfsNodeType, peerCount, importDir, openViaWebUI }) {
    state.ipfsNodeType = ipfsNodeType
    state.peerCount = peerCount
    // This event will fire repeatedly,
    // we need to ensure we don't unset the user's preferences
    // when they change the options on this page
    if (!state.userChangedImportDir) {
      state.importDir = importDir
    }
    if (!state.userChangedOpenViaWebUI) {
      state.openViaWebUI = openViaWebUI
    }
  }

  let port

  emitter.on('DOMContentLoaded', async () => {
    browser.runtime.sendMessage({ telemetry: { trackView: 'quick-import' } })
    // 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) {
        console.log('In browser action, received message from background:', message)
        updateState(message.statusUpdate)
        emitter.emit('render')
      }
    })
  })

  emitter.on('fileInputChange', event => processFiles(state, emitter, event.target.files))

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

  // drag & drop anywhere
  drop(document.body, files => processFiles(state, emitter, files))
}

async function processFiles (state, emitter, files) {
  console.log('Processing files', files)
  const { ipfsCompanion } = await browser.runtime.getBackgroundPage()
  try {
    console.log('importing files', files)
    if (!files.length) {
      // File list may be empty in some rare cases
      // eg. when user drags something from proprietary browser context
      // We just ignore those UI interactions.
      throw new Error('found no valid sources, try selecting a local file instead')
    }
    const {
      copyImportResultsToFiles,
      copyShareLink,
      preloadFilesAtPublicGateway,
      openFilesAtGateway,
      openFilesAtWebUI
    } = ipfsCompanion.ipfsImportHandler
    const importTab = await browser.tabs.getCurrent()
    const importDir = formatImportDirectory(state.importDir)
    const httpStreaming = ipfsCompanion.state.ipfsNodeType === 'external'

    const data = []
    let total = 0
    for (const file of files) {
      data.push({
        path: file.name,
        content: (httpStreaming ? file : file.stream())
      })
      total += file.size
    }
    const humanTotal = filesize(total, { round: 2 })
    state.progress = `Importing ${humanTotal}... (keep this page open)`
    emitter.emit('render')

    const progress = (bytes) => {
      const percent = ((bytes / total) * 100).toFixed(0)
      state.progress = `Importing... (${percent}% of ${humanTotal})`
      emitter.emit('render')
    }
    const options = {
      progress,
      wrapWithDirectory: true,
      pin: false // we use MFS for implicit pinning instead
    }

    let ipfs
    if (httpStreaming) {
      // We create separate instance of http client running in thie same page to
      // avoid serialization issues in Chromium
      // (https://bugs.chromium.org/p/chromium/issues/detail?id=112163) when
      // crossing process boundary, which enables streaming upload of big files
      // (4GB+) without buffering entire thing.
      ipfs = await externalApiClient.init(browser, ipfsCompanion.state)
      // Note: at the time of writing this it was possible to use SharedWorker,
      // but it felt brittle given Google's approach to browser extension APIs,
      // and this way seems to be more future-proof.
    } else {
      ipfs = ipfsCompanion.ipfs
    }
    // ipfs.add
    const results = await all(ipfs.addAll(data, options))

    // ipfs cp → MFS
    await copyImportResultsToFiles(results, importDir)
    state.progress = 'Completed'
    emitter.emit('render')
    console.log(`Successfully imported ${files.length} files`)

    // copy shareable URL & preload
    copyShareLink(results)
    preloadFilesAtPublicGateway(results)

    // update preferred import dir if user specified one while importing
    if (state.userChangedImportDir) {
      emitter.emit('optionChange', { key: 'importDir', value: state.importDir })
    }
    // present result to the user using the beast available way
    if (!state.openViaWebUI || state.ipfsNodeType.startsWith('embedded')) {
      await openFilesAtGateway(importDir)
    } else {
      await openFilesAtWebUI(importDir)
    }

    // close import tab as it will be replaced with a new tab with imported content
    await browser.tabs.remove(importTab.id)
  } catch (err) {
    console.error('Failed to import files to IPFS', err)
    // keep import tab and display error message in it
    state.message = 'Unable to import to IPFS:'
    state.progress = `${err}`
    emitter.emit('render')
    ipfsCompanion.notify('notify_importErrorTitle', 'notify_inlineErrorMsg', `${err.message}`)
  }
}

function quickImportOptions (state, emit) {
  const onExpandOptions = (e) => { state.expandOptions = true; emit('render') }
  const onDirectoryChange = (e) => { state.userChangedImportDir = true; state.importDir = e.target.value }
  const onOpenViaWebUIChange = (e) => { state.userChangedOpenViaWebUI = true; state.openViaWebUI = e.target.checked }
  const displayOpenWebUI = state.ipfsNodeType !== 'embedded'

  if (state.expandOptions) {
    return html`

        ${displayOpenWebUI
        ? html`
          <input id='openViaWebUI' type='checkbox' onchange=${onOpenViaWebUIChange} checked=${state.openViaWebUI} />

          ${browser.i18n.getMessage('quickImport_options_openViaWebUI')}
        `
        : null}

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

          <input id='importDir' class='w-40 bg-transparent aqua monospace br1 ba b--aqua pa2' type='text' oninput=${onDirectoryChange} value=${state.importDir} />

    `
  }
  return html`
    <button class='mt3 f6 lh-copy link bn bg-transparent moon-gray dib pa0 pointer' style='color: #6ACAD1' onclick=${onExpandOptions}>
      ${browser.i18n.getMessage('quickImport_options_show')} »
    </button>
  `
}

function quickImportPage (state, emit) {
  const onFileInputChange = (e) => emit('fileInputChange', e)
  const { peerCount } = state

  return html`

  ${logo({
    size: 80,
    path: '../../icons',
    heartbeat: false
  })}

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

              ${browser.i18n.getMessage('quickImport_subhead_peers', [peerCount])}

          <input class="db pointer w-100 h-100 top-0 o-0" type="file" id="quickImportInput" multiple onchange=${onFileInputChange} />

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

                <emph class='underline pl3 pr2 moon-gray'>
                  ${browser.i18n.getMessage('quickImport_or')}
                </emph>
                ${browser.i18n.getMessage('quickImport_drop_it_here')}

${state.message}${state.progress}

        ${quickImportOptions(state, emit)}

  `
}


text:
<!DOCTYPE html>

  <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/importPage.css">


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

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

function logoFileName (nodeType, isIpfsOnline) {
  let prefix
  if (nodeType.startsWith('embedded')) prefix = 'js-'
  if (nodeType === braveNodeType) prefix = 'brave-'
  return `${prefix || ''}ipfs-logo-${isIpfsOnline ? 'on' : 'off'}.svg`
}

export default function logo ({
  heartbeat = true,
  ipfsNodeType = 'external',
  isIpfsOnline = true,
  path,
  size = 52
}) {
  return html`
    <img
      alt="IPFS"
      src="${path}/${logoFileName(ipfsNodeType, isIpfsOnline)}"
      class="v-mid ${isIpfsOnline ? '' : 'o-40'} ${isIpfsOnline && heartbeat ? 'heartbeat' : ''}"
      style="width:${size}px; height:${size}px" />
  `
}


text:
@keyframes heartbeat {
  0% {
    transform: scale(1);
  }
  5% {
    transform: scale(1.05);
    filter: drop-shadow(0 0 1.05em rgba(95, 203, 207, 0.5));
  }
  10% {
    transform: scale(1.025);
    filter: drop-shadow(0 0 1.025em rgba(95, 203, 207, 0.25));
  }
  15% {
    transform: scale(1.075);
    filter: drop-shadow(0 0 1.075em rgba(95, 203, 207, 0.5));
  }
  50% {
    transform: scale(1);
  }
  100% {
    transform: scale(1);
  }
}

.heartbeat {
  animation-name: heartbeat;
  animation-iteration-count: infinite;
  animation-duration: 2.5s;
}


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

html, body, #root {
  height: 100%;
}

.hover-inner-shadow {
  transition: box-shadow 0.2s ease-in-out;
}
.hover-inner-shadow:hover {
  box-shadow: inset 0 0 10px 5px rgba(211, 235, 237, 0.2);
}

.no-user-select {
  user-select: none;
  -moz-user-select: none;
  -webkit-user-select: none;
}

/* Temporary CSS for custom checkbox (TODO: move/replace with ipfs-css */
input[type='checkbox'] {
  display:none;
}

input[type=checkbox] + .mark {
  background: transparent;
  height: 16px;
  width: 16px;
  display: inline-block;
  padding: 0;
  border: 1px #6ACAD1 solid;
  position: relative;
}

input[type=checkbox]:checked + .mark::after {
  color: #6ACAD1;
  position: absolute;
  left: 3px;
  top: 3px;
  content: "";
  height: 5px;
  width: 8px;
  border-left: 2px solid;
  border-bottom: 2px solid;
  transform: rotate(-45deg);
}


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

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

function statusEntry ({ label, labelLegend, title, value, check, valueClass = '' }) {
  const offline = browser.i18n.getMessage('panel_statusOffline')
  label = label ? browser.i18n.getMessage(label) : null
  labelLegend = labelLegend ? browser.i18n.getMessage(labelLegend) : label
  value = value || value === 0 ? value : offline
  return html`

        ${value.substring(0, 13)}

    `
}

export default function ipfsVersion ({
  gatewayVersion
}) {
  return html`
  ${statusEntry({
    label: 'panel_statusGatewayVersion',
    title: browser.i18n.getMessage('panel_statusGatewayVersionTitle'),
    value: gatewayVersion,
    check: gatewayVersion
  })}
  `
}


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

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

export default function navItem ({
  disabled,
  helperText,
  onClick,
  style,
  switchValue,
  text,
  title
}) {
  let buttonStyle = 'black button-reset db w-100 bg-white b--none outline-0--focus pt2 ph3 f6 tl'
  if (disabled) {
    buttonStyle += ' o-40'
  } else {
    buttonStyle += ' pointer bg-near-white--hover'
  }
  if (style) {
    buttonStyle += ` ${style}`
  }
  if (disabled) {
    title = ''
  }

  return html`

    <button class="${buttonStyle}"
            onclick=${disabled ? null : onClick}  title="${title || ''}" ${disabled ? 'disabled' : ''}>

${text}
${switchToggle({ checked: switchValue, disabled, style: 'fr ml2' })}

${helperText}

    </button>
  `
}


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

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function powerIcon ({
  action,
  active,
  size = '1.8rem',
  title
}) {
  const svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
        class="fill-current-color mr1"
        style="width:${size}; height:${size}">
        <path d="M50 20.11A29.89 29.89 0 1 0 79.89 50 29.89 29.89 0 0 0 50 20.11zm-3.22 17a3.22 3.22 0 0 1 6.44 0v6.43a3.22 3.22 0 0 1-6.44 0zM50 66.08a16.14 16.14 0 0 1-11.41-27.49 3.28 3.28 0 0 1 1.76-.65 2.48 2.48 0 0 1 2.42 2.41 2.58 2.58 0 0 1-.77 1.77A10.81 10.81 0 0 0 38.59 50a11.25 11.25 0 0 0 22.5 0 10.93 10.93 0 0 0-3.21-7.88 3.37 3.37 0 0 1-.65-1.77 2.48 2.48 0 0 1 2.42-2.41 2.16 2.16 0 0 1 1.76.65A16.14 16.14 0 0 1 50 66.08z"/>
      </svg>
    `
  return icon({ svg, title, active, action })
}


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

import html from 'choo/html/index.js'
import logo from '../logo.js'
import versionUpdateIcon from './version-update-icon.js'
import powerIcon from './power-icon.js'
import optionsIcon from './options-icon.js'
import ipfsVersion from './ipfs-version.js'
import gatewayStatus from './gateway-status.js'

export default function header (props) {
  const { ipfsNodeType, active, onToggleActive, onOpenPrefs, onOpenReleaseNotes, isIpfsOnline, onOpenWelcomePage, newVersion } = props
  return html`

  ${logo({
    size: 54,
    path: '../../../icons',
    ipfsNodeType,
    isIpfsOnline: (active && isIpfsOnline)
  })}

                IPFS

            ${ipfsVersion(props)}

          ${newVersion
          ? versionUpdateIcon({
            newVersion,
            active,
            title: 'panel_headerNewVersionTitle',
            action: onOpenReleaseNotes
          })
          : null}
          ${powerIcon({
            active,
            title: 'panel_headerActiveToggleTitle',
            action: onToggleActive
          })}
          ${optionsIcon({
            active,
            title: 'panel_openPreferences',
            action: onOpenPrefs
          })}

        ${gatewayStatus(props)}

  `
}


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

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

export default function icon ({
  svg, title, active, action, className
}) {
  return html`
    <button class="header-icon pa0 ma0 dib bn bg-transparent transition-all ${className} ${action ? 'pointer' : null} ${active ? 'aqua' : 'gray'}"
      style="outline:none;"
      title="${browser.i18n.getMessage(title) || title}"
      onclick=${action}>
      ${svg}
    </button>
  `
}


text:
<!DOCTYPE html>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <link rel="stylesheet" href="/dist/bundles/uiCommons.css">
    <link rel="stylesheet" href="/dist/bundles/browserAction.css">


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

import html from 'choo/html/index.js'
import header from './header.js'
import { activeTabActions } from './context-actions.js'
import tools from './tools.js'

// Render the browser action 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 browserActionPage (state, emit) {
  const onViewOnGateway = () => emit('viewOnGateway')
  const onCopy = (copyAction) => emit('copy', copyAction)
  const onFilesCpImport = () => emit('filesCpImport')

  const onQuickImport = () => emit('quickImport')
  const onOpenWebUi = () => emit('openWebUi', '/')
  const onOpenWelcomePage = () => emit('openWelcomePage')
  const onOpenPrefs = () => emit('openPrefs')
  const onOpenReleaseNotes = () => emit('openReleaseNotes')
  const onToggleGlobalRedirect = () => emit('toggleGlobalRedirect')
  const onToggleSiteIntegrations = () => emit('toggleSiteIntegrations')
  const onToggleActive = () => emit('toggleActive')

  const headerProps = Object.assign({ onToggleActive, onOpenPrefs, onOpenReleaseNotes, onOpenWelcomePage }, state)
  const activeTabActionsProps = Object.assign({ onViewOnGateway, onToggleSiteIntegrations, onCopy, onFilesCpImport }, state)
  const opsProps = Object.assign({ onQuickImport, onOpenWebUi, onToggleGlobalRedirect }, state)

  return html`

        ${header(headerProps)}
        ${tools(opsProps)}

      ${activeTabActions(activeTabActionsProps)}

  `
}


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

import './browser-action.css'

import choo from 'choo'
import browserActionPage from './page.js'
import browserActionStore from './store.js'

const app = choo()

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

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

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


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

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function optionsIcon ({
  action,
  active,
  size = '1.8rem',
  title
}) {
  const svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
        class="fill-current-color"
        style="width:${size}; height:${size}">
        <path d="M74.05 50.23c-.07-3.58 1.86-5.85 5.11-7.1-.2-2-2.48-7.45-3.63-8.76-3.11 1.46-6.06 1.23-8.54-1.22s-2.72-5.46-1.26-8.64a29.24 29.24 0 0 0-8.8-3.63c-1.06 3.08-3.12 5-6.35 5.25-3.82.29-6.29-1.69-7.61-5.22a30.11 30.11 0 0 0-8.77 3.67c1.5 3.16 1.3 6.1-1.15 8.6s-5.45 2.76-8.64 1.29a29.33 29.33 0 0 0-3.58 8.79C24 44.43 25.94 46.62 26 50s-1.82 5.84-5.1 7.12a29.21 29.21 0 0 0 3.68 8.71c3.09-1.38 6-1.15 8.42 1.22s2.79 5.33 1.41 8.49a29.72 29.72 0 0 0 8.76 3.57 1.46 1.46 0 0 0 .11-.21 7.19 7.19 0 0 1 13.53-.16c.13.33.28.32.55.25a29.64 29.64 0 0 0 8-3.3 4 4 0 0 0 .37-.25c-1.27-2.86-1.15-5.57.88-7.94 2.44-2.84 5.5-3.26 8.91-1.8a29.23 29.23 0 0 0 3.65-8.7c-3.17-1.22-5.05-3.38-5.12-6.77zM50 59.54a8.57 8.57 0 1 1 8.59-8.31A8.58 8.58 0 0 1 50 59.54z"/>
      </svg>
    `
  return icon({ svg, title, active, action })
}


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

import html from 'choo/html/index.js'

export default function toolsButton ({
  disabled,
  iconD,
  iconSize,
  onClick,
  style,
  text,
  title
}) {
  let buttonStyle = 'header-icon fade-in w-50 ba bw1 snow b--snow bg-transparent f7 ph1 pv0 br4 ma1 flex justify-center items-center truncate'
  if (disabled) {
    buttonStyle += ' o-60'
  } else {
    buttonStyle += ' pointer'
  }
  if (style) {
    buttonStyle += ` ${style}`
  }
  if (disabled) {
    title = ''
  }

  return html`

      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="mr1" width="${iconSize}" height="${iconSize}"><path fill="currentColor" d="${iconD}"/></svg>

${text}

  `
}


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

.bg-near-white--hover:hover {
  background-color: #F4F4F4;
}

.header-icon:active {
  color: #edf0f4;
  transform: translateY(2px);
}
.header-icon[disabled],
.header-icon[disabled]:active {
  cursor: not-allowed;
  pointer-events: none;
  transform: none;
}

.outline-0--focus:focus {
  outline: 0;
}

.no-user-select {
  -webkit-user-select: none; /* Old Chrome, Safari */
  -moz-user-select: none;    /* Firefox */
  -ms-user-select: none;     /* Internet Explorer/Edge */
  user-select: none;         /* Non-prefixed version, Chrome and Opera */
}

.force-select-all {
  -webkit-user-select: all;  /* Chrome 49+ */
  -moz-user-select: all;     /* Firefox 43+ */
  -ms-user-select: all;      /* No support yet */
  user-select: all;          /* Likely future */
}

.fade-in {
  animation: fade-in 600ms;
}
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.blink {
  animation: blink 1s linear infinite;
}

@keyframes blink {
  50% {
    opacity: 0;
  }
}


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

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function versionUpdateIcon ({
  action,
  active,
  className,
  newVersion,
  size = '1.8rem',
  title
}) {
  let svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
        class="fill-yellow-muted mr1"
        style="width:${size}; height:${size}">
        <path xmlns="http://www.w3.org/2000/svg" d="M71.13 28.87a29.88 29.88 0 100 42.26 29.86 29.86 0 000-42.26zm-18.39 37.6h-5.48V44.71h5.48zm0-26.53h-5.48v-5.49h5.48z"/>
      </svg>
    `
  // special handling for beta and dev builds
  // TODO: remove when we have no users on beta channel anymore
  if (newVersion.match(/\./g).length > 2) {
    svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
          class="fill-red-muted mr1"
          style="width:${size}; height:${size}">
          <path 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>
    `
    title = 'Beta channel is deprecated, please switch to regular releases'
    className = `${className} blink`
  }
  return icon({ svg, title, active, action, className })
}


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

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import navItem from './nav-item.js'
import navHeader from './nav-header.js'
import { sameGateway } from '../../lib/ipfs-path.js'
import { formatImportDirectory } from '../../lib/ipfs-import.js'
import {
  contextMenuViewOnGateway,
  contextMenuCopyAddressAtPublicGw,
  contextMenuCopyPermalink,
  contextMenuCopyRawCid,
  contextMenuCopyCanonicalAddress,
  contextMenuCopyCidAddress
} from '../../lib/context-menus.js'

const notReady = browser.i18n.getMessage('panelCopy_notReadyHint')

// Context Actions are displayed in Browser Action and Page Action (FF only)
export function contextActions ({
  active,
  redirect,
  isRedirectContext,
  pubGwURLString,
  gwURLString,
  currentTab,
  currentFqdn,
  currentDnslinkFqdn,
  currentTabIntegrationsOptOut,
  currentTabContentPath = notReady,
  currentTabImmutablePath = notReady,
  currentTabCid = notReady,
  currentTabPublicUrl = notReady,
  currentTabPermalink = notReady,
  ipfsNodeType,
  isIpfsContext,
  isIpfsOnline,
  isApiAvailable,
  onToggleSiteIntegrations,
  onViewOnGateway,
  onCopy,
  importDir,
  onFilesCpImport
}) {
  const activeCidResolver = active && isIpfsOnline && isApiAvailable && currentTabCid
  const activeFilesCpImport = active && isIpfsOnline && isApiAvailable && !ipfsNodeType.startsWith('embedded')
  const isMutable = currentTabContentPath.startsWith('/ipns/')
  const activeViewOnGateway = (currentTab) => {
    if (!currentTab) return false
    const { url } = currentTab
    return !(url.startsWith('ip') || sameGateway(url, gwURLString) || sameGateway(url, pubGwURLString))
  }
  const renderIpfsContextItems = () => {
    if (!isIpfsContext) return
    return html`

  ${activeViewOnGateway(currentTab)
    ? navItem({
      text: browser.i18n.getMessage(contextMenuViewOnGateway),
      onClick: () => onViewOnGateway(contextMenuViewOnGateway)
    })
    : null}
  ${navItem({
    text: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw),
    title: browser.i18n.getMessage('panel_copyCurrentPublicGwUrlTooltip'),
    helperText: currentTabPublicUrl,
    onClick: () => onCopy(contextMenuCopyAddressAtPublicGw)
  })}
  ${isMutable
    ? navItem({
      text: browser.i18n.getMessage(contextMenuCopyPermalink),
      title: browser.i18n.getMessage('panel_copyCurrentPermalinkTooltip'),
      helperText: currentTabPermalink,
      onClick: () => onCopy(contextMenuCopyPermalink)
    })
    : ''}
  ${isMutable
    ? navItem({
      text: browser.i18n.getMessage(contextMenuCopyCanonicalAddress),
      title: browser.i18n.getMessage('panelCopy_currentIpnsAddressTooltip'),
      helperText: currentTabContentPath,
      onClick: () => onCopy(contextMenuCopyCanonicalAddress)
    })
    : ''}
  ${navItem({
    text: browser.i18n.getMessage(contextMenuCopyCidAddress),
    title: browser.i18n.getMessage('panelCopy_currentIpfsAddressTooltip'),
    helperText: currentTabImmutablePath,
    onClick: () => onCopy(contextMenuCopyCidAddress)
  })}
  ${navItem({
    text: browser.i18n.getMessage(contextMenuCopyRawCid),
    title: browser.i18n.getMessage('panelCopy_copyRawCidTooltip'),
    helperText: currentTabCid,
    disabled: !activeCidResolver,
    onClick: () => onCopy(contextMenuCopyRawCid)
  })}
  ${navItem({
    text: browser.i18n.getMessage('panel_importCurrentIpfsAddress'),
    title: browser.i18n.getMessage('panel_importCurrentIpfsAddressTooltip'),
    helperText: formatImportDirectory(importDir),
    disabled: !activeFilesCpImport,
    onClick: onFilesCpImport
  })}

    `
  }
  const renderSiteIntegrationsToggle = () => {
    if (!isRedirectContext) return
    return html`
  ${navItem({
    text: browser.i18n.getMessage('panel_activeTabSiteIntegrationsToggle', currentFqdn),
    title: browser.i18n.getMessage('panel_activeTabSiteIntegrationsToggleTooltip', currentFqdn),
    style: 'truncate',
    disabled: !(active),
    switchValue: active && !currentTabIntegrationsOptOut,
    onClick: onToggleSiteIntegrations
  })}
      `
  }
  return html`

  ${renderSiteIntegrationsToggle()}
  ${renderIpfsContextItems()}

  `
}

// "Active Tab" section is displayed in Browser Action  only
// if redirect can be toggled or current tab has any IPFS Context Actions
export function activeTabActions (state) {
  const showActiveTabSection = (state.isRedirectContext) || state.isIpfsContext
  if (!showActiveTabSection) return
  return html`

      ${navHeader('panel_activeTabSectionHeader')}

        ${contextActions(state)}

  `
}


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

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function redirectIcon ({
  action,
  active,
  size = '2rem',
  title
}) {
  const svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"
        class="fill-current-color"
        style="width:${size}; height:${size}">
        <path d="M75.4 41.6l-7.3-11.2c-.1-.2-.4-.2-.5 0l-7.3 11.3c-.1.2 0 .4.2.4h4.3l-.2 20.6c0 3.2-2.6 5.8-5.8 5.8h-.2a5.8 5.8 0 0 1-5.8-5.8V37.3a11.8 11.8 0 0 0-23.6 0L29 58.7h-4.3c-.2 0-.4.3-.3.5l7.3 11.6c.1.2.4.2.6 0l7.3-11.6c.1-.2 0-.5-.3-.5h-4.2l.2-21.5a5.8 5.8 0 0 1 11.6 0v25.3c0 6.5 5.3 11.8 11.8 11.8h.2c6.5 0 11.8-5.3 11.8-11.8l.2-20.4h4.4c.4 0 .2-.4.1-.5z"/>
      </svg>
    `
  return icon({
    svg,
    title,
    active,
    action
  })
}


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

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

export default function tools ({
  active,
  ipfsNodeType,
  isApiAvailable,
  isIpfsOnline,
  onOpenWebUi,
  onQuickImport
}) {
  const activeQuickImport = active && isIpfsOnline && isApiAvailable
  const activeWebUI = active && isIpfsOnline && ipfsNodeType !== 'embedded'

  return html`
    <div class="flex pb2 ph2 justify-between">
  ${toolsButton({
    text: browser.i18n.getMessage('panel_quickImport'),
    title: browser.i18n.getMessage('panel_quickImportTooltip'),
    disabled: !activeQuickImport,
    onClick: onQuickImport,
    iconSize: 20,
    iconD: 'M71.13 28.87a29.88 29.88 0 100 42.26 29.86 29.86 0 000-42.26zm-18.39 37.6h-5.48V52.74H33.53v-5.48h13.73V33.53h5.48v13.73h13.73v5.48H52.74z'
  })}
  ${toolsButton({
    text: browser.i18n.getMessage('panel_openWebui'),
    title: browser.i18n.getMessage('panel_openWebuiTooltip'),
    disabled: !activeWebUI,
    onClick: onOpenWebUi,
    iconSize: 18,
    iconD: 'M69.69 20.57c-.51-.51-1.06-1-1.62-1.47l-.16-.1c-.56-.46-1.15-.9-1.76-1.32l-.5-.35c-.25-.17-.52-.32-.79-.48A28.27 28.27 0 0050 12.23h-.69a28.33 28.33 0 00-27.52 28.36c0 13.54 19.06 37.68 26 46a3.21 3.21 0 005 0c6.82-8.32 25.46-32.25 25.46-45.84a28.13 28.13 0 00-8.56-20.18zM51.07 49.51a9.12 9.12 0 119.13-9.12 9.12 9.12 0 01-9.13 9.12z'
  })}
    </div>
  `
}


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

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

export default function navHeader (label) {
  return html`
    <div class="no-select w-100 outline-0--focus tl ph3 pt2 mt1 pb1 o-40 f6">
      ${browser.i18n.getMessage(label)}
    </div>
  `
}


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

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

function statusEntry ({ label, labelLegend, value, check, itemClass = '', valueClass = '' }) {
  const offline = browser.i18n.getMessage('panel_statusOffline')
  label = label ? browser.i18n.getMessage(label) : null
  labelLegend = labelLegend ? browser.i18n.getMessage(labelLegend) : label
  value = value || value === 0 ? value : offline
  return html`

        ${label}
        ${value}

    `
}

export default function gatewayStatus ({
  gatewayAddress,
  gatewayVersion,
  ipfsApiUrl,
  ipfsNodeType,
  swarmPeers
}) {
  const api = ipfsApiUrl && ipfsNodeType === 'embedded' ? 'js-ipfs' : ipfsApiUrl
  return html`

    ${statusEntry({
      label: 'panel_statusSwarmPeers',
      labelLegend: 'panel_statusSwarmPeersTitle',
      value: swarmPeers,
      check: swarmPeers
    })}
    ${statusEntry({
      label: 'panel_statusGatewayAddress',
      labelLegend: 'panel_statusGatewayAddressTitle',
      value: gatewayAddress,
      check: gatewayAddress
    })}
    ${statusEntry({
      label: 'panel_statusApiAddress',
      labelLegend: 'panel_statusApiAddressTitle',
      value: api,
      check: gatewayVersion
    })}

  `
}


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()
}


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)