@helia/verified-fetch 2.3.1 → 2.5.0

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 (119) hide show
  1. package/README.md +200 -0
  2. package/dist/index.min.js +357 -35
  3. package/dist/src/index.d.ts +220 -0
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +200 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/plugins/errors.d.ts +25 -0
  8. package/dist/src/plugins/errors.d.ts.map +1 -0
  9. package/dist/src/plugins/errors.js +33 -0
  10. package/dist/src/plugins/errors.js.map +1 -0
  11. package/dist/src/plugins/index.d.ts +8 -0
  12. package/dist/src/plugins/index.d.ts.map +1 -0
  13. package/dist/src/plugins/index.js +7 -0
  14. package/dist/src/plugins/index.js.map +1 -0
  15. package/dist/src/plugins/plugin-base.d.ts +19 -0
  16. package/dist/src/plugins/plugin-base.d.ts.map +1 -0
  17. package/dist/src/plugins/plugin-base.js +26 -0
  18. package/dist/src/plugins/plugin-base.js.map +1 -0
  19. package/dist/src/plugins/plugin-handle-car.d.ts +11 -0
  20. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -0
  21. package/dist/src/plugins/plugin-handle-car.js +28 -0
  22. package/dist/src/plugins/plugin-handle-car.js.map +1 -0
  23. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts +11 -0
  24. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -0
  25. package/dist/src/plugins/plugin-handle-dag-cbor.js +73 -0
  26. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -0
  27. package/dist/src/plugins/plugin-handle-dag-pb.d.ts +15 -0
  28. package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -0
  29. package/dist/src/plugins/plugin-handle-dag-pb.js +152 -0
  30. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -0
  31. package/dist/src/plugins/plugin-handle-dag-walk.d.ts +16 -0
  32. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -0
  33. package/dist/src/plugins/plugin-handle-dag-walk.js +45 -0
  34. package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -0
  35. package/dist/src/plugins/plugin-handle-dir-index-html.d.ts +9 -0
  36. package/dist/src/plugins/plugin-handle-dir-index-html.d.ts.map +1 -0
  37. package/dist/src/plugins/plugin-handle-dir-index-html.js +37 -0
  38. package/dist/src/plugins/plugin-handle-dir-index-html.js.map +1 -0
  39. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +12 -0
  40. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -0
  41. package/dist/src/plugins/plugin-handle-ipns-record.js +62 -0
  42. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -0
  43. package/dist/src/plugins/plugin-handle-json.d.ts +11 -0
  44. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -0
  45. package/dist/src/plugins/plugin-handle-json.js +51 -0
  46. package/dist/src/plugins/plugin-handle-json.js.map +1 -0
  47. package/dist/src/plugins/plugin-handle-raw.d.ts +8 -0
  48. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -0
  49. package/dist/src/plugins/plugin-handle-raw.js +80 -0
  50. package/dist/src/plugins/plugin-handle-raw.js.map +1 -0
  51. package/dist/src/plugins/plugin-handle-tar.d.ts +12 -0
  52. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -0
  53. package/dist/src/plugins/plugin-handle-tar.js +36 -0
  54. package/dist/src/plugins/plugin-handle-tar.js.map +1 -0
  55. package/dist/src/plugins/plugins.d.ts +5 -0
  56. package/dist/src/plugins/plugins.d.ts.map +1 -0
  57. package/dist/src/plugins/plugins.js +5 -0
  58. package/dist/src/plugins/plugins.js.map +1 -0
  59. package/dist/src/plugins/types.d.ts +68 -0
  60. package/dist/src/plugins/types.d.ts.map +1 -0
  61. package/dist/src/plugins/types.js +2 -0
  62. package/dist/src/plugins/types.js.map +1 -0
  63. package/dist/src/types.d.ts +0 -23
  64. package/dist/src/types.d.ts.map +1 -1
  65. package/dist/src/types.js +1 -2
  66. package/dist/src/types.js.map +1 -1
  67. package/dist/src/utils/dir-index-html.d.ts +16 -0
  68. package/dist/src/utils/dir-index-html.d.ts.map +1 -0
  69. package/dist/src/utils/dir-index-html.js +387 -0
  70. package/dist/src/utils/dir-index-html.js.map +1 -0
  71. package/dist/src/utils/get-e-tag.d.ts +1 -1
  72. package/dist/src/utils/get-e-tag.d.ts.map +1 -1
  73. package/dist/src/utils/get-e-tag.js +18 -3
  74. package/dist/src/utils/get-e-tag.js.map +1 -1
  75. package/dist/src/utils/parse-resource.d.ts +2 -1
  76. package/dist/src/utils/parse-resource.d.ts.map +1 -1
  77. package/dist/src/utils/parse-resource.js +4 -3
  78. package/dist/src/utils/parse-resource.js.map +1 -1
  79. package/dist/src/utils/parse-url-string.d.ts +8 -3
  80. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  81. package/dist/src/utils/parse-url-string.js +30 -4
  82. package/dist/src/utils/parse-url-string.js.map +1 -1
  83. package/dist/src/utils/server-timing.d.ts +13 -0
  84. package/dist/src/utils/server-timing.d.ts.map +1 -0
  85. package/dist/src/utils/server-timing.js +19 -0
  86. package/dist/src/utils/server-timing.js.map +1 -0
  87. package/dist/src/utils/walk-path.d.ts +3 -2
  88. package/dist/src/utils/walk-path.d.ts.map +1 -1
  89. package/dist/src/utils/walk-path.js +1 -1
  90. package/dist/src/utils/walk-path.js.map +1 -1
  91. package/dist/src/verified-fetch.d.ts +11 -20
  92. package/dist/src/verified-fetch.d.ts.map +1 -1
  93. package/dist/src/verified-fetch.js +174 -367
  94. package/dist/src/verified-fetch.js.map +1 -1
  95. package/dist/typedoc-urls.json +32 -24
  96. package/package.json +6 -2
  97. package/src/index.ts +223 -0
  98. package/src/plugins/errors.ts +37 -0
  99. package/src/plugins/index.ts +8 -0
  100. package/src/plugins/plugin-base.ts +30 -0
  101. package/src/plugins/plugin-handle-car.ts +32 -0
  102. package/src/plugins/plugin-handle-dag-cbor.ts +84 -0
  103. package/src/plugins/plugin-handle-dag-pb.ts +168 -0
  104. package/src/plugins/plugin-handle-dag-walk.ts +53 -0
  105. package/src/plugins/plugin-handle-dir-index-html.ts +44 -0
  106. package/src/plugins/plugin-handle-ipns-record.ts +69 -0
  107. package/src/plugins/plugin-handle-json.ts +57 -0
  108. package/src/plugins/plugin-handle-raw.ts +92 -0
  109. package/src/plugins/plugin-handle-tar.ts +44 -0
  110. package/src/plugins/plugins.ts +4 -0
  111. package/src/plugins/types.ts +73 -0
  112. package/src/types.ts +0 -29
  113. package/src/utils/dir-index-html.ts +445 -0
  114. package/src/utils/get-e-tag.ts +20 -3
  115. package/src/utils/parse-resource.ts +5 -4
  116. package/src/utils/parse-url-string.ts +38 -7
  117. package/src/utils/server-timing.ts +37 -0
  118. package/src/utils/walk-path.ts +3 -3
  119. package/src/verified-fetch.ts +198 -403
