OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / test / catalog-manager-safety.test.js

302 lines · javascript ref:
import test from 'node:test'
import assert from 'node:assert/strict'
import safetyMod from '../backend/catalog-safety.cjs'

const { aggregateCatalogApps, catalogAppSearchText, catalogAppStableKey, normalizeAppType, normalizeCatalogApp, normalizeCatalogData, safeJSONParse, sanitizePersonalCatalogEntry, searchAppsList } = safetyMod
const key = (ch) => ch.repeat(64)

test('safe catalog JSON parse strips prototype-pollution keys recursively', () => {
  const parsed = safeJSONParse(`{
    "name": "Bad Catalog",
    "__proto__": { "polluted": true },
    "constructor": { "prototype": { "polluted": true } },
    "apps": [
      {
        "name": "App",
        "__proto__": { "polluted": true },
        "nested": {
          "constructor": { "prototype": { "polluted": true } },
          "rows": [{ "prototype": { "polluted": true }, "ok": true }]
        }
      }
    ]
  }`)

  assert.equal(Object.prototype.hasOwnProperty.call(parsed, '__proto__'), false)
  assert.equal(Object.prototype.hasOwnProperty.call(parsed, 'constructor'), false)
  assert.equal(Object.prototype.hasOwnProperty.call(parsed.apps[0], '__proto__'), false)
  assert.equal(Object.prototype.hasOwnProperty.call(parsed.apps[0].nested, 'constructor'), false)
  assert.equal(Object.prototype.hasOwnProperty.call(parsed.apps[0].nested.rows[0], 'prototype'), false)
  assert.equal(parsed.apps[0].nested.rows[0].ok, true)
  assert.equal({}.polluted, undefined)
})

test('catalog app search tolerates empty and non-string queries', () => {
  const apps = [
    { id: 'one', name: 'PearBrowser', description: 'P2P browser', categories: ['Tools'], catalogName: 'Pear Apps', source: 'sheets', author: 'Holepunch' },
    { id: 'two', name: 'HiveRelay', description: 'Relay infrastructure', version: '2.1.0', verification: 'relay-listed' }
  ]

  assert.equal(searchAppsList(apps, '').length, 2)
  assert.equal(searchAppsList(apps, null).length, 2)
  assert.deepEqual(searchAppsList(apps, 'relay').map((a) => a.id), ['two'])
  assert.deepEqual(searchAppsList(apps, 'tools').map((a) => a.id), ['one'])
  assert.deepEqual(searchAppsList(apps, 'pear apps').map((a) => a.id), ['one'])
  assert.deepEqual(searchAppsList(apps, '2.1').map((a) => a.id), ['two'])
  assert.ok(catalogAppSearchText(apps[0]).includes('holepunch'))
})

test('catalog JSON normalizer accepts relay items and entries envelopes', () => {
  const itemKey = 'a'.repeat(64)
  const entryKey = 'b'.repeat(64)

  const gatewayCatalog = normalizeCatalogData(safeJSONParse(`{
    "version": 2,
    "name": "HiveRelay Content Catalog",
    "items": [
      {
        "appKey": "${itemKey}",
        "type": "drive",
        "name": "Relay Site",
        "description": "Pinned public drive",
        "__proto__": { "polluted": true }
      }
    ]
  }`))

  assert.equal(gatewayCatalog.apps.length, 1)
  assert.equal(gatewayCatalog.apps[0].driveKey, itemKey)
  assert.equal(gatewayCatalog.apps[0].id, itemKey)
  assert.equal(Object.prototype.hasOwnProperty.call(gatewayCatalog.apps[0], '__proto__'), false)
  assert.equal({}.polluted, undefined)

  const legacyCatalog = normalizeCatalogData({
    name: 'Legacy Relay',
    entries: [
      { key: entryKey, name: 'Legacy Entry' }
    ]
  })

  assert.equal(legacyCatalog.apps.length, 1)
  assert.equal(legacyCatalog.apps[0].driveKey, entryKey)
  assert.equal(legacyCatalog.apps[0].id, entryKey)
})

test('catalog app normalizer drops unsafe and executable targets', () => {
  const driveKey = 'c'.repeat(64)

  assert.equal(normalizeCatalogApp({ id: 'bad-key', driveKey: 'not-a-key', name: 'Bad key' }), null)
  assert.equal(normalizeCatalogApp({ id: 'bad-link', link: 'javascript:alert(1)', name: 'Bad link' }), null)
  assert.equal(normalizeCatalogApp({ id: 'targetless', name: 'No target' }), null)

  assert.equal(normalizeCatalogApp({ id: 'linked', driveKey: 'not-a-key', link: 'pear://keet', name: 'Keet' }), null)

  const hyper = normalizeCatalogApp({ link: `hyper://${driveKey}/app`, name: 'Site' })
  assert.equal(hyper.driveKey, driveKey)
  assert.equal(hyper.link, `hyper://${driveKey}/app`)
})

