Socket
Socket
Sign inDemoInstall

@cloudflare/kv-asset-handler

Package Overview
Dependencies
Maintainers
1
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.3.1 to 0.3.2

4

dist/index.d.ts

@@ -1,2 +0,2 @@

import { Options, CacheControl, MethodNotAllowedError, NotFoundError, InternalError } from './types';
import { CacheControl, InternalError, MethodNotAllowedError, NotFoundError, Options } from "./types";
declare global {

@@ -36,2 +36,2 @@ var __STATIC_CONTENT: any, __STATIC_CONTENT_MANIFEST: string;

export { getAssetFromKV, mapRequestToAsset, serveSinglePageApp };
export { Options, CacheControl, MethodNotAllowedError, NotFoundError, InternalError };
export { Options, CacheControl, MethodNotAllowedError, NotFoundError, InternalError, };

@@ -6,21 +6,23 @@ "use strict";

const types_1 = require("./types");
Object.defineProperty(exports, "InternalError", { enumerable: true, get: function () { return types_1.InternalError; } });
Object.defineProperty(exports, "MethodNotAllowedError", { enumerable: true, get: function () { return types_1.MethodNotAllowedError; } });
Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return types_1.NotFoundError; } });
Object.defineProperty(exports, "InternalError", { enumerable: true, get: function () { return types_1.InternalError; } });
const defaultCacheControl = {
browserTTL: null,
edgeTTL: 2 * 60 * 60 * 24, // 2 days
edgeTTL: 2 * 60 * 60 * 24,
bypassCache: false, // do not bypass Cloudflare's cache
};
const parseStringAsObject = (maybeString) => typeof maybeString === 'string' ? JSON.parse(maybeString) : maybeString;
const parseStringAsObject = (maybeString) => typeof maybeString === "string"
? JSON.parse(maybeString)
: maybeString;
const getAssetFromKVDefaultOptions = {
ASSET_NAMESPACE: typeof __STATIC_CONTENT !== 'undefined' ? __STATIC_CONTENT : undefined,
ASSET_MANIFEST: typeof __STATIC_CONTENT_MANIFEST !== 'undefined'
ASSET_NAMESPACE: typeof __STATIC_CONTENT !== "undefined" ? __STATIC_CONTENT : undefined,
ASSET_MANIFEST: typeof __STATIC_CONTENT_MANIFEST !== "undefined"
? parseStringAsObject(__STATIC_CONTENT_MANIFEST)
: {},
cacheControl: defaultCacheControl,
defaultMimeType: 'text/plain',
defaultDocument: 'index.html',
defaultMimeType: "text/plain",
defaultDocument: "index.html",
pathIsEncoded: false,
defaultETag: 'strong',
defaultETag: "strong",
};

