@helia/verified-fetch 0.0.0-8a5bc6f → 0.0.0-f58d467

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.
Files changed (31) hide show
  1. package/README.md +80 -43
  2. package/dist/index.min.js +4 -29
  3. package/dist/src/index.d.ts +110 -51
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +86 -47
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/singleton.d.ts +3 -0
  8. package/dist/src/singleton.d.ts.map +1 -0
  9. package/dist/src/singleton.js +15 -0
  10. package/dist/src/singleton.js.map +1 -0
  11. package/dist/src/utils/get-stream-from-async-iterable.d.ts +10 -0
  12. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -0
  13. package/dist/src/utils/{get-stream-and-content-type.js → get-stream-from-async-iterable.js} +10 -9
  14. package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -0
  15. package/dist/src/verified-fetch.d.ts +4 -1
  16. package/dist/src/verified-fetch.d.ts.map +1 -1
  17. package/dist/src/verified-fetch.js +34 -6
  18. package/dist/src/verified-fetch.js.map +1 -1
  19. package/package.json +19 -19
  20. package/src/index.ts +117 -52
  21. package/src/singleton.ts +20 -0
  22. package/src/utils/{get-stream-and-content-type.ts → get-stream-from-async-iterable.ts} +9 -8
  23. package/src/verified-fetch.ts +41 -10
  24. package/dist/src/utils/get-content-type.d.ts +0 -11
  25. package/dist/src/utils/get-content-type.d.ts.map +0 -1
  26. package/dist/src/utils/get-content-type.js +0 -43
  27. package/dist/src/utils/get-content-type.js.map +0 -1
  28. package/dist/src/utils/get-stream-and-content-type.d.ts +0 -10
  29. package/dist/src/utils/get-stream-and-content-type.d.ts.map +0 -1
  30. package/dist/src/utils/get-stream-and-content-type.js.map +0 -1
  31. package/src/utils/get-content-type.ts +0 -55
@@ -0,0 +1,20 @@
1
+ import { createVerifiedFetch } from './index.js'
2
+ import type { Resource, VerifiedFetch, VerifiedFetchInit } from './index.js'
3
+
4
+ let impl: VerifiedFetch | undefined
5
+
6
+ export const verifiedFetch: VerifiedFetch = async function verifiedFetch (resource: Resource, options?: VerifiedFetchInit): Promise<Response> {
7
+ if (impl == null) {
8
+ impl = await createVerifiedFetch()
9
+ }
10
+
11
+ return impl(resource, options)
12
+ }
13
+
14
+ verifiedFetch.start = async function () {
15
+ await impl?.start()
16
+ }
17
+
18
+ verifiedFetch.stop = async function () {
19
+ await impl?.stop()
20
+ }
@@ -1,27 +1,25 @@
1
1
  import { CustomProgressEvent } from 'progress-events'
2
- import { getContentType } from './get-content-type.js'
3
2
  import type { VerifiedFetchInit } from '../index.js'
4
3
  import type { ComponentLogger } from '@libp2p/interface'
5
4
 
6
5
  /**
7
- * Converts an async iterator of Uint8Array bytes to a stream and attempts to determine the content type of those bytes.
6
+ * Converts an async iterator of Uint8Array bytes to a stream and returns the first chunk of bytes
8
7
  */
