@helia/verified-fetch 4.0.0 → 4.0.1
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 +52 -47
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +8 -50
- 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.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +0 -1
- 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 +13 -16
- 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 +3 -5
- 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 +3 -7
- 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 +5 -7
- package/dist/src/plugins/types.d.ts.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/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-stream-from-async-iterable.d.ts +2 -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 +2 -2
- package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
- 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 +12 -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 +2 -2
- package/src/index.ts +8 -50
- 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 +2 -1
- package/src/plugins/plugin-handle-dag-cbor.ts +3 -1
- package/src/plugins/plugin-handle-dag-pb.ts +23 -15
- package/src/plugins/plugin-handle-dag-walk.ts +10 -5
- package/src/plugins/plugin-handle-ipns-record.ts +5 -5
- package/src/plugins/plugin-handle-json.ts +1 -1
- package/src/plugins/plugin-handle-raw.ts +6 -7
- package/src/plugins/plugin-handle-tar.ts +2 -1
- package/src/plugins/types.ts +6 -8
- package/src/utils/byte-range-context.ts +3 -3
- package/src/utils/content-type-parser.ts +5 -11
- package/src/utils/error-to-object.ts +22 -0
- package/src/utils/get-stream-from-async-iterable.ts +4 -4
- package/src/utils/responses.ts +15 -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
73
|
if (path !== '' && 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 { PluginError } from './errors.js'
|
|
2
1
|
import type { ResolveURLResult, UrlQuery, 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
|
|
@@ -51,7 +50,6 @@ export interface PluginContext extends ResolveURLResult {
|
|
|
51
50
|
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
|
|
52
51
|
isDirectory?: boolean
|
|
53
52
|
directoryEntries?: UnixFSEntry[]
|
|
54
|
-
errors?: PluginError[]
|
|
55
53
|
reqFormat?: RequestFormatShorthand
|
|
56
54
|
pathDetails?: PathWalkerResponse
|
|
57
55
|
query: UrlQuery
|
|
@@ -65,6 +63,10 @@ export interface PluginContext extends ResolveURLResult {
|
|
|
65
63
|
byteRangeContext?: ByteRangeContext
|
|
66
64
|
serverTiming: ServerTiming
|
|
67
65
|
ipfsPath: string
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Allow arbitrary keys/values
|
|
69
|
+
*/
|
|
68
70
|
[key: string]: unknown
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -85,7 +87,3 @@ export interface PluginErrorOptions {
|
|
|
85
87
|
details?: Record<string, any>
|
|
86
88
|
response?: Response
|
|
87
89
|
}
|
|
88
|
-
|
|
89
|
-
export interface FatalPluginErrorOptions extends PluginErrorOptions {
|
|
90
|
-
response: Response
|
|
91
|
-
}
|
|
@@ -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,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
|
+
}
|
|
@@ -2,18 +2,18 @@ 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 {
|
|
5
|
+
import type { Logger } from '@libp2p/interface'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Converts an async iterator of Uint8Array bytes to a stream and returns the first chunk of bytes
|
|
9
9
|
*/
|
|
10
|
-
export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, path: string, logger:
|
|
11
|
-
const log = logger.
|
|
10
|
+
export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, path: string, logger: Logger, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
|
|
11
|
+
const log = logger.newScope('get-stream-from-async-iterable')
|
|
12
12
|
const reader = iterator[Symbol.asyncIterator]()
|
|
13
13
|
const { value: firstChunk, done } = await reader.next()
|
|
14
14
|
|
|
15
15
|
if (done === true) {
|
|
16
|
-
log.error('no content found for path', path)
|
|
16
|
+
log.error('no content found for path "%s"', path)
|
|
17
17
|
throw new NoContentError()
|
|
18
18
|
}
|
|
19
19
|
|
package/src/utils/responses.ts
CHANGED
|
@@ -44,6 +44,20 @@ export function okResponse (url: string, body?: SupportedBodyTypes, init?: Respo
|
|
|
44
44
|
return response
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function internalServerErrorResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
48
|
+
const response = new Response(body, {
|
|
49
|
+
...(init ?? {}),
|
|
50
|
+
status: 500,
|
|
51
|
+
statusText: 'Internal Server Error'
|
|
52
|
+
})
|
|
53
|
+
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
54
|
+
|
|
55
|
+
setType(response, 'basic')
|
|
56
|
+
setUrl(response, url)
|
|
57
|
+
|
|
58
|
+
return response
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
48
62
|
const response = new Response(body, {
|
|
49
63
|
...(init ?? {}),
|
|
@@ -57,7 +71,7 @@ export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init
|
|
|
57
71
|
return response
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
export function
|
|
74
|
+
export function notImplementedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
61
75
|
const response = new Response(body, {
|
|
62
76
|
...(init ?? {}),
|
|
63
77
|
status: 501,
|
package/src/utils/walk-path.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function isObjectNode (node: UnixFSEntry): node is ObjectNode {
|
|
|
52
52
|
*/
|
|
53
53
|
export async function handlePathWalking ({ cid, path, resource, options, blockstore, log }: PluginContext & { blockstore: Blockstore, log: Logger }): Promise<PathWalkerResponse | Response> {
|
|
54
54
|
try {
|
|
55
|
-
return await walkPath(blockstore, `${cid
|
|
55
|
+
return await walkPath(blockstore, `${cid}/${path}`, options)
|
|
56
56
|
} catch (err: any) {
|
|
57
57
|
if (options?.signal?.aborted) {
|
|
58
58
|
throw new AbortError(options?.signal?.reason)
|
package/src/verified-fetch.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { dnsLink } from '@helia/dnslink'
|
|
2
2
|
import { ipnsResolver } from '@helia/ipns'
|
|
3
3
|
import { AbortError } from '@libp2p/interface'
|
|
4
|
-
import { prefixLogger } from '@libp2p/logger'
|
|
5
4
|
import { CustomProgressEvent } from 'progress-events'
|
|
6
5
|
import QuickLRU from 'quick-lru'
|
|
7
6
|
import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
|
|
@@ -15,6 +14,7 @@ import { RawPlugin } from './plugins/plugin-handle-raw.js'
|
|
|
15
14
|
import { TarPlugin } from './plugins/plugin-handle-tar.js'
|
|
16
15
|
import { URLResolver } from './url-resolver.ts'
|
|
17
16
|
import { contentTypeParser } from './utils/content-type-parser.js'
|
|
17
|
+
import { errorToObject } from './utils/error-to-object.ts'
|
|
18
18
|
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
19
19
|
import { getETag } from './utils/get-e-tag.js'
|
|
20
20
|
import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
|
|
@@ -22,7 +22,7 @@ import { getRedirectResponse } from './utils/handle-redirects.js'
|
|
|
22
22
|
import { uriEncodeIPFSPath } from './utils/ipfs-path-to-string.ts'
|
|
23
23
|
import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
|
|
24
24
|
import { setCacheControlHeader } from './utils/response-headers.js'
|
|
25
|
-
import { badRequestResponse, notAcceptableResponse,
|
|
25
|
+
import { badRequestResponse, notAcceptableResponse, internalServerErrorResponse, notImplementedResponse } from './utils/responses.js'
|
|
26
26
|
import { selectOutputType } from './utils/select-output-type.js'
|
|
27
27
|
import { ServerTiming } from './utils/server-timing.js'
|
|
28
28
|
import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, ResolveURLResult, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
@@ -83,7 +83,7 @@ export class VerifiedFetch {
|
|
|
83
83
|
|
|
84
84
|
const pluginOptions: PluginOptions = {
|
|
85
85
|
...init,
|
|
86
|
-
logger:
|
|
86
|
+
logger: helia.logger.forComponent('verified-fetch'),
|
|
87
87
|
getBlockstore: (cid, resource, useSession, options) => this.getBlockstore(cid, resource, useSession, options),
|
|
88
88
|
helia,
|
|
89
89
|
contentTypeParser: this.contentTypeParser,
|
|
@@ -116,8 +116,6 @@ export class VerifiedFetch {
|
|
|
116
116
|
} else {
|
|
117
117
|
this.plugins = defaultPlugins
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
this.log.trace('created VerifiedFetch instance')
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
|
|
@@ -160,30 +158,26 @@ export class VerifiedFetch {
|
|
|
160
158
|
// set Content-Disposition header
|
|
161
159
|
let contentDisposition: string | undefined
|
|
162
160
|
|
|
163
|
-
this.log.trace('checking for content disposition')
|
|
164
|
-
|
|
165
161
|
// force download if requested
|
|
166
162
|
if (context?.query?.download === true) {
|
|
163
|
+
this.log.trace('download requested')
|
|
167
164
|
contentDisposition = 'attachment'
|
|
168
|
-
} else {
|
|
169
|
-
this.log.trace('download not requested')
|
|
170
165
|
}
|
|
171
166
|
|
|
172
167
|
// override filename if requested
|
|
173
168
|
if (context?.query?.filename != null) {
|
|
169
|
+
this.log.trace('specific filename requested')
|
|
170
|
+
|
|
174
171
|
if (contentDisposition == null) {
|
|
175
172
|
contentDisposition = 'inline'
|
|
176
173
|
}
|
|
177
174
|
|
|
178
175
|
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(context.query.filename)}`
|
|
179
|
-
} else {
|
|
180
|
-
this.log.trace('no filename specified in query')
|
|
181
176
|
}
|
|
182
177
|
|
|
183
178
|
if (contentDisposition != null) {
|
|
179
|
+
this.log.trace('content disposition %s', contentDisposition)
|
|
184
180
|
response.headers.set('Content-Disposition', contentDisposition)
|
|
185
|
-
} else {
|
|
186
|
-
this.log.trace('no content disposition specified')
|
|
187
181
|
}
|
|
188
182
|
|
|
189
183
|
if (context?.cid != null && response.headers.get('etag') == null) {
|
|
@@ -240,7 +234,7 @@ export class VerifiedFetch {
|
|
|
240
234
|
* Runs plugins in a loop. After each plugin that returns `null` (partial/no final),
|
|
241
235
|
* we re-check `canHandle()` for all plugins in the next iteration if the context changed.
|
|
242
236
|
*/
|
|
243
|
-
private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response
|
|
237
|
+
private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response> {
|
|
244
238
|
let finalResponse: Response | undefined
|
|
245
239
|
let passCount = 0
|
|
246
240
|
const pluginsUsed = new Set<string>()
|
|
@@ -251,20 +245,24 @@ export class VerifiedFetch {
|
|
|
251
245
|
this.log(`starting pipeline pass #${passCount + 1}`)
|
|
252
246
|
passCount++
|
|
253
247
|
|
|
248
|
+
this.log.trace('checking which plugins can handle %c%s with accept %o', context.cid, context.path != null ? `/${context.path}` : '', context.accept)
|
|
249
|
+
|
|
254
250
|
// gather plugins that say they can handle the *current* context, but haven't been used yet
|
|
255
251
|
const readyPlugins = this.plugins.filter(p => !pluginsUsed.has(p.id)).filter(p => p.canHandle(context))
|
|
252
|
+
|
|
256
253
|
if (readyPlugins.length === 0) {
|
|
257
|
-
this.log.trace('no plugins can handle the current context
|
|
254
|
+
this.log.trace('no plugins can handle the current context, checking by CID code')
|
|
258
255
|
const plugins = this.plugins.filter(p => p.codes.includes(context.cid.code))
|
|
256
|
+
|
|
259
257
|
if (plugins.length > 0) {
|
|
260
258
|
readyPlugins.push(...plugins)
|
|
261
259
|
} else {
|
|
262
|
-
this.log.trace('no plugins found that can handle request by CID code; exiting pipeline
|
|
260
|
+
this.log.trace('no plugins found that can handle request by CID code; exiting pipeline')
|
|
263
261
|
break
|
|
264
262
|
}
|
|
265
263
|
}
|
|
266
264
|
|
|
267
|
-
this.log.trace('plugins ready to handle request: ', readyPlugins.map(p => p.id).join(', '))
|
|
265
|
+
this.log.trace('plugins ready to handle request: %s', readyPlugins.map(p => p.id).join(', '))
|
|
268
266
|
|
|
269
267
|
// track if any plugin changed the context or returned a response
|
|
270
268
|
let contextChanged = false
|
|
@@ -272,10 +270,13 @@ export class VerifiedFetch {
|
|
|
272
270
|
|
|
273
271
|
for (const plugin of readyPlugins) {
|
|
274
272
|
try {
|
|
275
|
-
this.log
|
|
273
|
+
this.log('invoking plugin: %s', plugin.id)
|
|
276
274
|
pluginsUsed.add(plugin.id)
|
|
277
275
|
|
|
278
276
|
const maybeResponse = await plugin.handle(context)
|
|
277
|
+
|
|
278
|
+
this.log('plugin response %s %o', plugin.id, maybeResponse)
|
|
279
|
+
|
|
279
280
|
if (maybeResponse != null) {
|
|
280
281
|
// if a plugin returns a final Response, short-circuit
|
|
281
282
|
finalResponse = maybeResponse
|
|
@@ -286,12 +287,16 @@ export class VerifiedFetch {
|
|
|
286
287
|
if (context.options?.signal?.aborted) {
|
|
287
288
|
throw new AbortError(context.options?.signal?.reason)
|
|
288
289
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
290
|
+
|
|
291
|
+
this.log.error('error in plugin %s - %e', plugin.id, err)
|
|
292
|
+
|
|
293
|
+
return internalServerErrorResponse(context.resource, JSON.stringify({
|
|
294
|
+
error: errorToObject(err)
|
|
295
|
+
}), {
|
|
296
|
+
headers: {
|
|
297
|
+
'content-type': 'application/json'
|
|
298
|
+
}
|
|
299
|
+
})
|
|
295
300
|
} finally {
|
|
296
301
|
// on each plugin call, check for changes in the context
|
|
297
302
|
const newModificationId = context.modified
|
|
@@ -317,7 +322,13 @@ export class VerifiedFetch {
|
|
|
317
322
|
}
|
|
318
323
|
}
|
|
319
324
|
|
|
320
|
-
return finalResponse
|
|
325
|
+
return finalResponse ?? notImplementedResponse(context.resource, JSON.stringify({
|
|
326
|
+
error: errorToObject(new Error('No verified fetch plugin could handle the request'))
|
|
327
|
+
}), {
|
|
328
|
+
headers: {
|
|
329
|
+
'content-type': 'application/json'
|
|
330
|
+
}
|
|
331
|
+
})
|
|
321
332
|
}
|
|
322
333
|
|
|
323
334
|
/**
|
|
@@ -363,7 +374,7 @@ export class VerifiedFetch {
|
|
|
363
374
|
const acceptHeader = getResolvedAcceptHeader({ query: parsedResult.query, headers: options?.headers, logger: this.helia.logger })
|
|
364
375
|
|
|
365
376
|
const accept: AcceptHeader | undefined = selectOutputType(parsedResult.cid, acceptHeader)
|
|
366
|
-
this.log('
|
|
377
|
+
this.log('accept %o', accept)
|
|
367
378
|
|
|
368
379
|
if (acceptHeader != null && accept == null) {
|
|
369
380
|
this.log.error('could not fulfil request based on accept header')
|
|
@@ -394,9 +405,16 @@ export class VerifiedFetch {
|
|
|
394
405
|
|
|
395
406
|
const response = await this.runPluginPipeline(context)
|
|
396
407
|
|
|
397
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
|
|
408
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
|
|
409
|
+
cid: parsedResult.cid,
|
|
410
|
+
path: parsedResult.path
|
|
411
|
+
}))
|
|
412
|
+
|
|
413
|
+
if (response == null) {
|
|
414
|
+
this.log.error('no plugin could handle request for %s', resource)
|
|
415
|
+
}
|
|
398
416
|
|
|
399
|
-
return this.handleFinalResponse(response
|
|
417
|
+
return this.handleFinalResponse(response, context)
|
|
400
418
|
}
|
|
401
419
|
|
|
402
420
|
/**
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { FatalPluginErrorOptions, PluginErrorOptions } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* If a plugin encounters an error, it should throw an instance of this class.
|
|
4
|
-
*/
|
|
5
|
-
export declare class PluginError extends Error {
|
|
6
|
-
name: string;
|
|
7
|
-
code: string;
|
|
8
|
-
fatal: boolean;
|
|
9
|
-
details?: Record<string, any>;
|
|
10
|
-
response?: any;
|
|
11
|
-
constructor(code: string, message: string, options?: PluginErrorOptions);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
|
|
15
|
-
* an instance of this class.
|
|
16
|
-
*
|
|
17
|
-
* Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
|
|
18
|
-
* processed further. If you do not have a response to return to the client, you should consider throwing a
|
|
19
|
-
* `PluginError` instead.
|
|
20
|
-
*/
|
|
21
|
-
export declare class PluginFatalError extends PluginError {
|
|
22
|
-
name: string;
|
|
23
|
-
constructor(code: string, message: string, options: FatalPluginErrorOptions);
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/plugins/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE7E;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAC7B,IAAI,SAAgB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,QAAQ,CAAC,EAAE,GAAG,CAAA;gBAER,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB;CAOzE;AAED;;;;;;;GAOG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;IACxC,IAAI,SAAqB;gBAEnB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB;CAI7E"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* If a plugin encounters an error, it should throw an instance of this class.
|
|
3
|
-
*/
|
|
4
|
-
export class PluginError extends Error {
|
|
5
|
-
name = 'PluginError';
|
|
6
|
-
code;
|
|
7
|
-
fatal;
|
|
8
|
-
details;
|
|
9
|
-
response;
|
|
10
|
-
constructor(code, message, options) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.code = code;
|
|
13
|
-
this.fatal = options?.fatal ?? false;
|
|
14
|
-
this.details = options?.details;
|
|
15
|
-
this.response = options?.response;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
|
|
20
|
-
* an instance of this class.
|
|
21
|
-
*
|
|
22
|
-
* Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
|
|
23
|
-
* processed further. If you do not have a response to return to the client, you should consider throwing a
|
|
24
|
-
* `PluginError` instead.
|
|
25
|
-
*/
|
|
26
|
-
export class PluginFatalError extends PluginError {
|
|
27
|
-
name = 'PluginFatalError';
|
|
28
|
-
constructor(code, message, options) {
|
|
29
|
-
super(code, message, { ...options, fatal: true });
|
|
30
|
-
this.name = 'PluginFatalError';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
//# sourceMappingURL=errors.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/plugins/errors.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC7B,IAAI,GAAG,aAAa,CAAA;IACpB,IAAI,CAAQ;IACZ,KAAK,CAAS;IACd,OAAO,CAAsB;IAC7B,QAAQ,CAAM;IAErB,YAAa,IAAY,EAAE,OAAe,EAAE,OAA4B;QACtE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,KAAK,CAAA;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,CAAA;QAC/B,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,CAAA;IACnC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAiB,SAAQ,WAAW;IACxC,IAAI,GAAG,kBAAkB,CAAA;IAEhC,YAAa,IAAY,EAAE,OAAe,EAAE,OAAgC;QAC1E,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF"}
|
package/src/plugins/errors.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { FatalPluginErrorOptions, PluginErrorOptions } from './types.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* If a plugin encounters an error, it should throw an instance of this class.
|
|
5
|
-
*/
|
|
6
|
-
export class PluginError extends Error {
|
|
7
|
-
public name = 'PluginError'
|
|
8
|
-
public code: string
|
|
9
|
-
public fatal: boolean
|
|
10
|
-
public details?: Record<string, any>
|
|
11
|
-
public response?: any
|
|
12
|
-
|
|
13
|
-
constructor (code: string, message: string, options?: PluginErrorOptions) {
|
|
14
|
-
super(message)
|
|
15
|
-
this.code = code
|
|
16
|
-
this.fatal = options?.fatal ?? false
|
|
17
|
-
this.details = options?.details
|
|
18
|
-
this.response = options?.response
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
|
|
24
|
-
* an instance of this class.
|
|
25
|
-
*
|
|
26
|
-
* Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
|
|
27
|
-
* processed further. If you do not have a response to return to the client, you should consider throwing a
|
|
28
|
-
* `PluginError` instead.
|
|
29
|
-
*/
|
|
30
|
-
export class PluginFatalError extends PluginError {
|
|
31
|
-
public name = 'PluginFatalError'
|
|
32
|
-
|
|
33
|
-
constructor (code: string, message: string, options: FatalPluginErrorOptions) {
|
|
34
|
-
super(code, message, { ...options, fatal: true })
|
|
35
|
-
this.name = 'PluginFatalError'
|
|
36
|
-
}
|
|
37
|
-
}
|