diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0bc3b42 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "10:00" + open-pull-requests-limit: 10 + commit-message: + prefix: "deps" + prefix-development: "deps(dev)" diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..d57c2a0 --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,8 @@ +name: Automerge +on: [ pull_request ] + +jobs: + automerge: + uses: protocol/.github/.github/workflows/automerge.yml@master + with: + job: 'automerge' diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml new file mode 100644 index 0000000..d155996 --- /dev/null +++ b/.github/workflows/js-test-and-release.yml @@ -0,0 +1,145 @@ +name: test & maybe release +on: + push: + branches: + - master # with #262 - ${{{ github.default_branch }}} + pull_request: + branches: + - master # with #262 - ${{{ github.default_branch }}} + +jobs: + + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present lint + - run: npm run --if-present dep-check + + test-node: + needs: check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: [16] + fail-fast: true + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:node + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + flags: node + + test-chrome: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + flags: chrome + + test-chrome-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome-webworker + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + flags: chrome-webworker + + test-firefox: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + flags: firefox + + test-firefox-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox-webworker + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + flags: firefox-webworker + + test-electron-main: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-main + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + flags: electron-main + + test-electron-renderer: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-renderer + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + flags: electron-renderer + + release: + needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-electron-main, test-electron-renderer] + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' # with #262 - 'refs/heads/${{{ github.default_branch }}}' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - uses: ipfs/aegir/actions/docker-login@master + with: + docker-token: ${{ secrets.DOCKER_TOKEN }} + docker-username: ${{ secrets.DOCKER_USERNAME }} + - run: npm run --if-present release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e6e7b53..0000000 --- a/.travis.yml +++ /dev/null @@ -1,58 +0,0 @@ -language: node_js -cache: npm -dist: bionic -stages: - - check - - test - - cov - -branches: - only: - - master - - /^release\/.*$/ - -node_js: - - 'lts/*' - - 'node' - -os: - - linux - - osx - - windows - -script: npx aegir test -t node --cov --bail -after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov - -jobs: - include: - - stage: check - script: - - npx aegir dep-check - - npm run lint - - - stage: test - name: chrome - addons: - chrome: stable - script: npx aegir test -t browser -t webworker - - - stage: test - name: firefox - addons: - firefox: latest - script: npx aegir test -t browser -t webworker -- --browser firefox - - - stage: test - name: electron-main - os: osx - script: - - npx aegir test -t electron-main --bail - - - stage: test - name: electron-renderer - os: osx - script: - - npx aegir test -t electron-renderer --bail - -notifications: - email: false diff --git a/LICENSE b/LICENSE index 7d37874..20ce483 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,4 @@ -The MIT License (MIT) +This project is dual licensed under MIT and Apache-2.0. -Copyright (c) 2018 Protocol Labs, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..14478a3 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..72dc60d --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 61bd232..c2c48f2 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,52 @@ -is-ipfs 🕵️ -==== - -[![](https://img.shields.io/github/release/ipfs/is-ipfs.svg)](https://github.com/ipfs/is-ipfs/releases/latest) -[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](https://webchat.freenode.net/?channels=%23ipfs) - -> A set of utilities to help identify [IPFS](https://ipfs.io/) resources +# is-ipfs + +[![codecov](https://img.shields.io/codecov/c/github/ipfs-shipyard/is-ipfs.svg?style=flat-square)](https://codecov.io/gh/ipfs-shipyard/is-ipfs) +[![CI](https://img.shields.io/github/workflow/status/ipfs-shipyard/is-ipfs/test%20&%20maybe%20release/master?style=flat-square)](https://github.com/ipfs-shipyard/is-ipfs/actions/workflows/js-test-and-release.yml) + +> A set of utilities to help identify IPFS resources on the web + +## Table of contents + +- - [Install](#install) + - [Lead Maintainer](#lead-maintainer) + - [Browser: Browserify, Webpack, other bundlers](#browser-browserify-webpack-other-bundlers) + - [In the Browser through ` @@ -38,8 +70,9 @@ Loading this module through a script tag will make the ```IsIpfs``` obj availabl ``` # Usage + ```javascript -const isIPFS = require('is-ipfs') +const isIPFS from 'is-ipfs') isIPFS.multihash('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true isIPFS.multihash('noop') // false @@ -125,8 +158,9 @@ isIPFS.peerMultiaddr('/ip4/127.0.0.1/udp/1234') // false (key missing) A suite of util methods that provides efficient validation. Detection of IPFS Paths and identifiers in URLs is a two-stage process: -1. `pathPattern`/`pathGatewayPattern`/`subdomainGatewayPattern` regex is applied to quickly identify potential candidates -2. proper CID validation is applied to remove false-positives + +1. `pathPattern`/`pathGatewayPattern`/`subdomainGatewayPattern` regex is applied to quickly identify potential candidates +2. proper CID validation is applied to remove false-positives ## Content Identifiers @@ -180,7 +214,6 @@ Returns `true` if the provided string is a valid IPNS path or `false` otherwise. Returns `true` if the provided string is a valid "CID path" (IPFS path without `/ipfs/` prefix) or `false` otherwise. - ## Subdomains Validated subdomain convention: `cidv1b32.ip(f|n)s.domain.tld` @@ -206,7 +239,6 @@ return false-positives: - To ensure IPNS record exists, make a call to `/api/v0/name/resolve?arg=` - To ensure DNSLink exists, make a call to `/api/v0/dns?arg=` - ## Multiaddrs Below methods provide basic detection of [multiaddr](https://github.com/multiformats/multiaddr)s: composable and future-proof network addresses. @@ -221,6 +253,13 @@ Returns `true` if the provided `string`, [`Multiaddr`](https://github.com/multif Returns `true` if the provided `string`, [`Multiaddr`](https://github.com/multiformats/js-multiaddr) or `Uint8Array` represents a valid libp2p peer multiaddr (matching [`P2P` format from `mafmt`](https://github.com/multiformats/js-mafmt#api)) or `false` otherwise. -# License +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute -MIT +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/package.json b/package.json index ba940cb..831ba6f 100644 --- a/package.json +++ b/package.json @@ -2,72 +2,158 @@ "name": "is-ipfs", "version": "6.0.2", "description": "A set of utilities to help identify IPFS resources on the web", + "author": "Francisco Dias (http://franciscodias.net/)", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs-shipyard/is-ipfs#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs-shipyard/is-ipfs.git" + }, + "bugs": { + "url": "https://github.com/ipfs-shipyard/is-ipfs/issues" + }, "keywords": [ - "js-ipfs", - "ipns", - "gateway", "dnslink", - "ipfs" + "gateway", + "ipfs", + "ipns", + "js-ipfs" ], - "homepage": "https://github.com/ipfs/is-ipfs", - "bugs": { - "url": "https://github.com/ipfs/is-ipfs/issues" + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" }, - "license": "MIT", - "author": "Francisco Dias (http://franciscodias.net/)", - "leadMaintainer": "Marcin Rataj ", + "type": "module", + "types": "./dist/src/index.d.ts", "files": [ "src", - "dist" + "dist/src", + "!dist/test", + "!**/*.tsbuildinfo" ], - "types": "./dist/src/index.d.ts", - "main": "src/index.js", - "browser": { - "fs": false + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } }, - "repository": { - "type": "git", - "url": "https://github.com/ipfs/is-ipfs.git" + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "master" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] }, "scripts": { - "prepare": "aegir build --no-bundle", - "test:node": "aegir test --target node", - "test:browser": "aegir test --target browser", + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "generate": "protons src/pb/peer.proto src/pb/tags.proto", + "build": "aegir build", "test": "aegir test", - "prepublishOnly": "aegir build", - "lint": "aegir ts -p check && aegir lint-package-json && aegir lint", - "release": "aegir release", - "release-minor": "aegir release --type minor", - "release-major": "aegir release --type major" + "test:chrome": "aegir test -t browser", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" }, "dependencies": { + "@multiformats/mafmt": "^11.0.3", + "@multiformats/multiaddr": "^11.0.0", "iso-url": "^1.1.3", - "mafmt": "^10.0.0", - "multiaddr": "^10.0.0", "multiformats": "^9.0.0", "uint8arrays": "^3.0.0" }, "devDependencies": { - "aegir": "^35.0.2", - "pre-commit": "^1.2.2", - "util": "^0.12.4" - }, - "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" + "aegir": "^37.5.3" }, - "pre-commit": [ - "test", - "lint" - ], - "contributors": [ - "Marcin Rataj ", - "Francisco Baio Dias ", - "David Dias ", - "Alan Shaw ", - "Alex Potsides ", - "nginnever ", - "Hugo Dias ", - "Henrique Dias " - ] + "browser": { + "fs": false + } } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 64d1e3f..0000000 --- a/src/index.js +++ /dev/null @@ -1,263 +0,0 @@ -'use strict' - -const { base58btc } = require('multiformats/bases/base58') -const { base32 } = require('multiformats/bases/base32') -const Digest = require('multiformats/hashes/digest') -const { Multiaddr } = require('multiaddr') -const mafmt = require('mafmt') -const { CID } = require('multiformats/cid') -const { URL } = require('iso-url') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') - -const pathGatewayPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/ -const pathPattern = /^\/(ip[fn]s)\/([^/?#]+)/ -const defaultProtocolMatch = 1 -const defaultHashMath = 2 - -// CID, libp2p-key or DNSLink -const subdomainGatewayPattern = /^https?:\/\/([^/]+)\.(ip[fn]s)\.[^/?]+/ -const subdomainIdMatch = 1 -const subdomainProtocolMatch = 2 - -// Fully qualified domain name (FQDN) that has an explicit .tld suffix -const fqdnWithTld = /^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/ - -/** - * @param {*} hash - */ -function isMultihash (hash) { - const formatted = convertToString(hash) - try { - Digest.decode(base58btc.decode('z' + formatted)) - } catch { - return false - } - - return true -} - -/** - * @param {*} hash - */ -function isBase32EncodedMultibase (hash) { - try { - base32.decode(hash) - } catch { - return false - } - - return true -} - -/** - * @param {*} hash - */ -function isCID (hash) { - try { - if (typeof hash === 'string') { - return Boolean(CID.parse(hash)) - } - - if (hash instanceof Uint8Array) { - return Boolean(CID.decode(hash)) - } - - return Boolean(CID.asCID(hash)) // eslint-disable-line no-new - } catch (e) { - return false - } -} - -/** - * @param {*} input - */ -function isMultiaddr (input) { - if (!input) return false - if (Multiaddr.isMultiaddr(input)) return true - try { - new Multiaddr(input) // eslint-disable-line no-new - return true - } catch (e) { - return false - } -} - -/** - * @param {string | Uint8Array | Multiaddr} input - */ -function isPeerMultiaddr (input) { - return isMultiaddr(input) && mafmt.P2P.matches(input) -} - -/** - * @param {string | Uint8Array} input - * @param {RegExp | string} pattern - * @param {number} [protocolMatch=1] - * @param {number} [hashMatch=2] - */ -function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch = defaultHashMath) { - const formatted = convertToString(input) - if (!formatted) { - return false - } - - const match = formatted.match(pattern) - if (!match) { - return false - } - - if (match[protocolMatch] !== 'ipfs') { - return false - } - - let hash = match[hashMatch] - - if (hash && pattern === subdomainGatewayPattern) { - // when doing checks for subdomain context - // ensure hash is case-insensitive - // (browsers force-lowercase authority component anyway) - hash = hash.toLowerCase() - } - - return isCID(hash) -} - -/** - * - * @param {string | Uint8Array} input - * @param {string | RegExp} pattern - * @param {number} [protocolMatch=1] - * @param {number} [hashMatch=1] - */ -function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch = defaultHashMath) { - const formatted = convertToString(input) - if (!formatted) { - return false - } - const match = formatted.match(pattern) - if (!match) { - return false - } - - if (match[protocolMatch] !== 'ipns') { - return false - } - - let ipnsId = match[hashMatch] - - if (ipnsId && pattern === subdomainGatewayPattern) { - // when doing checks for subdomain context - // ensure ipnsId is case-insensitive - // (browsers force-lowercase authority compotent anyway) - ipnsId = ipnsId.toLowerCase() - // Check if it is cidv1 - if (isCID(ipnsId)) return true - // Check if it looks like FQDN - try { - if (!ipnsId.includes('.') && ipnsId.includes('-')) { - // name without tld, assuming its inlined into a single DNS label - // (https://github.com/ipfs/in-web-browsers/issues/169) - // en-wikipedia--on--ipfs-org → en.wikipedia-on-ipfs.org - ipnsId = ipnsId.replace(/--/g, '@').replace(/-/g, '.').replace(/@/g, '-') - } - // URL implementation in web browsers forces lowercase of the hostname - const { hostname } = new URL(`http://${ipnsId}`) // eslint-disable-line no-new - // Check if potential FQDN has an explicit TLD - return fqdnWithTld.test(hostname) - } catch (e) { - return false - } - } - - return true -} - -/** - * @param {any} input - */ -function isString (input) { - return typeof input === 'string' -} - -/** - * @param {Uint8Array | string} input - */ -function convertToString (input) { - if (input instanceof Uint8Array) { - return uint8ArrayToString(input, 'base58btc') - } - - if (isString(input)) { - return input - } - - return false -} - -/** - * @param {string | Uint8Array} url - */ -const ipfsSubdomain = (url) => isIpfs(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) -/** - * @param {string | Uint8Array} url - */ -const ipnsSubdomain = (url) => isIpns(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) -/** - * @param {string | Uint8Array} url - */ -const subdomain = (url) => ipfsSubdomain(url) || ipnsSubdomain(url) - -/** - * @param {string | Uint8Array} url - */ -const ipfsUrl = (url) => isIpfs(url, pathGatewayPattern) || ipfsSubdomain(url) -/** - * @param {string | Uint8Array} url - */ -const ipnsUrl = (url) => isIpns(url, pathGatewayPattern) || ipnsSubdomain(url) -/** - * @param {string | Uint8Array} url - */ -const url = (url) => ipfsUrl(url) || ipnsUrl(url) || subdomain(url) - -/** - * @param {string | Uint8Array} path - */ -const path = (path) => isIpfs(path, pathPattern) || isIpns(path, pathPattern) - -module.exports = { - multihash: isMultihash, - multiaddr: isMultiaddr, - peerMultiaddr: isPeerMultiaddr, - cid: isCID, - /** - * @param {CID | string | Uint8Array} cid - */ - base32cid: (cid) => (isBase32EncodedMultibase(cid) && isCID(cid)), - ipfsSubdomain, - ipnsSubdomain, - subdomain, - subdomainGatewayPattern, - ipfsUrl, - ipnsUrl, - url, - pathGatewayPattern: pathGatewayPattern, - /** - * @param {string | Uint8Array} path - */ - ipfsPath: (path) => isIpfs(path, pathPattern), - /** - * @param {string | Uint8Array} path - */ - ipnsPath: (path) => isIpns(path, pathPattern), - path, - pathPattern, - /** - * @param {string | Uint8Array} x - */ - urlOrPath: (x) => url(x) || path(x), - /** - * @param {string | Uint8Array | CID} path - */ - cidPath: path => isString(path) && !isCID(path) && isIpfs(`/ipfs/${path}`, pathPattern) -} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0af544a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,215 @@ +import { base58btc } from 'multiformats/bases/base58' +import { base32 } from 'multiformats/bases/base32' +import * as Digest from 'multiformats/hashes/digest' +import { multiaddr } from '@multiformats/multiaddr' +import type { Multiaddr } from '@multiformats/multiaddr' +import * as mafmt from '@multiformats/mafmt' +import { CID } from 'multiformats/cid' +import { URL } from 'iso-url' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' + +export const pathGatewayPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/ +export const pathPattern = /^\/(ip[fn]s)\/([^/?#]+)/ +const defaultProtocolMatch = 1 +const defaultHashMath = 2 + +// CID, libp2p-key or DNSLink +export const subdomainGatewayPattern = /^https?:\/\/([^/]+)\.(ip[fn]s)\.[^/?]+/ +const subdomainIdMatch = 1 +const subdomainProtocolMatch = 2 + +// Fully qualified domain name (FQDN) that has an explicit .tld suffix +const fqdnWithTld = /^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/ + +function isMultihash (hash: Uint8Array | string): boolean { + const formatted = convertToString(hash) + + if (formatted === false) { + return false + } + + try { + Digest.decode(base58btc.decode('z' + formatted)) + } catch { + return false + } + + return true +} + +function isMultiaddr (input: string | Uint8Array | Multiaddr): input is Multiaddr { + try { + return Boolean(multiaddr(input)) + } catch { + return false + } +} + +function isBase32EncodedMultibase (hash: CID | string | Uint8Array): boolean { + try { + let cid: CID | null + + if (isString(hash)) { + cid = CID.parse(hash) + } else { + cid = CID.asCID(hash) + } + + if (cid == null) { + return false + } + + base32.decode(cid.toString()) + } catch { + return false + } + + return true +} + +function isCID (hash: CID | Uint8Array | string): hash is CID { + try { + if (isString(hash)) { + return Boolean(CID.parse(hash)) + } + + if (hash instanceof Uint8Array) { + return Boolean(CID.decode(hash)) + } + + return Boolean(CID.asCID(hash)) // eslint-disable-line no-new + } catch { + return false + } +} + +/** + * @param {string | Uint8Array | Multiaddr} input + */ +function isPeerMultiaddr (input: string | Uint8Array | Multiaddr): boolean { + return isMultiaddr(input) && mafmt.P2P.matches(input) +} + +/** + * @param {string | Uint8Array} input + * @param {RegExp | string} pattern + * @param {number} [protocolMatch=1] + * @param {number} [hashMatch=2] + */ +function isIpfs (input: string | Uint8Array, pattern: RegExp | string, protocolMatch: number = defaultProtocolMatch, hashMatch: number = defaultHashMath) { + const formatted = convertToString(input) + if (formatted === false) { + return false + } + + const match = formatted.match(pattern) + if (match == null) { + return false + } + + if (match[protocolMatch] !== 'ipfs') { + return false + } + + let hash = match[hashMatch] + + if (hash != null && pattern === subdomainGatewayPattern) { + // when doing checks for subdomain context + // ensure hash is case-insensitive + // (browsers force-lowercase authority component anyway) + hash = hash.toLowerCase() + } + + return isCID(hash) +} + +/** + * + * @param {string | Uint8Array} input + * @param {string | RegExp} pattern + * @param {number} [protocolMatch=1] + * @param {number} [hashMatch=1] + */ +function isIpns (input: string | Uint8Array, pattern: RegExp | string, protocolMatch: number = defaultProtocolMatch, hashMatch: number = defaultHashMath) { + const formatted = convertToString(input) + if (formatted === false) { + return false + } + const match = formatted.match(pattern) + if (match == null) { + return false + } + + if (match[protocolMatch] !== 'ipns') { + return false + } + + let ipnsId = match[hashMatch] + + if (ipnsId != null && pattern === subdomainGatewayPattern) { + // when doing checks for subdomain context + // ensure ipnsId is case-insensitive + // (browsers force-lowercase authority compotent anyway) + ipnsId = ipnsId.toLowerCase() + // Check if it is cidv1 + if (isCID(ipnsId)) return true + // Check if it looks like FQDN + try { + if (!ipnsId.includes('.') && ipnsId.includes('-')) { + // name without tld, assuming its inlined into a single DNS label + // (https://github.com/ipfs/in-web-browsers/issues/169) + // en-wikipedia--on--ipfs-org → en.wikipedia-on-ipfs.org + ipnsId = ipnsId.replace(/--/g, '@').replace(/-/g, '.').replace(/@/g, '-') + } + // URL implementation in web browsers forces lowercase of the hostname + const { hostname } = new URL(`http://${ipnsId}`) // eslint-disable-line no-new + // Check if potential FQDN has an explicit TLD + return fqdnWithTld.test(hostname) + } catch (e) { + return false + } + } + + return true +} + +/** + * @param {any} input + */ +function isString (input: any): input is string { + return typeof input === 'string' +} + +/** + * @param {Uint8Array | string} input + */ +function convertToString (input: Uint8Array | string) { + if (input instanceof Uint8Array) { + return uint8ArrayToString(input, 'base58btc') + } + + if (isString(input)) { + return input + } + + return false +} + +export const ipfsSubdomain = (url: string | Uint8Array) => isIpfs(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) +export const ipnsSubdomain = (url: string | Uint8Array) => isIpns(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) +export const subdomain = (url: string | Uint8Array) => ipfsSubdomain(url) || ipnsSubdomain(url) +export const ipfsUrl = (url: string | Uint8Array) => isIpfs(url, pathGatewayPattern) || ipfsSubdomain(url) +export const ipnsUrl = (url: string | Uint8Array) => isIpns(url, pathGatewayPattern) || ipnsSubdomain(url) +export const url = (url: string | Uint8Array) => ipfsUrl(url) || ipnsUrl(url) || subdomain(url) +export const path = (path: string | Uint8Array) => isIpfs(path, pathPattern) || isIpns(path, pathPattern) + +export { isMultihash as multihash } +export { isMultiaddr as multiaddr } +export { isPeerMultiaddr as peerMultiaddr } +export { isCID as cid } + +export const base32cid = (cid: CID | string | Uint8Array) => (isCID(cid) && isBase32EncodedMultibase(cid)) +export const ipfsPath = (path: string | Uint8Array) => isIpfs(path, pathPattern) +export const ipnsPath = (path: string | Uint8Array) => isIpns(path, pathPattern) +export const urlOrPath = (x: string | Uint8Array) => url(x) || path(x) +export const cidPath = (path: string | Uint8Array | CID) => isString(path) && !isCID(path) && isIpfs(`/ipfs/${path}`, pathPattern) diff --git a/test/test-cid.spec.js b/test/test-cid.spec.ts similarity index 92% rename from test/test-cid.spec.js rename to test/test-cid.spec.ts index 91ec443..5fbd281 100644 --- a/test/test-cid.spec.js +++ b/test/test-cid.spec.ts @@ -1,10 +1,9 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const isIPFS = require('../src/index') -const { CID } = require('multiformats/cid') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +import { expect } from 'aegir/chai' +import * as isIPFS from '../src/index.js' +import { CID } from 'multiformats/cid' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' describe('ipfs cid', () => { it('isIPFS.cid should match a valid CID instance', (done) => { @@ -63,6 +62,7 @@ describe('ipfs cid', () => { }) it('isIPFS.cid should not match an invalid CID data type', (done) => { + // @ts-expect-error invalid input const actual = isIPFS.cid(4) expect(actual).to.equal(false) done() @@ -101,7 +101,7 @@ describe('ipfs base32cid', () => { }) it('isIPFS.base32cid should not match an invalid CID data type', (done) => { - // @ts-ignore data type is invalid + // @ts-expect-error data type is invalid const actual = isIPFS.base32cid(4) expect(actual).to.equal(false) done() diff --git a/test/test-multiaddr.spec.js b/test/test-multiaddr.spec.ts similarity index 93% rename from test/test-multiaddr.spec.js rename to test/test-multiaddr.spec.ts index f1ccb7d..6d6d554 100644 --- a/test/test-multiaddr.spec.js +++ b/test/test-multiaddr.spec.ts @@ -1,10 +1,9 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const isIPFS = require('../src/index') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +import { expect } from 'aegir/chai' +import { multiaddr } from '@multiformats/multiaddr' +import * as isIPFS from '../src/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' describe('ipfs multiaddr', () => { it('isIPFS.multiaddr should match a string with valid ip4 multiaddr', (done) => { @@ -32,14 +31,14 @@ describe('ipfs multiaddr', () => { }) it('isIPFS.multiaddr should match a valid Multiaddr instance', (done) => { - const ma = new Multiaddr('/ip6/::1/udp/1234/http') + const ma = multiaddr('/ip6/::1/udp/1234/http') const actual = isIPFS.multiaddr(ma) expect(actual).to.equal(true) done() }) it('isIPFS.multiaddr should match a Uint8Array with multiaddr', (done) => { - const ma = new Multiaddr('/ip6/::1/udp/1234/http') + const ma = multiaddr('/ip6/::1/udp/1234/http') const actual = isIPFS.multiaddr(ma.bytes) expect(actual).to.equal(true) done() @@ -70,6 +69,7 @@ describe('ipfs multiaddr', () => { }) it('isIPFS.multiaddr should not match an invalid multiaddr data type', (done) => { + // @ts-expect-error invalid input const actual = isIPFS.multiaddr(4) expect(actual).to.equal(false) done() @@ -117,7 +117,7 @@ describe('ipfs peerMultiaddr', () => { it('isIPFS.peerMultiaddr should match a valid Multiaddr instance', (done) => { for (const addr of validPeerMultiaddrs) { - const ma = new Multiaddr(addr) + const ma = multiaddr(addr) const actual = isIPFS.peerMultiaddr(ma) expect(actual, `isIPFS.peerMultiaddr(${addr})`).to.equal(true) } @@ -126,7 +126,7 @@ describe('ipfs peerMultiaddr', () => { it('isIPFS.peerMultiaddr should match a Uint8Array with multiaddr', (done) => { for (const addr of validPeerMultiaddrs) { - const ma = new Multiaddr(addr) + const ma = multiaddr(addr) const actual = isIPFS.peerMultiaddr(ma.bytes) expect(actual, `isIPFS.peerMultiaddr(${addr})`).to.equal(true) } @@ -165,7 +165,7 @@ describe('ipfs peerMultiaddr', () => { }) it('isIPFS.peerMultiaddr should not match an invalid multiaddr data type', (done) => { - // @ts-ignore data type is invalid + // @ts-expect-error data type is invalid const actual = isIPFS.peerMultiaddr(4) expect(actual).to.equal(false) done() diff --git a/test/test-multihash.spec.js b/test/test-multihash.spec.ts similarity index 87% rename from test/test-multihash.spec.js rename to test/test-multihash.spec.ts index d46525b..a01160e 100644 --- a/test/test-multihash.spec.js +++ b/test/test-multihash.spec.ts @@ -1,9 +1,8 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const isIPFS = require('../src/index') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +import { expect } from 'aegir/chai' +import * as isIPFS from '../src/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' describe('ipfs multihash', () => { it('isIPFS.multihash should match a valid multihash', (done) => { @@ -37,6 +36,7 @@ describe('ipfs multihash', () => { }) it('isIPFS.multihash should not match an invalid multihash data type', (done) => { + // @ts-expect-error invalid input const actual = isIPFS.multihash(4) expect(actual).to.equal(false) done() diff --git a/test/test-path.spec.js b/test/test-path.spec.ts similarity index 96% rename from test/test-path.spec.js rename to test/test-path.spec.ts index 2aa74d8..7cd9d96 100644 --- a/test/test-path.spec.js +++ b/test/test-path.spec.ts @@ -1,9 +1,8 @@ /* eslint-env mocha */ -'use strict' -const isIPFS = require('../src/index') -const { expect } = require('aegir/utils/chai') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +import * as isIPFS from '../src/index.js' +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' describe('ipfs path', () => { it('isIPFS.ipfsPath should match an ipfs path', (done) => { @@ -148,7 +147,7 @@ describe('ipfs path', () => { }) it('isIPFS.cidPath should not match a non string', () => { - // @ts-ignore data type is invalid + // @ts-expect-error data type is invalid const actual = isIPFS.cidPath({ toString: () => 'QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm/path/to/file' }) expect(actual).to.equal(false) }) diff --git a/test/test-subdomain.spec.js b/test/test-subdomain.spec.ts similarity index 97% rename from test/test-subdomain.spec.js rename to test/test-subdomain.spec.ts index c7bdae4..0846b44 100644 --- a/test/test-subdomain.spec.js +++ b/test/test-subdomain.spec.ts @@ -1,9 +1,8 @@ /* eslint-env mocha */ -'use strict' -const isIPFS = require('../src/index') -const { expect } = require('aegir/utils/chai') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +import * as isIPFS from '../src/index.js' +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' describe('ipfs subdomain', () => { it('isIPFS.ipfsSubdomain should match a cidv1b32', (done) => { diff --git a/test/test-url.spec.js b/test/test-url.spec.ts similarity index 94% rename from test/test-url.spec.js rename to test/test-url.spec.ts index d45acff..9329057 100644 --- a/test/test-url.spec.js +++ b/test/test-url.spec.ts @@ -1,9 +1,8 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const isIPFS = require('../src/index') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +import { expect } from 'aegir/chai' +import * as isIPFS from '../src/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' describe('ipfs url', () => { it('isIPFS.ipfsUrl should match an ipfs url', (done) => {