@helia/verified-fetch 4.0.1 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.min.js +41 -41
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +3 -3
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +5 -5
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.js +22 -24
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.js +2 -2
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.js +1 -1
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/types.d.ts +1 -4
- package/dist/src/plugins/types.d.ts.map +1 -1
- package/dist/src/url-resolver.d.ts +4 -3
- package/dist/src/url-resolver.d.ts.map +1 -1
- package/dist/src/url-resolver.js +35 -47
- package/dist/src/url-resolver.js.map +1 -1
- package/dist/src/utils/dnslink-label.d.ts +26 -0
- package/dist/src/utils/dnslink-label.d.ts.map +1 -0
- package/dist/src/utils/dnslink-label.js +35 -0
- package/dist/src/utils/dnslink-label.js.map +1 -0
- package/dist/src/utils/get-content-type.d.ts +1 -1
- package/dist/src/utils/get-content-type.d.ts.map +1 -1
- package/dist/src/utils/get-content-type.js +1 -1
- package/dist/src/utils/get-content-type.js.map +1 -1
- package/dist/src/utils/get-stream-from-async-iterable.d.ts +1 -2
- package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
- package/dist/src/utils/get-stream-from-async-iterable.js +1 -3
- package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
- package/dist/src/utils/handle-redirects.js +2 -2
- package/dist/src/utils/ipfs-path-to-url.d.ts +16 -0
- package/dist/src/utils/ipfs-path-to-url.d.ts.map +1 -0
- package/dist/src/utils/ipfs-path-to-url.js +45 -0
- package/dist/src/utils/ipfs-path-to-url.js.map +1 -0
- package/dist/src/utils/parse-url-string.d.ts +18 -5
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +126 -44
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/resource-to-cache-key.js +2 -2
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +4 -0
- package/dist/src/utils/responses.js.map +1 -1
- package/dist/src/utils/walk-path.js +1 -1
- package/dist/src/utils/walk-path.js.map +1 -1
- package/package.json +10 -10
- package/src/index.ts +4 -2
- package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +8 -8
- package/src/plugins/plugin-handle-dag-pb.ts +27 -23
- package/src/plugins/plugin-handle-ipns-record.ts +2 -2
- package/src/plugins/plugin-handle-raw.ts +1 -1
- package/src/plugins/types.ts +1 -4
- package/src/url-resolver.ts +37 -56
- package/src/utils/dnslink-label.ts +38 -0
- package/src/utils/get-content-type.ts +2 -2
- package/src/utils/get-stream-from-async-iterable.ts +1 -4
- package/src/utils/handle-redirects.ts +2 -2
- package/src/utils/ipfs-path-to-url.ts +54 -0
- package/src/utils/parse-url-string.ts +166 -49
- package/src/utils/resource-to-cache-key.ts +2 -2
- package/src/utils/responses.ts +6 -0
- package/src/utils/walk-path.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helia/verified-fetch",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "A fetch-like API for obtaining verified & trustless IPFS content on the web",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"homepage": "https://github.com/ipfs/helia-verified-fetch/tree/main/packages/verified-fetch#readme",
|
|
@@ -175,12 +175,12 @@
|
|
|
175
175
|
"@ipld/dag-cbor": "^9.2.3",
|
|
176
176
|
"@ipld/dag-json": "^10.2.4",
|
|
177
177
|
"@ipld/dag-pb": "^4.1.5",
|
|
178
|
-
"@libp2p/interface": "^3.
|
|
179
|
-
"@libp2p/kad-dht": "^16.
|
|
180
|
-
"@libp2p/peer-id": "^6.0.
|
|
181
|
-
"@libp2p/utils": "^7.0.
|
|
182
|
-
"@libp2p/webrtc": "^6.0.
|
|
183
|
-
"@libp2p/websockets": "^10.
|
|
178
|
+
"@libp2p/interface": "^3.1.0",
|
|
179
|
+
"@libp2p/kad-dht": "^16.1.0",
|
|
180
|
+
"@libp2p/peer-id": "^6.0.4",
|
|
181
|
+
"@libp2p/utils": "^7.0.7",
|
|
182
|
+
"@libp2p/webrtc": "^6.0.8",
|
|
183
|
+
"@libp2p/websockets": "^10.1.0",
|
|
184
184
|
"@multiformats/dns": "^1.0.6",
|
|
185
185
|
"cborg": "^4.2.11",
|
|
186
186
|
"file-type": "^21.0.0",
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
"it-tar": "^6.0.5",
|
|
194
194
|
"it-to-browser-readablestream": "^2.0.11",
|
|
195
195
|
"it-to-buffer": "^4.0.9",
|
|
196
|
-
"libp2p": "^3.
|
|
196
|
+
"libp2p": "^3.1.0",
|
|
197
197
|
"multiformats": "^13.3.6",
|
|
198
198
|
"progress-events": "^1.0.1",
|
|
199
199
|
"quick-lru": "^7.0.1"
|
|
@@ -204,8 +204,8 @@
|
|
|
204
204
|
"@helia/http": "^3.0.5",
|
|
205
205
|
"@helia/json": "^5.0.0",
|
|
206
206
|
"@ipld/car": "^5.4.2",
|
|
207
|
-
"@libp2p/crypto": "^5.1.
|
|
208
|
-
"@libp2p/logger": "^6.
|
|
207
|
+
"@libp2p/crypto": "^5.1.13",
|
|
208
|
+
"@libp2p/logger": "^6.2.0",
|
|
209
209
|
"@types/sinon": "^17.0.4",
|
|
210
210
|
"aegir": "^47.0.24",
|
|
211
211
|
"browser-readablestream-to-it": "^2.0.9",
|
package/src/index.ts
CHANGED
|
@@ -856,7 +856,7 @@ export interface ResourceDetail {
|
|
|
856
856
|
|
|
857
857
|
export interface CIDDetail {
|
|
858
858
|
cid: CID
|
|
859
|
-
path?: string
|
|
859
|
+
path?: string[]
|
|
860
860
|
}
|
|
861
861
|
|
|
862
862
|
export interface CIDDetailError extends CIDDetail {
|
|
@@ -1069,10 +1069,12 @@ export interface UrlQuery extends Record<string, string | unknown> {
|
|
|
1069
1069
|
}
|
|
1070
1070
|
|
|
1071
1071
|
export interface ResolveURLResult {
|
|
1072
|
+
url: URL
|
|
1072
1073
|
cid: CID
|
|
1073
1074
|
protocol: string
|
|
1074
1075
|
ttl: number
|
|
1075
|
-
path: string
|
|
1076
|
+
path: string[]
|
|
1077
|
+
fragment: string
|
|
1076
1078
|
query: UrlQuery
|
|
1077
1079
|
ipfsPath: string
|
|
1078
1080
|
}
|
|
@@ -98,7 +98,7 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
|
|
|
98
98
|
})
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
getHtml ({ path, obj, cid }: { path?: string, obj: Record<string, any>, cid: CID }): string {
|
|
101
|
+
getHtml ({ path, obj, cid }: { path?: string[], obj: Record<string, any>, cid: CID }): string {
|
|
102
102
|
const style = `
|
|
103
103
|
:root {
|
|
104
104
|
--sans-serif: "Plex", system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
|
|
@@ -221,7 +221,7 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
|
|
|
221
221
|
<main>
|
|
222
222
|
<header>
|
|
223
223
|
<div><strong>CID: </strong> <code class="nowrap">${cid}</code></div>
|
|
224
|
-
<div><strong>Codec: </strong> ${this.valueHTML('dag-cbor (0x71)',
|
|
224
|
+
<div><strong>Codec: </strong> ${this.valueHTML('dag-cbor (0x71)', [])}</div>
|
|
225
225
|
</header>
|
|
226
226
|
<section class="container">
|
|
227
227
|
<p>You can download this block as:</p>
|
|
@@ -242,7 +242,7 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
|
|
|
242
242
|
</html>`
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
valueHTML (value: any, link: string
|
|
245
|
+
valueHTML (value: any, link: string[]): string {
|
|
246
246
|
let valueString: string
|
|
247
247
|
const isALinkObject = isLink(value)
|
|
248
248
|
if (!isALinkObject && typeof value !== 'string') {
|
|
@@ -259,11 +259,11 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
|
|
|
259
259
|
return valueCodeBlock
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
private renderValue (key: string, value: any, currentPath: string): string {
|
|
262
|
+
private renderValue (key: string, value: any, currentPath: string[]): string {
|
|
263
263
|
let rows = ''
|
|
264
264
|
value.forEach((item: any, idx: number) => {
|
|
265
|
-
const itemPath = currentPath
|
|
266
|
-
rows += `<div>${this.valueHTML(idx,
|
|
265
|
+
const itemPath = [...currentPath, key, idx.toString()]
|
|
266
|
+
rows += `<div>${this.valueHTML(idx, [])}</div>`
|
|
267
267
|
if (isPrimitive(item)) {
|
|
268
268
|
rows += `<div>${this.valueHTML(item, itemPath)}</div>`
|
|
269
269
|
} else {
|
|
@@ -275,7 +275,7 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
|
|
|
275
275
|
return rows
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
renderRows (obj: Record<string, any>, currentPath: string =
|
|
278
|
+
renderRows (obj: Record<string, any>, currentPath: string[] = []): string {
|
|
279
279
|
let rows = ''
|
|
280
280
|
for (const [key, value] of Object.entries(obj)) {
|
|
281
281
|
if (Array.isArray(value)) {
|
|
@@ -284,7 +284,7 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
|
|
|
284
284
|
rows += this.renderValue(key, value, currentPath)
|
|
285
285
|
rows += '</div>'
|
|
286
286
|
} else {
|
|
287
|
-
const valuePath = currentPath
|
|
287
|
+
const valuePath = [...currentPath, key]
|
|
288
288
|
rows += `<div>${key}</div><div>${this.valueHTML(value, valuePath)}</div>`
|
|
289
289
|
}
|
|
290
290
|
}
|
|
@@ -40,23 +40,25 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
40
40
|
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#use-in-directory-url-normalization
|
|
41
41
|
*/
|
|
42
42
|
getRedirectUrl (context: PluginContext): string | null {
|
|
43
|
-
const { resource,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
url.pathname = `${url.pathname}/`
|
|
54
|
-
return url.toString()
|
|
55
|
-
} catch (err: any) {
|
|
56
|
-
// resource is likely a CID
|
|
57
|
-
return `${resource.toString()}/`
|
|
58
|
-
}
|
|
43
|
+
const { resource, url, isDirectory } = context
|
|
44
|
+
|
|
45
|
+
let uri: URL
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// try the requested resource
|
|
49
|
+
uri = new URL(resource)
|
|
50
|
+
} catch {
|
|
51
|
+
// fall back to the canonical URL
|
|
52
|
+
uri = url
|
|
59
53
|
}
|
|
54
|
+
|
|
55
|
+
// directories must be requested with a trailing slash
|
|
56
|
+
if (isDirectory && !uri.pathname.endsWith('/')) {
|
|
57
|
+
// make sure we append slash to end of the path
|
|
58
|
+
uri.pathname += '/'
|
|
59
|
+
return uri.toString()
|
|
60
|
+
}
|
|
61
|
+
|
|
60
62
|
return null
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -65,7 +67,7 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
65
67
|
const { contentTypeParser, helia, getBlockstore } = this.pluginOptions
|
|
66
68
|
const log = this.log
|
|
67
69
|
let resource = context.resource
|
|
68
|
-
|
|
70
|
+
const path = context.path
|
|
69
71
|
|
|
70
72
|
let redirected = false
|
|
71
73
|
|
|
@@ -75,8 +77,9 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
75
77
|
let resolvedCID = terminalElement.cid
|
|
76
78
|
const fs = unixfs({ ...helia, blockstore: getBlockstore(context.cid, context.resource, options?.session ?? true, options) })
|
|
77
79
|
|
|
80
|
+
context.isDirectory = terminalElement?.type === 'directory'
|
|
81
|
+
|
|
78
82
|
if (terminalElement?.type === 'directory') {
|
|
79
|
-
const dirCid = terminalElement.cid
|
|
80
83
|
const redirectUrl = this.getRedirectUrl(context)
|
|
81
84
|
|
|
82
85
|
if (redirectUrl != null) {
|
|
@@ -95,7 +98,9 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
95
98
|
redirected = true
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
const dirCid = terminalElement.cid
|
|
98
102
|
const rootFilePath = 'index.html'
|
|
103
|
+
|
|
99
104
|
try {
|
|
100
105
|
log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
101
106
|
|
|
@@ -105,7 +110,6 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
105
110
|
}))
|
|
106
111
|
|
|
107
112
|
log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, entry.cid)
|
|
108
|
-
path = rootFilePath
|
|
109
113
|
resolvedCID = entry.cid
|
|
110
114
|
} catch (err: any) {
|
|
111
115
|
if (options?.signal?.aborted) {
|
|
@@ -126,7 +130,7 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
126
130
|
// dir-index-html plugin or dir-index-json (future idea?) plugin should handle this
|
|
127
131
|
return null
|
|
128
132
|
} finally {
|
|
129
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: rootFilePath }))
|
|
133
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: [rootFilePath] }))
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
@@ -157,13 +161,13 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
157
161
|
})
|
|
158
162
|
log('got async iterator for %c/%s', cid, path)
|
|
159
163
|
|
|
160
|
-
const streamAndFirstChunk = await context.serverTiming.time('stream-and-chunk', '', getStreamFromAsyncIterable(asyncIter,
|
|
164
|
+
const streamAndFirstChunk = await context.serverTiming.time('stream-and-chunk', '', getStreamFromAsyncIterable(asyncIter, {
|
|
161
165
|
onProgress: options?.onProgress,
|
|
162
166
|
signal: options?.signal
|
|
163
167
|
}))
|
|
164
168
|
const stream = streamAndFirstChunk.stream
|
|
165
169
|
firstChunk = streamAndFirstChunk.firstChunk
|
|
166
|
-
contentType = await context.serverTiming.time('get-content-type', '', getContentType({ filename: query.filename, bytes: firstChunk,
|
|
170
|
+
contentType = await context.serverTiming.time('get-content-type', '', getContentType({ path, filename: query.filename, bytes: firstChunk, contentTypeParser, log }))
|
|
167
171
|
|
|
168
172
|
byteRangeContext.setBody(stream)
|
|
169
173
|
}
|
|
@@ -207,7 +211,7 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
207
211
|
length: 8192
|
|
208
212
|
})
|
|
209
213
|
|
|
210
|
-
const { firstChunk } = await getStreamFromAsyncIterable(asyncIter,
|
|
214
|
+
const { firstChunk } = await getStreamFromAsyncIterable(asyncIter, {
|
|
211
215
|
onProgress: options?.onProgress,
|
|
212
216
|
signal: options?.signal
|
|
213
217
|
})
|
|
@@ -13,7 +13,7 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
13
13
|
readonly id = 'ipns-record-plugin'
|
|
14
14
|
readonly codes = []
|
|
15
15
|
|
|
16
|
-
canHandle ({
|
|
16
|
+
canHandle ({ accept, query, byteRangeContext }: PluginContext): boolean {
|
|
17
17
|
if (byteRangeContext == null) {
|
|
18
18
|
return false
|
|
19
19
|
}
|
|
@@ -26,7 +26,7 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
26
26
|
const { ipnsResolver } = this.pluginOptions
|
|
27
27
|
context.reqFormat = 'ipns-record'
|
|
28
28
|
|
|
29
|
-
if (path
|
|
29
|
+
if (path.length > 0 || !(resource.startsWith('ipns://') || resource.includes('.ipns.') || resource.includes('/ipns/'))) {
|
|
30
30
|
this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, path)
|
|
31
31
|
return badRequestResponse(resource, new Error('Invalid IPNS name'))
|
|
32
32
|
}
|
|
@@ -70,7 +70,7 @@ export class RawPlugin extends BasePlugin {
|
|
|
70
70
|
log.trace('did not set content disposition, raw block will display inline')
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
if (path
|
|
73
|
+
if (path.length > 0 && cid.code === rawCode) {
|
|
74
74
|
log.trace('404-ing raw codec request for %c/%s', cid, path)
|
|
75
75
|
return notFoundResponse(resource)
|
|
76
76
|
}
|
package/src/plugins/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolveURLResult,
|
|
1
|
+
import type { ResolveURLResult, VerifiedFetchInit, ContentTypeParser, RequestFormatShorthand } from '../index.js'
|
|
2
2
|
import type { ByteRangeContext } from '../utils/byte-range-context.js'
|
|
3
3
|
import type { AcceptHeader } from '../utils/select-output-type.ts'
|
|
4
4
|
import type { ServerTiming } from '../utils/server-timing.ts'
|
|
@@ -31,8 +31,6 @@ export interface PluginOptions {
|
|
|
31
31
|
* - Ephemeral: Typically discarded once fetch(...) completes.
|
|
32
32
|
*/
|
|
33
33
|
export interface PluginContext extends ResolveURLResult {
|
|
34
|
-
readonly cid: CID
|
|
35
|
-
readonly path: string
|
|
36
34
|
readonly resource: string
|
|
37
35
|
readonly accept?: AcceptHeader
|
|
38
36
|
|
|
@@ -52,7 +50,6 @@ export interface PluginContext extends ResolveURLResult {
|
|
|
52
50
|
directoryEntries?: UnixFSEntry[]
|
|
53
51
|
reqFormat?: RequestFormatShorthand
|
|
54
52
|
pathDetails?: PathWalkerResponse
|
|
55
|
-
query: UrlQuery
|
|
56
53
|
|
|
57
54
|
/**
|
|
58
55
|
* ByteRangeContext contains information about the size of the content and range requests.
|
package/src/url-resolver.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { peerIdFromCID, peerIdFromString } from '@libp2p/peer-id'
|
|
2
2
|
import { CID } from 'multiformats/cid'
|
|
3
|
-
import {
|
|
3
|
+
import { parseURLString } from './utils/parse-url-string.ts'
|
|
4
4
|
import type { ResolveURLOptions, ResolveURLResult, Resource, URLResolver as URLResolverInterface } from './index.ts'
|
|
5
|
+
import type { ParsedURL } from './utils/parse-url-string.ts'
|
|
5
6
|
import type { ServerTiming } from './utils/server-timing.ts'
|
|
6
7
|
import type { DNSLink } from '@helia/dnslink'
|
|
7
8
|
import type { IPNSResolver } from '@helia/ipns'
|
|
@@ -15,29 +16,6 @@ export interface URLResolverComponents {
|
|
|
15
16
|
timing: ServerTiming
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function toQuery (query?: string): Record<string, any> {
|
|
19
|
-
if (query == null) {
|
|
20
|
-
return {}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const params = new URLSearchParams(query)
|
|
24
|
-
const output: Record<string, any> = {}
|
|
25
|
-
|
|
26
|
-
for (const [key, value] of params.entries()) {
|
|
27
|
-
output[key] = value
|
|
28
|
-
|
|
29
|
-
if (value === 'true') {
|
|
30
|
-
output[key] = true
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (value === 'false') {
|
|
34
|
-
output[key] = false
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return output
|
|
39
|
-
}
|
|
40
|
-
|
|
41
19
|
export class URLResolver implements URLResolverInterface {
|
|
42
20
|
private readonly components: URLResolverComponents
|
|
43
21
|
|
|
@@ -53,56 +31,60 @@ export class URLResolver implements URLResolverInterface {
|
|
|
53
31
|
const cid = CID.asCID(resource)
|
|
54
32
|
|
|
55
33
|
if (cid != null) {
|
|
56
|
-
return this.resolveCIDResource(cid,
|
|
34
|
+
return this.resolveCIDResource(cid, {
|
|
35
|
+
url: new URL(`ipfs://${cid}`)
|
|
36
|
+
}, options)
|
|
57
37
|
}
|
|
58
38
|
|
|
59
39
|
throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${resource}`)
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
async parseUrlString (urlString: string, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
|
|
63
|
-
const
|
|
43
|
+
const result = parseURLString(urlString)
|
|
64
44
|
|
|
65
|
-
if (protocol === 'ipfs') {
|
|
66
|
-
const cid = CID.parse(cidOrPeerIdOrDnsLink)
|
|
45
|
+
if (result.protocol === 'ipfs') {
|
|
46
|
+
const cid = CID.parse(result.cidOrPeerIdOrDnsLink)
|
|
67
47
|
|
|
68
|
-
return this.resolveCIDResource(cid,
|
|
48
|
+
return this.resolveCIDResource(cid, result, options)
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
if (protocol === 'ipns') {
|
|
51
|
+
if (result.protocol === 'ipns') {
|
|
72
52
|
// try to parse target as peer id
|
|
73
53
|
let peerId: PeerId
|
|
74
54
|
|
|
75
55
|
try {
|
|
76
|
-
peerId = peerIdFromString(cidOrPeerIdOrDnsLink)
|
|
56
|
+
peerId = peerIdFromString(result.cidOrPeerIdOrDnsLink)
|
|
77
57
|
} catch {
|
|
78
58
|
// fall back to DNSLink (e.g. /ipns/example.com)
|
|
79
|
-
return this.resolveDNSLink(cidOrPeerIdOrDnsLink,
|
|
59
|
+
return this.resolveDNSLink(result.cidOrPeerIdOrDnsLink, result, options)
|
|
80
60
|
}
|
|
81
61
|
|
|
82
62
|
// parse multihash from string (e.g. /ipns/QmFoo...)
|
|
83
|
-
return this.resolveIPNSName(cidOrPeerIdOrDnsLink, peerId,
|
|
63
|
+
return this.resolveIPNSName(result.cidOrPeerIdOrDnsLink, peerId, result, options)
|
|
84
64
|
}
|
|
85
65
|
|
|
86
66
|
throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${urlString}`)
|
|
87
67
|
}
|
|
88
68
|
|
|
89
|
-
async resolveCIDResource (cid: CID,
|
|
69
|
+
async resolveCIDResource (cid: CID, parsed: Partial<ParsedURL> & Pick<ParsedURL, 'url'>, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
|
|
90
70
|
if (cid.code === CODEC_LIBP2P_KEY) {
|
|
91
71
|
// special case - peer id encoded as a CID
|
|
92
|
-
return this.resolveIPNSName(cid.toString(), peerIdFromCID(cid),
|
|
72
|
+
return this.resolveIPNSName(cid.toString(), peerIdFromCID(cid), parsed, options)
|
|
93
73
|
}
|
|
94
74
|
|
|
95
75
|
return {
|
|
76
|
+
url: parsed.url,
|
|
96
77
|
cid,
|
|
97
78
|
protocol: 'ipfs',
|
|
98
|
-
query,
|
|
99
|
-
path,
|
|
79
|
+
query: parsed.query ?? {},
|
|
80
|
+
path: parsed.path ?? [],
|
|
81
|
+
fragment: parsed.fragment ?? '',
|
|
100
82
|
ttl: 29030400, // 1 year for ipfs content
|
|
101
|
-
ipfsPath: `/ipfs/${cid}${
|
|
83
|
+
ipfsPath: `/ipfs/${cid}${parsed.url.pathname}`
|
|
102
84
|
}
|
|
103
85
|
}
|
|
104
86
|
|
|
105
|
-
async resolveDNSLink (domain: string,
|
|
87
|
+
async resolveDNSLink (domain: string, parsed: ParsedURL, options?: ResolveURLOptions): Promise<ResolveURLResult> {
|
|
106
88
|
const results = await this.components.timing.time('dnsLink.resolve', `Resolve DNSLink ${domain}`, this.components.dnsLink.resolve(domain, options))
|
|
107
89
|
const result = results?.[0]
|
|
108
90
|
|
|
@@ -112,7 +94,7 @@ export class URLResolver implements URLResolverInterface {
|
|
|
112
94
|
|
|
113
95
|
// dnslink resolved to IPNS name
|
|
114
96
|
if (result.namespace === 'ipns') {
|
|
115
|
-
return this.resolveIPNSName(domain, result.peerId,
|
|
97
|
+
return this.resolveIPNSName(domain, result.peerId, parsed, options)
|
|
116
98
|
}
|
|
117
99
|
|
|
118
100
|
// dnslink resolved to CID
|
|
@@ -122,38 +104,37 @@ export class URLResolver implements URLResolverInterface {
|
|
|
122
104
|
}
|
|
123
105
|
|
|
124
106
|
return {
|
|
107
|
+
url: parsed.url,
|
|
125
108
|
cid: result.cid,
|
|
126
|
-
path: concatPaths(result.path, path),
|
|
109
|
+
path: concatPaths(...(result.path ?? '').split('/'), ...(parsed.path ?? [])),
|
|
110
|
+
fragment: parsed.fragment,
|
|
127
111
|
// dnslink is mutable so return 'ipns' protocol so we do not include immutable in cache-control header
|
|
128
112
|
protocol: 'ipns',
|
|
129
113
|
ttl: result.answer.TTL,
|
|
130
|
-
query,
|
|
131
|
-
ipfsPath: `/ipns/${domain}${
|
|
114
|
+
query: parsed.query,
|
|
115
|
+
ipfsPath: `/ipns/${domain}${parsed.url.pathname}`
|
|
132
116
|
}
|
|
133
117
|
}
|
|
134
118
|
|
|
135
|
-
async resolveIPNSName (resource: string, key: PeerId,
|
|
119
|
+
async resolveIPNSName (resource: string, key: PeerId, parsed: Partial<ParsedURL> & Pick<ParsedURL, 'url'>, options?: AbortOptions): Promise<ResolveURLResult> {
|
|
136
120
|
const result = await this.components.timing.time('ipns.resolve', `Resolve IPNS name ${key}`, this.components.ipnsResolver.resolve(key, options))
|
|
137
121
|
|
|
138
122
|
return {
|
|
123
|
+
url: parsed.url,
|
|
139
124
|
cid: result.cid,
|
|
140
|
-
path: concatPaths(result.path, path),
|
|
141
|
-
query,
|
|
125
|
+
path: concatPaths(...(result.path ?? '').split('/'), ...(parsed.path ?? [])),
|
|
126
|
+
query: parsed.query ?? {},
|
|
127
|
+
fragment: parsed.fragment ?? '',
|
|
142
128
|
protocol: 'ipns',
|
|
143
129
|
// IPNS ttl is in nanoseconds, convert to seconds
|
|
144
130
|
ttl: Number((result.record.ttl ?? 0n) / BigInt(1e9)),
|
|
145
|
-
ipfsPath: `/ipns/${resource}${
|
|
131
|
+
ipfsPath: `/ipns/${resource}${parsed.url.pathname}`
|
|
146
132
|
}
|
|
147
133
|
}
|
|
148
134
|
}
|
|
149
135
|
|
|
150
|
-
function concatPaths (...paths: Array<string | undefined>): string {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.join('/')
|
|
155
|
-
.replaceAll(/(\/+)/g, '/')
|
|
156
|
-
.replace(/^(\/)+/, '')
|
|
157
|
-
.replace(/(\/)+$/, '/')
|
|
158
|
-
}`
|
|
136
|
+
function concatPaths (...paths: Array<string | undefined>): string[] {
|
|
137
|
+
// @ts-expect-error undefined is filtered out
|
|
138
|
+
return paths
|
|
139
|
+
.filter(p => p != null && p !== '')
|
|
159
140
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For DNSLink see https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
|
|
3
|
+
* DNSLink names include . which means they must be inlined into a single DNS label to provide unique origin and work with wildcard TLS certificates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// DNS label can have up to 63 characters, consisting of alphanumeric
|
|
7
|
+
// characters or hyphens -, but it must not start or end with a hyphen.
|
|
8
|
+
const dnsLabelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if label looks like inlined DNSLink.
|
|
12
|
+
* (https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header)
|
|
13
|
+
*/
|
|
14
|
+
export function isInlinedDnsLink (label: string): boolean {
|
|
15
|
+
return dnsLabelRegex.test(label) && label.includes('-') && !label.includes('.')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* DNSLink label decoding
|
|
20
|
+
* - Every standalone - is replaced with .
|
|
21
|
+
* - Every remaining -- is replaced with -
|
|
22
|
+
*
|
|
23
|
+
* @example en-wikipedia--on--ipfs-org -> en.wikipedia-on-ipfs.org
|
|
24
|
+
*/
|
|
25
|
+
export function decodeDNSLinkLabel (label: string): string {
|
|
26
|
+
return label.replace(/--/g, '%').replace(/-/g, '.').replace(/%/g, '-')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* DNSLink label encoding
|
|
31
|
+
* - Every - is replaced with --
|
|
32
|
+
* - Every . is replaced with -
|
|
33
|
+
*
|
|
34
|
+
* @example en.wikipedia-on-ipfs.org -> en-wikipedia--on--ipfs-org
|
|
35
|
+
*/
|
|
36
|
+
export function encodeDNSLinkLabel (name: string): string {
|
|
37
|
+
return name.replace(/-/g, '--').replace(/\./g, '-')
|
|
38
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { Logger } from '@libp2p/interface'
|
|
|
5
5
|
|
|
6
6
|
export interface GetContentTypeOptions {
|
|
7
7
|
bytes: Uint8Array
|
|
8
|
-
path?: string
|
|
8
|
+
path?: string[]
|
|
9
9
|
defaultContentType?: string
|
|
10
10
|
contentTypeParser?: ContentTypeParser
|
|
11
11
|
log: Logger
|
|
@@ -25,7 +25,7 @@ export async function getContentType ({ bytes, path, contentTypeParser, log, def
|
|
|
25
25
|
try {
|
|
26
26
|
let fileName
|
|
27
27
|
if (filenameParam == null) {
|
|
28
|
-
fileName = path?.
|
|
28
|
+
fileName = path?.[path.length - 1]?.trim()
|
|
29
29
|
fileName = (fileName === '' || fileName?.split('.').length === 1) ? undefined : fileName
|
|
30
30
|
} else {
|
|
31
31
|
fileName = filenameParam
|
|
@@ -2,18 +2,15 @@ import { AbortError } from '@libp2p/interface'
|
|
|
2
2
|
import { CustomProgressEvent } from 'progress-events'
|
|
3
3
|
import { NoContentError } from '../errors.js'
|
|
4
4
|
import type { VerifiedFetchInit } from '../index.js'
|
|
5
|
-
import type { Logger } from '@libp2p/interface'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Converts an async iterator of Uint8Array bytes to a stream and returns the first chunk of bytes
|
|
9
8
|
*/
|
|
10
|
-
export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>,
|
|
11
|
-
const log = logger.newScope('get-stream-from-async-iterable')
|
|
9
|
+
export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
|
|
12
10
|
const reader = iterator[Symbol.asyncIterator]()
|
|
13
11
|
const { value: firstChunk, done } = await reader.next()
|
|
14
12
|
|
|
15
13
|
if (done === true) {
|
|
16
|
-
log.error('no content found for path "%s"', path)
|
|
17
14
|
throw new NoContentError()
|
|
18
15
|
}
|
|
19
16
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SubdomainNotSupportedError } from '../errors.js'
|
|
2
|
-
import {
|
|
2
|
+
import { parseURLString } from './parse-url-string.js'
|
|
3
3
|
import { movedPermanentlyResponse } from './responses.js'
|
|
4
4
|
import type { VerifiedFetchInit, Resource } from '../index.js'
|
|
5
5
|
import type { AbortOptions, ComponentLogger } from '@libp2p/interface'
|
|
@@ -47,7 +47,7 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
|
|
|
47
47
|
// if x-forwarded-host is passed, we need to set the location header to the
|
|
48
48
|
// subdomain so that the browser can redirect to the correct subdomain
|
|
49
49
|
try {
|
|
50
|
-
const urlParts =
|
|
50
|
+
const urlParts = parseURLString(resource)
|
|
51
51
|
const reqUrl = new URL(resource)
|
|
52
52
|
const actualHost = forwardedHost ?? reqUrl.host
|
|
53
53
|
const subdomainUrl = new URL(reqUrl)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { InvalidParametersError } from '@libp2p/interface'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Turns an IPFS or IPNS path into a HTTP URL. Path gateway syntax is used to
|
|
5
|
+
* preserve any case sensitivity
|
|
6
|
+
*
|
|
7
|
+
* - `/ipfs/cid` -> `https://example.org/ipfs/cid`
|
|
8
|
+
* - `/ipns/name` -> `https://example.org/ipns/name`
|
|
9
|
+
*/
|
|
10
|
+
export function ipfsPathToUrl (path: string): string {
|
|
11
|
+
if (!path.startsWith('/ipfs/') && !path.startsWith('/ipns/')) {
|
|
12
|
+
throw new InvalidParametersError(`Path ${path} did not start with /ipfs/ or /ipns/`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// trim fragment
|
|
16
|
+
const fragmentIndex = path.indexOf('#')
|
|
17
|
+
let fragment = ''
|
|
18
|
+
|
|
19
|
+
if (fragmentIndex > -1) {
|
|
20
|
+
fragment = path.substring(fragmentIndex)
|
|
21
|
+
path = path.substring(0, fragmentIndex)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// trim query
|
|
25
|
+
const queryIndex = path.indexOf('?')
|
|
26
|
+
let query = ''
|
|
27
|
+
|
|
28
|
+
if (queryIndex > -1) {
|
|
29
|
+
query = path.substring(queryIndex)
|
|
30
|
+
path = path.substring(0, queryIndex)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const type = path.substring(1, 5)
|
|
34
|
+
const rest = path.substring(6)
|
|
35
|
+
|
|
36
|
+
return `https://example.org/${type}/${rest}${query}${fragment}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Turns an IPFS or IPNS URL into a HTTP URL. Path gateway syntax is used to
|
|
41
|
+
* preserve and case sensitivity
|
|
42
|
+
*
|
|
43
|
+
* `ipfs://cid` -> `https://example.org/ipfs/cid`
|
|
44
|
+
*/
|
|
45
|
+
export function ipfsUrlToUrl (url: string): string {
|
|
46
|
+
if (!url.startsWith('ipfs://') && !url.startsWith('ipns://')) {
|
|
47
|
+
throw new InvalidParametersError(`URL ${url} did not start with ipfs:// or ipns://`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const type = url.substring(0, 4)
|
|
51
|
+
const rest = url.substring(7)
|
|
52
|
+
|
|
53
|
+
return `https://example.org/${type}/${rest}`
|
|
54
|
+
}
|