test('catalog app normalizer accepts explicit Pear v3 native delivery without treating it as tab content', () => {
  const installLink = `pear://${'a'.repeat(52)}`
  const app = normalizeCatalogApp({
    id: 'native-demo',
    name: 'Native Demo',
    verification: 'author-signed',
    nativeDelivery: {
      status: 'available',
      kind: 'pear-v3',
      installLink: installLink.toUpperCase(),
      productName: 'Native Demo',
      targets: ['darwin-arm64', 'linux-x64', '../bad']
    }
  })

  assert.equal(app.link, undefined)
  assert.deepEqual(app.nativeDelivery, {
    status: 'available',
    kind: 'pear-v3',
    installLink,
    productName: 'Native Demo',
    targets: ['darwin-arm64', 'linux-x64']
  })
  assert.equal(catalogAppStableKey(app), `native:${installLink}`)
  assert.ok(catalogAppSearchText(app).includes('linux-x64'))

  const migrated = normalizeCatalogApp({
    id: 'native-with-legacy-link',
    name: 'Migrated Native Demo',
    link: `pear://${'b'.repeat(52)}`,
    nativeDelivery: app.nativeDelivery
  })
  assert.equal(migrated.link, undefined)
  assert.equal(migrated.nativeDelivery.installLink, installLink)

  assert.equal(normalizeCatalogApp({
    name: 'Missing v3 metadata',
    nativeDelivery: { status: 'available', kind: 'pear-v3', installLink: 'pear://keet' }
  }), null)
})

test('shared catalog app normalizer builds stable target keys', () => {
  const driveKey = key('d')
  const app = normalizeCatalogApp({
    id: 'custom-id',
    name: '  Demo  ',
    appKey: driveKey.toUpperCase(),
    link: ` hyper://${driveKey}/demo `,
    version: 2,
    categories: [' tools ', '', 'p2p']
  }, { source: 'hiveindex', catalogKey: 'hiveindex:cat', catalogName: 'Relay Index' })

  assert.equal(app.id, 'custom-id')
  assert.equal(app.name, 'Demo')
  assert.equal(app.driveKey, driveKey)
  assert.equal(app.link, `hyper://${driveKey}/demo`)
  assert.equal(app.version, '2')
  assert.deepEqual(app.categories, ['tools', 'p2p'])
  assert.equal(app.verification, 'unverified')
  assert.equal(app.source, 'hiveindex')
  assert.equal(catalogAppStableKey(app), `drive:${driveKey}`)

  assert.equal(catalogAppStableKey({ link: ` hyper://${key('e')}/demo ` }), `drive:${key('e')}`)
  assert.equal(catalogAppStableKey({ id: 'only-id' }), 'id:only-id')
  assert.equal(catalogAppStableKey({ id: 'site', link: `hyper://${driveKey}/app` }), `drive:${driveKey}`)
})

test('catalog app normalizer keeps only supported launch types', () => {
  assert.equal(normalizeAppType(' standalone '), 'standalone')
  assert.equal(normalizeAppType('HYPERSITE'), 'hypersite')
  assert.equal(normalizeAppType('desktop'), '')

  const standalone = normalizeCatalogApp({ name: 'Hyper App', type: 'standalone', link: `hyper://${key('f')}/app` })
  assert.equal(standalone.type, 'standalone')

  const invalid = normalizeCatalogApp({ name: 'Bad Type App', type: 'desktop', link: `hyper://${key('e')}/app` })
  assert.equal(invalid.type, undefined)
})

test('personal catalog entry sanitizer accepts link-only apps and rejects targetless rows', () => {
  assert.deepEqual(sanitizePersonalCatalogEntry({
    id: 'keet',
    name: ' Keet ',
    link: ` hyper://${key('c')}/keet `,
    categories: [' chat ', '']
  }), {
    id: 'keet',
    name: 'Keet',
    description: '',
    driveKey: key('c'),
    link: `hyper://${key('c')}/keet`,
    version: '',
    author: '',
    categories: ['chat']
  })

  assert.equal(sanitizePersonalCatalogEntry({ id: 'site', link: `hyper://${key('a')}/app` }).driveKey, key('a'))
  assert.throws(() => sanitizePersonalCatalogEntry({ id: 'empty', name: 'No target' }), /valid 64-hex drive key/)
})

