@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
@@ -0,0 +1,445 @@
1
+ import type { Logger } from '@libp2p/interface'
2
+ import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
3
+
4
+ /**
5
+ * Types taken from:
6
+ *
7
+ * * https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/assets/assets.go#L92C1-L96C2
8
+ * * https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/assets/assets.go#L114C1-L135C2
9
+ */
10
+
11
+ interface GlobalData {
12
+ // Menu []MenuItem
13
+ gatewayURL: string
14
+ dnsLink: boolean
15
+ // root: UnixFSEntry
16
+ }
17
+
18
+ interface DirectoryTemplateData {
19
+ globalData: GlobalData
20
+ listing: DirectoryItem[]
21
+ size: string
22
+ path: string
23
+ breadcrumbs: Breadcrumb[]
24
+ backLink: string
25
+ hash: string
26
+ name: string
27
+ }
28
+
29
+ interface DirectoryItem {
30
+ size: string
31
+ name: string
32
+ path: string
33
+ hash: string
34
+ shortHash: string
35
+ }
36
+
37
+ interface Breadcrumb {
38
+ name: string
39
+ path: string
40
+ }
41
+
42
+ export interface DirIndexHtmlOptions {
43
+ gatewayURL: string
44
+ dnsLink?: boolean
45
+ log: Logger
46
+ }
47
+
48
+ // see https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/assets/templates.go#L19C1-L25C2
49
+ function iconFromExt (name: string): string {
50
+ // not implemented yet
51
+ // TODO: optimize icons: https://github.com/ipfs-shipyard/ipfs-css/issues/71
52
+ return 'ipfs-_blank'
53
+ }
54
+
55
+ function itemShortHashCell (item: DirectoryItem, dirData: DirectoryTemplateData): string {
56
+ const href = dirData.globalData.dnsLink ? `https://inbrowser.dev/ipfs/${item.hash}` : `${dirData.globalData.gatewayURL}/ipfs/${item.hash}?filename=${item.name}`
57
+
58
+ return `<a class="ipfs-hash" translate="no" href="${href}">${item.shortHash}</a>`
59
+ }
60
+
61
+ function dirListingTitle (dirData: DirectoryTemplateData): string {
62
+ if (dirData.path != null) {
63
+ const href = `${dirData.globalData.gatewayURL}/${dirData.path}`
64
+ return `Index of <a href="${href}">${dirData.name}</a>`
65
+ }
66
+ return `Index of ${dirData.name} ${dirData.path}`
67
+ }
68
+
69
+ function getAllDirListingRows (dirData: DirectoryTemplateData): string {
70
+ return dirData.listing.map((item) => `<div class="type-icon">
71
+ <div class="${iconFromExt(item.name)}">&nbsp;</div>
72
+ </div>
73
+ <div>
74
+ <a href="${item.path}">${item.name}</a>
75
+ </div>
76
+ <div class="nowrap">
77
+ ${itemShortHashCell(item, dirData)}
78
+ </div>
79
+ <div class="nowrap" title="Cumulative size of IPFS DAG (data + metadata)">${item.size}</div>`).join(' ')
80
+ }
81
+
82
+ function getItemPath (item: UnixFSEntry): string {
83
+ const itemPathParts = item.path.split('/')
84
+
85
+ return itemPathParts.pop() ?? item.path
86
+ }
87
+
88
+ /**
89
+ * todo: https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/handler_unixfs_dir.go#L200-L208
90
+ *
91
+ * @see https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/assets/directory.html
92
+ * @see https://github.com/ipfs/boxo/pull/298
93
+ * @see https://github.com/ipfs/kubo/pull/8555
94
+ */
95
+ export const dirIndexHtml = (dir: UnixFSEntry, items: UnixFSEntry[], { gatewayURL, dnsLink, log }: DirIndexHtmlOptions): string => {
96
+ log('loading directory html for %s', dir.path)
97
+
98
+ const dirData: DirectoryTemplateData = {
99
+ globalData: {
100
+ gatewayURL,
101
+ dnsLink: dnsLink ?? false
102
+ },
103
+ listing: items.map((item) => {
104
+ return {
105
+ size: item.size.toString(),
106
+ name: item.name,
107
+ path: getItemPath(item),
108
+ hash: item.cid.toString(),
109
+ shortHash: item.cid.toString().slice(0, 8)
110
+ } satisfies DirectoryItem
111
+ }),
112
+ name: dir.name,
113
+ size: dir.size.toString(),
114
+ path: dir.path,
115
+ breadcrumbs: [],
116
+ backLink: '',
117
+ hash: dir.cid.toString()
118
+ }
119
+
120
+ return `
121
+ <!DOCTYPE html>
122
+ <!--{{ $root := . }}-->
123
+ <html lang="en">
124
+ <head>
125
+ <meta charset="utf-8" />
126
+ <meta name="description" content="A directory of content-addressed files hosted on IPFS.">
127
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
128
+ <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
129
+ <title>${dirData.path}</title>
130
+ <style>${style}</style>
131
+ </head>
132
+ <body>
133
+ <!--
134
+ # Some JSON content for debugging:
135
+
136
+ ## dirData
137
+ ${JSON.stringify(dirData, null, 2)}
138
+ -->
139
+ <header id="header">
140
+ <div class="ipfs-logo">&nbsp;</div>
141
+ <!--
142
+ <nav>
143
+ <a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About<span class="dn-mobile"> IPFS</span></a>
144
+ <a href="https://docs.ipfs.tech/install/" target="_blank" rel="noopener noreferrer">Install<span class="dn-mobile"> IPFS</span></a>
145
+ </nav>
146
+ -->
147
+ </header>
148
+ <main id="main">
149
+ <header class="flex flex-wrap">
150
+ <div>
151
+ <strong>${dirListingTitle(dirData)}</strong>
152
+ ${dirData.hash == null
153
+ ? ''
154
+ : `<div class="ipfs-hash" translate="no">
155
+ ${dirData.hash}
156
+ </div>`
157
+ }
158
+ </div>
159
+ ${dirData.size == null
160
+ ? ''
161
+ : `<div class="nowrap flex-shrink ml-auto">
162
+ <strong title="Cumulative size of IPFS DAG (data + metadata)">&nbsp;${dirData.size}</strong>
163
+ </div>`
164
+ }
165
+ </header>
166
+ <section>
167
+ <div class="grid dir">
168
+ <!--{{ if .BackLink }}
169
+ <div class="type-icon">
170
+ <div class="ipfs-_blank">&nbsp;</div>
171
+ </div>
172
+ <div>
173
+ <a href="{{.BackLink | urlEscape}}">..</a>
174
+ </div>
175
+ <div></div>
176
+ <div></div>
177
+ </tr>
178
+ {{ end }}-->
179
+ ${getAllDirListingRows(dirData)}
180
+ </div>
181
+ </section>
182
+ </main>
183
+ </body>
184
+ </html>
185
+ `
186
+ }
187
+
188
+ const style = `
189
+
190
+ .ipfs-_blank {
191
+ background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='36' y1='1' x2='36' y2='99' gradientTransform='matrix(1 0 0 -1 0 100)'%3E%3Cstop offset='0' stop-color='%23c8d4db'/%3E%3Cstop offset='.139' stop-color='%23d8e1e6'/%3E%3Cstop offset='.359' stop-color='%23ebf0f3'/%3E%3Cstop offset='.617' stop-color='%23f9fafb'/%3E%3Cstop offset='1' stop-color='%23fff'/%3E%3C/linearGradient%3E%3Cpath d='M45 1l27 26.7V99H0V1h45z' fill='url(%23a)'/%3E%3Cpath d='M45 1l27 26.7V99H0V1h45z' fill-opacity='0' stroke='%237191a1' stroke-width='2'/%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='45.068' y1='72.204' x2='58.568' y2='85.705' gradientTransform='matrix(1 0 0 -1 0 100)'%3E%3Cstop offset='0' stop-color='%23fff'/%3E%3Cstop offset='.35' stop-color='%23fafbfb'/%3E%3Cstop offset='.532' stop-color='%23edf1f4'/%3E%3Cstop offset='.675' stop-color='%23dde5e9'/%3E%3Cstop offset='.799' stop-color='%23c7d3da'/%3E%3Cstop offset='.908' stop-color='%23adbdc7'/%3E%3Cstop offset='1' stop-color='%2392a5b0'/%3E%3C/linearGradient%3E%3Cpath d='M45 1l27 26.7H45V1z' fill='url(%23b)'/%3E%3Cpath d='M45 1l27 26.7H45V1z' fill-opacity='0' stroke='%237191a1' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E");
192
+ background-repeat: no-repeat;
193
+ background-size: contain
194
+ }
195
+
196
+ :root {
197
+ --sans-serif: "Plex",system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;
198
+ --monospace: Consolas, monaco, monospace;
199
+ --navy: #073a53;
200
+ --teal: #6bc4ce;
201
+ --turquoise: #47AFB4;
202
+ --steel-gray: #3f5667;
203
+ --dark-white: #d9dbe2;
204
+ --light-white: #edf0f4;
205
+ --near-white: #f7f8fa;
206
+ --radius: 4px;
207
+ }
208
+
209
+ body {
210
+ color: #34373f;
211
+ font-family: var(--sans-serif);
212
+ line-height: 1.43;
213
+ margin: 0;
214
+ word-break: break-all;
215
+ -webkit-text-size-adjust: 100%;
216
+ -ms-text-size-adjust: 100%;
217
+ -webkit-tap-highlight-color: transparent;
218
+ }
219
+
220
+ pre, code {
221
+ font-family: var(--monospace);
222
+ }
223
+
224
+ a {
225
+ color: #117eb3;
226
+ text-decoration: none;
227
+ }
228
+
229
+ a:hover {
230
+ color: #00b0e9;
231
+ text-decoration: underline;
232
+ }
233
+
234
+ a:active,a:visited {
235
+ color: #00b0e9;
236
+ }
237
+
238
+ .flex {
239
+ display: flex;
240
+ }
241
+
242
+ .flex-wrap {
243
+ flex-flow: wrap;
244
+ }
245
+
246
+ .flex-shrink {
247
+ flex-shrink: 1;
248
+ }
249
+
250
+ .ml-auto {
251
+ margin-left: auto;
252
+ }
253
+
254
+ .nowrap {
255
+ white-space: nowrap
256
+ }
257
+
258
+ .ipfs-hash {
259
+ color: #7f8491;
260
+ font-family: var(--monospace);
261
+ }
262
+
263
+ #header {
264
+ align-items: center;
265
+ background: var(--navy);
266
+ border-bottom: 4px solid var(--teal);
267
+ color: #fff;
268
+ display: flex;
269
+ font-weight: 500;
270
+ justify-content: space-between;
271
+ padding: 0 1em;
272
+ }
273
+
274
+ #header a {
275
+ color: var(--teal);
276
+ }
277
+
278
+ #header a:active {
279
+ color: #9ad4db;
280
+ }
281
+
282
+ #header a:hover {
283
+ color: #fff;
284
+ }
285
+
286
+ #header .ipfs-logo {
287
+ height: 2.25em;
288
+ margin: .7em .7em .7em 0;
289
+ width: 7.15em
290
+ }
291
+
292
+ #header nav {
293
+ align-items: center;
294
+ display: flex;
295
+ margin: .65em 0;
296
+ }
297
+
298
+ #header nav a {
299
+ margin: 0 .6em;
300
+ }
301
+
302
+ #header nav a:last-child {
303
+ margin: 0 0 0 .6em;
304
+ }
305
+
306
+ #header nav svg {
307
+ fill: var(--teal);
308
+ height: 1.8em;
309
+ margin-top: .125em;
310
+ }
311
+
312
+ #header nav svg:hover {
313
+ fill: #fff;
314
+ }
315
+
316
+ main {
317
+ border: 1px solid var(--dark-white);
318
+ border-radius: var(--radius);
319
+ overflow: hidden;
320
+ margin: 1em;
321
+ font-size: .875em;
322
+ }
323
+
324
+ main header,main .container {
325
+ padding-left: 1em;
326
+ padding-right: 1em;
327
+ }
328
+
329
+ main header {
330
+ padding-top: .7em;
331
+ padding-bottom: .7em;
332
+ background-color: var(--light-white);
333
+ }
334
+
335
+ main header,main section:not(:last-child) {
336
+ border-bottom: 1px solid var(--dark-white);
337
+ }
338
+
339
+ main section header {
340
+ background-color: var(--near-white);
341
+ }
342
+
343
+ .grid {
344
+ display: grid;
345
+ overflow-x: auto;
346
+ }
347
+
348
+ .grid .grid {
349
+ overflow-x: visible;
350
+ }
351
+
352
+ .grid > div {
353
+ padding: .7em;
354
+ border-bottom: 1px solid var(--dark-white);
355
+ }
356
+
357
+ .grid.dir {
358
+ grid-template-columns: min-content 1fr min-content min-content;
359
+ }
360
+
361
+ .grid.dir > div:nth-of-type(4n+1) {
362
+ padding-left: 1em;
363
+ }
364
+
365
+ .grid.dir > div:nth-of-type(4n+4) {
366
+ padding-right: 1em;
367
+ }
368
+
369
+ .grid.dir > div:nth-last-child(-n+4) {
370
+ border-bottom: 0;
371
+ }
372
+
373
+ .grid.dir > div:nth-of-type(8n+5),.grid.dir > div:nth-of-type(8n+6),.grid.dir > div:nth-of-type(8n+7),.grid.dir > div:nth-of-type(8n+8) {
374
+ background-color: var(--near-white);
375
+ }
376
+
377
+ .grid.dag {
378
+ grid-template-columns: max-content 1fr;
379
+ }
380
+
381
+ .grid.dag pre {
382
+ margin: 0;
383
+ }
384
+
385
+ .grid.dag .grid {
386
+ padding: 0;
387
+ }
388
+
389
+ .grid.dag > div:nth-last-child(-n+2) {
390
+ border-bottom: 0;
391
+ }
392
+
393
+ .grid.dag > div {
394
+ background: white
395
+ }
396
+
397
+ .grid.dag > div:nth-child(4n),.grid.dag > div:nth-child(4n+3) {
398
+ background-color: var(--near-white);
399
+ }
400
+
401
+ section > .grid.dag > div:nth-of-type(2n+1) {
402
+ padding-left: 1em;
403
+ }
404
+
405
+ .type-icon,.type-icon > * {
406
+ width: 1.15em
407
+ }
408
+
409
+ .terminal {
410
+ background: var(--steel-gray);
411
+ color: white;
412
+ padding: .7em;
413
+ border-radius: var(--radius);
414
+ word-wrap: break-word;
415
+ white-space: break-spaces;
416
+ }
417
+
418
+ @media print {
419
+ #header {
420
+ display: none;
421
+ }
422
+
423
+ #main header,.ipfs-hash,body {
424
+ color: #000;
425
+ }
426
+
427
+ #main,#main header {
428
+ border-color: #000;
429
+ }
430
+
431
+ a,a:visited {
432
+ color: #000;
433
+ text-decoration: underline;
434
+ }
435
+
436
+ a[href]:after {
437
+ content: " (" attr(href) ")"
438
+ }
439
+ }
440
+
441
+ @media only screen and (max-width: 500px) {
442
+ .dn-mobile {
443
+ display: none;
444
+ }
445
+ }`
@@ -15,19 +15,36 @@ interface GetETagArg {
15
15
  */
16
16
  weak?: boolean
17
17
  }
