Socket
Socket
Sign inDemoInstall

@cloudflare/kv-asset-handler

Package Overview
Dependencies
Maintainers
2
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cloudflare/kv-asset-handler - npm Package Compare versions

Comparing version 0.0.2 to 0.0.4

.github/workflows/test.yml

2

package.json
{
"name": "@cloudflare/kv-asset-handler",
"version": "0.0.2",
"version": "0.0.4",
"description": "Routes requests to KV assets",

@@ -5,0 +5,0 @@ "main": "./src/index.js",

@@ -34,11 +34,11 @@ # @cloudflare/kv-asset-handler

})
const customKeyModifier = url => {
//custom key mapping optional
if (url.endsWith('/')) url += 'index.html'
return url.replace('/docs', '').replace(/^\/+/, '')
}
async function handleRequest(request) {
if (request.url.includes('/docs')) {
try {
return await getAssetFromKV(request, url => {
//custom key mapping optional
if (url.endsWith('/')) url += 'index.html'
return url.replace('/docs', '')
})
return await getAssetFromKV(request, customKeyModifier)
} catch (e) {

@@ -45,0 +45,0 @@ return new Response(`"${customKeyModifier(request.url)}" not found`, {

@@ -1,16 +0,47 @@

import mime from 'mime/lite'
import mime from 'mime'
const defaultKeyModifier = pathname => {
/**
* maps the path of incoming request to the request pathKey to look up
* in bucket and in cache
* e.g. for a path '/' returns '/index.html' which serves
* the content of bucket/index.html
* @param {Request} request incoming request
*/
const mapRequestToAsset = request => {
const parsedUrl = new URL(request.url)
let pathname = parsedUrl.pathname
if (pathname.endsWith('/')) {
// If path looks like a directory append index.html
// e.g. If path is /about/ -> /about/index.html
pathname = pathname.concat('index.html')
} else if (!mime.getType(pathname)) {
// If path doesn't look like valid content
// e.g. /about.me -> /about.me/index.html
pathname = pathname.concat('/index.html')
}
return pathname
parsedUrl.pathname = pathname
return new Request(parsedUrl, request)
}
const defaultCacheControl = {
browserTTL: 0,
edgeTTL: 100 * 60 * 60 * 24, // 100 days
bypassCache: false,
bypassCache: false, // do not bypass Cloudflare's cache
}
/**
* takes the path of the incoming request, gathers the approriate cotent from KV, and returns
* the response
*
* @param {event} event the fetch event of the triggered request
* @param {{mapRequestToAsset: (string: Request) => Request, cacheControl: {bypassCache:boolean, edgeTTL: number, browserTTL:number}, ASSET_NAMESPACE: any, ASSET_MANIFEST:any}} [options] configurable options
* @param {CacheControl} [options.cacheControl] determine how to cache on Cloudflare and the browser
* @param {typeof(options.mapRequestToAsset)} [options.mapRequestToAsset] maps the path of incoming request to the request pathKey to look up
* @param {any} [options.ASSET_NAMESPACE] the binding to the namespace that script references
* @param {any} [options.ASSET_MANIFEST] the map of the key to cache and store in KV
* */
const getAssetFromKV = async (event, options) => {
// Assign any missing options passed in to the default
options = Object.assign(

@@ -20,3 +51,3 @@ {

ASSET_MANIFEST: __STATIC_CONTENT_MANIFEST,
keyModifier: defaultKeyModifier,
mapRequestToAsset: mapRequestToAsset,
cacheControl: defaultCacheControl,

@@ -34,30 +65,34 @@ },

}
if (typeof ASSET_NAMESPACE === 'undefined') {
throw new Error(`there is no ${ASSET_NAMESPACE} namespace bound to the script`)
}
const parsedUrl = new URL(request.url)
const pathname = options.keyModifier(parsedUrl.pathname)
// remove prepended /
let key = pathname.slice(1)
// determine the requestKey based on the actual file served for the incoming request
const requestKey = options.mapRequestToAsset(request)
const parsedUrl = new URL(requestKey.url)
const pathname = parsedUrl.pathname
// pathKey is the file path to look up in the manifest
let pathKey = pathname.replace(/^\/+/, '') // remove prepended /
const cache = caches.default
const mimeType = mime.getType(pathKey) || 'text/plain'
let shouldEdgeCache = false
let shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash
// check manifest for map from file path to hash
if (typeof ASSET_MANIFEST !== 'undefined') {
let hashKey = JSON.parse(ASSET_MANIFEST)[key]
if (typeof hashKey !== 'undefined') {
key = hashKey
// cache on edge if content is hashed
shouldEdgeCache = true
if (JSON.parse(ASSET_MANIFEST)[pathKey]) {
pathKey = JSON.parse(ASSET_MANIFEST)[pathKey]
shouldEdgeCache = true // cache on edge if pathKey is a unique hash
}
}
// this excludes search params from cache key
const cacheKey = `${parsedUrl.origin}/${key}`
// TODO cacheKey should be a request and this excludes search params from cache
const cacheKey = `${parsedUrl.origin}/${pathKey}`
// options.cacheControl can be a function that takes a request or an object
// the result should be formed like the defaultCacheControl object
// if argument passed in for cacheControl is a function then
// evaluate that function. otherwise return the Object passed in
// or default Object
const evalCacheOpts = (() => {

@@ -78,6 +113,10 @@ switch (typeof options.cacheControl) {

if (options.cacheControl.bypassCache) {
shouldEdgeCache = !options.cacheControl.bypassCache
shouldEdgeCache = false
}
let response = await cache.match(cacheKey)
let response = null
if (shouldEdgeCache) {
response = await cache.match(cacheKey)
}
if (response) {

@@ -88,23 +127,27 @@ let headers = new Headers(response.headers)

} else {
const mimeType = mime.getType(pathname)
const body = await __STATIC_CONTENT.get(key, 'arrayBuffer')
const body = await __STATIC_CONTENT.get(pathKey, 'arrayBuffer')
if (body === null) {
throw new Error(`could not find ${key} in your content namespace`)
throw new Error(`could not find ${pathKey} in your content namespace`)
}
response = new Response(body)
response.headers.set('Content-Type', mimeType)
// TODO: could implement CF-Cache-Status REVALIDATE if path w/o hash existed in manifest
if (shouldEdgeCache === true) {
if (shouldEdgeCache) {
response.headers.set('CF-Cache-Status', 'MISS')
// determine Cloudflare cache behavior
response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`)
event.waitUntil(cache.put(cacheKey, response.clone()))
// don't assume we want same cache behavior on client
// so remove the header from the response we'll return
response.headers.delete('Cache-Control')
}
}
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`)
response.headers.set('Content-Type', mimeType)
if (options.cacheControl.browserTTL) {
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`)
}
return response
}
export { getAssetFromKV }
export { getAssetFromKV, mapRequestToAsset }
const makeServiceWorkerEnv = require('service-worker-mock')
const HASH = '123-I-AM-A-HASH-BROWN'
const HASH = '123HASHBROWN'
export const getEvent = request => {
const waitUntil = callback => {
callback
}
return {
request,
waitUntil,
}
}
export const mockKV = () => {
const store = {
'key1.txt-123-I-AM-A-HASH-BROWN': 'val1',
'key1.png-123-I-AM-A-HASH-BROWN': 'val1',
'index.html-123-I-AM-A-HASH-BROWN': 'index.html',
'key1.123HASHBROWN.txt': 'val1',
'key1.123HASHBROWN.png': 'val1',
'index.123HASHBROWN.html': 'index.html',
'cache.123HASHBROWN.html': 'cache me if you can',
'nohash.txt': 'no hash but still got some result',
'sub/blah.123HASHBROWN.png': 'picturedis',
}

@@ -18,14 +31,19 @@ return {

return JSON.stringify({
'key1.txt': `key1.txt-${HASH}`,
'key1.png': `key1.png-${HASH}`,
'index.html': `index.html-${HASH}`,
'key1.txt': `key1.${HASH}.txt`,
'key1.png': `key1.${HASH}.png`,
'cache.html': `cache.${HASH}.html`,
'index.html': `index.${HASH}.html`,
'sub/blah.png': `sub/blah.${HASH}.png`,
})
}
let cacheStore = {}
export const mockCaches = () => {
const store = { 'https://blah.com/key1.txt-123-I-AM-A-HASH-BROWN': 'val1' }
return {
default: {
match: () => null,
put: a => {},
match: key => {
return cacheStore[key] || null
},
put: (key, val) => {
return (cacheStore[key] = val)
},
},

@@ -32,0 +50,0 @@ }

import test from 'ava'
import { getAssetFromKV } from '../src/index'
import { mockGlobal } from '../src/mocks'
import { mockGlobal, getEvent } from '../src/mocks'
import { getAssetFromKV, mapRequestToAsset } from '../src/index'
const getEvent = request => {
const waitUntil = callback => {}
return {
request,
waitUntil,
}
}
test('mapRequestToAsset() correctly changes /about -> /about/index.html', async t => {
mockGlobal()
let path = '/about'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + '/index.html')
})
test('mapRequestToAsset() correctly changes /about/ -> /about/index.html', async t => {
let path = '/about/'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + 'index.html')
})
test('mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html', async t => {
let path = '/about.me/'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + 'index.html')
})
test('getAssetFromKV return correct val from KV and default caching', async t => {
mockGlobal()
const event = getEvent(new Request('https://blah.com/key1.txt'))

@@ -19,5 +32,6 @@ const res = await getAssetFromKV(event)

if (res) {
t.is(res.headers.get('cache-control'), 'max-age=0')
t.is(res.headers.get('Cf-Cache-Status'), 'MISS')
t.is(res.headers.get('cache-control'), null)
t.is(res.headers.get('cf-cache-status'), 'MISS')
t.is(await res.text(), 'val1')
t.true(res.headers.get('content-type').includes('text'))
} else {

@@ -27,3 +41,15 @@ t.fail('Response was undefined')

})
test('getAssetFromKV if not in asset manifest still returns nohash.txt', async t => {
mockGlobal()
const event = getEvent(new Request('https://blah.com/nohash.txt'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'no hash but still got some result')
t.true(res.headers.get('content-type').includes('text'))
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV gets index.html by default for / requests', async t => {

@@ -36,2 +62,3 @@ mockGlobal()

t.is(await res.text(), 'index.html')
t.true(res.headers.get('content-type').includes('html'))
} else {

@@ -44,15 +71,16 @@ t.fail('Response was undefined')

mockGlobal()
const event = getEvent(new Request('https://blah.com/docs/index.html'))
const event = getEvent(new Request('https://blah.com/docs/sub/blah.png'))
const customKeyModifier = pathname => {
if (pathname === '/') {
pathname += 'index.html'
}
return pathname.replace('/docs', '')
const customRequestMapper = request => {
let defaultModifiedRequest = mapRequestToAsset(request)
let url = new URL(defaultModifiedRequest.url)
url.pathname = url.pathname.replace('/docs', '')
return new Request(url, request)
}
const res = await getAssetFromKV(event, { keyModifier: customKeyModifier })
const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper })
if (res) {
t.is(await res.text(), 'index.html')
t.is(await res.text(), 'picturedis')
} else {

@@ -96,5 +124,6 @@ t.fail('Response was undefined')

if (res1 && res2) {
t.is(res1.headers.get('cache-control'), 'max-age=0')
t.is(res1.headers.get('cache-control'), null)
t.true(res2.headers.get('content-type').includes('png'))
t.is(res2.headers.get('cache-control'), 'max-age=720')
t.is(res2.headers.get('Cf-Cache-Status'), 'MISS')
t.is(res2.headers.get('cf-cache-status'), 'MISS')
} else {

@@ -104,3 +133,22 @@ t.fail('Response was undefined')

})
test('getAssetFromKV caches on two sequential requests', async t => {
mockGlobal()
const sleep = milliseconds => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
const event = getEvent(new Request('https://blah.com/cache.html'))
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720, browserTTL: 720 } })
await sleep(1)
const res2 = await getAssetFromKV(event)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res1.headers.get('cache-control'), 'max-age=720')
t.is(res2.headers.get('cf-cache-status'), 'HIT')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV does not cache on Cloudflare when bypass cache set', async t => {

@@ -113,4 +161,4 @@ mockGlobal()

if (res) {
t.is(res.headers.get('cache-control'), 'max-age=0')
t.is(res.headers.get('Cf-Cache-Status'), null)
t.is(res.headers.get('cache-control'), null)
t.is(res.headers.get('cf-cache-status'), null)
} else {

@@ -132,2 +180,13 @@ t.fail('Response was undefined')

test('getAssetFromKV with no trailing slash on a subdirectory', async t => {
mockGlobal()
const event = getEvent(new Request('https://blah.com/sub/blah.png'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'picturedis')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV no result throws an error', async t => {

@@ -134,0 +193,0 @@ mockGlobal()

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc