@helia/verified-fetch 4.0.0 → 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/README.md +7 -49
- package/dist/index.min.js +56 -51
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +12 -52
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -50
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/index.d.ts +0 -1
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/index.js +0 -1
- package/dist/src/plugins/index.js.map +1 -1
- package/dist/src/plugins/plugin-base.d.ts.map +1 -1
- package/dist/src/plugins/plugin-base.js +3 -2
- package/dist/src/plugins/plugin-base.js.map +1 -1
- package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-car.js +0 -1
- package/dist/src/plugins/plugin-handle-car.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 -6
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.js +0 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.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 +34 -39
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts +8 -4
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.js +8 -5
- package/dist/src/plugins/plugin-handle-dag-walk.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 +4 -6
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-json.js +0 -1
- package/dist/src/plugins/plugin-handle-json.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.js +4 -8
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.js +0 -1
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
- package/dist/src/plugins/types.d.ts +6 -11
- 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/byte-range-context.d.ts +2 -2
- package/dist/src/utils/byte-range-context.d.ts.map +1 -1
- package/dist/src/utils/byte-range-context.js +1 -1
- package/dist/src/utils/byte-range-context.js.map +1 -1
- package/dist/src/utils/content-type-parser.d.ts.map +1 -1
- package/dist/src/utils/content-type-parser.js +0 -10
- package/dist/src/utils/content-type-parser.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/error-to-object.d.ts +6 -0
- package/dist/src/utils/error-to-object.d.ts.map +1 -0
- package/dist/src/utils/error-to-object.js +20 -0
- package/dist/src/utils/error-to-object.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 +2 -1
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +16 -1
- 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/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +36 -28
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +0 -2
- package/package.json +10 -10
- package/src/index.ts +12 -52
- package/src/plugins/index.ts +0 -1
- package/src/plugins/plugin-base.ts +3 -2
- package/src/plugins/plugin-handle-car.ts +0 -2
- package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +10 -9
- package/src/plugins/plugin-handle-dag-cbor.ts +3 -1
- package/src/plugins/plugin-handle-dag-pb.ts +49 -37
- package/src/plugins/plugin-handle-dag-walk.ts +10 -5
- package/src/plugins/plugin-handle-ipns-record.ts +6 -6
- package/src/plugins/plugin-handle-json.ts +1 -1
- package/src/plugins/plugin-handle-raw.ts +7 -8
- package/src/plugins/plugin-handle-tar.ts +2 -1
- package/src/plugins/types.ts +7 -12
- package/src/url-resolver.ts +37 -56
- package/src/utils/byte-range-context.ts +3 -3
- package/src/utils/content-type-parser.ts +5 -11
- package/src/utils/dnslink-label.ts +38 -0
- package/src/utils/error-to-object.ts +22 -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 +21 -1
- package/src/utils/walk-path.ts +1 -1
- package/src/verified-fetch.ts +46 -28
- package/dist/src/plugins/errors.d.ts +0 -25
- package/dist/src/plugins/errors.d.ts.map +0 -1
- package/dist/src/plugins/errors.js +0 -33
- package/dist/src/plugins/errors.js.map +0 -1
- package/src/plugins/errors.ts +0 -37
|
@@ -3,7 +3,6 @@ import { code as rawCode } from 'multiformats/codecs/raw'
|
|
|
3
3
|
import { identity } from 'multiformats/hashes/identity'
|
|
4
4
|
import { getContentType } from '../utils/get-content-type.js'
|
|
5
5
|
import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
|
|
6
|
-
import { PluginFatalError } from './errors.js'
|
|
7
6
|
import { BasePlugin } from './plugin-base.js'
|
|
8
7
|
import type { PluginContext } from './types.js'
|
|
9
8
|
import type { AcceptHeader } from '../utils/select-output-type.ts'
|
|
@@ -49,10 +48,10 @@ export class RawPlugin extends BasePlugin {
|
|
|
49
48
|
codes: number[] = [rawCode, identity.code]
|
|
50
49
|
|
|
51
50
|
canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
|
|
52
|
-
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
53
51
|
if (byteRangeContext == null) {
|
|
54
52
|
return false
|
|
55
53
|
}
|
|
54
|
+
|
|
56
55
|
return accept?.mimeType === 'application/vnd.ipld.raw' || query.format === 'raw'
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -66,21 +65,20 @@ export class RawPlugin extends BasePlugin {
|
|
|
66
65
|
context.reqFormat = 'raw'
|
|
67
66
|
context.query.download = true
|
|
68
67
|
context.query.filename = context.query.filename ?? `${cid.toString()}.bin`
|
|
69
|
-
log.trace('
|
|
68
|
+
log.trace('set content disposition to force download')
|
|
70
69
|
} else {
|
|
71
|
-
log.trace('
|
|
70
|
+
log.trace('did not set content disposition, raw block will display inline')
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
if (path
|
|
73
|
+
if (path.length > 0 && cid.code === rawCode) {
|
|
75
74
|
log.trace('404-ing raw codec request for %c/%s', cid, path)
|
|
76
|
-
|
|
77
|
-
// return notFoundResponse(resource, 'Raw codec does not support paths')
|
|
78
|
-
throw new PluginFatalError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths', { response: notFoundResponse(resource, 'Raw codec does not support paths') })
|
|
75
|
+
return notFoundResponse(resource)
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
|
|
82
79
|
const blockstore = getBlockstore(terminalCid, resource, session, options)
|
|
83
80
|
const result = await toBuffer(blockstore.get(terminalCid, options))
|
|
81
|
+
|
|
84
82
|
context.byteRangeContext.setBody(result)
|
|
85
83
|
|
|
86
84
|
// if the user has specified an `Accept` header that corresponds to a raw
|
|
@@ -94,6 +92,7 @@ export class RawPlugin extends BasePlugin {
|
|
|
94
92
|
contentTypeParser,
|
|
95
93
|
log
|
|
96
94
|
})
|
|
95
|
+
|
|
97
96
|
const response = okRangeResponse(resource, context.byteRangeContext.getBody(contentType), { byteRangeContext: context.byteRangeContext, log }, {
|
|
98
97
|
redirected: false
|
|
99
98
|
})
|
|
@@ -14,11 +14,12 @@ import type { PluginContext } from './types.js'
|
|
|
14
14
|
export class TarPlugin extends BasePlugin {
|
|
15
15
|
readonly id = 'tar-plugin'
|
|
16
16
|
readonly codes = []
|
|
17
|
+
|
|
17
18
|
canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
|
|
18
|
-
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
19
19
|
if (byteRangeContext == null) {
|
|
20
20
|
return false
|
|
21
21
|
}
|
|
22
|
+
|
|
22
23
|
return accept?.mimeType === 'application/x-tar' || query.format === 'tar'
|
|
23
24
|
}
|
|
24
25
|
|
package/src/plugins/types.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { ResolveURLResult, UrlQuery, VerifiedFetchInit, ContentTypeParser, RequestFormatShorthand } from '../index.js'
|
|
1
|
+
import type { ResolveURLResult, VerifiedFetchInit, ContentTypeParser, RequestFormatShorthand } from '../index.js'
|
|
3
2
|
import type { ByteRangeContext } from '../utils/byte-range-context.js'
|
|
4
3
|
import type { AcceptHeader } from '../utils/select-output-type.ts'
|
|
5
4
|
import type { ServerTiming } from '../utils/server-timing.ts'
|
|
6
5
|
import type { PathWalkerResponse } from '../utils/walk-path.js'
|
|
7
6
|
import type { IPNSResolver } from '@helia/ipns'
|
|
8
|
-
import type { AbortOptions,
|
|
7
|
+
import type { AbortOptions, Logger } from '@libp2p/interface'
|
|
9
8
|
import type { Helia } from 'helia'
|
|
10
9
|
import type { Blockstore } from 'interface-blockstore'
|
|
11
10
|
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
@@ -18,7 +17,7 @@ import type { CustomProgressEvent } from 'progress-events'
|
|
|
18
17
|
* - Persistent: Relevant even after the request completes (e.g., logging or metrics).
|
|
19
18
|
*/
|
|
20
19
|
export interface PluginOptions {
|
|
21
|
-
logger:
|
|
20
|
+
logger: Logger
|
|
22
21
|
getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions): Blockstore
|
|
23
22
|
contentTypeParser?: ContentTypeParser
|
|
24
23
|
helia: Helia
|
|
@@ -32,8 +31,6 @@ export interface PluginOptions {
|
|
|
32
31
|
* - Ephemeral: Typically discarded once fetch(...) completes.
|
|
33
32
|
*/
|
|
34
33
|
export interface PluginContext extends ResolveURLResult {
|
|
35
|
-
readonly cid: CID
|
|
36
|
-
readonly path: string
|
|
37
34
|
readonly resource: string
|
|
38
35
|
readonly accept?: AcceptHeader
|
|
39
36
|
|
|
@@ -51,10 +48,8 @@ export interface PluginContext extends ResolveURLResult {
|
|
|
51
48
|
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
|
|
52
49
|
isDirectory?: boolean
|
|
53
50
|
directoryEntries?: UnixFSEntry[]
|
|
54
|
-
errors?: PluginError[]
|
|
55
51
|
reqFormat?: RequestFormatShorthand
|
|
56
52
|
pathDetails?: PathWalkerResponse
|
|
57
|
-
query: UrlQuery
|
|
58
53
|
|
|
59
54
|
/**
|
|
60
55
|
* ByteRangeContext contains information about the size of the content and range requests.
|
|
@@ -65,6 +60,10 @@ export interface PluginContext extends ResolveURLResult {
|
|
|
65
60
|
byteRangeContext?: ByteRangeContext
|
|
66
61
|
serverTiming: ServerTiming
|
|
67
62
|
ipfsPath: string
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Allow arbitrary keys/values
|
|
66
|
+
*/
|
|
68
67
|
[key: string]: unknown
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -85,7 +84,3 @@ export interface PluginErrorOptions {
|
|
|
85
84
|
details?: Record<string, any>
|
|
86
85
|
response?: Response
|
|
87
86
|
}
|
|
88
|
-
|
|
89
|
-
export interface FatalPluginErrorOptions extends PluginErrorOptions {
|
|
90
|
-
response: Response
|
|
91
|
-
}
|
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
|
}
|
|
@@ -3,7 +3,7 @@ import { InvalidRangeError } from '../errors.js'
|
|
|
3
3
|
import { calculateByteRangeIndexes, getHeader } from './request-headers.js'
|
|
4
4
|
import { getContentRangeHeader } from './response-headers.js'
|
|
5
5
|
import type { SupportedBodyTypes } from '../index.js'
|
|
6
|
-
import type {
|
|
6
|
+
import type { Logger } from '@libp2p/interface'
|
|
7
7
|
|
|
8
8
|
type SliceableBody = Exclude<SupportedBodyTypes, ReadableStream<Uint8Array> | null>
|
|
9
9
|
|
|
@@ -90,8 +90,8 @@ export class ByteRangeContext {
|
|
|
90
90
|
// to be set by isValidRangeRequest so that we don't need to re-check the byteRanges
|
|
91
91
|
private _isValidRangeRequest: boolean = false
|
|
92
92
|
|
|
93
|
-
constructor (logger:
|
|
94
|
-
this.log = logger.
|
|
93
|
+
constructor (logger: Logger, private readonly headers?: HeadersInit) {
|
|
94
|
+
this.log = logger.newScope('byte-range-context')
|
|
95
95
|
this.rangeRequestHeader = getHeader(this.headers, 'Range')
|
|
96
96
|
|
|
97
97
|
if (this.rangeRequestHeader != null) {
|
|
@@ -1,28 +1,22 @@
|
|
|
1
|
-
import { logger } from '@libp2p/logger'
|
|
2
1
|
import { fileTypeFromBuffer } from 'file-type'
|
|
3
2
|
|
|
4
|
-
const log = logger('helia:verified-fetch:content-type-parser')
|
|
5
|
-
|
|
6
3
|
export const defaultMimeType = 'application/octet-stream'
|
|
7
4
|
function checkForSvg (text: string): boolean {
|
|
8
|
-
log('checking for svg')
|
|
9
5
|
return /^(<\?xml[^>]+>)?[^<^\w]+<svg/ig.test(text)
|
|
10
6
|
}
|
|
11
7
|
|
|
12
8
|
async function checkForJson (text: string): Promise<boolean> {
|
|
13
|
-
log('checking for json')
|
|
14
9
|
try {
|
|
15
10
|
JSON.parse(text)
|
|
16
11
|
return true
|
|
17
12
|
} catch (err) {
|
|
18
|
-
log('failed to parse as json', err)
|
|
19
13
|
return false
|
|
20
14
|
}
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
function getText (bytes: Uint8Array): string | null {
|
|
24
|
-
log('checking for text')
|
|
25
18
|
const decoder = new TextDecoder('utf-8', { fatal: true })
|
|
19
|
+
|
|
26
20
|
try {
|
|
27
21
|
return decoder.decode(bytes)
|
|
28
22
|
} catch (err) {
|
|
@@ -31,25 +25,24 @@ function getText (bytes: Uint8Array): string | null {
|
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
async function checkForHtml (text: string): Promise<boolean> {
|
|
34
|
-
log('checking for html')
|
|
35
28
|
return /^\s*<(?:!doctype\s+html|html|head|body)\b/i.test(text)
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
export async function contentTypeParser (bytes: Uint8Array, fileName?: string): Promise<string> {
|
|
39
|
-
log('contentTypeParser called for fileName: %s, byte size=%s', fileName, bytes.length)
|
|
40
32
|
const detectedType = (await fileTypeFromBuffer(bytes))?.mime
|
|
33
|
+
|
|
41
34
|
if (detectedType != null) {
|
|
42
|
-
log('detectedType: %s', detectedType)
|
|
43
35
|
if (detectedType === 'application/xml' && fileName?.toLowerCase().endsWith('.svg')) {
|
|
44
36
|
return 'image/svg+xml'
|
|
45
37
|
}
|
|
38
|
+
|
|
46
39
|
return detectedType
|
|
47
40
|
}
|
|
48
|
-
log('no detectedType')
|
|
49
41
|
|
|
50
42
|
if (fileName == null) {
|
|
51
43
|
// it's likely text... no other way to determine file-type.
|
|
52
44
|
const text = getText(bytes)
|
|
45
|
+
|
|
53
46
|
if (text != null) {
|
|
54
47
|
// check for svg, json, html, or it's plain text.
|
|
55
48
|
if (checkForSvg(text)) {
|
|
@@ -62,6 +55,7 @@ export async function contentTypeParser (bytes: Uint8Array, fileName?: string):
|
|
|
62
55
|
return 'text/plain; charset=utf-8'
|
|
63
56
|
}
|
|
64
57
|
}
|
|
58
|
+
|
|
65
59
|
return defaultMimeType
|
|
66
60
|
}
|
|
67
61
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function isAggregateError (err?: any): err is AggregateError {
|
|
2
|
+
return err instanceof AggregateError || (err?.name === 'AggregateError' && Array.isArray(err?.errors))
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error instance properties are not enumerable so we must transform the error
|
|
7
|
+
* into a plain object if we want to pass it to `JSON.stringify` or similar.
|
|
8
|
+
*/
|
|
9
|
+
export function errorToObject (err: Error): any {
|
|
10
|
+
let errors
|
|
11
|
+
|
|
12
|
+
if (isAggregateError(err)) {
|
|
13
|
+
errors = err.errors.map(err => errorToObject(err))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
name: err.name,
|
|
18
|
+
message: err.message,
|
|
19
|
+
stack: err.stack,
|
|
20
|
+
errors
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -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 { ComponentLogger } 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.forComponent('helia:verified-fetch: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', 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
|
+
}
|