test('backend aggregation dedupes by stable target across all catalog sources', () => {
  const driveKey = key('b')
  const otherDriveKey = key('e')
  const sameIdFirstDriveKey = key('f')
  const catalogs = new Map()

  catalogs.set('drive-cat', {
    drive: {},
    data: {
      name: 'Hyperdrive Cat',
      apps: [
        { id: 'drive-old', name: 'Drive Old', driveKey: driveKey.toUpperCase(), version: '9.0.0', verification: 'unverified', icon: '/old.png' },
        { id: 'same-id', name: 'First Same Id', driveKey: sameIdFirstDriveKey, version: '1.0.0' }
      ]
    }
  })
  catalogs.set('bee:cat', {
    type: 'hyperbee',
    data: {
      name: 'Hyperbee Cat',
      apps: [
        { id: 'drive-newer', name: 'Drive Newer', driveKey, version: '10.0.0', verification: 'unverified', iconData: 'data:image/png;base64,aaa' }
      ]
    }
  })
  catalogs.set('autobee:cat', {
    type: 'autobee',
    data: {
      name: 'Autobee Cat',
      apps: [
        { id: 'drive-signed', name: 'Drive Signed', driveKey, version: '1.0.0', verification: 'author-signed' }
      ]
    }
  })
  catalogs.set('sheets:cat', {
    type: 'sheets',
    data: {
      name: 'Sheets Cat',
      apps: [
        { id: 'link-low', name: 'Link Low', link: `hyper://${key('1')}/same`, version: '1.0.0', verification: 'relay-listed' }
      ]
    }
  })
  catalogs.set('hiveindex:cat', {
    type: 'hiveindex',
    data: {
      name: 'Hiveindex Cat',
      apps: [
        { id: 'link-high', name: 'Link High', link: `hyper://${key('1')}/same`, version: '2.0.0', verification: 'relay-listed' },
        { id: 'same-id', name: 'Second Same Id Different Drive', driveKey: otherDriveKey, version: '1.0.0' }
      ]
    }
  })

  const apps = aggregateCatalogApps(catalogs)
  assert.equal(apps.length, 4)

  const byName = Object.fromEntries(apps.map((app) => [app.name, app]))
  assert.equal(byName['Drive Signed'].driveKey, driveKey)
  assert.equal(byName['Drive Signed'].verification, 'author-signed')
  assert.equal(byName['Drive Signed'].icon, '/old.png')
  assert.equal(byName['Drive Signed'].iconData, 'data:image/png;base64,aaa')
  assert.deepEqual(byName['Drive Signed']._sources.sort(), ['Autobee Cat', 'Hyperbee Cat', 'Hyperdrive Cat'])

  assert.equal(byName['Link High'].link, `hyper://${key('1')}/same`)
  assert.equal(byName['Link High'].catalogKey, 'hiveindex:cat')
  assert.deepEqual(byName['Link High']._sources.sort(), ['Hiveindex Cat', 'Sheets Cat'])

  assert.equal(byName['First Same Id'].driveKey, sameIdFirstDriveKey)
  assert.equal(byName['Second Same Id Different Drive'].driveKey, otherDriveKey)
})

test('backend aggregation collapses link-only duplicates with different catalog ids', () => {
  const catalogs = new Map()

  catalogs.set('bee:signed', {
    type: 'hyperbee',
    data: {
      name: 'Signed Catalog',
      apps: [
        { id: 'signed-row', name: 'Signed Link App', link: `hyper://${key('2')}/shared-app`, type: 'hypersite', version: '1.0.0', verification: 'author-signed' }
      ]
    }
  })
  catalogs.set('sheets:community', {
    type: 'sheets',
    data: {
      name: 'Community Catalog',
      apps: [
        { id: 'sheet-row-uuid', name: 'Community Link App', link: `hyper://${key('2')}/shared-app`, type: 'hypersite', version: '9.0.0', verification: 'unverified' }
      ]
    }
  })

  const apps = aggregateCatalogApps(catalogs)
  assert.equal(apps.length, 1)
  assert.equal(apps[0].id, 'signed-row')
  assert.equal(apps[0].name, 'Signed Link App')
  assert.equal(apps[0].link, `hyper://${key('2')}/shared-app`)
  assert.equal(apps[0].type, 'hypersite')
  assert.equal(apps[0].verification, 'author-signed')
  assert.deepEqual(apps[0]._sources.sort(), ['Community Catalog', 'Signed Catalog'])
})