@@ -43,3 +45,3 @@ function assignOptions(options) {

let pathname = parsedUrl.pathname;
if (pathname.endsWith('/')) {
if (pathname.endsWith("/")) {
// If path looks like a directory append options.defaultDocument

@@ -52,3 +54,3 @@ // e.g. If path is /about/ -> /about/index.html

// e.g. /about.me -> /about.me/index.html
pathname = pathname.concat('/' + options.defaultDocument);
pathname = pathname.concat("/" + options.defaultDocument);
}

@@ -72,3 +74,3 @@ parsedUrl.pathname = pathname;

// a HTML file in some specific directory.
if (parsedUrl.pathname.endsWith('.html')) {
if (parsedUrl.pathname.endsWith(".html")) {
// If expected HTML file was missing, just return the root index.html (or options.defaultDocument)

@@ -89,6 +91,6 @@ return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request);

const ASSET_MANIFEST = parseStringAsObject(options.ASSET_MANIFEST);
if (typeof ASSET_NAMESPACE === 'undefined') {
if (typeof ASSET_NAMESPACE === "undefined") {
throw new types_1.InternalError(`there is no KV namespace bound to the script`);
}
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, ''); // strip any preceding /'s
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, ""); // strip any preceding /'s
let pathIsEncoded = options.pathIsEncoded;

@@ -110,3 +112,3 @@ let requestKey;

const mappedRequest = mapRequestToAsset(request);
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '');
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, "");
if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) {

@@ -121,3 +123,3 @@ pathIsEncoded = true;

}
const SUPPORTED_METHODS = ['GET', 'HEAD'];
const SUPPORTED_METHODS = ["GET", "HEAD"];
if (!SUPPORTED_METHODS.includes(requestKey.method)) {

@@ -127,14 +129,16 @@ throw new types_1.MethodNotAllowedError(`${requestKey.method} is not a valid request method`);

const parsedUrl = new URL(requestKey.url);
const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname; // decode percentage encoded path only when necessary
const pathname = pathIsEncoded
? decodeURIComponent(parsedUrl.pathname)
: parsedUrl.pathname; // decode percentage encoded path only when necessary
// pathKey is the file path to look up in the manifest
let pathKey = pathname.replace(/^\/+/, ''); // remove prepended /
let pathKey = pathname.replace(/^\/+/, ""); // remove prepended /
// @ts-ignore
const cache = caches.default;
let mimeType = mime.getType(pathKey) || options.defaultMimeType;
if (mimeType.startsWith('text') || mimeType === 'application/javascript') {
mimeType += '; charset=utf-8';
if (mimeType.startsWith("text") || mimeType === "application/javascript") {
mimeType += "; charset=utf-8";
}
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') {
if (typeof ASSET_MANIFEST !== "undefined") {
if (ASSET_MANIFEST[pathKey]) {

@@ -153,5 +157,5 @@ pathKey = ASSET_MANIFEST[pathKey];

switch (typeof options.cacheControl) {
case 'function':
case "function":
return options.cacheControl(request);
case 'object':
case "object":
return options.cacheControl;

@@ -168,7 +172,7 @@ default:

if (!entityId) {
return '';
return "";
}
switch (validatorType) {
case 'weak':
if (!entityId.startsWith('W/')) {
case "weak":
if (!entityId.startsWith("W/")) {
if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) {

@@ -180,5 +184,5 @@ return `W/${entityId}`;

return entityId;
case 'strong':
case "strong":
if (entityId.startsWith(`W/"`)) {
entityId = entityId.replace('W/', '');
entityId = entityId.replace("W/", "");
}

@@ -190,3 +194,3 @@ if (!entityId.endsWith(`"`)) {

default:
return '';
return "";
}

@@ -198,7 +202,7 @@ };

options.cacheControl.edgeTTL === null ||
request.method == 'HEAD') {
request.method == "HEAD") {
shouldEdgeCache = false;
}
// only set max-age if explicitly passed in a number as an arg
const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number';
const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === "number";
let response = null;

@@ -210,3 +214,3 @@ if (shouldEdgeCache) {

if (response.status > 300 && response.status < 400) {
if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) {
if (response.body && "cancel" in Object.getPrototypeOf(response.body)) {
// Body exists and environment supports readable streams

@@ -225,5 +229,5 @@ response.body.cancel();

status: 0,
statusText: '',
statusText: "",
};
opts.headers.set('cf-cache-status', 'HIT');
opts.headers.set("cf-cache-status", "HIT");
if (response.status) {

@@ -233,9 +237,9 @@ opts.status = response.status;

}
else if (opts.headers.has('Content-Range')) {
else if (opts.headers.has("Content-Range")) {
opts.status = 206;
opts.statusText = 'Partial Content';
opts.statusText = "Partial Content";
}
else {
opts.status = 200;
opts.statusText = 'OK';
opts.statusText = "OK";
}

@@ -246,3 +250,3 @@ response = new Response(response.body, opts);

else {
const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer');
const body = await ASSET_NAMESPACE.get(pathKey, "arrayBuffer");
if (body === null) {

@@ -253,34 +257,34 @@ throw new types_1.NotFoundError(`could not find ${pathKey} in your content namespace`);

if (shouldEdgeCache) {
response.headers.set('Accept-Ranges', 'bytes');
response.headers.set('Content-Length', String(body.byteLength));
response.headers.set("Accept-Ranges", "bytes");
response.headers.set("Content-Length", String(body.byteLength));
// set etag before cache insertion
if (!response.headers.has('etag')) {
response.headers.set('etag', formatETag(pathKey));
if (!response.headers.has("etag")) {
response.headers.set("etag", formatETag(pathKey));
}
// determine Cloudflare cache behavior
response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`);
response.headers.set("Cache-Control", `max-age=${options.cacheControl.edgeTTL}`);
event.waitUntil(cache.put(cacheKey, response.clone()));
response.headers.set('CF-Cache-Status', 'MISS');
response.headers.set("CF-Cache-Status", "MISS");
}
}
response.headers.set('Content-Type', mimeType);
response.headers.set("Content-Type", mimeType);
if (response.status === 304) {
let etag = formatETag(response.headers.get('etag'));
let ifNoneMatch = cacheKey.headers.get('if-none-match');
let proxyCacheStatus = response.headers.get('CF-Cache-Status');
let etag = formatETag(response.headers.get("etag"));
let ifNoneMatch = cacheKey.headers.get("if-none-match");
let proxyCacheStatus = response.headers.get("CF-Cache-Status");
if (etag) {
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') {
response.headers.set('CF-Cache-Status', 'EXPIRED');
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === "MISS") {
response.headers.set("CF-Cache-Status", "EXPIRED");
}
else {
response.headers.set('CF-Cache-Status', 'REVALIDATED');
response.headers.set("CF-Cache-Status", "REVALIDATED");
}
response.headers.set('etag', formatETag(etag, 'weak'));
response.headers.set("etag", formatETag(etag, "weak"));
}
}
if (shouldSetBrowserCache) {
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`);
response.headers.set("Cache-Control", `max-age=${options.cacheControl.browserTTL}`);
}
else {
response.headers.delete('Cache-Control');
response.headers.delete("Cache-Control");
}

@@ -287,0 +291,0 @@ return response;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sleep = exports.mockGlobalScope = exports.mockRequestScope = exports.mockCaches = exports.mockManifest = exports.mockKV = exports.getEvent = void 0;
const makeServiceWorkerEnv = require('service-worker-mock');
const HASH = '123HASHBROWN';
const makeServiceWorkerEnv = require("service-worker-mock");
const HASH = "123HASHBROWN";
const getEvent = (request) => {

@@ -17,19 +17,19 @@ const waitUntil = async (callback) => {

const store = {
'key1.123HASHBROWN.txt': 'val1',
'key1.123HASHBROWN.png': 'val1',
'index.123HASHBROWN.html': 'index.html',
'cache.123HASHBROWN.html': 'cache me if you can',
'测试.123HASHBROWN.html': 'My filename is non-ascii',
'%not-really-percent-encoded.123HASHBROWN.html': 'browser percent encoded',
'%2F.123HASHBROWN.html': 'user percent encoded',
'你好.123HASHBROWN.html': 'I shouldnt be served',
'%E4%BD%A0%E5%A5%BD.123HASHBROWN.html': 'Im important',
'nohash.txt': 'no hash but still got some result',
'sub/blah.123HASHBROWN.png': 'picturedis',
'sub/index.123HASHBROWN.html': 'picturedis',
'client.123HASHBROWN': 'important file',
'client.123HASHBROWN/index.html': 'Im here but serve my big bro above',
'image.123HASHBROWN.png': 'imagepng',
'image.123HASHBROWN.webp': 'imagewebp',
'你好/index.123HASHBROWN.html': 'My path is non-ascii',
"key1.123HASHBROWN.txt": "val1",
"key1.123HASHBROWN.png": "val1",
"index.123HASHBROWN.html": "index.html",
"cache.123HASHBROWN.html": "cache me if you can",
"测试.123HASHBROWN.html": "My filename is non-ascii",
"%not-really-percent-encoded.123HASHBROWN.html": "browser percent encoded",
"%2F.123HASHBROWN.html": "user percent encoded",
"你好.123HASHBROWN.html": "I shouldnt be served",
"%E4%BD%A0%E5%A5%BD.123HASHBROWN.html": "Im important",
"nohash.txt": "no hash but still got some result",
"sub/blah.123HASHBROWN.png": "picturedis",
"sub/index.123HASHBROWN.html": "picturedis",
"client.123HASHBROWN": "important file",
"client.123HASHBROWN/index.html": "Im here but serve my big bro above",
"image.123HASHBROWN.png": "imagepng",
"image.123HASHBROWN.webp": "imagewebp",
"你好/index.123HASHBROWN.html": "My path is non-ascii",
};

@@ -44,18 +44,18 @@ const mockKV = (store) => {

return JSON.stringify({
'key1.txt': `key1.${HASH}.txt`,
'key1.png': `key1.${HASH}.png`,
'cache.html': `cache.${HASH}.html`,
'测试.html': `测试.${HASH}.html`,
'你好.html': `你好.${HASH}.html`,
'%not-really-percent-encoded.html': `%not-really-percent-encoded.${HASH}.html`,
'%2F.html': `%2F.${HASH}.html`,
'%E4%BD%A0%E5%A5%BD.html': `%E4%BD%A0%E5%A5%BD.${HASH}.html`,
'index.html': `index.${HASH}.html`,
'sub/blah.png': `sub/blah.${HASH}.png`,
'sub/index.html': `sub/index.${HASH}.html`,
"key1.txt": `key1.${HASH}.txt`,
"key1.png": `key1.${HASH}.png`,
"cache.html": `cache.${HASH}.html`,
"测试.html": `测试.${HASH}.html`,
"你好.html": `你好.${HASH}.html`,
"%not-really-percent-encoded.html": `%not-really-percent-encoded.${HASH}.html`,
"%2F.html": `%2F.${HASH}.html`,
"%E4%BD%A0%E5%A5%BD.html": `%E4%BD%A0%E5%A5%BD.${HASH}.html`,
"index.html": `index.${HASH}.html`,
"sub/blah.png": `sub/blah.${HASH}.png`,
"sub/index.html": `sub/index.${HASH}.html`,
client: `client.${HASH}`,
'client/index.html': `client.${HASH}`,
'image.png': `image.${HASH}.png`,
'image.webp': `image.${HASH}.webp`,
'你好/index.html': `你好/index.${HASH}.html`,
"client/index.html": `client.${HASH}`,
"image.png": `image.${HASH}.png`,
"image.webp": `image.${HASH}.webp`,
"你好/index.html": `你好/index.${HASH}.html`,
});

@@ -74,5 +74,7 @@ };

let response;
if (key.headers.has('if-none-match')) {
let makeStrongEtag = key.headers.get('if-none-match').replace('W/', '');
Reflect.set(cacheKey.headers, 'etag', makeStrongEtag);
if (key.headers.has("if-none-match")) {
let makeStrongEtag = key.headers
.get("if-none-match")
.replace("W/", "");
Reflect.set(cacheKey.headers, "etag", makeStrongEtag);
response = cacheStore.get(JSON.stringify(cacheKey));

@@ -91,11 +93,11 @@ }

// TODO: write test to accomodate for rare scenarios with where range requests accomodate etags
if (response && !key.headers.has('if-none-match')) {
if (response && !key.headers.has("if-none-match")) {
// this appears overly verbose, but is necessary to document edge cache behavior
// The Range request header triggers the response header Content-Range ...
const range = key.headers.get('range');
const range = key.headers.get("range");
if (range) {
response.headers.set('content-range', `bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`);
response.headers.set("content-range", `bytes ${range.split("=").pop()}/${response.headers.get("content-length")}`);
}
// ... which we are using in this repository to set status 206
if (response.headers.has('content-range')) {
if (response.headers.has("content-range")) {
response.status = 206;

@@ -106,5 +108,5 @@ }

}
let etag = response.headers.get('etag');
if (etag && !etag.includes('W/')) {
response.headers.set('etag', `W/${etag}`);
let etag = response.headers.get("etag");
if (etag && !etag.includes("W/")) {
response.headers.set("etag", `W/${etag}`);
}

@@ -122,3 +124,3 @@ }

headers: {
etag: `"${url.pathname.replace('/', '')}"`,
etag: `"${url.pathname.replace("/", "")}"`,
},

@@ -125,0 +127,0 @@ };

@@ -15,3 +15,3 @@ export type CacheControl = {

pathIsEncoded: boolean;
defaultETag: 'strong' | 'weak';
defaultETag: "strong" | "weak";
};

@@ -18,0 +18,0 @@ export declare class KVError extends Error {

{
"name": "@cloudflare/kv-asset-handler",
"version": "0.3.1",
"description": "Routes requests to KV assets",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepack": "npm run build",
"build": "tsc -d",
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
"pretest": "npm run build",
"lint:code": "prettier --check \"**/*.{js,ts,json,md}\"",
"lint:markdown": "markdownlint \"**/*.md\" --ignore node_modules",
"test": "ava dist/test/*.js --verbose"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cloudflare/kv-asset-handler.git"
},
"keywords": [
"kv",
"cloudflare",
"workers",
"wrangler",
"assets"
],
"files": [
"src",
"dist",
"!src/test",
"!dist/test",
"LICENSE_APACHE",
"LICENSE_MIT"
],
"author": "wrangler@cloudflare.com",
"license": "MIT OR Apache-2.0",
"bugs": {
"url": "https://github.com/cloudflare/kv-asset-handler/issues"
},
"homepage": "https://github.com/cloudflare/kv-asset-handler#readme",
"dependencies": {
"mime": "^3.0.0"
},
"devDependencies": {
"@ava/typescript": "^4.1.0",
"@cloudflare/workers-types": "^4.20231218.0",
"@types/mime": "^3.0.4",
"@types/node": "^18.11.12",
"ava": "^6.0.1",
"prettier": "^3.2.2",
"service-worker-mock": "^2.0.5",
"typescript": "^5.3.3"
}
}
"name": "@cloudflare/kv-asset-handler",
"version": "0.3.2",
"description": "Routes requests to KV assets",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/cloudflare/workers-sdk.git",
"directory": "packages/kv-asset-handler"
},
"keywords": [
"kv",
"cloudflare",
"workers",
"wrangler",
"assets"
],
"files": [
"src",
"dist",
"!src/test",
"!dist/test"
],
"author": "wrangler@cloudflare.com",
"license": "MIT OR Apache-2.0",
"bugs": {
"url": "https://github.com/cloudflare/workers-sdk/issues"
},
"homepage": "https://github.com/cloudflare/workers-sdk#readme",
"dependencies": {
"mime": "^3.0.0"
},
"devDependencies": {
"@ava/typescript": "^4.1.0",
"@cloudflare/workers-types": "^4.20240419.0",
"@types/mime": "^3.0.4",
"@types/node": "^18.11.12",
"ava": "^6.0.1",
"service-worker-mock": "^2.0.5"
},
"engines": {
"node": ">=16.13"
},
"publishConfig": {
"access": "public"
},
"workers-sdk": {
"prerelease": true
},
"scripts": {
"build": "tsc -d",
"pretest": "npm run build",
"test": "ava dist/test/*.js --verbose",
"test:ci": "npm run build && ava dist/test/*.js --verbose"
}
}

@@ -15,3 +15,3 @@ # @cloudflare/kv-asset-handler

The Cloudflare Workers Discord server is an active place where Workers users get help, share feedback, and collaborate on making our platform better. The `#workers` channel in particular is a great place to chat about `kv-asset-handler`, and building cool experiences for your users using these tools! If you have questions, want to share what you're working on, or give feedback, [join us in Discord and say hello](https://discord.gg/cloudflaredev)!
The Cloudflare Workers Discord server is an active place where Workers users get help, share feedback, and collaborate on making our platform better. The `#workers` channel in particular is a great place to chat about `kv-asset-handler`, and building cool experiences for your users using these tools! If you have questions, want to share what you're working on, or give feedback, [join us in Discord and say hello](https://discord.cloudflare.com)!

@@ -75,9 +75,14 @@ - [Installation](#installation)

```js
import { getAssetFromKV, NotFoundError, MethodNotAllowedError } from '@cloudflare/kv-asset-handler'
import manifestJSON from '__STATIC_CONTENT_MANIFEST'
const assetManifest = JSON.parse(manifestJSON)
import manifestJSON from "__STATIC_CONTENT_MANIFEST";
import {
getAssetFromKV,
MethodNotAllowedError,
NotFoundError,
} from "@cloudflare/kv-asset-handler";
const assetManifest = JSON.parse(manifestJSON);
export default {
async fetch(request, env, ctx) {
if (request.url.includes('/docs')) {
if (request.url.includes("/docs")) {
try {

@@ -88,3 +93,3 @@ return await getAssetFromKV(

waitUntil(promise) {
return ctx.waitUntil(promise)
return ctx.waitUntil(promise);
},

@@ -95,4 +100,4 @@ },

ASSET_MANIFEST: assetManifest,
},
)
}
);
} catch (e) {

@@ -104,8 +109,8 @@ if (e instanceof NotFoundError) {

} else {
return new Response('An unexpected error occurred', { status: 500 })
return new Response("An unexpected error occurred", { status: 500 });
}
}
} else return fetch(request)
} else return fetch(request);
},
}
};
```

@@ -116,12 +121,16 @@

```js
import { getAssetFromKV, NotFoundError, MethodNotAllowedError } from '@cloudflare/kv-asset-handler'
import {
getAssetFromKV,
MethodNotAllowedError,
NotFoundError,
} from "@cloudflare/kv-asset-handler";
addEventListener('fetch', (event) => {
event.respondWith(handleEvent(event))
})
addEventListener("fetch", (event) => {
event.respondWith(handleEvent(event));
});
async function handleEvent(event) {
if (event.request.url.includes('/docs')) {
if (event.request.url.includes("/docs")) {
try {
return await getAssetFromKV(event)
return await getAssetFromKV(event);
} catch (e) {

@@ -133,6 +142,6 @@ if (e instanceof NotFoundError) {

} else {
return new Response('An unexpected error occurred', { status: 500 })
return new Response("An unexpected error occurred", { status: 500 });
}
}
} else return fetch(event.request)
} else return fetch(event.request);
}

@@ -186,3 +195,3 @@ ```

bypassCache: false, // do not bypass Cloudflare's cache
}
};
```

@@ -340,3 +349,3 @@

bypassCache: true,
}
};
```

@@ -343,0 +352,0 @@

@@ -1,13 +0,13 @@

import * as mime from 'mime'
import * as mime from "mime";
import {
Options,
AssetManifestType,
CacheControl,
InternalError,
MethodNotAllowedError,
NotFoundError,
InternalError,
AssetManifestType,
} from './types'
Options,
} from "./types";
declare global {
var __STATIC_CONTENT: any, __STATIC_CONTENT_MANIFEST: string
var __STATIC_CONTENT: any, __STATIC_CONTENT_MANIFEST: string;
}

@@ -19,19 +19,22 @@

bypassCache: false, // do not bypass Cloudflare's cache
}
};
const parseStringAsObject = <T>(maybeString: string | T): T =>
typeof maybeString === 'string' ? (JSON.parse(maybeString) as T) : maybeString
typeof maybeString === "string"
? (JSON.parse(maybeString) as T)
: maybeString;
const getAssetFromKVDefaultOptions: Partial<Options> = {
ASSET_NAMESPACE: typeof __STATIC_CONTENT !== 'undefined' ? __STATIC_CONTENT : undefined,
ASSET_NAMESPACE:
typeof __STATIC_CONTENT !== "undefined" ? __STATIC_CONTENT : undefined,
ASSET_MANIFEST:
typeof __STATIC_CONTENT_MANIFEST !== 'undefined'
typeof __STATIC_CONTENT_MANIFEST !== "undefined"
? parseStringAsObject<AssetManifestType>(__STATIC_CONTENT_MANIFEST)
: {},
cacheControl: defaultCacheControl,
defaultMimeType: 'text/plain',
defaultDocument: 'index.html',
defaultMimeType: "text/plain",
defaultDocument: "index.html",
pathIsEncoded: false,
defaultETag: 'strong',
}
defaultETag: "strong",
};

@@ -41,3 +44,3 @@ function assignOptions(options?: Partial<Options>): Options {

// options.mapRequestToAsset is handled manually later
return <Options>Object.assign({}, getAssetFromKVDefaultOptions, options)
return <Options>Object.assign({}, getAssetFromKVDefaultOptions, options);
}

@@ -53,20 +56,20 @@

const mapRequestToAsset = (request: Request, options?: Partial<Options>) => {
options = assignOptions(options)
options = assignOptions(options);
const parsedUrl = new URL(request.url)
let pathname = parsedUrl.pathname
const parsedUrl = new URL(request.url);
let pathname = parsedUrl.pathname;
if (pathname.endsWith('/')) {
if (pathname.endsWith("/")) {
// If path looks like a directory append options.defaultDocument
// e.g. If path is /about/ -> /about/index.html
pathname = pathname.concat(options.defaultDocument)
pathname = pathname.concat(options.defaultDocument);
} else if (!mime.getType(pathname)) {
// If path doesn't look like valid content
// e.g. /about.me -> /about.me/index.html
pathname = pathname.concat('/' + options.defaultDocument)
pathname = pathname.concat("/" + options.defaultDocument);
}
parsedUrl.pathname = pathname
return new Request(parsedUrl.toString(), request)
}
parsedUrl.pathname = pathname;
return new Request(parsedUrl.toString(), request);
};

@@ -78,20 +81,26 @@ /**

*/
function serveSinglePageApp(request: Request, options?: Partial<Options>): Request {
options = assignOptions(options)
function serveSinglePageApp(
request: Request,
options?: Partial<Options>
): Request {
options = assignOptions(options);
// First apply the default handler, which already has logic to detect
// paths that should map to HTML files.
request = mapRequestToAsset(request, options)
request = mapRequestToAsset(request, options);
const parsedUrl = new URL(request.url)
const parsedUrl = new URL(request.url);
// Detect if the default handler decided to map to
// a HTML file in some specific directory.
if (parsedUrl.pathname.endsWith('.html')) {
if (parsedUrl.pathname.endsWith(".html")) {
// If expected HTML file was missing, just return the root index.html (or options.defaultDocument)
return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request)
return new Request(
`${parsedUrl.origin}/${options.defaultDocument}`,
request
);
} else {
// The default handler decided this is not an HTML page. It's probably
// an image, CSS, or JS file. Leave it as-is.
return request
return request;
}

@@ -113,66 +122,78 @@ }

type Evt = {
request: Request
waitUntil: (promise: Promise<any>) => void
}
request: Request;
waitUntil: (promise: Promise<any>) => void;
};
const getAssetFromKV = async (event: Evt, options?: Partial<Options>): Promise<Response> => {
options = assignOptions(options)
const getAssetFromKV = async (
event: Evt,
options?: Partial<Options>
): Promise<Response> => {
options = assignOptions(options);
const request = event.request
const ASSET_NAMESPACE = options.ASSET_NAMESPACE
const ASSET_MANIFEST = parseStringAsObject<AssetManifestType>(options.ASSET_MANIFEST)
const request = event.request;
const ASSET_NAMESPACE = options.ASSET_NAMESPACE;
const ASSET_MANIFEST = parseStringAsObject<AssetManifestType>(
options.ASSET_MANIFEST
);
if (typeof ASSET_NAMESPACE === 'undefined') {
throw new InternalError(`there is no KV namespace bound to the script`)
if (typeof ASSET_NAMESPACE === "undefined") {
throw new InternalError(`there is no KV namespace bound to the script`);
}
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s
let pathIsEncoded = options.pathIsEncoded
let requestKey
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, ""); // strip any preceding /'s
let pathIsEncoded = options.pathIsEncoded;
let requestKey;
// if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions
// otherwise handle request as normal, with default mapRequestToAsset below
if (options.mapRequestToAsset) {
requestKey = options.mapRequestToAsset(request)
requestKey = options.mapRequestToAsset(request);
} else if (ASSET_MANIFEST[rawPathKey]) {
requestKey = request
requestKey = request;
} else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) {
pathIsEncoded = true
requestKey = request
pathIsEncoded = true;
requestKey = request;
} else {
const mappedRequest = mapRequestToAsset(request)
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '')
const mappedRequest = mapRequestToAsset(request);
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(
/^\/+/,
""
);
if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) {
pathIsEncoded = true
requestKey = mappedRequest
pathIsEncoded = true;
requestKey = mappedRequest;
} else {
// use default mapRequestToAsset
requestKey = mapRequestToAsset(request, options)
requestKey = mapRequestToAsset(request, options);
}
}
const SUPPORTED_METHODS = ['GET', 'HEAD']
const SUPPORTED_METHODS = ["GET", "HEAD"];
if (!SUPPORTED_METHODS.includes(requestKey.method)) {
throw new MethodNotAllowedError(`${requestKey.method} is not a valid request method`)
throw new MethodNotAllowedError(
`${requestKey.method} is not a valid request method`
);
}
const parsedUrl = new URL(requestKey.url)
const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary
const parsedUrl = new URL(requestKey.url);
const pathname = pathIsEncoded
? decodeURIComponent(parsedUrl.pathname)
: parsedUrl.pathname; // decode percentage encoded path only when necessary
// pathKey is the file path to look up in the manifest
let pathKey = pathname.replace(/^\/+/, '') // remove prepended /
let pathKey = pathname.replace(/^\/+/, ""); // remove prepended /
// @ts-ignore
const cache = caches.default
let mimeType = mime.getType(pathKey) || options.defaultMimeType
if (mimeType.startsWith('text') || mimeType === 'application/javascript') {
mimeType += '; charset=utf-8'
const cache = caches.default;
let mimeType = mime.getType(pathKey) || options.defaultMimeType;
if (mimeType.startsWith("text") || mimeType === "application/javascript") {
mimeType += "; charset=utf-8";
}
let shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash
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') {
if (typeof ASSET_MANIFEST !== "undefined") {
if (ASSET_MANIFEST[pathKey]) {
pathKey = ASSET_MANIFEST[pathKey]
pathKey = ASSET_MANIFEST[pathKey];
// if path key is in asset manifest, we can assume it contains a content hash and can be cached
shouldEdgeCache = true
shouldEdgeCache = true;
}

@@ -182,3 +203,3 @@ }

// TODO this excludes search params from cache, investigate ideal behavior
let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request)
let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request);

@@ -190,10 +211,10 @@ // if argument passed in for cacheControl is a function then

switch (typeof options.cacheControl) {
case 'function':
return options.cacheControl(request)
case 'object':
return options.cacheControl
case "function":
return options.cacheControl(request);
case "object":
return options.cacheControl;
default:
return defaultCacheControl
return defaultCacheControl;
}
})()
})();

@@ -204,29 +225,32 @@ // formats the etag depending on the response context. if the entityId

// header is "null". Could be modified in future to base64 encode etc
const formatETag = (entityId: any = pathKey, validatorType: string = options.defaultETag) => {
const formatETag = (
entityId: any = pathKey,
validatorType: string = options.defaultETag
) => {
if (!entityId) {
return ''
return "";
}
switch (validatorType) {
case 'weak':
if (!entityId.startsWith('W/')) {
case "weak":
if (!entityId.startsWith("W/")) {
if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) {
return `W/${entityId}`
return `W/${entityId}`;
}
return `W/"${entityId}"`
return `W/"${entityId}"`;
}
return entityId
case 'strong':
return entityId;
case "strong":
if (entityId.startsWith(`W/"`)) {
entityId = entityId.replace('W/', '')
entityId = entityId.replace("W/", "");
}
if (!entityId.endsWith(`"`)) {
entityId = `"${entityId}"`
entityId = `"${entityId}"`;
}
return entityId
return entityId;
default:
return ''
return "";
}
}
};
options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts)
options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts);

