hypercore-fetch 9.4.0 → 9.6.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 (4) hide show
  1. package/README.md +12 -0
  2. package/index.js +46 -6
  3. package/package.json +1 -1
  4. package/test.js +71 -1
package/README.md CHANGED
@@ -53,6 +53,10 @@ Thus you can get the previous version of a file by using `hyper://NAME/$/version
53
53
 
54
54
  If the resource is a file, it may contain the `Last-Modified` header if the file has had a `metadata.mtime` flag set upon update.
55
55
 
56
+ If the resource is a directory, it will contain the `Allow` header to
57
+ indicate whether a hyperdrive is writable (`'HEAD,GET'`) or not
58
+ (`'HEAD,GET,PUT,DELETE'`).
59
+
56
60
  ### `fetch('hyper://NAME/example.txt', {method: 'GET'})`
57
61
 
58
62
  This will attempt to load `example.txt` from the archive labeled by `NAME`.
@@ -124,6 +128,9 @@ The `body` can be any of the options supported by the Fetch API such as a `Strin
124
128
 
125
129
  Note that this is only available with the `writable: true` flag.
126
130
 
131
+ An attempt to `PUT` a file to a hyperdrive which is not writable will
132
+ fail with status `403`.
133
+
127
134
  ### `fetch('hyper://NAME/folder/', {method: 'PUT', body: new FormData()})`
128
135
 
129
136
  You can add multiple files to a folder using the `PUT` method with a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) body.
@@ -141,6 +148,11 @@ You can delete a file or directory tree in a Hyperdrive by using the `DELETE` me
141
148
 
142
149
  `NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive or Hypercore , or a domain to parse with the [DNSLink](https://www.dnslink.io/) standard.
143
150
 
151
+ Note that this is only available with the `writable: true` flag.
152
+
153
+ An attempt to `DELETE` a file in a hyperdrive which is not writable
154
+ will fail with status `403`.
155
+
144
156
  ### `fetch('hyper://NAME/$/extensions/')`
145
157
 
146
158
  You can list the current [hypercore extensions](https://github.com/hypercore-protocol/hypercore#ext--feedregisterextensionname-handlers) that are enabled by doing a `GET` on the `/$/extensions/` directory.
package/index.js CHANGED
@@ -89,7 +89,9 @@ export default async function makeHyperFetch ({
89
89
  router.get(`hyper://${SPECIAL_DOMAIN}/`, getKey)
90
90
  router.post(`hyper://${SPECIAL_DOMAIN}/`, createKey)
91
91
 
92
+ router.put(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, putFilesVersioned)
92
93
  router.put('hyper://*/**', putFiles)
94
+ router.delete(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, deleteFilesVersioned)
93
95
  router.delete('hyper://*/**', deleteFiles)
94
96
  }
95
97
 