9
- export async function getStreamAndContentType (iterator: AsyncIterable<Uint8Array>, path: string, logger: ComponentLogger, options?: Pick<VerifiedFetchInit, 'onProgress'>): Promise<{ contentType: string, stream: ReadableStream<Uint8Array> }> {
10
- const log = logger.forComponent('helia:verified-fetch:get-stream-and-content-type')
8
+ export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, path: string, logger: ComponentLogger, options?: Pick<VerifiedFetchInit, 'onProgress'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
9
+ const log = logger.forComponent('helia:verified-fetch:get-stream-from-async-iterable')
11
10
  const reader = iterator[Symbol.asyncIterator]()
12
- const { value, done } = await reader.next()
11
+ const { value: firstChunk, done } = await reader.next()
13
12
 
14
13
  if (done === true) {
15
14
  log.error('No content found for path', path)
16
15
  throw new Error('No content found')
17
16
  }
18
17
 
19
- const contentType = await getContentType({ bytes: value, path })
20
18
  const stream = new ReadableStream({
21
19
  async start (controller) {
22
20
  // the initial value is already available
23
21
  options?.onProgress?.(new CustomProgressEvent<void>('verified-fetch:request:progress:chunk'))
24
- controller.enqueue(value)
22
+ controller.enqueue(firstChunk)
25
23
  },
26
24
  async pull (controller) {
27
25
  const { value, done } = await reader.next()
@@ -40,5 +38,8 @@ export async function getStreamAndContentType (iterator: AsyncIterable<Uint8Arra
40
38
  }
41
39
  })
42
40
 
43
- return { contentType, stream }
41
+ return {
42
+ stream,
43
+ firstChunk
44
+ }
44
45
  }
@@ -10,10 +10,10 @@ import { code as dagPbCode } from '@ipld/dag-pb'
10
10
  import { code as jsonCode } from 'multiformats/codecs/json'
11
11
  import { decode, code as rawCode } from 'multiformats/codecs/raw'
12
12
  import { CustomProgressEvent } from 'progress-events'
13
- import { getStreamAndContentType } from './utils/get-stream-and-content-type.js'
13
+ import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
14
14
  import { parseResource } from './utils/parse-resource.js'
15
15
  import { walkPath, type PathWalkerFn } from './utils/walk-path.js'
16
- import type { CIDDetail, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
16
+ import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
17
17
  import type { Helia } from '@helia/interface'
18
18
  import type { AbortOptions, Logger } from '@libp2p/interface'
19
19
  import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
@@ -32,9 +32,8 @@ interface VerifiedFetchComponents {
32
32
  /**
33
33
  * Potential future options for the VerifiedFetch constructor.
34
34
  */
35
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
36
35
  interface VerifiedFetchInit {
37
-
36
+ contentTypeParser?: ContentTypeParser
38
37
  }
39
38
 
40
39
  interface FetchHandlerFunctionArg {
@@ -72,6 +71,7 @@ export class VerifiedFetch {
72
71
  private readonly json: JSON
73
72
  private readonly pathWalker: PathWalkerFn
74
73
  private readonly log: Logger
74
+ private readonly contentTypeParser: ContentTypeParser | undefined
75
75
 
76
76
  constructor ({ helia, ipns, unixfs, dagJson, json, dagCbor, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
77
77
  this.helia = helia
@@ -87,6 +87,7 @@ export class VerifiedFetch {
87
87
  this.json = json ?? heliaJson(helia)
88
88
  this.dagCbor = dagCbor ?? heliaDagCbor(helia)
89
89
  this.pathWalker = pathWalker ?? walkPath
90
+ this.contentTypeParser = init?.contentTypeParser
90
91
  this.log.trace('created VerifiedFetch instance')
91
92
  }
92
93
 
@@ -133,13 +134,13 @@ export class VerifiedFetch {
133
134
  private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
134
135
  this.log.trace('fetching %c/%s', cid, path)
135
136
  options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: cid.toString(), path }))
136
- const result = await this.dagCbor.get(cid, {
137
+ const result = await this.dagCbor.get<Uint8Array>(cid, {
137
138
  signal: options?.signal,
138
139
  onProgress: options?.onProgress
139
140
  })
140
141
  options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: cid.toString(), path }))
141
- const response = new Response(JSON.stringify(result), { status: 200 })
142
- response.headers.set('content-type', 'application/json')
142
+ const response = new Response(result, { status: 200 })
143
+ await this.setContentType(result, path, response)
143
144
  return response
144
145
  }
145
146
 
@@ -179,11 +180,11 @@ export class VerifiedFetch {
179
180
  options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID.toString(), path: '' }))
180
181
  this.log('got async iterator for %c/%s', cid, path)
181
182
 
182
- const { contentType, stream } = await getStreamAndContentType(asyncIter, path ?? '', this.helia.logger, {
183
+ const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
183
184
  onProgress: options?.onProgress
184
185
  })
185
186
  const response = new Response(stream, { status: 200 })
186
- response.headers.set('content-type', contentType)
187
+ await this.setContentType(firstChunk, path, response)
187
188
 
188
189
  return response
189
190
  }
@@ -194,10 +195,36 @@ export class VerifiedFetch {
194
195
  const result = await this.helia.blockstore.get(cid)
195
196
  options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: cid.toString(), path }))
196
197
  const response = new Response(decode(result), { status: 200 })
197
- response.headers.set('content-type', 'application/octet-stream')
198
+ await this.setContentType(result, path, response)
198
199
  return response
199
200
  }
200
201
 