@@ -237,12 +261,13 @@ // override shouldEdgeCache if options say to bypassCache

options.cacheControl.edgeTTL === null ||
request.method == 'HEAD'
request.method == "HEAD"
) {
shouldEdgeCache = false
shouldEdgeCache = false;
}
// only set max-age if explicitly passed in a number as an arg
const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number'
const shouldSetBrowserCache =
typeof options.cacheControl.browserTTL === "number";
let response = null
let response = null;
if (shouldEdgeCache) {
response = await cache.match(cacheKey)
response = await cache.match(cacheKey);
}

@@ -252,9 +277,9 @@

if (response.status > 300 && response.status < 400) {
if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) {
if (response.body && "cancel" in Object.getPrototypeOf(response.body)) {
// Body exists and environment supports readable streams
response.body.cancel()
response.body.cancel();
} else {
// Environment doesnt support readable streams, or null repsonse body. Nothing to do
}
response = new Response(null, response)
response = new Response(null, response);
} else {

@@ -265,63 +290,77 @@ // fixes #165

status: 0,
statusText: '',
}
statusText: "",
};
opts.headers.set('cf-cache-status', 'HIT')
opts.headers.set("cf-cache-status", "HIT");
if (response.status) {
opts.status = response.status
opts.statusText = response.statusText
} else if (opts.headers.has('Content-Range')) {
opts.status = 206
opts.statusText = 'Partial Content'
opts.status = response.status;
opts.statusText = response.statusText;
} else if (opts.headers.has("Content-Range")) {
opts.status = 206;
opts.statusText = "Partial Content";
} else {
opts.status = 200
opts.statusText = 'OK'
opts.status = 200;
opts.statusText = "OK";
}
response = new Response(response.body, opts)
response = new Response(response.body, opts);
}
} else {
const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer')
const body = await ASSET_NAMESPACE.get(pathKey, "arrayBuffer");
if (body === null) {
throw new NotFoundError(`could not find ${pathKey} in your content namespace`)
throw new NotFoundError(
`could not find ${pathKey} in your content namespace`
);
}
response = new Response(body)
response = new Response(body);
if (shouldEdgeCache) {
response.headers.set('Accept-Ranges', 'bytes')
response.headers.set('Content-Length', String(body.byteLength))
response.headers.set("Accept-Ranges", "bytes");
response.headers.set("Content-Length", String(body.byteLength));
// set etag before cache insertion
if (!response.headers.has('etag')) {
response.headers.set('etag', formatETag(pathKey))
if (!response.headers.has("etag")) {
response.headers.set("etag", formatETag(pathKey));
}
// determine Cloudflare cache behavior
response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`)
event.waitUntil(cache.put(cacheKey, response.clone()))
response.headers.set('CF-Cache-Status', 'MISS')
response.headers.set(
"Cache-Control",
`max-age=${options.cacheControl.edgeTTL}`
);
event.waitUntil(cache.put(cacheKey, response.clone()));
response.headers.set("CF-Cache-Status", "MISS");
}
}
response.headers.set('Content-Type', mimeType)
response.headers.set("Content-Type", mimeType);
if (response.status === 304) {
let etag = formatETag(response.headers.get('etag'))
let ifNoneMatch = cacheKey.headers.get('if-none-match')
let proxyCacheStatus = response.headers.get('CF-Cache-Status')
let etag = formatETag(response.headers.get("etag"));
let ifNoneMatch = cacheKey.headers.get("if-none-match");
let proxyCacheStatus = response.headers.get("CF-Cache-Status");
if (etag) {
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') {
response.headers.set('CF-Cache-Status', 'EXPIRED')
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === "MISS") {
response.headers.set("CF-Cache-Status", "EXPIRED");
} else {
response.headers.set('CF-Cache-Status', 'REVALIDATED')
response.headers.set("CF-Cache-Status", "REVALIDATED");
}
response.headers.set('etag', formatETag(etag, 'weak'))
response.headers.set("etag", formatETag(etag, "weak"));
}
}
if (shouldSetBrowserCache) {
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`)
response.headers.set(
"Cache-Control",
`max-age=${options.cacheControl.browserTTL}`
);
} else {
response.headers.delete('Cache-Control')
response.headers.delete("Cache-Control");
}
return response
}
return response;
};
export { getAssetFromKV, mapRequestToAsset, serveSinglePageApp }
export { Options, CacheControl, MethodNotAllowedError, NotFoundError, InternalError }
export { getAssetFromKV, mapRequestToAsset, serveSinglePageApp };
export {
Options,
CacheControl,
MethodNotAllowedError,
NotFoundError,
InternalError,
};

