hypercore-fetch 9.4.0 → 9.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 (4) hide show
  1. package/README.md +12 -0
  2. package/index.js +19 -1
  3. package/package.json +1 -1
  4. package/test.js +55 -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,10 @@ 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
+
364
370
  if (isFormData) {
365
371
  // It's a form! Get the files out and process them
366
372
  const formData = await request.formData()
@@ -394,12 +400,20 @@ export default async function makeHyperFetch ({
394
400
  return { status: 201, headers: { Location: request.url } }
395
401
  }
396
402
 
403
+ function putFilesVersioned (request) {
404
+ return { status: 405, body: 'Cannot PUT file to old version', headers: { Location: request.url } }
405
+ }
406
+
397
407
  async function deleteFiles (request) {
398
408
  const { hostname, pathname: rawPathname } = new URL(request.url)
399
409
  const pathname = decodeURI(ensureLeadingSlash(rawPathname))
400
410
 
401
411
  const drive = await getDrive(`hyper://${hostname}`)
402
412
 
413
+ if (!drive.db.feed.writable) {
414
+ return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
415
+ }
416
+
403
417
  if (pathname.endsWith('/')) {
404
418
  let didDelete = false
405
419
  for await (const entry of drive.list(pathname)) {
@@ -422,6 +436,10 @@ export default async function makeHyperFetch ({
422
436
  return { status: 200 }
423
437
  }
424
438
 
439
+ function deleteFilesVersioned (request) {
440
+ return { status: 405, body: 'Cannot DELETE old version', headers: { Location: request.url } }
441
+ }
442
+
425
443
  async function headFilesVersioned (request) {
426
444
  const url = new URL(request.url)
427
445
  const { hostname, pathname: rawPathname, searchParams } = url
@@ -460,7 +478,7 @@ export default async function makeHyperFetch ({
460
478
  const isDirectory = pathname.endsWith('/')
461
479
  const fullURL = new URL(pathname, drive.url).href
462
480
 
463
- const isWritable = writable && drive.writable
481
+ const isWritable = writable && drive.db.feed.writable
464
482
 
465
483
  const Allow = isWritable ? BASIC_METHODS.concat(WRITABLE_METHODS) : BASIC_METHODS
466
484
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypercore-fetch",
3
- "version": "9.4.0",
3
+ "version": "9.5.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
@@ -484,7 +484,7 @@ test('Error on invalid hostname', async (t) => {
484
484
  }
485
485
  })
486
486
 
487
- test('GET older version of file from VERSION folder', async (t) => {
487
+ test('Old versions in VERSION folder', async (t) => {
488
488
  const created = await nextURL(t)
489
489
 
490
490
  const fileName = 'example.txt'
@@ -512,6 +512,27 @@ test('GET older version of file from VERSION folder', async (t) => {
512
512
  await checkResponse(versionedRootResponse, t, 'Able to GET versioned root')
513
513
  const versionedRootContents = await versionedRootResponse.json()
514
514
  t.deepEqual(versionedRootContents, [], 'Old root content got loaded')
515
+
516
+ // PUT on old version should fail
517
+ const putResponse = await fetch(versionFileURL, {
518
+ method: 'PUT',
519
+ body: SAMPLE_CONTENT
520
+ })
521
+ if (putResponse.ok) {
522
+ throw new Error('PUT old version of file should have failed')
523
+ } else {
524
+ t.equal(putResponse.status, 405, 'PUT old version returned status 405 Not Allowed')
525
+ }
526
+
527
+ // DELETE on old version should fail
528
+ const deleteResponse = await fetch(versionFileURL, {
529
+ method: 'delete'
530
+ })
531
+ if (deleteResponse.ok) {
532
+ throw new Error('DELETE old version of file should have failed')
533
+ } else {
534
+ t.equal(deleteResponse.status, 405, 'DELETE old version returned status 405 Not Allowed')
535
+ }
515
536
  })
516
537
 
517
538
  test('Handle empty string pathname', async (t) => {
@@ -565,6 +586,39 @@ test('Handle empty string pathname', async (t) => {
565
586
  t.deepEqual(await versionedGetResponse.json(), ['example.txt', 'example2.txt'], 'Returns root directory prior to DELETE')
566
587
  })
567
588
 
589
+ test('Return status 403 Forbidden on attempt to modify read-only hyperdrive', async (t) => {
590
+ const readOnlyURL = 'hyper://blog.mauve.moe/new-file.txt'
591
+ const putResponse = await fetch(readOnlyURL, { method: 'PUT', body: SAMPLE_CONTENT })
592
+ if (putResponse.ok) {
593
+ throw new Error('PUT file to read-only drive should have failed')
594
+ } else {
595
+ t.equal(putResponse.status, 403, 'PUT file to read-only drive returned status 403 Forbidden')
596
+ }
597
+
598
+ const deleteResponse = await fetch(readOnlyURL, { method: 'DELETE' })
599
+ if (deleteResponse.ok) {
600
+ throw new Error('DELETE file in read-only drive should have failed')
601
+ } else {
602
+ t.equal(deleteResponse.status, 403, 'DELETE file to read-only drive returned status 403 Forbidden')
603
+ }
604
+ })
605
+
606
+ test('Check hyperdrive writability', async (t) => {
607
+ const created = await nextURL(t)
608
+
609
+ const readOnlyRootDirectory = 'hyper://blog.mauve.moe/?noResolve'
610
+ const readOnlyHeadResponse = await fetch(readOnlyRootDirectory, { method: 'HEAD' })
611
+ await checkResponse(readOnlyHeadResponse, t, 'Able to load HEAD')
612
+ const readOnlyHeadersAllow = readOnlyHeadResponse.headers.get('Allow')
613
+ t.equal(readOnlyHeadersAllow, 'HEAD,GET', 'Expected read-only Allows header')
614
+
615
+ const writableRootDirectory = new URL('/', created)
616
+ const writableHeadResponse = await fetch(writableRootDirectory, { method: 'HEAD' })
617
+ await checkResponse(writableHeadResponse, t, 'Able to load HEAD')
618
+ const writableHeadersAllow = writableHeadResponse.headers.get('Allow')
619
+ t.equal(writableHeadersAllow, 'HEAD,GET,PUT,DELETE', 'Expected writable Allows header')
620
+ })
621
+
568
622
  async function checkResponse (response, t, successMessage = 'Response OK') {
569
623
  if (!response.ok) {
570
624
  const message = await response.text()