202
+ private async setContentType (bytes: Uint8Array, path: string, response: Response): Promise<void> {
203
+ let contentType = 'application/octet-stream'
204
+
205
+ if (this.contentTypeParser != null) {
206
+ try {
207
+ let fileName = path.split('/').pop()?.trim()
208
+ fileName = fileName === '' ? undefined : fileName
209
+ const parsed = this.contentTypeParser(bytes, fileName)
210
+
211
+ if (isPromise(parsed)) {
212
+ const result = await parsed
213
+
214
+ if (result != null) {
215
+ contentType = result
216
+ }
217
+ } else if (parsed != null) {
218
+ contentType = parsed
219
+ }
220
+ } catch (err) {
221
+ this.log.error('Error parsing content type', err)
222
+ }
223
+ }
224
+
225
+ response.headers.set('content-type', contentType)
226
+ }
227
+
201
228
  /**
202
229
  * Determines the format requested by the client, defaults to `null` if no format is requested.
203
230
  *
@@ -321,3 +348,7 @@ export class VerifiedFetch {
321
348
  await this.helia.stop()
322
349
  }
323
350
  }
351
+
352
+ function isPromise <T> (p?: any): p is Promise<T> {
353
+ return p?.then != null
354
+ }
@@ -1,11 +0,0 @@
1
- interface TestInput {
2
- bytes: Uint8Array;
3
- path: string;
4
- }
5
- export declare const DEFAULT_MIME_TYPE = "application/octet-stream";
6
- /**
7
- * Get the content type from the input based on the tests.
8
- */
9
- export declare function getContentType(input: TestInput): Promise<string>;
10
- export {};
11
- //# sourceMappingURL=get-content-type.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-content-type.d.ts","sourceRoot":"","sources":["../../../src/utils/get-content-type.ts"],"names":[],"mappings":"AAEA,UAAU,SAAS;IACjB,KAAK,EAAE,UAAU,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb;AAID,eAAO,MAAM,iBAAiB,6BAA6B,CAAA;AAkC3D;;GAEG;AACH,wBAAsB,cAAc,CAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAQvE"}
@@ -1,43 +0,0 @@
1
- import mime from 'mime-types';
2
- export const DEFAULT_MIME_TYPE = 'application/octet-stream';
3
- const xmlRegex = /^(<\?xml[^>]+>)?[^<^\w]+<svg/ig;
4
- /**
5
- * Tests to determine the content type of the input.
6
- * The order is important on this one.
7
- */
8
- const tests = [
9
- // svg
10
- async ({ bytes }) => xmlRegex.test(new TextDecoder().decode(bytes.slice(0, 64)))
11
- ? 'image/svg+xml'
12
- : undefined,
13
- // testing file-type from path
14
- async ({ path }) => {
15
- const mimeType = mime.lookup(path);
16
- if (mimeType !== false) {
17
- return mimeType;
18
- }
19
- return undefined;
20
- }
21
- ];
22
- const overrides = {
23
- 'video/quicktime': 'video/mp4'
24
- };
25
- /**
26
- * Override the content type based on overrides.
27
- */
28
- function overrideContentType(type) {
29
- return overrides[type] ?? type;
30
- }
31
- /**
32
- * Get the content type from the input based on the tests.
33
- */
34
- export async function getContentType(input) {
35
- for (const test of tests) {
36
- const type = await test(input);
37
- if (type !== undefined) {
38
- return overrideContentType(type);
39
- }
40
- }
41
- return DEFAULT_MIME_TYPE;
42
- }
43
- //# sourceMappingURL=get-content-type.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-content-type.js","sourceRoot":"","sources":["../../../src/utils/get-content-type.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,YAAY,CAAA;AAS7B,MAAM,CAAC,MAAM,iBAAiB,GAAG,0BAA0B,CAAA;AAE3D,MAAM,QAAQ,GAAG,gCAAgC,CAAA;AAEjD;;;GAGG;AACH,MAAM,KAAK,GAA4C;IACrD,MAAM;IACN,KAAK,EAAE,EAAE,KAAK,EAAE,EAAc,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1F,CAAC,CAAC,eAAe;QACjB,CAAC,CAAC,SAAS;IACb,8BAA8B;IAC9B,KAAK,EAAE,EAAE,IAAI,EAAE,EAAc,EAAE;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAClC,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YACvB,OAAO,QAAQ,CAAA;QACjB,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;CACF,CAAA;AAED,MAAM,SAAS,GAA2B;IACxC,iBAAiB,EAAE,WAAW;CAC/B,CAAA;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAE,IAAY;IACxC,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;AAChC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAE,KAAgB;IACpD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,CAAA;QAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAA;QAClC,CAAC;IACH,CAAC;IACD,OAAO,iBAAiB,CAAA;AAC1B,CAAC"}
@@ -1,10 +0,0 @@
1
- import type { VerifiedFetchInit } from '../index.js';
2
- import type { ComponentLogger } from '@libp2p/interface';
3
- /**
4
- * Converts an async iterator of Uint8Array bytes to a stream and attempts to determine the content type of those bytes.
5
- */
6
- export declare function getStreamAndContentType(iterator: AsyncIterable<Uint8Array>, path: string, logger: ComponentLogger, options?: Pick<VerifiedFetchInit, 'onProgress'>): Promise<{
7
- contentType: string;
8
- stream: ReadableStream<Uint8Array>;
9
- }>;
10
- //# sourceMappingURL=get-stream-and-content-type.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-stream-and-content-type.d.ts","sourceRoot":"","sources":["../../../src/utils/get-stream-and-content-type.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD;;GAEG;AACH,wBAAsB,uBAAuB,CAAE,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;CAAE,CAAC,CAmChP"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-stream-and-content-type.js","sourceRoot":"","sources":["../../../src/utils/get-stream-and-content-type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAItD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAE,QAAmC,EAAE,IAAY,EAAE,MAAuB,EAAE,OAA+C;IACxK,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,kDAAkD,CAAC,CAAA;IACnF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAA;IAC/C,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;IAE3C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,GAAG,CAAC,KAAK,CAAC,2BAA2B,EAAE,IAAI,CAAC,CAAA;QAC5C,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACrC,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAChE,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC;QAChC,KAAK,CAAC,KAAK,CAAE,UAAU;YACrB,yCAAyC;YACzC,OAAO,EAAE,UAAU,EAAE,CAAC,IAAI,mBAAmB,CAAO,uCAAuC,CAAC,CAAC,CAAA;YAC7F,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;QACD,KAAK,CAAC,IAAI,CAAE,UAAU;YACpB,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;YAE3C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;oBAClB,OAAO,EAAE,UAAU,EAAE,CAAC,IAAI,mBAAmB,CAAO,uCAAuC,CAAC,CAAC,CAAA;oBAC7F,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;gBAC3B,CAAC;gBACD,UAAU,CAAC,KAAK,EAAE,CAAA;gBAClB,OAAM;YACR,CAAC;YAED,OAAO,EAAE,UAAU,EAAE,CAAC,IAAI,mBAAmB,CAAO,uCAAuC,CAAC,CAAC,CAAA;YAC7F,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;KACF,CAAC,CAAA;IAEF,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,CAAA;AAChC,CAAC"}
@@ -1,55 +0,0 @@
1
- import mime from 'mime-types'
2
-
3
- interface TestInput {
4
- bytes: Uint8Array
5
- path: string
6
- }
7
-
8
- type TestOutput = Promise<string | undefined>
9
-
10
- export const DEFAULT_MIME_TYPE = 'application/octet-stream'
11
-
12
- const xmlRegex = /^(<\?xml[^>]+>)?[^<^\w]+<svg/ig
13
-
14
- /**
15
- * Tests to determine the content type of the input.
16
- * The order is important on this one.
17
- */
18
- const tests: Array<(input: TestInput) => TestOutput> = [
19
- // svg
20
- async ({ bytes }): TestOutput => xmlRegex.test(new TextDecoder().decode(bytes.slice(0, 64)))
21
- ? 'image/svg+xml'
22
- : undefined,
23
- // testing file-type from path
24
- async ({ path }): TestOutput => {
25
- const mimeType = mime.lookup(path)
26
- if (mimeType !== false) {
27
- return mimeType
28
- }
29
- return undefined
30
- }
31
- ]
32
-
33
- const overrides: Record<string, string> = {
34
- 'video/quicktime': 'video/mp4'
35
- }
36
-
37
- /**
38
- * Override the content type based on overrides.
39
- */
40
- function overrideContentType (type: string): string {
41
- return overrides[type] ?? type
42
- }
43
-
44
- /**
45
- * Get the content type from the input based on the tests.
46
- */
47
- export async function getContentType (input: TestInput): Promise<string> {
48
- for (const test of tests) {
49
- const type = await test(input)
50
- if (type !== undefined) {
51
- return overrideContentType(type)
52
- }
53
- }
54
- return DEFAULT_MIME_TYPE
55
- }