@@ -1,64 +0,64 @@

const makeServiceWorkerEnv = require('service-worker-mock')
const makeServiceWorkerEnv = require("service-worker-mock");
const HASH = '123HASHBROWN'
const HASH = "123HASHBROWN";
export const getEvent = (request: Request): any => {
const waitUntil = async (callback: any) => {
await callback
}
await callback;
};
return {
request,
waitUntil,
}
}
};
};
const store: any = {
'key1.123HASHBROWN.txt': 'val1',
'key1.123HASHBROWN.png': 'val1',
'index.123HASHBROWN.html': 'index.html',
'cache.123HASHBROWN.html': 'cache me if you can',
'测试.123HASHBROWN.html': 'My filename is non-ascii',
'%not-really-percent-encoded.123HASHBROWN.html': 'browser percent encoded',
'%2F.123HASHBROWN.html': 'user percent encoded',
'你好.123HASHBROWN.html': 'I shouldnt be served',
'%E4%BD%A0%E5%A5%BD.123HASHBROWN.html': 'Im important',
'nohash.txt': 'no hash but still got some result',
'sub/blah.123HASHBROWN.png': 'picturedis',
'sub/index.123HASHBROWN.html': 'picturedis',
'client.123HASHBROWN': 'important file',
'client.123HASHBROWN/index.html': 'Im here but serve my big bro above',
'image.123HASHBROWN.png': 'imagepng',
'image.123HASHBROWN.webp': 'imagewebp',
'你好/index.123HASHBROWN.html': 'My path is non-ascii',
}
"key1.123HASHBROWN.txt": "val1",
"key1.123HASHBROWN.png": "val1",
"index.123HASHBROWN.html": "index.html",
"cache.123HASHBROWN.html": "cache me if you can",
"测试.123HASHBROWN.html": "My filename is non-ascii",
"%not-really-percent-encoded.123HASHBROWN.html": "browser percent encoded",
"%2F.123HASHBROWN.html": "user percent encoded",
"你好.123HASHBROWN.html": "I shouldnt be served",
"%E4%BD%A0%E5%A5%BD.123HASHBROWN.html": "Im important",
"nohash.txt": "no hash but still got some result",
"sub/blah.123HASHBROWN.png": "picturedis",
"sub/index.123HASHBROWN.html": "picturedis",
"client.123HASHBROWN": "important file",
"client.123HASHBROWN/index.html": "Im here but serve my big bro above",
"image.123HASHBROWN.png": "imagepng",
"image.123HASHBROWN.webp": "imagewebp",
"你好/index.123HASHBROWN.html": "My path is non-ascii",
};
export const mockKV = (store: any) => {
return {
get: (path: string) => store[path] || null,
}
}
};
};
export const mockManifest = () => {
return JSON.stringify({
'key1.txt': `key1.${HASH}.txt`,
'key1.png': `key1.${HASH}.png`,
'cache.html': `cache.${HASH}.html`,
'测试.html': `测试.${HASH}.html`,
'你好.html': `你好.${HASH}.html`,
'%not-really-percent-encoded.html': `%not-really-percent-encoded.${HASH}.html`,
'%2F.html': `%2F.${HASH}.html`,
'%E4%BD%A0%E5%A5%BD.html': `%E4%BD%A0%E5%A5%BD.${HASH}.html`,
'index.html': `index.${HASH}.html`,
'sub/blah.png': `sub/blah.${HASH}.png`,
'sub/index.html': `sub/index.${HASH}.html`,
"key1.txt": `key1.${HASH}.txt`,
"key1.png": `key1.${HASH}.png`,
"cache.html": `cache.${HASH}.html`,
"测试.html": `测试.${HASH}.html`,
"你好.html": `你好.${HASH}.html`,
"%not-really-percent-encoded.html": `%not-really-percent-encoded.${HASH}.html`,
"%2F.html": `%2F.${HASH}.html`,
"%E4%BD%A0%E5%A5%BD.html": `%E4%BD%A0%E5%A5%BD.${HASH}.html`,
"index.html": `index.${HASH}.html`,
"sub/blah.png": `sub/blah.${HASH}.png`,
"sub/index.html": `sub/index.${HASH}.html`,
client: `client.${HASH}`,
'client/index.html': `client.${HASH}`,
'image.png': `image.${HASH}.png`,
'image.webp': `image.${HASH}.webp`,
'你好/index.html': `你好/index.${HASH}.html`,
})
}
"client/index.html": `client.${HASH}`,
"image.png": `image.${HASH}.png`,
"image.webp": `image.${HASH}.webp`,
"你好/index.html": `你好/index.${HASH}.html`,
});
};
let cacheStore: any = new Map()
let cacheStore: any = new Map();
interface CacheKey {
url: object
headers: object
url: object;
headers: object;
}