package/README.md CHANGED
@@ -635,6 +635,14 @@ Some known header specifications:
635
635
  - <https://specs.ipfs.tech/http-gateways/trustless-gateway/#response-headers>
636
636
  - <https://specs.ipfs.tech/http-gateways/subdomain-gateway/#response-headers>
637
637
 
638
+ #### Server Timing headers
639
+
640
+ By default, we do not include [Server Timing](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Server_timing) headers in responses. If you want to include them, you can pass an
641
+ `withServerTiming` option to the `createVerifiedFetch` function to include them in all future responses. You can
642
+ also pass the `withServerTiming` option to each fetch call to include them only for that specific response.
643
+
644
+ See PR where this was added, <https://github.com/ipfs/helia-verified-fetch/pull/164>, for more information.
645
+
638
646
  ### Possible Scenarios that could cause confusion
639
647
 
640
648
  #### Attempting to fetch the CID for content that does not make sense
@@ -650,6 +658,198 @@ Known Errors that can be thrown:
650
658
  3. `TypeError` - If the options argument is passed and is malformed.
651
659
  4. `AbortError` - If the content request is aborted due to user aborting provided AbortSignal. Note that this is a `AbortError` from `@libp2p/interface` and not the standard `AbortError` from the Fetch API.
652
660
 
