Skip to content

Commit a793da7

Browse files
committed
feat: support cidv1b32 in subdomains
Closes #19
1 parent ae0f738 commit a793da7

7 files changed

+359
-34
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ lib-cov
1616

1717
# Coverage directory used by tools like istanbul
1818
coverage
19+
.nyc_output
1920

2021
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
2122
.grunt

README.md

+69-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ is-ipfs
1010

1111
[Marcin Rataj](https://github.com/lidel)
1212

13-
## Install
13+
# Install
1414

1515
### In Node.js through npm
1616

@@ -37,17 +37,25 @@ Loading this module through a script tag will make the ```IsIpfs``` obj availabl
3737
<script src="https://unpkg.com/is-ipfs/dist/index.js"></script>
3838
```
3939

40-
## Usage
40+
# Usage
4141
```javascript
4242
const isIPFS = require('is-ipfs')
4343

4444
isIPFS.multihash('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
4545
isIPFS.multihash('noop') // false
4646

47+
isIPFS.multibase('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va') // 'base32'
48+
isIPFS.multibase('zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ7') // 'base58btc'
49+
isIPFS.multibase('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false (no multibase prefix in CIDv0)
50+
isIPFS.multibase('noop') // false
51+
4752
isIPFS.cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true (CIDv0)
4853
isIPFS.cid('zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ7') // true (CIDv1)
4954
isIPFS.cid('noop') // false
5055

56+
isIPFS.base32cid('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va') // true
57+
isIPFS.base32cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false
58+
5159
isIPFS.url('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
5260
isIPFS.url('https://ipfs.io/ipns/github.com') // true
5361
isIPFS.url('https://github.com/ipfs/js-ipfs/blob/master/README.md') // false
@@ -74,29 +82,52 @@ isIPFS.ipfsPath('/ipfs/invalid-hash') // false
7482

7583
isIPFS.ipnsPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false
7684
isIPFS.ipnsPath('/ipns/github.com') // true
85+
86+
isIPFS.subdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // true
87+
isIPFS.subdomain('http://www.bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // false
88+
isIPFS.subdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.dweb.link') // false
89+
90+
isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true
91+
isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.dweb.link') // false
92+
93+
isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // true
94+
isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.dweb.link') // false
95+
isIPFS.ipnsSubdomain('http://QmcNioXSC1bfJj1dcFErhUfyjFzoX2HodkRccsFFVJJvg8.ipns.dweb.link') // false
96+
isIPFS.ipnsSubdomain('http://foo-bar.ipns.dweb.link') // false (not a PeerID)
7797
```
7898

79-
## API
99+
# API
100+
101+
A suite of util methods provides efficient validation.
102+
103+
Detection of IPFS Paths and identifiers in URLs is a two-stage process:
104+
1. `urlPattern`/`pathPattern`/`subdomainPattern` regex is applied to quickly identify potential candidates
105+
2. proper CID validation is applied to remove false-positives
106+
107+
108+
## Utils
80109

81110
### `isIPFS.multihash(hash)`
82111

83112
Returns `true` if the provided string is a valid `multihash` or `false` otherwise.
84113

114+
### `isIPFS.multibase(cid)`
115+
116+
Returns a string with multibase name if the provided CID has `multibase` prefix or `false` otherwise.
117+
85118
### `isIPFS.cid(hash)`
86119

87120
Returns `true` if the provided string is a valid `CID` or `false` otherwise.
88121

89-
### `isIPFS.url(url)`
122+
### `isIPFS.base32cid(hash)`
90123

91-
Returns `true` if the provided string is a valid IPFS or IPNS url or `false` otherwise.
124+
Returns `true` if the provided string is a valid `CID` in Base32 encoding or `false` otherwise.
92125

93-
### `isIPFS.path(path)`
126+
## URLs
94127

95-
Returns `true` if the provided string is a valid IPFS or IPNS path or `false` otherwise.
96-
97-
### `isIPFS.urlOrPath(path)`
128+
### `isIPFS.url(url)`
98129

99-
Returns `true` if the provided string is a valid IPFS or IPNS url or path or `false` otherwise.
130+
Returns `true` if the provided string is a valid IPFS or IPNS url or `false` otherwise.
100131

101132
### `isIPFS.ipfsUrl(url)`
102133

@@ -106,6 +137,19 @@ Returns `true` if the provided string is a valid IPFS url or `false` otherwise.
106137

107138
Returns `true` if the provided string is a valid IPNS url or `false` otherwise.
108139

140+
## Paths
141+
142+
Standalone validation of IPFS Paths: `/ip(f|n)s/<cid>/..`
143+
144+
### `isIPFS.path(path)`
145+
146+
Returns `true` if the provided string is a valid IPFS or IPNS path or `false` otherwise.
147+
148+
### `isIPFS.urlOrPath(path)`
149+
150+
Returns `true` if the provided string is a valid IPFS or IPNS url or path or `false` otherwise.
151+
152+
109153
### `isIPFS.ipfsPath(path)`
110154

111155
Returns `true` if the provided string is a valid IPFS path or `false` otherwise.
@@ -114,10 +158,23 @@ Returns `true` if the provided string is a valid IPFS path or `false` otherwise.
114158

115159
Returns `true` if the provided string is a valid IPNS path or `false` otherwise.
116160

161+
## Subdomains
162+
163+
Validated subdomain convention: `cidv1b32.ip(f|n)s.domain.tld`
164+
165+
### `isIPFS.subdomain(url)`
166+
167+
Returns `true` if the provided string includes a valid IPFS or IPNS subdomain or `false` otherwise.
168+
169+
### `isIPFS.ipfsSubdomain(url)`
170+
171+
Returns `true` if the provided string includes a valid IPFS subdomain or `false` otherwise.
172+
173+
### `isIPFS.ipnsSubdomain(url)`
117174

118-
**Note:** the regex used for these checks is also exported as `isIPFS.urlPattern`
175+
Returns `true` if the provided string includes a valid IPNS subdomain or `false` otherwise.
119176

120-
## License
121177

178+
# License
122179

123180
MIT

package.json

+18-17
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
{
22
"name": "is-ipfs",
3-
"version": "0.3.3",
3+
"version": "0.4.0",
44
"description": "A set of utilities to help identify IPFS resources",
55
"main": "src/index.js",
66
"browser": {
77
"fs": false
88
},
99
"scripts": {
10-
"test:node": "aegir-test node",
11-
"test:browser": "aegir-test browser",
12-
"test": "aegir-test",
13-
"lint": "aegir-lint",
14-
"release": "aegir-release",
15-
"release-minor": "aegir-release --type minor",
16-
"release-major": "aegir-release --type major",
17-
"build": "aegir-build",
18-
"coverage": "aegir-coverage",
19-
"coverage-publish": "aegir-coverage publish"
10+
"test:node": "aegir test --target node",
11+
"test:browser": "aegir test --target browser",
12+
"test": "aegir test",
13+
"lint": "aegir lint",
14+
"release": "aegir release",
15+
"release-minor": "aegir release --type minor",
16+
"release-major": "aegir release --type major",
17+
"build": "aegir build",
18+
"coverage": "aegir coverage",
19+
"coverage-publish": "aegir coverage --upload"
2020
},
2121
"pre-commit": [
2222
"test",
@@ -29,14 +29,15 @@
2929
"author": "Francisco Dias <francisco@baiodias.com> (http://franciscodias.net/)",
3030
"license": "MIT",
3131
"dependencies": {
32-
"cids": "~0.5.1",
33-
"bs58": "^4.0.1",
34-
"multihashes": "~0.4.9"
32+
"bs58": "4.0.1",
33+
"cids": "0.5.3",
34+
"multibase": "0.4.0",
35+
"multihashes": "0.4.13"
3536
},
3637
"devDependencies": {
37-
"aegir": "^11.0.2",
38-
"chai": "^4.1.2",
39-
"pre-commit": "^1.2.2"
38+
"aegir": "15.0.1",
39+
"chai": "4.1.2",
40+
"pre-commit": "1.2.2"
4041
},
4142
"repository": {
4243
"type": "git",

src/index.js

+43-5
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
const base58 = require('bs58')
44
const multihash = require('multihashes')
5+
const multibase = require('multibase')
56
const CID = require('cids')
67

78
const urlPattern = /^https?:\/\/[^/]+\/(ip(f|n)s)\/((\w+).*)/
89
const pathPattern = /^\/(ip(f|n)s)\/((\w+).*)/
10+
const defaultProtocolMatch = 1
11+
const defaultHashMath = 4
12+
13+
const fqdnPattern = /^https?:\/\/([^/]+)\.(ip(?:f|n)s)\.[^/]+/
14+
const fqdnHashMatch = 1
15+
const fqdnProtocolMatch = 2
916

1017
function isMultihash (hash) {
1118
const formatted = convertToString(hash)
@@ -18,6 +25,14 @@ function isMultihash (hash) {
1825
}
1926
}
2027

28+
function isMultibase (hash) {
29+
try {
30+
return multibase.isEncoded(hash)
31+
} catch (e) {
32+
return false
33+
}
34+
}
35+
2136
function isCID (hash) {
2237
try {
2338
return CID.isCID(new CID(hash))
@@ -26,7 +41,7 @@ function isCID (hash) {
2641
}
2742
}
2843

29-
function isIpfs (input, pattern) {
44+
function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch = defaultHashMath) {
3045
const formatted = convertToString(input)
3146
if (!formatted) {
3247
return false
@@ -37,15 +52,23 @@ function isIpfs (input, pattern) {
3752
return false
3853
}
3954

40-
if (match[1] !== 'ipfs') {
55+
if (match[protocolMatch] !== 'ipfs') {
4156
return false
4257
}
4358

44-
const hash = match[4]
59+
let hash = match[hashMatch]
60+
61+
if (hash && pattern === fqdnPattern) {
62+
// when doing checks for subdomain context
63+
// ensure hash is case-insensitive
64+
// (browsers force-lowercase authority compotent anyway)
65+
hash = hash.toLowerCase()
66+
}
67+
4568
return isCID(hash)
4669
}
4770

48-
function isIpns (input, pattern) {
71+
function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch) {
4972
const formatted = convertToString(input)
5073
if (!formatted) {
5174
return false
@@ -55,10 +78,19 @@ function isIpns (input, pattern) {
5578
return false
5679
}
5780

58-
if (match[1] !== 'ipns') {
81+
if (match[protocolMatch] !== 'ipns') {
5982
return false
6083
}
6184

85+
if (hashMatch && pattern === fqdnPattern) {
86+
let hash = match[hashMatch]
87+
// when doing checks for subdomain context
88+
// ensure hash is case-insensitive
89+
// (browsers force-lowercase authority compotent anyway)
90+
hash = hash.toLowerCase()
91+
return isCID(hash)
92+
}
93+
6294
return true
6395
}
6496

@@ -76,7 +108,13 @@ function convertToString (input) {
76108

77109
module.exports = {
78110
multihash: isMultihash,
111+
multibase: isMultibase,
79112
cid: isCID,
113+
base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)),
114+
ipfsSubdomain: (url) => isIpfs(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch),
115+
ipnsSubdomain: (url) => isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch),
116+
subdomain: (url) => (isIpfs(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch) || isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch)),
117+
subdomainPattern: fqdnPattern,
80118
ipfsUrl: (url) => isIpfs(url, urlPattern),
81119
ipnsUrl: (url) => isIpns(url, urlPattern),
82120
url: (url) => (isIpfs(url, urlPattern) || isIpns(url, urlPattern)),

test/test-cid.spec.js

+9
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,19 @@ describe('ipfs cid', () => {
3636
done()
3737
})
3838

39+
it('isIPFS.cid should match a valid CIDv1 in Base32', (done) => {
40+
const actual = isIPFS.cid('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va')
41+
expect(actual).to.equal(true)
42+
done()
43+
})
44+
3945
it('isIPFS.cid should not match an invalid CIDv1 (with a typo)', (done) => {
4046
const actual = isIPFS.cid('zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ')
4147
expect(actual).to.equal(false)
4248
done()
4349
})
4450

51+
4552
it('isIPFS.cid should not match an invalid CID', (done) => {
4653
const actual = isIPFS.cid('noop')
4754
expect(actual).to.equal(false)
@@ -53,4 +60,6 @@ describe('ipfs cid', () => {
5360
expect(actual).to.equal(false)
5461
done()
5562
})
63+
64+
5665
})

0 commit comments

Comments
 (0)