@@ -72,15 +72,17 @@ export const mockCaches = () => {

headers: {},
}
let response
if (key.headers.has('if-none-match')) {
let makeStrongEtag = key.headers.get('if-none-match').replace('W/', '')
Reflect.set(cacheKey.headers, 'etag', makeStrongEtag)
response = cacheStore.get(JSON.stringify(cacheKey))
};
let response;
if (key.headers.has("if-none-match")) {
let makeStrongEtag = key.headers
.get("if-none-match")
.replace("W/", "");
Reflect.set(cacheKey.headers, "etag", makeStrongEtag);
response = cacheStore.get(JSON.stringify(cacheKey));
} else {
// if client doesn't send if-none-match, we need to iterate through these keys
// and just test the URL
const activeCacheKeys: Array<string> = Array.from(cacheStore.keys())
const activeCacheKeys: Array<string> = Array.from(cacheStore.keys());
for (const cacheStoreKey of activeCacheKeys) {
if (JSON.parse(cacheStoreKey).url === key.url) {
response = cacheStore.get(cacheStoreKey)
response = cacheStore.get(cacheStoreKey);
}

@@ -90,51 +92,53 @@ }

// TODO: write test to accomodate for rare scenarios with where range requests accomodate etags
if (response && !key.headers.has('if-none-match')) {
if (response && !key.headers.has("if-none-match")) {
// this appears overly verbose, but is necessary to document edge cache behavior
// The Range request header triggers the response header Content-Range ...
const range = key.headers.get('range')
const range = key.headers.get("range");
if (range) {
response.headers.set(
'content-range',
`bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`,
)
"content-range",
`bytes ${range.split("=").pop()}/${response.headers.get(
"content-length"
)}`
);
}
// ... which we are using in this repository to set status 206
if (response.headers.has('content-range')) {
response.status = 206
if (response.headers.has("content-range")) {
response.status = 206;
} else {
response.status = 200
response.status = 200;
}
let etag = response.headers.get('etag')
if (etag && !etag.includes('W/')) {
response.headers.set('etag', `W/${etag}`)
let etag = response.headers.get("etag");
if (etag && !etag.includes("W/")) {
response.headers.set("etag", `W/${etag}`);
}
}
return response
return response;
},
async put(key: any, val: Response) {
let headers = new Headers(val.headers)
let url = new URL(key.url)
let resWithBody = new Response(val.body, { headers, status: 200 })
let resNoBody = new Response(null, { headers, status: 304 })
let headers = new Headers(val.headers);
let url = new URL(key.url);
let resWithBody = new Response(val.body, { headers, status: 200 });
let resNoBody = new Response(null, { headers, status: 304 });
let cacheKey: CacheKey = {
url: key.url,
headers: {
etag: `"${url.pathname.replace('/', '')}"`,
etag: `"${url.pathname.replace("/", "")}"`,
},
}
cacheStore.set(JSON.stringify(cacheKey), resNoBody)
cacheKey.headers = {}
cacheStore.set(JSON.stringify(cacheKey), resWithBody)
return
};
cacheStore.set(JSON.stringify(cacheKey), resNoBody);
cacheKey.headers = {};
cacheStore.set(JSON.stringify(cacheKey), resWithBody);
return;
},
},
}
}
};
};
// mocks functionality used inside worker request
export function mockRequestScope() {
Object.assign(global, makeServiceWorkerEnv())
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() })
Object.assign(global, { __STATIC_CONTENT: mockKV(store) })
Object.assign(global, { caches: mockCaches() })
Object.assign(global, makeServiceWorkerEnv());
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() });
Object.assign(global, { __STATIC_CONTENT: mockKV(store) });
Object.assign(global, { caches: mockCaches() });
}