661
+ ## Pluggability and Extensibility
662
+
663
+ Verified‑fetch can now be extended to alter how it handles requests by using plugins.
664
+ Plugins are classes that extend the `BasePlugin` class and implement the `VerifiedFetchPlugin`
665
+ interface. They are instantiated with `PluginOptions` when the `VerifiedFetch` class is created.
666
+
667
+ Each plugin must implement two methods:
668
+
669
+ - **`canHandle(context: PluginContext): boolean`**
670
+ Inspects the current `PluginContext` (which includes the CID, path, query, accept header, etc.)
671
+ and returns `true` if the plugin can operate on the current state of the request.
672
+
673
+ - **`handle(context: PluginContext): Promise<Response | null>`**
674
+ Performs the plugin’s work. It may:
675
+ - **Return a final `Response`**: This stops the pipeline immediately.
676
+ - **Return `null`**: This indicates that the plugin has only partially processed the request
677
+ (for example, by performing path walking or decoding) and the pipeline should continue.
678
+ - **Throw a `PluginError`**: This logs a non-fatal error and continues the pipeline.
679
+ - **Throw a `PluginFatalError`**: This logs a fatal error and stops the pipeline immediately.
680
+
681
+ Plugins are executed in a chain (a **plugin pipeline**):
682
+
683
+ 1. **Initialization:**
684
+ - The `VerifiedFetch` class is instantiated with a list of plugins.
685
+ - When a request is made via the `fetch` method, the resource and options are parsed to
686
+ create a mutable `PluginContext` object.
687
+
688
+ 2. **Pipeline Execution:**
689
+
690
+ - The pipeline repeatedly checks, up to a maximum number of passes (default = 3), which plugins
691
+ are currently able to handle the request by calling each plugin’s `canHandle()` method.
692
+ - Plugins that have not yet been called in the current run and return `true` for `canHandle()`
693
+ are invoked in sequence.
694
+ - If a plugin returns a final `Response` or throws a `PluginFatalError`, the pipeline immediately
695
+ stops and that response is returned.
696
+ - If a plugin returns `null`, it may have updated the context (for example, by
697
+ performing path walking), other plugins that said they `canHandle` will run.
698
+ - If no plugin modifies the context (i.e. no change to `context.modified`) and no final response is
699
+ produced after iterating through all plugins, the pipeline exits and a default “Not Supported”
700
+ response is returned.
701
+
702
+ **Diagram of the Plugin Pipeline:**
703
+
704
+ ```mermaid
705
+ flowchart TD
706
+ A[Resource & Options] --> B[Parse into PluginContext]
707
+ B --> C[Plugin Pipeline]
708
+ subgraph IP[Iterative Passes max 3 passes]
709
+ C1[Check canHandle for each plugin]
710
+ C2[Call handle on ready plugins]
711
+ C3[Update PluginContext if partial work is done]
712
+ C1 --> C2
713
+ C2 --> C3
714
+ end
715
+ C --> IP
716
+ IP --> D[Final Response]
717
+ ```
718
+
719
+ 3. **Finalization:**
720
+ - After the pipeline completes, the resulting response & context is processed (e.g. headers such as ETag,
721
+ Cache‑Control, and Content‑Disposition are set) and returned.
722
+
723
+ Please see the original discussion on extensibility in [Issue #167](https://github.com/ipfs/helia-verified-fetch/issues/167).
724
+
725
+ ***
726
+
727
+ ### Extending Verified‑Fetch with Custom Plugins
728
+
729
+ To add your own plugin:
730
+
731
+ 1. **Extend the BasePlugin:**
732
+
733
+ Create a new class that extends `BasePlugin` and implements:
734
+
735
+ - `canHandle(context: PluginContext): boolean`
736
+ - `handle(context: PluginContext): Promise<Response | null>`
737
+
738
+ ## Example - custom plugin
739
+
740
+ ```typescript
741
+ import { BasePlugin, type PluginContext, type VerifiedFetchPluginFactory, type PluginOptions } from '@helia/verified-fetch'
742
+ import { okResponse } from './dist/src/utils/responses.js'
743
+
744
+ export class MyCustomPlugin extends BasePlugin {
745
+ // Optionally, list any codec codes your plugin supports:
746
+ codes = [] //
747
+
748
+ canHandle(context: PluginContext): boolean {
749
+ // Only handle requests if the Accept header matches your custom type
750
+ // Or check context for pathDetails, custom values, etc...
751
+ return context.accept === 'application/vnd.my-custom-type'
752
+ }
753
+
754
+ async handle(context: PluginContext): Promise<Response | null> {
755
+ // Perform any partial processing here, e.g., modify the context:
756
+ context.customProcessed = true;
757
+
758
+ // If you are ready to finalize the response:
759
+ return new Response('Hello, world!', {
760
+ status: 200,
761
+ headers: {
762
+ 'Content-Type': 'text/plain'
763
+ }
764
+ });
765
+
766
+ // Or, if further processing is needed by another plugin, simply return null.
767
+ }
768
+ }
769
+ export const myCustomPluginFactory: VerifiedFetchPluginFactory = (opts: PluginOptions) => new MyCustomPlugin(opts)
770
+ ```
771
+
772
+ 2. **Integrate Your Plugin:**
773
+
774
+ Add your custom plugin to Verified‑Fetch’s plugin list when instantiating Verified‑Fetch:
775
+
776
+ ## Example - Integrate custom plugin
777
+
778
+ ```typescript
779
+ import { createVerifiedFetch, type VerifiedFetchPluginFactory } from '@helia/verified-fetch'
780
+ import { createHelia } from 'helia'
781
+
782
+ const helia = await createHelia()
783
+ const plugins: VerifiedFetchPluginFactory[] = [
784
+ // myCustomPluginFactory
785
+ ]
786
+
787
+ const fetch = await createVerifiedFetch(helia, { plugins })
788
+ ```
789
+
790
+ ***
791
+
792
+ ### Error Handling in the Plugin Pipeline
793
+
794
+ Verified‑Fetch distinguishes between two types of errors thrown by plugins:
795
+
796
+ - **PluginError (Non‑Fatal):**
797
+ - Use this when your plugin encounters an issue that should be logged but does not prevent the pipeline
798
+ from continuing.
799
+ - When a plugin throws a `PluginError`, the error is logged and the pipeline continues with the next plugin.
800
+
801
+ - **PluginFatalError (Fatal):**
802
+ - Use this when a critical error occurs that should immediately abort the request.
803
+ - When a plugin throws a `PluginFatalError`, the pipeline immediately terminates and the provided error
804
+ response is returned.
805
+
806
+ ## Example - Plugin error Handling
807
+
808
+ ```typescript
809
+ import { PluginError, PluginFatalError } from '@helia/verified-fetch'
810
+
811
+ // async handle(context: PluginContext): Promise<Response | null> {
812
+ const recoverable = Math.random() > 0.5 // Use more sophisticated logic here ;)
813
+ if (recoverable === true) {
814
+ throw new PluginError('MY_CUSTOM_WARNING', 'A non‑fatal issue occurred', {
815
+ details: {
816
+ someKey: 'Additional details here'
817
+ }
818
+ });
819
+ }
820
+
821
+ if (recoverable === false) {
822
+ throw new PluginFatalError('MY_CUSTOM_FATAL', 'A critical error occurred', {
823
+ response: new Response('Something happened', { status: 500 }) // Required: supply your own error response
824
+ });
825
+ }
826
+
827
+ // Otherwise, continue processing...
828
+ // }
829
+ ```
830
+
831
+ ### How the Plugin Pipeline Works
832
+
833
+ - **Shared Context:**
834
+ A mutable `PluginContext` is created for each request. It includes the parsed CID, path, query parameters,
835
+ accept header, and any other metadata. Plugins can update this context as they perform partial work (for example,
836
+ by doing path walking or decoding).
837
+
838
+ - **Iterative Processing:**
839
+ The pipeline repeatedly checks which plugins can currently handle the request by calling `canHandle(context)`.
840
+ - Plugins that perform partial processing update the context and return `null`, allowing subsequent passes by other plugins.
841
+ - Once a plugin is ready to finalize the response, it returns a final `Response` and the pipeline terminates.
842
+
843
+ - **No Strict Ordering:**
844
+ Plugins are invoked based solely on whether they can handle the current state of the context.
845
+ This means you do not have to specify a rigid order, each plugin simply checks the context and acts if appropriate.
846
+
847
+ - **Error Handling:**
848
+ - A thrown `PluginError` is considered non‑fatal and is logged, allowing the pipeline to continue.
849
+ - A thrown `PluginFatalError` immediately stops the pipeline and returns the error response.
850
+
851
+ For a detailed explanation of the pipeline, please refer to the discussion in [Issue #167](https://github.com/ipfs/helia-verified-fetch/issues/167).
852
+
653
853
  # Install
654
854
 
655
855
  ```console