18
+ const getPrefix = ({ weak, reqFormat }: Partial<GetETagArg>): string => {
19
+ if (reqFormat === 'tar' || reqFormat === 'car' || reqFormat === 'ipns-record' || weak === true) {
20
+ return 'W/'
21
+ }
22
+ return ''
23
+ }
24
+
25
+ const getFormatSuffix = ({ reqFormat }: Partial<GetETagArg>): string => {
26
+ if (reqFormat == null) {
27
+ return ''
28
+ }
29
+ if (reqFormat === 'tar') {
30
+ return '.x-tar'
31
+ }
32
+
33
+ return `.${reqFormat}`
34
+ }
18
35
 
19
36
  /**
20
37
  * etag
21
38
  * you need to wrap cid with ""
22
- * we use strong Etags for immutable responses and weak one (prefixed with W/ ) for mutable/generated ones (ipns and generated HTML).
39
+ * we use strong Etags for immutable responses and weak one (prefixed with W/ ) for mutable/generated ones (ipns, car, tar, and generated HTML).
23
40
  * block and car responses should have different etag than deserialized one, so you can add some prefix like we do in existing gateway
24
41
  *
25
42
  * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
26
43
  * @see https://specs.ipfs.tech/http-gateways/path-gateway/#etag-response-header
27
44
  */