@@ -144,8 +148,8 @@

export function mockGlobalScope() {
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() })
Object.assign(global, { __STATIC_CONTENT: mockKV(store) })
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() });
Object.assign(global, { __STATIC_CONTENT: mockKV(store) });
}
export const sleep = (milliseconds: number) => {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}
return new Promise((resolve) => setTimeout(resolve, milliseconds));
};
export type CacheControl = {
browserTTL: number
edgeTTL: number
bypassCache: boolean
}
browserTTL: number;
edgeTTL: number;
bypassCache: boolean;
};
export type AssetManifestType = Record<string, string>
export type AssetManifestType = Record<string, string>;
export type Options = {
cacheControl: ((req: Request) => Partial<CacheControl>) | Partial<CacheControl>
ASSET_NAMESPACE: any
ASSET_MANIFEST: AssetManifestType | string
mapRequestToAsset?: (req: Request, options?: Partial<Options>) => Request
defaultMimeType: string
defaultDocument: string
pathIsEncoded: boolean
defaultETag: 'strong' | 'weak'
}
cacheControl:
| ((req: Request) => Partial<CacheControl>)
| Partial<CacheControl>;
ASSET_NAMESPACE: any;
ASSET_MANIFEST: AssetManifestType | string;
mapRequestToAsset?: (req: Request, options?: Partial<Options>) => Request;
defaultMimeType: string;
defaultDocument: string;
pathIsEncoded: boolean;
defaultETag: "strong" | "weak";
};
export class KVError extends Error {
constructor(message?: string, status: number = 500) {
super(message)
super(message);
// see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html
Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain
this.name = KVError.name // stack traces display correctly now
this.status = status
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
this.name = KVError.name; // stack traces display correctly now
this.status = status;
}
status: number
status: number;
}
export class MethodNotAllowedError extends KVError {
constructor(message: string = `Not a valid request method`, status: number = 405) {
super(message, status)
constructor(
message: string = `Not a valid request method`,
status: number = 405
) {
super(message, status);
}

@@ -37,9 +42,12 @@ }

constructor(message: string = `Not Found`, status: number = 404) {
super(message, status)
super(message, status);
}
}
export class InternalError extends KVError {
constructor(message: string = `Internal Error in KV Asset Handler`, status: number = 500) {
super(message, status)
constructor(
message: string = `Internal Error in KV Asset Handler`,
status: number = 500
) {
super(message, status);
}
}
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