@helia/verified-fetch 7.2.15 → 8.0.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.
- package/README.md +8 -12
- package/dist/index.min.js +70 -59
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +27 -17
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +38 -34
- package/dist/src/index.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 +23 -5
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-unixfs.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-unixfs.js +7 -4
- package/dist/src/plugins/plugin-handle-unixfs.js.map +1 -1
- package/dist/src/url-resolver.d.ts.map +1 -1
- package/dist/src/url-resolver.js +33 -9
- package/dist/src/url-resolver.js.map +1 -1
- package/dist/src/utils/convert-output.d.ts +2 -2
- package/dist/src/utils/convert-output.d.ts.map +1 -1
- package/dist/src/utils/convert-output.js +11 -5
- package/dist/src/utils/convert-output.js.map +1 -1
- package/dist/src/utils/ipfs-path-to-cid.d.ts +6 -0
- package/dist/src/utils/ipfs-path-to-cid.d.ts.map +1 -0
- package/dist/src/utils/ipfs-path-to-cid.js +9 -0
- package/dist/src/utils/ipfs-path-to-cid.js.map +1 -0
- package/dist/src/utils/libp2p-defaults.browser.js +1 -1
- package/dist/src/utils/libp2p-defaults.browser.js.map +1 -1
- package/dist/src/utils/libp2p-defaults.d.ts.map +1 -1
- package/dist/src/utils/libp2p-defaults.js +2 -10
- package/dist/src/utils/libp2p-defaults.js.map +1 -1
- package/dist/src/utils/response-headers.d.ts +12 -4
- package/dist/src/utils/response-headers.d.ts.map +1 -1
- package/dist/src/utils/response-headers.js +25 -13
- package/dist/src/utils/response-headers.js.map +1 -1
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +6 -4
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +28 -31
- package/src/index.ts +65 -41
- package/src/plugins/plugin-handle-ipns-record.ts +31 -6
- package/src/plugins/plugin-handle-unixfs.ts +7 -4
- package/src/url-resolver.ts +43 -9
- package/src/utils/convert-output.ts +13 -7
- package/src/utils/ipfs-path-to-cid.ts +9 -0
- package/src/utils/libp2p-defaults.browser.ts +2 -2
- package/src/utils/libp2p-defaults.ts +3 -11
- package/src/utils/response-headers.ts +33 -15
- package/src/verified-fetch.ts +7 -4
package/src/index.ts
CHANGED
|
@@ -100,23 +100,19 @@
|
|
|
100
100
|
* You can see variations of Helia and js-libp2p configuration options at <https://ipfs.github.io/helia/interfaces/helia.HeliaInit.html>.
|
|
101
101
|
*
|
|
102
102
|
* ```typescript
|
|
103
|
-
* import {
|
|
104
|
-
* import {
|
|
103
|
+
* import { createHeliaLight } from 'helia'
|
|
104
|
+
* import { withHTTP } from '@helia/http'
|
|
105
105
|
* import { delegatedHTTPRouting, httpGatewayRouting } from '@helia/routers'
|
|
106
106
|
* import { createVerifiedFetch } from '@helia/verified-fetch'
|
|
107
107
|
*
|
|
108
108
|
* const fetch = await createVerifiedFetch(
|
|
109
|
-
* await
|
|
110
|
-
*
|
|
111
|
-
*
|
|
109
|
+
* await withHTTP(createHeliaLight(), {
|
|
110
|
+
* delegatedRouters: [
|
|
111
|
+
* 'http://delegated-ipfs.dev'
|
|
112
112
|
* ],
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* }),
|
|
117
|
-
* httpGatewayRouting({
|
|
118
|
-
* gateways: ['https://mygateway.example.net', 'https://trustless-gateway.link']
|
|
119
|
-
* })
|
|
113
|
+
* recursiveGateways: [
|
|
114
|
+
* 'https://mygateway.example.net',
|
|
115
|
+
* 'https://trustless-gateway.link'
|
|
120
116
|
* ]
|
|
121
117
|
* })
|
|
122
118
|
* )
|
|
@@ -856,19 +852,23 @@
|
|
|
856
852
|
* fetch if not.
|
|
857
853
|
*/
|
|
858
854
|
|
|
859
|
-
import {
|
|
860
|
-
import {
|
|
861
|
-
import {
|
|
855
|
+
import { withBitswap } from '@helia/bitswap'
|
|
856
|
+
import { delegatedRoutingV1HttpApiClientContentRouting, delegatedRoutingV1HttpApiClientPeerRouting } from '@helia/delegated-routing-v1-http-api-client'
|
|
857
|
+
import { withHTTP } from '@helia/http'
|
|
858
|
+
import { withLibp2p } from '@helia/libp2p'
|
|
859
|
+
import * as dagCbor from '@ipld/dag-cbor'
|
|
860
|
+
import * as dagJson from '@ipld/dag-json'
|
|
862
861
|
import { dns } from '@multiformats/dns'
|
|
863
|
-
import {
|
|
862
|
+
import { createHeliaLight } from 'helia'
|
|
864
863
|
import { createLibp2p } from 'libp2p'
|
|
864
|
+
import * as json from 'multiformats/codecs/json'
|
|
865
865
|
import { getLibp2pConfig } from './utils/libp2p-defaults.ts'
|
|
866
866
|
import { VerifiedFetch as VerifiedFetchClass } from './verified-fetch.ts'
|
|
867
867
|
import type { RangeHeader } from './utils/get-range-header.ts'
|
|
868
868
|
import type { ServerTiming } from './utils/server-timing.ts'
|
|
869
869
|
import type { RequestedMimeType } from './verified-fetch.ts'
|
|
870
870
|
import type { DNSLink, ResolveProgressEvents as ResolveDNSLinkProgressEvents } from '@helia/dnslink'
|
|
871
|
-
import type { GetBlockProgressEvents, Helia, ProviderOptions
|
|
871
|
+
import type { GetBlockProgressEvents, Helia, ProviderOptions } from '@helia/interface'
|
|
872
872
|
import type { ResolveProgressEvents as ResolveIPNSNameProgressEvents, IPNSRoutingProgressEvents, IPNSResolver } from '@helia/ipns'
|
|
873
873
|
import type { AbortOptions, Libp2p, ServiceMap, Logger, PeerId, PublicKey } from '@libp2p/interface'
|
|
874
874
|
import type { DNSResolvers, DNS } from '@multiformats/dns'
|
|
@@ -1098,7 +1098,7 @@ export interface CreateVerifiedFetchInit {
|
|
|
1098
1098
|
|
|
1099
1099
|
/**
|
|
1100
1100
|
* In order to parse DNSLink records, we need to resolve DNS queries. You can
|
|
1101
|
-
* pass a list of DNS resolvers that we will provide to the
|
|
1101
|
+
* pass a list of DNS resolvers that we will provide to the `@helia/ipns`
|
|
1102
1102
|
* instance for you. You must construct them using the `dnsJsonOverHttps` or
|
|
1103
1103
|
* `dnsOverHttps` functions exported from `@helia/ipns/dns-resolvers`.
|
|
1104
1104
|
*
|
|
@@ -1109,12 +1109,19 @@ export interface CreateVerifiedFetchInit {
|
|
|
1109
1109
|
dnsResolvers?: DNSResolver[] | DNSResolvers
|
|
1110
1110
|
|
|
1111
1111
|
/**
|
|
1112
|
-
* By default sha256
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1112
|
+
* By default sha256 and identity hashes are supported for retrieval
|
|
1113
|
+
* operations. To retrieve blocks by CIDs using other hashes pass
|
|
1114
|
+
* appropriate MultihashHashers here.
|
|
1115
1115
|
*/
|
|
1116
1116
|
hashers?: HeliaInit['hashers']
|
|
1117
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* By default raw, dag-pb, dag-json, dag-cbor, json and cbor codecs are
|
|
1120
|
+
* supported for retrieval operations. To retrieve blocks with CIDs that use
|
|
1121
|
+
* other codecs pass appropriate BlockCodecs here.
|
|
1122
|
+
*/
|
|
1123
|
+
codecs?: HeliaInit['codecs']
|
|
1124
|
+
|
|
1118
1125
|
/**
|
|
1119
1126
|
* By default we will not connect to any HTTP Gateways providers over local or
|
|
1120
1127
|
* loopback addresses, this is because they are typically running on remote
|
|
@@ -1354,7 +1361,18 @@ export interface ResolveURLOptions extends AbortOptions, ProviderOptions, Progre
|
|
|
1354
1361
|
|
|
1355
1362
|
export interface ResolveURLResult {
|
|
1356
1363
|
url: URL
|
|
1357
|
-
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* How long this result is valid for - in general re-resolving this URL will
|
|
1367
|
+
* result in the same final URL within this many seconds
|
|
1368
|
+
*/
|
|
1369
|
+
ttl?: number
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* This can represent an IPNS records EOL validity expiration
|
|
1373
|
+
*/
|
|
1374
|
+
expires?: Date
|
|
1375
|
+
|
|
1358
1376
|
blockstore: Blockstore
|
|
1359
1377
|
ipfsRoots: CID[]
|
|
1360
1378
|
terminalElement: PathEntry
|
|
@@ -1374,6 +1392,7 @@ export interface URLResolver {
|
|
|
1374
1392
|
*/
|
|
1375
1393
|
export async function createVerifiedFetch (init?: Helia | CreateVerifiedFetchInit, options?: CreateVerifiedFetchOptions): Promise<VerifiedFetch> {
|
|
1376
1394
|
let libp2p: Libp2p<any> | undefined
|
|
1395
|
+
|
|
1377
1396
|
if (!isHelia(init)) {
|
|
1378
1397
|
const dns = createDns(init?.dnsResolvers)
|
|
1379
1398
|
|
|
@@ -1382,35 +1401,40 @@ export async function createVerifiedFetch (init?: Helia | CreateVerifiedFetchIni
|
|
|
1382
1401
|
|
|
1383
1402
|
const delegatedRouters = init?.routers ?? ['https://delegated-ipfs.dev']
|
|
1384
1403
|
for (let index = 0; index < delegatedRouters.length; index++) {
|
|
1385
|
-
libp2pConfig.services[`
|
|
1404
|
+
libp2pConfig.services[`delegatedContentRouting${index}`] = delegatedRoutingV1HttpApiClientContentRouting({
|
|
1405
|
+
url: delegatedRouters[index]
|
|
1406
|
+
})
|
|
1407
|
+
libp2pConfig.services[`delegatedPeerRouting${index}`] = delegatedRoutingV1HttpApiClientPeerRouting({
|
|
1386
1408
|
url: delegatedRouters[index]
|
|
1387
1409
|
})
|
|
1388
1410
|
}
|
|
1411
|
+
|
|
1389
1412
|
// merge any passed options from init.libp2pConfig into libp2pConfig if it exists
|
|
1390
1413
|
if (init?.libp2pConfig != null) {
|
|
1391
1414
|
Object.assign(libp2pConfig, init.libp2pConfig)
|
|
1392
1415
|
}
|
|
1393
|
-
libp2p = await createLibp2p(libp2pConfig)
|
|
1394
1416
|
|
|
1395
|
-
|
|
1396
|
-
bitswap()
|
|
1397
|
-
]
|
|
1398
|
-
const routers: Array<Partial<Routing>> = [
|
|
1399
|
-
libp2pRouting(libp2p)
|
|
1400
|
-
]
|
|
1401
|
-
if (init?.gateways == null || init.gateways.length > 0) {
|
|
1402
|
-
// if gateways is null, or set to a non-empty array, use trustless gateways.
|
|
1403
|
-
blockBrokers.push(trustlessGateway({ allowInsecure: init?.allowInsecure, allowLocal: init?.allowLocal }))
|
|
1404
|
-
routers.push(httpGatewayRouting({ gateways: init?.gateways ?? ['https://trustless-gateway.link'] }))
|
|
1405
|
-
}
|
|
1417
|
+
libp2p = await createLibp2p(libp2pConfig)
|
|
1406
1418
|
|
|
1407
|
-
init = await
|
|
1408
|
-
libp2p,
|
|
1409
|
-
blockBrokers,
|
|
1419
|
+
init = await withBitswap(withLibp2p(withHTTP(createHeliaLight({
|
|
1410
1420
|
dns,
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1421
|
+
hashers: init?.hashers,
|
|
1422
|
+
codecs: [
|
|
1423
|
+
dagCbor,
|
|
1424
|
+
dagJson,
|
|
1425
|
+
json,
|
|
1426
|
+
...(init?.codecs ?? [])
|
|
1427
|
+
]
|
|
1428
|
+
}), {
|
|
1429
|
+
delegatedRouters,
|
|
1430
|
+
recursiveGateways: init?.gateways ?? [
|
|
1431
|
+
'https://trustless-gateway.link'
|
|
1432
|
+
],
|
|
1433
|
+
trustlessGatewayBlockBrokerInit: {
|
|
1434
|
+
allowInsecure: init?.allowInsecure,
|
|
1435
|
+
allowLocal: init?.allowLocal
|
|
1436
|
+
}
|
|
1437
|
+
}), libp2p)).start()
|
|
1414
1438
|
init.logger.forComponent('helia:verified-fetch').trace('created verified-fetch with libp2p config: %j', libp2pConfig)
|
|
1415
1439
|
}
|
|
1416
1440
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { IPNSEntry } from '@helia/ipns'
|
|
2
|
+
import * as dagCbor from '@ipld/dag-cbor'
|
|
1
3
|
import { peerIdFromString } from '@libp2p/peer-id'
|
|
2
|
-
import {
|
|
4
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
5
|
import { CONTENT_TYPE_IPNS, MEDIA_TYPE_IPNS_RECORD } from '../utils/content-types.ts'
|
|
4
6
|
import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
|
|
7
|
+
import { splitIPNSName } from '../utils/ipfs-path-to-cid.ts'
|
|
5
8
|
import { badRequestResponse, okResponse } from '../utils/responses.ts'
|
|
6
9
|
import { BasePlugin } from './plugin-base.ts'
|
|
7
10
|
import type { PluginContext } from '../index.ts'
|
|
11
|
+
import type { IPNSRecordData, IPNSResolveResult } from '@helia/ipns'
|
|
8
12
|
import type { PeerId } from '@libp2p/interface'
|
|
9
13
|
|
|
10
14
|
/**
|
|
@@ -19,7 +23,7 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
19
23
|
return accept.some(header => header.contentType.mediaType === MEDIA_TYPE_IPNS_RECORD)
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
async handle (context: Pick<PluginContext, 'resource' | 'url' | 'range' | 'redirected' | 'signal' | 'onProgress'>): Promise<Response> {
|
|
26
|
+
async handle (context: Pick<PluginContext, 'resource' | 'url' | 'range' | 'redirected' | 'signal' | 'onProgress' | 'ttl' | 'expires'>): Promise<Response> {
|
|
23
27
|
const { resource, url, range } = context
|
|
24
28
|
const { ipnsResolver } = this.pluginOptions
|
|
25
29
|
|
|
@@ -43,8 +47,30 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
43
47
|
return badRequestResponse(resource, err)
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
let result: IPNSResolveResult | undefined
|
|
51
|
+
|
|
52
|
+
for await (const res of ipnsResolver.resolve(peerId.toCID().multihash, context)) {
|
|
53
|
+
result = res
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (result == null) {
|
|
57
|
+
const err = new Error(`Could not resolve IPNS record for ${peerId}`)
|
|
58
|
+
this.log.error('could not parse peer id from IPNS url %s', resource, err)
|
|
59
|
+
|
|
60
|
+
return badRequestResponse(resource, err)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const block = IPNSEntry.encode(result.record)
|
|
64
|
+
const data = dagCbor.decode<IPNSRecordData>(result.record.data ?? new Uint8Array(0))
|
|
65
|
+
|
|
66
|
+
// 0 is EOL
|
|
67
|
+
// @ts-expect-error TODO: remove TypeScript enums
|
|
68
|
+
if (data.ValidityType === 0 && data.Validity instanceof Uint8Array) {
|
|
69
|
+
const eol = new Date(uint8ArrayToString(data.Validity))
|
|
70
|
+
|
|
71
|
+
context.expires = eol
|
|
72
|
+
context.ttl = Math.round(Number((result.record.ttl ?? 0n) / BigInt(1e9)))
|
|
73
|
+
}
|
|
48
74
|
|
|
49
75
|
return okResponse(resource, block, {
|
|
50
76
|
redirected: context.redirected,
|
|
@@ -54,8 +80,7 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
54
80
|
'content-disposition': `attachment; ${
|
|
55
81
|
getContentDispositionFilename(url.searchParams.get('filename') ?? `${peerId}${CONTENT_TYPE_IPNS.extension}`)
|
|
56
82
|
}`,
|
|
57
|
-
'x-ipfs-roots': result.
|
|
58
|
-
'cache-control': `public, max-age=${Number((result.record.ttl ?? 0n) / BigInt(1e9))}`,
|
|
83
|
+
'x-ipfs-roots': splitIPNSName(result.value).name,
|
|
59
84
|
'accept-ranges': 'none'
|
|
60
85
|
}
|
|
61
86
|
})
|
|
@@ -6,6 +6,7 @@ import last from 'it-last'
|
|
|
6
6
|
import itToBrowserReadableStream from 'it-to-browser-readablestream'
|
|
7
7
|
import toBuffer from 'it-to-buffer'
|
|
8
8
|
import * as raw from 'multiformats/codecs/raw'
|
|
9
|
+
import { withArrayBuffer } from 'uint8arrays/with-array-buffer'
|
|
9
10
|
import { MEDIA_TYPE_OCTET_STREAM, MEDIA_TYPE_DAG_PB } from '../utils/content-types.ts'
|
|
10
11
|
import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
|
|
11
12
|
import { badGatewayResponse, movedPermanentlyResponse, partialContentResponse, okResponse } from '../utils/responses.ts'
|
|
@@ -147,15 +148,17 @@ export class UnixFSPlugin extends BasePlugin {
|
|
|
147
148
|
Name: dirEntry.name,
|
|
148
149
|
Hash: dirEntry.cid,
|
|
149
150
|
|
|
150
|
-
// @
|
|
151
|
-
|
|
151
|
+
// TODO: downcast bigint to number until @ipld/dag-pb supports the
|
|
152
|
+
// correct field type
|
|
153
|
+
Tsize: Number(dirEntry.size)
|
|
152
154
|
})
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
// TODO: remove withArrayBuffer when dag-pb is explicit about it's return type
|
|
158
|
+
block = withArrayBuffer(dagPb.encode({
|
|
156
159
|
Data: data.Data,
|
|
157
160
|
Links: links.sort((a, b) => a.Name?.localeCompare(b.Name ?? '') ?? 0)
|
|
158
|
-
})
|
|
161
|
+
}))
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
return okResponse(resource, block, {
|
package/src/url-resolver.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { DoesNotExistError } from '@helia/unixfs/errors'
|
|
2
|
-
import
|
|
2
|
+
import * as cborg from 'cborg'
|
|
3
3
|
import { exporter, InvalidParametersError, walkPath } from 'ipfs-unixfs-exporter'
|
|
4
|
+
import last from 'it-last'
|
|
4
5
|
import toBuffer from 'it-to-buffer'
|
|
6
|
+
import { base58btc } from 'multiformats/bases/base58'
|
|
5
7
|
import { CID } from 'multiformats/cid'
|
|
8
|
+
import * as Digest from 'multiformats/hashes/digest'
|
|
6
9
|
import QuickLRU from 'quick-lru'
|
|
7
10
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
8
11
|
import { CODEC_LIBP2P_KEY, SESSION_CACHE_MAX_SIZE, SESSION_CACHE_TTL_MS } from './constants.ts'
|
|
9
12
|
import { abbreviate } from './utils/abbreviate.ts'
|
|
10
13
|
import { applyRedirects } from './utils/apply-redirect.ts'
|
|
14
|
+
import { splitIPNSName } from './utils/ipfs-path-to-cid.ts'
|
|
11
15
|
import { ServerTiming } from './utils/server-timing.ts'
|
|
12
16
|
import type { ResolveURLOptions, ResolveURLResult, URLResolver as URLResolverInterface } from './index.ts'
|
|
13
17
|
import type { DNSLink } from '@helia/dnslink'
|
|
@@ -16,6 +20,7 @@ import type { AbortOptions, Logger } from '@libp2p/interface'
|
|
|
16
20
|
import type { Helia, ProviderOptions, SessionBlockstore } from 'helia'
|
|
17
21
|
import type { Blockstore } from 'interface-blockstore'
|
|
18
22
|
import type { PathEntry, UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
23
|
+
import type { MultihashDigest } from 'multiformats/cid'
|
|
19
24
|
import type { ProgressOptions } from 'progress-events'
|
|
20
25
|
|
|
21
26
|
// 1 year in seconds for ipfs content
|
|
@@ -137,7 +142,7 @@ export class URLResolver implements URLResolverInterface {
|
|
|
137
142
|
|
|
138
143
|
if (result.namespace === 'ipns') {
|
|
139
144
|
// dnslink resolved to IPNS name
|
|
140
|
-
const ipnsUrl = new URL(`ipns://${result.
|
|
145
|
+
const ipnsUrl = new URL(`ipns://${base58btc.baseEncode(result.value.bytes)}${path}`)
|
|
141
146
|
resolveResult = await this.resolveIPNSName(ipnsUrl, serverTiming, options)
|
|
142
147
|
} else if (result.namespace === 'ipfs') {
|
|
143
148
|
// dnslink resolved to CID
|
|
@@ -161,22 +166,43 @@ export class URLResolver implements URLResolverInterface {
|
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
private async resolveIPNSName (url: URL, serverTiming: ServerTiming, options?: ResolveURLOptions): Promise<ResolveURLResult | Response> {
|
|
164
|
-
const
|
|
165
|
-
const result = await serverTiming.time(abbreviate('ipns.resolve'), '', this.components.ipnsResolver.resolve(
|
|
166
|
-
const path = normalizePath(`${result.path ?? ''}/${url.pathname}`)
|
|
169
|
+
const multihash = parseMultihash(url.hostname)
|
|
170
|
+
const result = await serverTiming.time(abbreviate('ipns.resolve'), '', last(this.components.ipnsResolver.resolve(multihash, options)))
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
if (result == null) {
|
|
173
|
+
throw new InvalidParametersError('Could not resolve IPNS name')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const {
|
|
177
|
+
ns, name, path
|
|
178
|
+
} = splitIPNSName(result.value)
|
|
179
|
+
|
|
180
|
+
if (ns !== 'ipfs') {
|
|
181
|
+
throw new InvalidParametersError('IPNS name resolved to non-IPFS path')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ipfsUrl = new URL(`ipfs://${name}${normalizePath(`${path ?? ''}/${url.pathname}`)}`)
|
|
169
185
|
const ipfsResult = await this.resolveIPFSPath(ipfsUrl, serverTiming, options)
|
|
170
186
|
|
|
171
187
|
if (ipfsResult instanceof Response) {
|
|
172
188
|
return ipfsResult
|
|
173
189
|
}
|
|
174
190
|
|
|
191
|
+
let expires: Date | undefined
|
|
192
|
+
const data = cborg.decode(result.record.data ?? new Uint8Array(0))
|
|
193
|
+
|
|
194
|
+
// 0 is EOL
|
|
195
|
+
if (data.ValidityType === 0 && data.Validity instanceof Uint8Array) {
|
|
196
|
+
expires = new Date(uint8ArrayToString(data.Validity))
|
|
197
|
+
}
|
|
198
|
+
|
|
175
199
|
return {
|
|
176
200
|
...ipfsResult,
|
|
177
201
|
url,
|
|
178
|
-
// IPNS ttl is in nanoseconds, convert to seconds
|
|
179
|
-
|
|
202
|
+
// IPNS ttl is in nanoseconds, convert to seconds and round to the nearest
|
|
203
|
+
// integer
|
|
204
|
+
ttl: Math.round(Number((result.record.ttl ?? 0n) / BigInt(1e9))),
|
|
205
|
+
expires
|
|
180
206
|
}
|
|
181
207
|
}
|
|
182
208
|
|
|
@@ -189,7 +215,7 @@ export class URLResolver implements URLResolverInterface {
|
|
|
189
215
|
|
|
190
216
|
if (walkPathResult.terminalElement.cid.code === CODEC_LIBP2P_KEY) {
|
|
191
217
|
// special case, peer id encoded as libp2p key CID - interpret as IPNS
|
|
192
|
-
const ipnsUrl = new URL(`ipns://${
|
|
218
|
+
const ipnsUrl = new URL(`ipns://${base58btc.baseEncode(walkPathResult.terminalElement.cid.multihash.bytes)}`)
|
|
193
219
|
const ipnsResult = await this.resolveIPNSName(ipnsUrl, serverTiming, options)
|
|
194
220
|
|
|
195
221
|
if (ipnsResult instanceof Response) {
|
|
@@ -331,3 +357,11 @@ function normalizePath (path: string): string {
|
|
|
331
357
|
|
|
332
358
|
return ''
|
|
333
359
|
}
|
|
360
|
+
|
|
361
|
+
function parseMultihash (str: string): MultihashDigest {
|
|
362
|
+
if (str.startsWith('1') || str.startsWith('Q')) {
|
|
363
|
+
return Digest.decode(base58btc.baseDecode(str))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return CID.parse(str).multihash
|
|
367
|
+
}
|
|
@@ -4,12 +4,13 @@ import * as dagPb from '@ipld/dag-pb'
|
|
|
4
4
|
import * as json from 'multiformats/codecs/json'
|
|
5
5
|
import * as raw from 'multiformats/codecs/raw'
|
|
6
6
|
import { identity } from 'multiformats/hashes/identity'
|
|
7
|
+
import { withArrayBuffer } from 'uint8arrays/with-array-buffer'
|
|
7
8
|
import { CODEC_CBOR } from '../constants.ts'
|
|
8
9
|
import { getContentTypesForCid, MEDIA_TYPE_CBOR, MEDIA_TYPE_DAG_CBOR, MEDIA_TYPE_DAG_JSON, MEDIA_TYPE_JSON, MEDIA_TYPE_OCTET_STREAM, MEDIA_TYPE_RAW } from './content-types.ts'
|
|
9
10
|
import type { AcceptHeader, ContentType } from '../index.ts'
|
|
10
11
|
import type { CID } from 'multiformats/cid'
|
|
11
12
|
|
|
12
|
-
const CONVERSIONS: Record<number, Record<string, (buf: Uint8Array) => Uint8Array
|
|
13
|
+
const CONVERSIONS: Record<number, Record<string, (buf: Uint8Array<ArrayBuffer>) => Uint8Array<ArrayBuffer>>> = {
|
|
13
14
|
[dagCbor.code]: {
|
|
14
15
|
[MEDIA_TYPE_CBOR]: (buf) => {
|
|
15
16
|
return buf
|
|
@@ -18,7 +19,8 @@ const CONVERSIONS: Record<number, Record<string, (buf: Uint8Array) => Uint8Array
|
|
|
18
19
|
return buf
|
|
19
20
|
},
|
|
20
21
|
[MEDIA_TYPE_JSON]: (buf) => {
|
|
21
|
-
return
|
|
22
|
+
// TODO: remove withArrayBuffer when dag-json is explicit about it's return type
|
|
23
|
+
return withArrayBuffer(dagJson.encode(dagCbor.decode(buf)))
|
|
22
24
|
},
|
|
23
25
|
[MEDIA_TYPE_DAG_JSON]: () => {
|
|
24
26
|
throw new Error('Cannot perform conversion since IPIP-0524')
|
|
@@ -41,7 +43,8 @@ const CONVERSIONS: Record<number, Record<string, (buf: Uint8Array) => Uint8Array
|
|
|
41
43
|
return buf
|
|
42
44
|
},
|
|
43
45
|
[MEDIA_TYPE_DAG_JSON]: (buf) => {
|
|
44
|
-
return
|
|
46
|
+
// TODO: remove withArrayBuffer when dag-json is explicit about it's return type
|
|
47
|
+
return withArrayBuffer(dagJson.encode(dagCbor.decode(buf)))
|
|
45
48
|
},
|
|
46
49
|
[MEDIA_TYPE_RAW]: (buf) => {
|
|
47
50
|
return buf
|
|
@@ -75,7 +78,8 @@ const CONVERSIONS: Record<number, Record<string, (buf: Uint8Array) => Uint8Array
|
|
|
75
78
|
return buf
|
|
76
79
|
},
|
|
77
80
|
[MEDIA_TYPE_DAG_CBOR]: (buf) => {
|
|
78
|
-
return
|
|
81
|
+
// TODO: remove withArrayBuffer when dag-cbor is explicit about it's return type
|
|
82
|
+
return withArrayBuffer(dagCbor.encode(json.decode(buf)))
|
|
79
83
|
},
|
|
80
84
|
[MEDIA_TYPE_JSON]: (buf) => {
|
|
81
85
|
return buf
|
|
@@ -135,13 +139,15 @@ const CONVERSIONS: Record<number, Record<string, (buf: Uint8Array) => Uint8Array
|
|
|
135
139
|
return buf
|
|
136
140
|
},
|
|
137
141
|
[MEDIA_TYPE_DAG_CBOR]: (buf) => {
|
|
138
|
-
return
|
|
142
|
+
// TODO: remove withArrayBuffer when dag-cbor is explicit about it's return type
|
|
143
|
+
return withArrayBuffer(dagCbor.encode(buf))
|
|
139
144
|
},
|
|
140
145
|
[MEDIA_TYPE_JSON]: (buf) => {
|
|
141
146
|
return buf
|
|
142
147
|
},
|
|
143
148
|
[MEDIA_TYPE_DAG_JSON]: (buf) => {
|
|
144
|
-
return
|
|
149
|
+
// TODO: remove withArrayBuffer when dag-json is explicit about it's return type
|
|
150
|
+
return withArrayBuffer(dagJson.encode(buf))
|
|
145
151
|
},
|
|
146
152
|
[MEDIA_TYPE_RAW]: (buf) => {
|
|
147
153
|
return buf
|
|
@@ -163,7 +169,7 @@ export interface ConvertedOutput {
|
|
|
163
169
|
* the deserialized value to that format and serialize it back to a block -
|
|
164
170
|
* return the first successful result.
|
|
165
171
|
*/
|
|
166
|
-
export function convertOutput (cid: CID, block: Uint8Array
|
|
172
|
+
export function convertOutput (cid: CID, block: Uint8Array<ArrayBuffer>, accept: AcceptHeader[]): { contentType: ContentType, output: Uint8Array<ArrayBuffer> } {
|
|
167
173
|
if (accept.length === 0) {
|
|
168
174
|
// return current format
|
|
169
175
|
return {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { libp2pDefaults } from '@helia/libp2p'
|
|
1
2
|
import { webRTCDirect } from '@libp2p/webrtc'
|
|
2
3
|
import { webSockets } from '@libp2p/websockets'
|
|
3
|
-
import { libp2pDefaults } from 'helia'
|
|
4
4
|
import type { ServiceFactoryMap } from './libp2p-types.ts'
|
|
5
|
-
import type { DefaultLibp2pServices } from 'helia'
|
|
5
|
+
import type { DefaultLibp2pServices } from '@helia/libp2p'
|
|
6
6
|
import type { Libp2pOptions } from 'libp2p'
|
|
7
7
|
|
|
8
8
|
type ServiceMap = Pick<DefaultLibp2pServices, 'dcutr' | 'identify' | 'keychain' | 'ping'>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
+
import { libp2pDefaults } from '@helia/libp2p'
|
|
1
2
|
import { kadDHT } from '@libp2p/kad-dht'
|
|
2
|
-
import { libp2pDefaults } from 'helia'
|
|
3
|
-
import { ipnsSelector } from 'ipns/selector'
|
|
4
|
-
import { ipnsValidator } from 'ipns/validator'
|
|
5
3
|
import type { ServiceFactoryMap } from './libp2p-types.ts'
|
|
6
|
-
import type { DefaultLibp2pServices } from 'helia'
|
|
4
|
+
import type { DefaultLibp2pServices } from '@helia/libp2p'
|
|
7
5
|
import type { Libp2pOptions } from 'libp2p'
|
|
8
6
|
|
|
9
7
|
type ServiceMap = Pick<DefaultLibp2pServices, 'autoNAT' | 'dcutr' | 'dht' | 'identify' | 'keychain' | 'ping' | 'upnp'>
|
|
@@ -17,13 +15,7 @@ export function getLibp2pConfig (): Libp2pOptions & Required<Pick<Libp2pOptions,
|
|
|
17
15
|
autoNAT: libp2pDefaultOptions.services.autoNAT,
|
|
18
16
|
dcutr: libp2pDefaultOptions.services.dcutr,
|
|
19
17
|
dht: kadDHT({
|
|
20
|
-
clientMode: true
|
|
21
|
-
validators: {
|
|
22
|
-
ipns: ipnsValidator
|
|
23
|
-
},
|
|
24
|
-
selectors: {
|
|
25
|
-
ipns: ipnsSelector
|
|
26
|
-
}
|
|
18
|
+
clientMode: true
|
|
27
19
|
}),
|
|
28
20
|
identify: libp2pDefaultOptions.services.identify,
|
|
29
21
|
keychain: libp2pDefaultOptions.services.keychain,
|
|
@@ -4,42 +4,60 @@ interface CacheControlHeaderOptions {
|
|
|
4
4
|
/**
|
|
5
5
|
* This should be seconds as a number.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives
|
|
8
8
|
*/
|
|
9
9
|
ttl?: number
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This can represent an IPNS record's EOL validity
|
|
13
|
+
*/
|
|
14
|
+
expires?: Date
|
|
10
15
|
protocol: string
|
|
11
16
|
response: Response
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
|
-
* Implementations may place an upper bound on any TTL received, as noted in
|
|
20
|
+
* Implementations may place an upper bound on any TTL received, as noted in
|
|
21
|
+
* Section 8 of [rfc2181].
|
|
22
|
+
*
|
|
16
23
|
* If TTL value is unknown, implementations should not send a Cache-Control
|
|
17
|
-
*
|
|
24
|
+
*
|
|
25
|
+
* No matter if TTL value is known or not, implementations should always send a
|
|
26
|
+
* Last-Modified header with the timestamp of the record resolution.
|
|
18
27
|
*
|
|
19
28
|
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#cache-control-response-header
|
|
20
29
|
*/
|
|
21
|
-
export function setCacheControlHeader ({ ttl, protocol, response }: CacheControlHeaderOptions): void {
|
|
22
|
-
|
|
23
|
-
// don't set the header if it's already set by a plugin
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let headerValue: string
|
|
30
|
+
export function setCacheControlHeader ({ ttl, expires, protocol, response }: CacheControlHeaderOptions): void {
|
|
31
|
+
let cacheControl: string
|
|
28
32
|
|
|
29
33
|
if (protocol === 'ipfs:') {
|
|
30
|
-
|
|
34
|
+
cacheControl = 'public, max-age=29030400, immutable'
|
|
31
35
|
} else if (ttl == null) {
|
|
32
36
|
/**
|
|
33
|
-
* default limit for unknown TTL: "use 5 minute as default fallback when it
|
|
37
|
+
* default limit for unknown TTL: "use 5 minute as default fallback when it
|
|
38
|
+
* is not available."
|
|
34
39
|
*
|
|
35
40
|
* @see https://github.com/ipfs/boxo/issues/329#issuecomment-1995236409
|
|
36
41
|
*/
|
|
37
|
-
|
|
42
|
+
cacheControl = 'public, max-age=300'
|
|
38
43
|
} else {
|
|
39
|
-
|
|
44
|
+
cacheControl = `public, max-age=${ttl}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (expires != null) {
|
|
48
|
+
const lifetimeRemaining = Math.max(0, Math.round((expires.getTime() - Date.now()) / 1000))
|
|
49
|
+
cacheControl += `, stale-while-revalidate=${lifetimeRemaining}, stale-if-error=${lifetimeRemaining}`
|
|
50
|
+
|
|
51
|
+
// add the expires header if it's not been set by a plugin
|
|
52
|
+
if (!response.headers.has('expires')) {
|
|
53
|
+
response.headers.set('expires', expires.toUTCString())
|
|
54
|
+
}
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
response.headers.
|
|
57
|
+
if (!response.headers.has('cache-control')) {
|
|
58
|
+
// don't set the cache-control header if it's already set by a plugin
|
|
59
|
+
response.headers.set('cache-control', cacheControl)
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
/**
|
package/src/verified-fetch.ts
CHANGED
|
@@ -198,15 +198,17 @@ export class VerifiedFetch {
|
|
|
198
198
|
return notAcceptableResponse(resource, requestedMimeTypes, [])
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
const context = {
|
|
202
202
|
...options,
|
|
203
203
|
range,
|
|
204
204
|
url,
|
|
205
205
|
resource,
|
|
206
|
-
redirected: false
|
|
207
|
-
}), withServerTiming, {
|
|
206
|
+
redirected: false,
|
|
208
207
|
serverTiming
|
|
209
|
-
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// @ts-expect-error headers type is wrong
|
|
211
|
+
return this.handleFinalResponse(await ipnsRecordPlugin.handle(context), withServerTiming, context)
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
const resolveResult = await this.urlResolver.resolve(url, serverTiming, options)
|
|
@@ -421,6 +423,7 @@ export class VerifiedFetch {
|
|
|
421
423
|
setCacheControlHeader({
|
|
422
424
|
response,
|
|
423
425
|
ttl: context.ttl,
|
|
426
|
+
expires: context.expires,
|
|
424
427
|
protocol: context.url.protocol
|
|
425
428
|
})
|
|
426
429
|
}
|