28
45
  export function getETag ({ cid, reqFormat, weak, rangeStart, rangeEnd }: GetETagArg): string {
29
- const prefix = weak === true ? 'W/' : ''
30
- let suffix = reqFormat == null ? '' : `.${reqFormat}`
46
+ const prefix = getPrefix({ weak, reqFormat })
47
+ let suffix = getFormatSuffix({ reqFormat })
31
48
  if (rangeStart != null || rangeEnd != null) {
32
49
  suffix += `.${rangeStart ?? '0'}-${rangeEnd ?? 'N'}`
33
50
  }
@@ -11,16 +11,16 @@ export interface ParseResourceComponents {
11
11
  }
12
12
 
13
13
  export interface ParseResourceOptions extends ParseUrlStringOptions {
14
-
14
+ withServerTiming?: boolean
15
15
  }
16
16
  /**
17
17
  * Handles the different use cases for the `resource` argument.
18
18
  * The resource can represent an IPFS path, IPNS path, or CID.
19
19
  * If the resource represents an IPNS path, we need to resolve it to a CID.
20
20
  */
21
- export async function parseResource (resource: Resource, { ipns, logger }: ParseResourceComponents, options?: ParseResourceOptions): Promise<ParsedUrlStringResults> {
21
+ export async function parseResource (resource: Resource, { ipns, logger }: ParseResourceComponents, { withServerTiming = false, ...options }: ParseResourceOptions = { withServerTiming: false }): Promise<ParsedUrlStringResults> {
22
22
  if (typeof resource === 'string') {
23
- return parseUrlString({ urlString: resource, ipns, logger }, options)
23
+ return parseUrlString({ urlString: resource, ipns, logger, withServerTiming }, options)
24
24
  }
25
25
 
26
26
  const cid = CID.asCID(resource)
@@ -33,7 +33,8 @@ export async function parseResource (resource: Resource, { ipns, logger }: Parse
33
33
  path: '',
34
34
  query: {},
35
35
  ipfsPath: `/ipfs/${cid.toString()}`,
36
- ttl: 29030400 // 1 year for ipfs content
36
+ ttl: 29030400, // 1 year for ipfs content
37
+ serverTimings: []
37
38
  } satisfies ParsedUrlStringResults
38
39
  }
39
40
 
@@ -1,5 +1,6 @@
1
1
  import { CID } from 'multiformats/cid'
2
2
  import { getPeerIdFromString } from './get-peer-id-from-string.js'
3
+ import { serverTiming, type ServerTimingResult } from './server-timing.js'
3
4
  import { TLRU } from './tlru.js'
4
5
  import type { RequestFormatShorthand } from '../types.js'
5
6
  import type { DNSLinkResolveResult, IPNS, IPNSResolveResult, IPNSRoutingEvents, ResolveDNSLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
@@ -12,6 +13,7 @@ export interface ParseUrlStringInput {
12
13
  urlString: string
13
14
  ipns: IPNS
14
15
  logger: ComponentLogger
16
+ withServerTiming?: boolean
15
17
  }
16
18
  export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDNSLinkProgressEvents>, AbortOptions {
17
19
 
@@ -23,7 +25,7 @@ export interface ParsedUrlQuery extends Record<string, string | unknown> {
23
25
  filename?: string
24
26
  }
25
27
 
26
- interface ParsedUrlStringResultsBase extends ResolveResult {
28
+ export interface ParsedUrlStringResults extends ResolveResult {
27
29
  protocol: 'ipfs' | 'ipns'
28
30
  query: ParsedUrlQuery
29
31
 
@@ -41,9 +43,12 @@ interface ParsedUrlStringResultsBase extends ResolveResult {
41
43
  * seconds as a number
42
44
  */
43
45
  ttl?: number
44
- }
45
46
 
46
- export type ParsedUrlStringResults = ParsedUrlStringResultsBase
47
+ /**
48
+ * serverTiming items
49
+ */
50
+ serverTimings: Array<ServerTimingResult<any>>
51
+ }
47
52
 
48
53
  const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
49
54
  const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
@@ -145,7 +150,7 @@ function dnsLinkLabelDecoder (linkLabel: string): string {
145
150
  * @todo we need to break out each step of this function (cid parsing, ipns resolving, dnslink resolving) into separate functions and then remove the eslint-disable comment
146
151
  */
147
152
  // eslint-disable-next-line complexity
148
- export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
153
+ export async function parseUrlString ({ urlString, ipns, logger, withServerTiming = false }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
149
154
  const log = logger.forComponent('helia:verified-fetch:parse-url-string')
150
155
  const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = matchURLString(urlString)
151
156
 
@@ -153,6 +158,7 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
153
158
  let resolvedPath: string | undefined
154
159
  const errors: Error[] = []
155
160
  let resolveResult: IPNSResolveResult | DNSLinkResolveResult | undefined
161
+ const serverTimings: Array<ServerTimingResult<any>> = []
156
162
 
157
163
  if (protocol === 'ipfs') {
158
164
  try {
@@ -182,7 +188,19 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
182
188
  if (peerId.publicKey == null) {
183
189
  throw new TypeError('cidOrPeerIdOrDnsLink contains no public key')
184
190
  }
185
- resolveResult = await ipns.resolve(peerId.publicKey, options)
191
+
192
+ if (withServerTiming) {
193
+ const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, ipns.resolve.bind(null, peerId.publicKey, options))
194
+ serverTimings.push(resolveResultWithServerTiming)
195
+
196
+ // eslint-disable-next-line max-depth
197
+ if (resolveResultWithServerTiming.error != null) {
198
+ throw resolveResultWithServerTiming.error
199
+ }
200
+ resolveResult = resolveResultWithServerTiming.result
201
+ } else {
202
+ resolveResult = await ipns.resolve(peerId.publicKey, options)
203
+ }
186
204
  cid = resolveResult?.cid
187
205
  resolvedPath = resolveResult?.path
188
206
  log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
@@ -207,7 +225,19 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
207
225
  log.trace('Attempting to resolve DNSLink for %s', decodedDnsLinkLabel)
208
226
 
209
227
  try {
210
- resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
228
+ // eslint-disable-next-line max-depth
229
+ if (withServerTiming) {
230
+ const resolveResultWithServerTiming = await serverTiming('ipns.resolveDNSLink', `Resolve DNSLink ${decodedDnsLinkLabel}`, ipns.resolveDNSLink.bind(ipns, decodedDnsLinkLabel, options))
231
+ serverTimings.push(resolveResultWithServerTiming)
232
+ // eslint-disable-next-line max-depth
233
+ if (resolveResultWithServerTiming.error != null) {
234
+ throw resolveResultWithServerTiming.error
235
+ }
236
+ resolveResult = resolveResultWithServerTiming.result
237
+ } else {
238
+ resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
239
+ }
240
+
211
241
  cid = resolveResult?.cid
212
242
  resolvedPath = resolveResult?.path
213
243
  log.trace('resolved %s to %c', decodedDnsLinkLabel, cid)
@@ -263,7 +293,8 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
263
293
  path: joinPaths(resolvedPath, urlPath ?? ''),
264
294
  query,
265
295
  ttl,
266
- ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`
296
+ ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`,
297
+ serverTimings
267
298
  } satisfies ParsedUrlStringResults
268
299
  }
269
300
 
@@ -0,0 +1,37 @@
1
+ export interface ServerTimingSuccess<T> {
2
+ error: null
3
+ result: T
4
+ header: string
5
+ }
6
+ export interface ServerTimingError {
7
+ result: null
8
+ error: Error
9
+ header: string
10
+ }
11
+ export type ServerTimingResult<T> = ServerTimingSuccess<T> | ServerTimingError
12
+
13
+ export async function serverTiming<T> (
14
+ name: string,
15
+ description: string,
16
+ fn: () => Promise<T>
17
+ ): Promise<ServerTimingResult<T>> {
18
+ const startTime = performance.now()
19
+
20
+ try {
21
+ const result = await fn() // Execute the function
22
+ const endTime = performance.now()
23
+
24
+ const duration = (endTime - startTime).toFixed(1) // Duration in milliseconds
25
+
26
+ // Create the Server-Timing header string
27
+ const header = `${name};dur=${duration};desc="${description}"`
28
+ return { result, header, error: null }
29
+ } catch (error: any) {
30
+ const endTime = performance.now()
31
+ const duration = (endTime - startTime).toFixed(1)
32
+
33
+ // Still return a timing header even on error
34
+ const header = `${name};dur=${duration};desc="${description}"`
35
+ return { result: null, error, header } // Pass error with timing info
36
+ }
37
+ }
@@ -2,8 +2,8 @@ import { DoesNotExistError } from '@helia/unixfs/errors'
2
2
  import { type Logger } from '@libp2p/interface'
3
3
  import { type Blockstore } from 'interface-blockstore'
4
4
  import { walkPath as exporterWalk, type ExporterOptions, type ReadableStorage, type ObjectNode, type UnixFSEntry } from 'ipfs-unixfs-exporter'
5
- import { type FetchHandlerFunctionArg } from '../types.js'
6
5
  import { badGatewayResponse, notFoundResponse } from './responses.js'
6
+ import type { PluginContext } from '../plugins/types.js'
7
7
  import type { CID } from 'multiformats/cid'
8
8
 
9
9
  export interface PathWalkerOptions extends ExporterOptions {
@@ -12,7 +12,6 @@ export interface PathWalkerOptions extends ExporterOptions {
12
12
  export interface PathWalkerResponse {
13
13
  ipfsRoots: CID[]
14
14
  terminalElement: UnixFSEntry
15
-
16
15
  }
17
16
 
18
17
  export interface PathWalkerFn {
@@ -47,8 +46,9 @@ export function isObjectNode (node: UnixFSEntry): node is ObjectNode {
47
46
  * If the signal is aborted, the function will throw an AbortError
48
47
  * If a terminal element is not found, a notFoundResponse is returned
49
48
  * If another unknown error occurs, a badGatewayResponse is returned
49
+ *
50
50
  */
51
- export async function handlePathWalking ({ cid, path, resource, options, blockstore, log }: Omit<FetchHandlerFunctionArg, 'session'> & { blockstore: Blockstore, log: Logger }): Promise<PathWalkerResponse | Response> {
51
+ export async function handlePathWalking ({ cid, path, resource, options, blockstore, log }: PluginContext & { blockstore: Blockstore, log: Logger }): Promise<PathWalkerResponse | Response> {
52
52
  try {
53
53
  return await walkPath(blockstore, `${cid.toString()}/${path}`, options)
54
54
  } catch (err: any) {