@@ -361,6 +363,12 @@ export default async function makeHyperFetch ({
361
363
 
362
364
  const drive = await getDrive(`hyper://${hostname}`)
363
365
 
366
+ if (!drive.db.feed.writable) {
367
+ return { status: 403, body: `Cannot PUT file to read-only drive: ${drive.url}`, headers: { Location: request.url } }
368
+ }
369
+
370
+ const mtime = Date.now()
371
+
364
372
  if (isFormData) {
365
373
  // It's a form! Get the files out and process them
366
374
  const formData = await request.formData()
@@ -371,7 +379,7 @@ export default async function makeHyperFetch ({
371
379
  Readable.from(data.stream()),
372
380
  drive.createWriteStream(filePath, {
373
381
  metadata: {
374
- mtime: Date.now()
382
+ mtime
375
383
  }
376
384
  })
377
385
  )
@@ -384,14 +392,31 @@ export default async function makeHyperFetch ({
384
392
  Readable.from(request.body),
385
393
  drive.createWriteStream(pathname, {
386
394
  metadata: {
387
- mtime: Date.now()
395
+ mtime
388
396
  }
389
397
  })
390
398
  )
391
399
  }
392
400
  }
393
401
 
394
- return { status: 201, headers: { Location: request.url } }
402
+ const fullURL = new URL(pathname, drive.url).href
403
+
404
+ const headers = {
405
+ Location: request.url,
406
+ ETag: `${drive.version}`,
407
+ Link: `<${fullURL}>; rel="canonical"`,
408
+ [HEADER_LAST_MODIFIED]: isFormData ? undefined : new Date(mtime).toUTCString()
409
+ }
410
+
411
+ return { status: 201, headers }
412
+ }
413
+
414
+ function putFilesVersioned (request) {
415
+ return {
416
+ status: 405,
417
+ body: 'Cannot PUT file to old version',
418
+ headers: { Location: request.url }
419
+ }
395
420
  }
396
421
 
397
422
  async function deleteFiles (request) {
@@ -400,6 +425,10 @@ export default async function makeHyperFetch ({
400
425
 
401
426
  const drive = await getDrive(`hyper://${hostname}`)
402
427
 
428
+ if (!drive.db.feed.writable) {
429
+ return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
430
+ }
431
+
403
432
  if (pathname.endsWith('/')) {
404
433
  let didDelete = false
405
434
  for await (const entry of drive.list(pathname)) {
@@ -419,7 +448,18 @@ export default async function makeHyperFetch ({
419
448
  }
420
449
  await drive.del(pathname)
421
450
 
422
- return { status: 200 }
451
+ const fullURL = new URL(pathname, drive.url).href
452
+
453
+ const headers = {
454
+ ETag: `${drive.version}`,
455
+ Link: `<${fullURL}>; rel="canonical"`
456
+ }
457
+
458
+ return { status: 200, headers }
459
+ }
460
+
461
+ function deleteFilesVersioned (request) {
462
+ return { status: 405, body: 'Cannot DELETE old version', headers: { Location: request.url } }
423
463
  }
424
464
 
425
465
  async function headFilesVersioned (request) {
@@ -460,7 +500,7 @@ export default async function makeHyperFetch ({
460
500
  const isDirectory = pathname.endsWith('/')
461
501
  const fullURL = new URL(pathname, drive.url).href
462
502
 
463
- const isWritable = writable && drive.writable
503
+ const isWritable = writable && drive.db.feed.writable
464
504
 
465
505
  const Allow = isWritable ? BASIC_METHODS.concat(WRITABLE_METHODS) : BASIC_METHODS
466
506
 
@@ -723,7 +763,7 @@ function makeToTry (pathname) {
723
763
 
724
764
  async function resolvePath (drive, pathname, noResolve) {
725
765
  if (noResolve) {
726
- const entry = drive.entry(pathname)
766
+ const entry = await drive.entry(pathname)
727
767
 
728
768
  return { entry, path: pathname }
729
769
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypercore-fetch",
3
- "version": "9.4.0",
3
+ "version": "9.6.0",
4
4
  "description": "Implementation of Fetch that uses the Dat SDK for loading p2p content",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/test.js CHANGED
@@ -333,6 +333,22 @@ test('Ignore index.html with noResolve', async (t) => {
333
333
  const entries = await listDirRequest.json()
334
334
  t.deepEqual(entries, ['index.html'], 'able to list index.html')
335
335
  })
336
+ test('Ensure that noResolve works with file paths', async (t) => {
337
+ const created = await nextURL(t)
338
+ const uploadLocation = new URL('./example.txt', created)
339
+ const uploadResponse = await fetch(uploadLocation, {
340
+ method: 'put',
341
+ body: SAMPLE_CONTENT
342
+ })
343
+ await checkResponse(uploadResponse, t)
344
+
345
+ const noResolve = uploadLocation.href + '?noResolve'
346
+ const getRequest = await fetch(noResolve)
347
+ await checkResponse(getRequest, t)
348
+
349
+ const headRequest = await fetch(noResolve, { method: 'HEAD' })
350
+ await checkResponse(headRequest, t)
351
+ })
336
352
  test('Render index.gmi', async (t) => {
337
353
  const created = await nextURL(t)
338
354
  const uploadLocation = new URL('./index.gmi', created)
@@ -484,7 +500,7 @@ test('Error on invalid hostname', async (t) => {
484
500
  }
485
501
  })
486
502
 
487
- test('GET older version of file from VERSION folder', async (t) => {
503
+ test('Old versions in VERSION folder', async (t) => {
488
504
  const created = await nextURL(t)
489
505
 
490
506
  const fileName = 'example.txt'
@@ -512,6 +528,27 @@ test('GET older version of file from VERSION folder', async (t) => {
512
528
  await checkResponse(versionedRootResponse, t, 'Able to GET versioned root')
513
529
  const versionedRootContents = await versionedRootResponse.json()
514
530
  t.deepEqual(versionedRootContents, [], 'Old root content got loaded')
531
+
532
+ // PUT on old version should fail
533
+ const putResponse = await fetch(versionFileURL, {
534
+ method: 'PUT',
535
+ body: SAMPLE_CONTENT
536
+ })
537
+ if (putResponse.ok) {
538
+ throw new Error('PUT old version of file should have failed')
539
+ } else {
540
+ t.equal(putResponse.status, 405, 'PUT old version returned status 405 Not Allowed')
541
+ }
542
+
543
+ // DELETE on old version should fail
544
+ const deleteResponse = await fetch(versionFileURL, {
545
+ method: 'delete'
546
+ })
547
+ if (deleteResponse.ok) {
548
+ throw new Error('DELETE old version of file should have failed')
549
+ } else {
550
+ t.equal(deleteResponse.status, 405, 'DELETE old version returned status 405 Not Allowed')
551
+ }
515
552
  })
516
553
 
517
554
  test('Handle empty string pathname', async (t) => {
@@ -565,6 +602,39 @@ test('Handle empty string pathname', async (t) => {
565
602
  t.deepEqual(await versionedGetResponse.json(), ['example.txt', 'example2.txt'], 'Returns root directory prior to DELETE')
566
603
  })
567
604
 
605
+ test('Return status 403 Forbidden on attempt to modify read-only hyperdrive', async (t) => {
606
+ const readOnlyURL = 'hyper://blog.mauve.moe/new-file.txt'
607
+ const putResponse = await fetch(readOnlyURL, { method: 'PUT', body: SAMPLE_CONTENT })
608
+ if (putResponse.ok) {
609
+ throw new Error('PUT file to read-only drive should have failed')
610
+ } else {
611
+ t.equal(putResponse.status, 403, 'PUT file to read-only drive returned status 403 Forbidden')
612
+ }
613
+
614
+ const deleteResponse = await fetch(readOnlyURL, { method: 'DELETE' })
615
+ if (deleteResponse.ok) {
616
+ throw new Error('DELETE file in read-only drive should have failed')
617
+ } else {
618
+ t.equal(deleteResponse.status, 403, 'DELETE file to read-only drive returned status 403 Forbidden')
619
+ }
620
+ })
621
+
622
+ test('Check hyperdrive writability', async (t) => {
623
+ const created = await nextURL(t)
624
+
625
+ const readOnlyRootDirectory = 'hyper://blog.mauve.moe/?noResolve'
626
+ const readOnlyHeadResponse = await fetch(readOnlyRootDirectory, { method: 'HEAD' })
627
+ await checkResponse(readOnlyHeadResponse, t, 'Able to load HEAD')
628
+ const readOnlyHeadersAllow = readOnlyHeadResponse.headers.get('Allow')
629
+ t.equal(readOnlyHeadersAllow, 'HEAD,GET', 'Expected read-only Allows header')
630
+
631
+ const writableRootDirectory = new URL('/', created)
632
+ const writableHeadResponse = await fetch(writableRootDirectory, { method: 'HEAD' })
633
+ await checkResponse(writableHeadResponse, t, 'Able to load HEAD')
634
+ const writableHeadersAllow = writableHeadResponse.headers.get('Allow')
635
+ t.equal(writableHeadersAllow, 'HEAD,GET,PUT,DELETE', 'Expected writable Allows header')
636
+ })
637
+
568
638
  async function checkResponse (response, t, successMessage = 'Response OK') {
569
639
  if (!response.ok) {
570
640
  const message = await response.text()