From 1260a2d67edc8077f3ecaf434fe587d751f133cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:20:21 +0000 Subject: [PATCH 1/2] deps(dev): bump aegir from 37.12.1 to 42.2.3 Bumps [aegir](https://github.com/ipfs/aegir) from 37.12.1 to 42.2.3. - [Release notes](https://github.com/ipfs/aegir/releases) - [Changelog](https://github.com/ipfs/aegir/blob/master/CHANGELOG.md) - [Commits](https://github.com/ipfs/aegir/compare/v37.12.1...v42.2.3) --- updated-dependencies: - dependency-name: aegir dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71538a3..57226c3 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "uint8arrays": "^4.0.2" }, "devDependencies": { - "aegir": "^37.10.1" + "aegir": "^42.2.3" }, "browser": { "fs": false From eec623758731a77054d146f865f3b9fe489fbf92 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 6 Feb 2024 16:38:09 +0100 Subject: [PATCH 2/2] chore: update project and linting --- .github/dependabot.yml | 2 +- .github/workflows/js-test-and-release.yml | 2 + .github/workflows/semantic-pull-request.yml | 12 ++ .github/workflows/stale.yml | 13 ++ .gitignore | 46 +--- .npmignore | 1 - README.md | 160 ++------------ package.json | 27 +-- src/index.ts | 222 ++++++++++++++++++-- test/test-cid.spec.ts | 2 +- test/test-multiaddr.spec.ts | 4 +- test/test-multihash.spec.ts | 2 +- test/test-path.spec.ts | 2 +- test/test-subdomain.spec.ts | 2 +- test/test-url.spec.ts | 2 +- typedoc.json | 5 + 16 files changed, 280 insertions(+), 224 deletions(-) create mode 100644 .github/workflows/semantic-pull-request.yml create mode 100644 .github/workflows/stale.yml delete mode 100644 .npmignore create mode 100644 typedoc.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0bc3b42..d401a77 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ updates: schedule: interval: daily time: "10:00" - open-pull-requests-limit: 10 + open-pull-requests-limit: 20 commit-message: prefix: "deps" prefix-development: "deps(dev)" diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml index 2c7a14b..359eb97 100644 --- a/.github/workflows/js-test-and-release.yml +++ b/.github/workflows/js-test-and-release.yml @@ -9,7 +9,9 @@ on: permissions: contents: write + id-token: write packages: write + pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml new file mode 100644 index 0000000..bd00f09 --- /dev/null +++ b/.github/workflows/semantic-pull-request.yml @@ -0,0 +1,12 @@ +name: Semantic PR + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..16d65d7 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,13 @@ +name: Close and mark stale issue + +on: + schedule: + - cron: '0 0 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3 diff --git a/.gitignore b/.gitignore index 1fbd791..7ad9e67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,9 @@ -package-lock.json -yarn.lock - -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - +build dist -lib -docs +.docs +.coverage +node_modules +package-lock.json +yarn.lock +.vscode diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 1b26874..0000000 --- a/.npmignore +++ /dev/null @@ -1 +0,0 @@ -test-*.js diff --git a/README.md b/README.md index fa24c75..5133f14 100644 --- a/README.md +++ b/README.md @@ -5,54 +5,18 @@ > A set of utilities to help identify IPFS resources on the web -## Table of contents - -- [Install](#install) - - [Browser ` -``` +1. `pathPattern`/`pathGatewayPattern`/`subdomainGatewayPattern` regex is applied to quickly identify potential candidates +2. proper CID validation is applied to remove false-positives -## Usage +## Example -```javascript +```TypeScript import * as isIPFS from 'is-ipfs' isIPFS.multihash('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true @@ -134,117 +98,31 @@ isIPFS.peerMultiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQ isIPFS.peerMultiaddr('/ip4/127.0.0.1/udp/1234') // false (key missing) ``` -## API - -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 - -### Content Identifiers - -#### `isIPFS.multihash(hash)` - -Returns `true` if the provided string or `Uint8Array` is a valid `multihash` or `false` otherwise. - -#### `isIPFS.cid(hash)` - -Returns `true` if the provided string, `Uint8Array` or [`CID`](https://github.com/multiformats/js-multiformats/#readme) object represents a valid [CID](https://docs.ipfs.io/guides/concepts/cid/) or `false` otherwise. - -#### `isIPFS.base32cid(hash)` - -Returns `true` if the provided string is a valid `CID` in Base32 encoding or `false` otherwise. - -### URLs - -#### `isIPFS.url(url)` - -Returns `true` if the provided string is a valid IPFS or IPNS url or `false` otherwise. - -#### `isIPFS.ipfsUrl(url)` - -Returns `true` if the provided string is a valid IPFS url or `false` otherwise. - -#### `isIPFS.ipnsUrl(url)` - -Returns `true` if the provided string is a valid IPNS url or `false` otherwise. +# Install -### Paths - -Standalone validation of IPFS Paths: `/ip(f|n)s//..` - -#### `isIPFS.path(path)` - -Returns `true` if the provided string is a valid IPFS or IPNS path or `false` otherwise. - -#### `isIPFS.urlOrPath(path)` - -Returns `true` if the provided string is a valid IPFS or IPNS url or path or `false` otherwise. - -#### `isIPFS.ipfsPath(path)` - -Returns `true` if the provided string is a valid IPFS path or `false` otherwise. - -#### `isIPFS.ipnsPath(path)` - -Returns `true` if the provided string is a valid IPNS path or `false` otherwise. - -#### `isIPFS.cidPath(path)` - -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` - -#### `isIPFS.subdomain(url)` - -Returns `true` if the provided `url` string includes a valid IPFS, looks like IPNS/DNSLink subdomain or `false` otherwise. - -#### `isIPFS.ipfsSubdomain(url)` - -Returns `true` if the provided `url` string includes a valid IPFS subdomain (case-insensitive CIDv1) or `false` otherwise. - -#### `isIPFS.ipnsSubdomain(url)` - -Returns `true` if the provided `url` string looks like a valid IPNS subdomain -(CIDv1 with `libp2p-key` multicodec or something that looks like a FQDN, for example `en.wikipedia-on-ipfs.org.ipns.localhost:8080`) or `false` -otherwise. - -**Note:** `ipnsSubdomain` method works in offline mode: it does not perform -actual IPNS record lookup over DHT or other content routing method. It may -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. - -Complex validation of multiaddr can be built using `isIPFS.multiaddr` and [`mafmt`](https://github.com/multiformats/js-mafmt) library. - -#### `isIPFS.multiaddr(addr)` +```console +$ npm i is-ipfs +``` -Returns `true` if the provided `string`, [`Multiaddr`](https://github.com/multiformats/js-multiaddr) or `Uint8Array` represents a valid multiaddr or `false` otherwise. +## Browser ` +``` -## API Docs +# API Docs - -## License +# License Licensed under either of - Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) - MIT ([LICENSE-MIT](LICENSE-MIT) / ) -## Contribution +# Contribution 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 5874458..8d59b63 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "bugs": { "url": "https://github.com/ipfs-shipyard/is-ipfs/issues" }, + "publishConfig": { + "access": "public", + "provenance": true + }, "keywords": [ "dnslink", "gateway", @@ -19,10 +23,6 @@ "ipns", "js-ipfs" ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, "type": "module", "types": "./dist/src/index.d.ts", "files": [ @@ -40,6 +40,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -129,18 +130,16 @@ ] }, "scripts": { - "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", - "test:chrome": "aegir test -t browser", + "test:node": "aegir test -t node --cov", + "test:chrome": "aegir test -t browser --cov", "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", + "lint": "aegir lint", + "dep-check": "aegir dep-check", "release": "aegir release", "docs": "aegir docs" }, @@ -154,7 +153,9 @@ "devDependencies": { "aegir": "^42.2.3" }, - "browser": { - "fs": false - } + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "sideEffects": false } diff --git a/src/index.ts b/src/index.ts index 0af544a..0d7b9be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,107 @@ -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' +/** + * @packageDocumentation + * + * 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 + * + * @example + * + * ```TypeScript + * import * as isIPFS from 'is-ipfs' + * + * isIPFS.multihash('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true + * isIPFS.multihash('noop') // false + * + * isIPFS.cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true (CIDv0) + * isIPFS.cid('bafybeiasb5vpmaounyilfuxbd3lryvosl4yefqrfahsb2esg46q6tu6y5q') // true (CIDv1 in Base32) + * isIPFS.cid('zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ7') // true (CIDv1 in Base58btc) + * isIPFS.cid('noop') // false + * + * isIPFS.base32cid('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va') // true + * isIPFS.base32cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false + * + * isIPFS.url('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true + * isIPFS.url('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?filename=guardian.jpg') // true + * isIPFS.url('https://ipfs.io/ipns/github.com') // true + * isIPFS.url('https://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true + * isIPFS.url('http://en.wikipedia-on-ipfs.org.ipfs.localhost:8080') // true + * isIPFS.url('https://github.com/ipfs/js-ipfs/blob/master/README.md') // false + * isIPFS.url('https://google.com') // false + * + * isIPFS.path('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true + * isIPFS.path('/ipfs/QmbcBPAwCDxRMB1Qe7CRQmxdrTSkxKwM9y6rZw2FjGtbsb/?weird-filename=test.jpg') // true + * isIPFS.path('/ipns/github.com') // true + * isIPFS.path('/ipfs/js-ipfs/blob/master/README.md') // false + * + * isIPFS.urlOrPath('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true + * isIPFS.urlOrPath('https://ipfs.io/ipns/github.com') // true + * isIPFS.urlOrPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true + * isIPFS.urlOrPath('/ipns/github.com') // true + * isIPFS.urlOrPath('https://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true + * isIPFS.urlOrPath('https://google.com') // false + * + * isIPFS.ipfsUrl('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true + * isIPFS.ipfsUrl('https://ipfs.io/ipfs/invalid-hash') // false + * + * isIPFS.ipnsUrl('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false + * isIPFS.ipnsUrl('https://ipfs.io/ipns/github.com') // true + * + * isIPFS.ipfsPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true + * isIPFS.ipfsPath('/ipfs/invalid-hash') // false + * + * isIPFS.ipnsPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false + * isIPFS.ipnsPath('/ipns/github.com') // true + * + * isIPFS.cidPath('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o/path/to/file') // true + * isIPFS.cidPath('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o/') // true + * isIPFS.cidPath('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false + * isIPFS.cidPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false + * isIPFS.cidPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o/file') // false + * + * isIPFS.subdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true + * isIPFS.subdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // true + * isIPFS.subdomain('http://www.bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // false + * isIPFS.subdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.dweb.link') // false + * + * isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true + * isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.dweb.link') // false + * + * isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // true + * isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.dweb.link') // false + * isIPFS.ipnsSubdomain('http://QmcNioXSC1bfJj1dcFErhUfyjFzoX2HodkRccsFFVJJvg8.ipns.dweb.link') // false + * isIPFS.ipnsSubdomain('http://en.wikipedia-on-ipfs.org.ipns.localhost:8080') // true (assuming DNSLink) + * isIPFS.ipnsSubdomain('http://en-wikipedia--on--ipfs-org.ipns.localhost:8080') // true (assuming inlined DNSLink) + * isIPFS.ipnsSubdomain('http://hostname-without-tld-.ipns.dweb.link') // false (not a CID, invalid DNS label) + * + * isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234') // true + * isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234/http') // true + * isIPFS.multiaddr('/ip6/::1/udp/1234') // true + * isIPFS.multiaddr('ip6/::1/udp/1234') // false + * isIPFS.multiaddr('/yoloinvalid/::1/udp/1234') // false + * + * isIPFS.peerMultiaddr('/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') // true + * isIPFS.peerMultiaddr('/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') // true (legacy notation) + * isIPFS.peerMultiaddr('/ip4/127.0.0.1/tcp/1234/ws/p2p/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj') // true + * isIPFS.peerMultiaddr('/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4/p2p-circuit/p2p/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj') // true + * isIPFS.peerMultiaddr('/dnsaddr/bootstrap.libp2p.io') // false (key missing, needs additional DNS lookup to tell if this is valid) + * isIPFS.peerMultiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN') // true (key present, ip and port can be resolved later) + * isIPFS.peerMultiaddr('/ip4/127.0.0.1/udp/1234') // false (key missing) + * ``` + */ + import * as mafmt from '@multiformats/mafmt' -import { CID } from 'multiformats/cid' +import { multiaddr } from '@multiformats/multiaddr' import { URL } from 'iso-url' +import { base32 } from 'multiformats/bases/base32' +import { base58btc } from 'multiformats/bases/base58' +import { CID } from 'multiformats/cid' +import * as Digest from 'multiformats/hashes/digest' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Multiaddr } from '@multiformats/multiaddr' export const pathGatewayPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/ export const pathPattern = /^\/(ip[fn]s)\/([^/?#]+)/ @@ -29,7 +124,7 @@ function isMultihash (hash: Uint8Array | string): boolean { } try { - Digest.decode(base58btc.decode('z' + formatted)) + Digest.decode(base58btc.decode(`z${formatted}`)) } catch { return false } @@ -96,7 +191,7 @@ function isPeerMultiaddr (input: string | Uint8Array | Multiaddr): boolean { * @param {number} [protocolMatch=1] * @param {number} [hashMatch=2] */ -function isIpfs (input: string | Uint8Array, pattern: RegExp | string, protocolMatch: number = defaultProtocolMatch, hashMatch: number = defaultHashMath) { +function isIpfs (input: string | Uint8Array, pattern: RegExp | string, protocolMatch: number = defaultProtocolMatch, hashMatch: number = defaultHashMath): boolean { const formatted = convertToString(input) if (formatted === false) { return false @@ -130,7 +225,7 @@ function isIpfs (input: string | Uint8Array, pattern: RegExp | string, protocolM * @param {number} [protocolMatch=1] * @param {number} [hashMatch=1] */ -function isIpns (input: string | Uint8Array, pattern: RegExp | string, protocolMatch: number = defaultProtocolMatch, hashMatch: number = defaultHashMath) { +function isIpns (input: string | Uint8Array, pattern: RegExp | string, protocolMatch: number = defaultProtocolMatch, hashMatch: number = defaultHashMath): boolean { const formatted = convertToString(input) if (formatted === false) { return false @@ -183,7 +278,7 @@ function isString (input: any): input is string { /** * @param {Uint8Array | string} input */ -function convertToString (input: Uint8Array | string) { +function convertToString (input: Uint8Array | string): string | false { if (input instanceof Uint8Array) { return uint8ArrayToString(input, 'base58btc') } @@ -195,21 +290,104 @@ function convertToString (input: Uint8Array | string) { 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) +/** + * Returns `true` if the provided `url` string includes a valid IPFS subdomain + * (case-insensitive CIDv1) or `false` otherwise. + */ +export const ipfsSubdomain = (url: string | Uint8Array): boolean => isIpfs(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) + +/** + * Returns `true` if the provided `url` string looks like a valid IPNS subdomain + * (CIDv1 with `libp2p-key` multicodec or something that looks like a FQDN, for + * example `en.wikipedia-on-ipfs.org.ipns.localhost:8080`) or `false` otherwise. + * + * **Note:** `ipnsSubdomain` method works in offline mode: it does not perform + * actual IPNS record lookup over DHT or other content routing method. It may + * 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=` + */ +export const ipnsSubdomain = (url: string | Uint8Array): boolean => isIpns(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) + +/** + * Returns `true` if the provided `url` string includes a valid IPFS, looks like + * an IPNS/DNSLink subdomain or `false` otherwise. + */ +export const subdomain = (url: string | Uint8Array): boolean => ipfsSubdomain(url) || ipnsSubdomain(url) + +/** + * Returns `true` if the provided string is a valid IPFS url or `false` + * otherwise. + */ +export const ipfsUrl = (url: string | Uint8Array): boolean => isIpfs(url, pathGatewayPattern) || ipfsSubdomain(url) + +/** + * Returns `true` if the provided string is a valid IPNS url or `false` + * otherwise. + */ +export const ipnsUrl = (url: string | Uint8Array): boolean => isIpns(url, pathGatewayPattern) || ipnsSubdomain(url) + +/** + * Returns `true` if the provided string is a valid IPFS or IPNS url or `false` + * otherwise. + */ +export const url = (url: string | Uint8Array): boolean => ipfsUrl(url) || ipnsUrl(url) || subdomain(url) +export const path = (path: string | Uint8Array): boolean => isIpfs(path, pathPattern) || isIpns(path, pathPattern) +/** + * Returns `true` if the provided string or `Uint8Array` is a valid `multihash` + * or `false` otherwise. + */ export { isMultihash as multihash } + +/** + * Returns `true` if the provided `string`, [`Multiaddr`](https://github.com/multiformats/js-multiaddr) + * or `Uint8Array` represents a valid multiaddr or `false` otherwise. + */ export { isMultiaddr as multiaddr } + +/** + * 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. + */ export { isPeerMultiaddr as peerMultiaddr } + +/** + * Returns `true` if the provided string, `Uint8Array` or [`CID`](https://github.com/multiformats/js-multiformats/#readme) + * object represents a valid [CID](https://docs.ipfs.io/guides/concepts/cid/) or + * `false` otherwise. + */ 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) +/** + * Returns `true` if the provided string is a valid `CID` in Base32 encoding or + * `false` otherwise. + */ +export const base32cid = (cid: CID | string | Uint8Array): boolean => (isCID(cid) && isBase32EncodedMultibase(cid)) + +/** + * Returns `true` if the provided string is a valid IPFS or IPNS path or `false` + * otherwise. + */ +export const ipfsPath = (path: string | Uint8Array): boolean => isIpfs(path, pathPattern) + +/** + * Returns `true` if the provided string is a valid IPNS path or `false` + * otherwise. + */ +export const ipnsPath = (path: string | Uint8Array): boolean => isIpns(path, pathPattern) + +/** + * Returns `true` if the provided string is a valid IPFS or IPNS url or path or + * `false` otherwise. + */ +export const urlOrPath = (x: string | Uint8Array): boolean => url(x) || path(x) + +/** + * Returns `true` if the provided string is a valid "CID path" (IPFS path + * without `/ipfs/` prefix) or `false` otherwise. + */ +export const cidPath = (path: string | Uint8Array | CID): boolean => isString(path) && !isCID(path) && isIpfs(`/ipfs/${path}`, pathPattern) diff --git a/test/test-cid.spec.ts b/test/test-cid.spec.ts index 5fbd281..44d5e48 100644 --- a/test/test-cid.spec.ts +++ b/test/test-cid.spec.ts @@ -1,9 +1,9 @@ /* eslint-env mocha */ 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' +import * as isIPFS from '../src/index.js' describe('ipfs cid', () => { it('isIPFS.cid should match a valid CID instance', (done) => { diff --git a/test/test-multiaddr.spec.ts b/test/test-multiaddr.spec.ts index 6d6d554..a61f74b 100644 --- a/test/test-multiaddr.spec.ts +++ b/test/test-multiaddr.spec.ts @@ -1,9 +1,9 @@ /* eslint-env mocha */ -import { expect } from 'aegir/chai' import { multiaddr } from '@multiformats/multiaddr' -import * as isIPFS from '../src/index.js' +import { expect } from 'aegir/chai' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as isIPFS from '../src/index.js' describe('ipfs multiaddr', () => { it('isIPFS.multiaddr should match a string with valid ip4 multiaddr', (done) => { diff --git a/test/test-multihash.spec.ts b/test/test-multihash.spec.ts index a01160e..e43c6f5 100644 --- a/test/test-multihash.spec.ts +++ b/test/test-multihash.spec.ts @@ -1,8 +1,8 @@ /* eslint-env mocha */ import { expect } from 'aegir/chai' -import * as isIPFS from '../src/index.js' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as isIPFS from '../src/index.js' describe('ipfs multihash', () => { it('isIPFS.multihash should match a valid multihash', (done) => { diff --git a/test/test-path.spec.ts b/test/test-path.spec.ts index 7cd9d96..6725ae1 100644 --- a/test/test-path.spec.ts +++ b/test/test-path.spec.ts @@ -1,8 +1,8 @@ /* eslint-env mocha */ -import * as isIPFS from '../src/index.js' import { expect } from 'aegir/chai' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as isIPFS from '../src/index.js' describe('ipfs path', () => { it('isIPFS.ipfsPath should match an ipfs path', (done) => { diff --git a/test/test-subdomain.spec.ts b/test/test-subdomain.spec.ts index 0846b44..040a250 100644 --- a/test/test-subdomain.spec.ts +++ b/test/test-subdomain.spec.ts @@ -1,8 +1,8 @@ /* eslint-env mocha */ -import * as isIPFS from '../src/index.js' import { expect } from 'aegir/chai' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as isIPFS from '../src/index.js' describe('ipfs subdomain', () => { it('isIPFS.ipfsSubdomain should match a cidv1b32', (done) => { diff --git a/test/test-url.spec.ts b/test/test-url.spec.ts index 9329057..bd5bfb3 100644 --- a/test/test-url.spec.ts +++ b/test/test-url.spec.ts @@ -1,8 +1,8 @@ /* eslint-env mocha */ import { expect } from 'aegir/chai' -import * as isIPFS from '../src/index.js' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as isIPFS from '../src/index.js' describe('ipfs url', () => { it('isIPFS.ipfsUrl should match an ipfs url', (done) => { diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..f599dc7 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +}