Add akamai-edgeworkers (#40716)

* Add akamai-edgeworkers

* PR feedback for Akamai-Edgeworkers
This commit is contained in:
evan-hughes
2019-12-13 14:54:59 -05:00
committed by Ron Buckton
parent e70729bb83
commit 288692b272
8 changed files with 780 additions and 0 deletions

View File

@@ -0,0 +1 @@
README.md

View File

@@ -0,0 +1,58 @@
Bindings for the Akamai [EdgeWorker API]. This allows you to write
your EdgeWorkers in TypeScript.
Types are available for the `Request` and `Response` objects, as well as the
built-in modules.
# User Guide
EdgeWorkers are written in ECMAScript6, so you need to set your
`tsconfig.json` to use `es6` as the compilation target and module
code generator:
```json5
{
"compilerOptions": {
"module": "es6",
"target": "es6",
//...
}
}
```
## Using the `Request` and `Response` Objects
The predefined EdgeWorker callbacks take Request and Response objects as
arguments. After you have installed this package, you can create a `main.ts`
with the following stubs:
```typescript
/// <reference types="akamai-edgeworkers"/>
export function onClientRequest(request : EW.MutableRequest & EW.HasRespondWith){}
export function onOriginRequest(request : EW.MutableRequest) {}
export function onOriginResponse(request : EW.ImmutableRequest & EW.HasRespondWith, response : EW.Response) {}
export function onClientResponse(request : EW.ImmutableRequest, response : EW.Response) {}
```
The triple-slashed first line references this package and pulls `EW` into your
namespace.
## Using Built-In Modules
Bindings are available for the built-in `cookies` and `url-search-params`
modules. Once you've added the triple-slash reference to `akamai-edgeworkers`
you can import them normally:
```typescript
/// <reference types="akamai-edgeworkers"/>
import { Cookies } from 'cookies';
function onClientRequest(request: EW.MutableRequest & EW.HasRespondWith) {
const cookie = new Cookies(request.getHeader('cookies') || undefined);
//...
}
```
[EdgeWorker API]: https://developer.akamai.com/api/web_performance/edgeworkers/v1.html

461
types/akamai-edgeworkers/index.d.ts vendored Normal file
View File

@@ -0,0 +1,461 @@
// Type definitions for non-npm package Akamai EdgeWorkers JavaScript API 1.0
// Project: https://developer.akamai.com/akamai-edgeworkers-overview
// Definitions by: Evan Hughes <https://github.com/evan-hughes>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
declare namespace EW {
interface ReadsHeaders {
/**
* Provides header values by header name
*/
getHeader(name: string): string[] | null;
}
interface MutatesHeaders {
/**
* Sets header value(s), replacing previous one(s)
*/
setHeader(name: string, value: string | string[]): void;
/**
* Add value(s) to header
*/
addHeader(name: string, value: string | string[]): void;
/**
* Removes header
*/
removeHeader(name: string): void;
}
interface ReadsVariables {
/**
* Get's the value of a request variable
*/
getVariable(name: string): string | undefined;
}
interface HasRespondWith {
/**
* Indicates that a complete response is being generated for a
* request, rather than fetching a response from cache or the origin.
*
* If called multiple times within an event handler, the last
* Response arguments passed in would be the arguments used to
* generate a response.
*
* The maximum response body string length is 2K characters. If
* validation of the passed in Response object fails it will throw
* an exception. For example, a Response body bigger than the limit
* will cause an exception.
*
* The deny_reason is an optional argument, and only used if the
* status code is 403.
*
* @param status The HTTP status code
* @param headers Properties used as key/value pairs for the response
* headers
* @param body The content of the response body
* @param deny_reason The deny reason set if the status code is a 403
*/
respondWith(status: number, headers: object, body: string, deny_reason?: string): void;
}
interface HasStatus {
/**
* The HTTP status of a response sent to the client.
*/
status: number;
}
interface Request {
/**
* The Host header value of the incoming request.
*/
readonly host: string;
/**
* The HTTP method of the incoming request.
*/
readonly method: string;
/**
* The URL path of the incoming request, including the filename and
* extension, but without any query string.
*/
readonly path: string;
/**
* The scheme of the incoming request ("http" or "https").
*/
readonly scheme: string;
/**
* The query string of the incoming request.
*/
readonly query: string;
/**
* The Relative URL of the incoming request. This includes the path as well
* as the query string.
*/
readonly url: string;
/**
* Object containing properties specifying the end user's geographic
* location. This value of this property will be null if the contract
* associated with the request does not have the appropriate entitlements.
*/
readonly userLocation: UserLocation | undefined;
/**
* Object containing properties specifying the device characteristics. This
* value of this property will be null if the contract associated with the
* request does not have entitlements for EDC.
*/
readonly device: Device | undefined;
/**
* The cpcode used for reporting.
*/
readonly cpCode: number;
}
interface MutableRequest extends MutatesHeaders, ReadsHeaders, ReadsVariables, Request {
}
interface ImmutableRequest extends ReadsHeaders, ReadsVariables, Request {
}
interface Response extends HasStatus, MutatesHeaders, ReadsHeaders {
}
/**
* Notes:
* * If the IP address is in the reserved IP space (as designated by the
* Internet Assigned Numbers Authority), every property will have the
* value of reserved.
* * If user location properties can not be supplied for any reason,
* undefined is returned for that property
*/
interface UserLocation {
/**
* The continent value is a two-letter code for the continent that
* the IP address maps to.
*/
continent: string | undefined;
/**
* The country value is an ISO-3166, two-letter code for the country
* where the IP address maps to.
*/
country: string | undefined;
/**
* The region value is an ISO-3166, two-letter code for the state,
* province, or region where the IP address maps to.
*/
region: string | undefined;
/**
* The city value is the city (within a 50-mile radius) that the IP
* address maps to.
*/
city: string | undefined;
/**
* The zipCode value is the zipcode that the IP address maps to
* (multiple values possible).
*
* Contiguous zip codes will be represented as a range of the form
* "FirstZipInRange LastZipInRange", and multiple ranges may be
* present (each range separated by the plus (+) character).
*
* For example, the following strings are all valid zipCode values:
*
* * 10001
* * 10001+10003
* * 10001-10003+10005
* * 10001-10003+10005-10008
*
* For country = US and country = PR, zip refers to the 5 digit
* zipcode.
*
* For country = CA, zip refers to the forward sortation area (FSA).
* For more information on FSA, go to http://www.canadapost.ca and
* search for FSA.
*
* See the EdgeScape Users Guide for more details.
*/
zipCode: string | undefined;
}
/**
* Notes:
* * If device properties can not be supplied for any reason,
* undefined is returned for each property
*/
interface Device {
/**
* Brand name of the device.
*/
brandName: string | undefined;
/**
* Model name of the device.
*/
modelName: string | undefined;
/**
* Marketing name of the device.
*/
marketingName: string | undefined;
/**
* Indicates if the device is a wireless device.
*/
isWireless: boolean | undefined;
/**
* Indicates if the device is a tablet.
*/
isTablet: boolean | undefined;
/**
* The device operation system.
*/
os: string | undefined;
/**
* The device operating system version.
*/
osVersion: string | undefined;
/**
* The mobile browser name.
*/
mobileBrowser: string | undefined;
/**
* The mobile browser version.
*/
mobileBrowserVersion: string | undefined;
/**
* The screen resolution width, in pixels.
*/
resolutionWidth: number | undefined;
/**
* The screen resolution height, in pixels.
*/
resolutionHeight: number | undefined;
/**
* The physical screen height, in millimeters.
*/
physicalScreenHeight: number | undefined;
/**
* The physical screen width, in millimeters.
*/
physicalScreenWidth: number | undefined;
/**
* Indicates if the browser supports cookies.
*/
hasCookieSupport: boolean | undefined;
/**
* Indicates if the device supports all of the following
* JavaScript functions: "alert confirm access form elements
* setTimeout setInterval and document.location"
*/
hasAjaxSupport: boolean | undefined;
/**
* Indicates if the browser supports Flash.
*/
hasFlashSupport: boolean | undefined;
/**
* Indicates if the browser accepts third party cookies.
*/
acceptsThirdPartyCookie: boolean | undefined;
/**
* Indicates the level of support for XHTML.
*/
xhtmlSupportLevel: number | undefined;
/**
* Indicates if the device is a mobile device.
*/
isMobile: boolean | undefined;
}
}
/**
* Query, add, and remove cookies.
*/
declare module "cookies" {
/**
* Provides access to the Cookies header of a request, allowing the
* addition, removal, or modification of cookie values.
*/
class Cookies {
/**
* Constructor for a new "Cookies" struct to hold cookies.
*
* @param cookieHeader The raw Cookie header to pass to the constructor
* to parse. If an array is passed, the first element must be a
* string and that is used as the cookies string to parse. If this
* is not passed, an empty cookies object is returned.
*
* @param options Only used when parsing an existing Cookie header.
* Object to override the default decode of the Cookie values. This
* object must have a function named 'decode' on it, which should
* take a string and return the result of the custom decoding of
* that string.
*/
constructor(header?: string | string[], options?: object);
/**
* Returns the string representation to use when setting the Cookie
* header, encoding values by default.
*/
toHeader(): string;
/**
* Get the first instance of the cookie matching the given name.
*
* @param name Cookie name.
*/
get(name: string): string | undefined;
/**
* Get all Instances of the cookie matching the given name.
*
* @param name cookie name.
*/
getAll(name: string): string[];
/**
* Get all names of existing cookies held by this Cookies object.
*/
names(): string[];
/**
* Adds a cookie.
* @param name Name of the cookie
* @param value Value of the cookie.
*/
add(name: string, value: string): void;
/**
* Removes all cookies with a given name.
*
* @param name Cookie name.
*/
delete(name: string): void;
}
/**
* Provides access to the SetCookies header of a request.
*/
class SetCookie {
/**
* Constructor for a new "SetCookie" struct to hold a specific Set-Cookie
* header representation.
*/
constructor(opts?: {
name?: string;
value?: string;
maxAge?: number;
domain?: string;
path?: string;
expires?: { toUTCString: () => string };
httpOnly?: boolean;
secure?: boolean;
sameSite?: "Strict" | "Lax" | "None" | true;
});
/**
* Returns the string representation to use when setting the Set-Cookie
* header, encoding values by default.
*/
toHeader(): string;
name: string;
value: string;
maxAge: number;
domain: string;
path: string;
expires: { toUTCString: () => string };
httpOnly: boolean;
secure: boolean;
sameSite: "Strict" | "Lax" | "None" | true;
}
}
/**
* Query, add, and remove parameters from the query string.
*/
declare module "url-search-params" {
export default class URLSearchParams {
/**
* Create a new URLSearchParams object.
*/
constructor(init?: string | URLSearchParams);
/**
* Add a new name/value to the receiver.
*/
append(name: string, value: string): void;
/**
* Remove the given name/value from the receiver.
*/
delete(name: string): void;
/**
* Return the first value with the specified name.
*/
get(name: string): string | null;
/**
* Check if the given name exists.
*/
has(name: string): boolean;
/**
* Return *all* values association with the specified name.
*/
getAll(name: string): string[];
/**
* Iterate through the name/value pairs.
*/
entries(): IterableIterator<[string, string]>;
/**
* Iterate through the names.
*/
keys(): IterableIterator<string>;
/**
* Iterate through the values.
*/
values(): IterableIterator<string>;
/**
* Replace all instances of `name` with a single name/value pair.
*/
set(name: string, value: string): void;
/**
* Return a query string suitable for use in a URL.
*/
toString(): string;
}
}

View File

@@ -0,0 +1,117 @@
export function onClientRequest(request: EW.MutableRequest & EW.HasRespondWith) {
// Exercise EW.ClientRequest.setHeader()
request.setHeader("from-set-header-1", ["value-1", "trailer-1"]);
// Exercise EW.ClientRequest.addHeader()
request.addHeader("from-add-header-3", ["value-3", "trailer-2"]);
// Exercise EW.ClientRequest.removeHeader()
request.removeHeader("to-remove-1");
// Exercise EW.ClientRequest.getVariable()
request.respondWith(505, [], "Missing get-variable-present");
request.respondWith(505, { no: 'bad' }, 'Expected var to be missing');
// Exercise respondWith
const target = request.getHeader("target");
if (target != null && target[0] === 'onClientRequest-respondWith') {
request.respondWith(418, { 'from-respond-with': "frw value" }, "frw body");
}
}
export function onOriginRequest(request: EW.MutableRequest) {
// getHeader
const h = request.getHeader("onOriginRequest-getHeader");
if (h == null) {
return;
}
// setHeader
request.setHeader("onOriginRequest-getHeader-set", h[0]);
// addHeader
request.addHeader("onOriginRequest-addHeader-single", "single");
request.addHeader("onOriginRequest-addHeader-multi", ["multi-1", "multi-2"]);
// removeHeader
request.getHeader("onOriginRequest-removeHeader-bye");
request.removeHeader("onOriginRequest-removeHeader-bye");
// getVariable
const v = request.getVariable("var") || [];
request.setHeader("variable", v);
}
export function onOriginResponse(request: EW.ImmutableRequest & EW.HasRespondWith, response: EW.Response) {
if (response.getHeader("should-respondWith")) {
request.respondWith(444, {}, "wanted a respond with");
return;
}
if (response.getHeader("should-status")) {
response.status = 456;
return;
}
// Req - getHeader
let h = request.getHeader("onOriginResponse-req-getHeader") || [];
response.setHeader("header-from-req", h);
// getVariable
const v = request.getVariable("req-var") || [];
response.setHeader("variable", v);
// Resp - getHeader
h = response.getHeader("onOriginResponse-resp-getHeader") || [];
// Resp- setHeader
response.setHeader("onOriginResponse-getHeader-resp-set", h);
// Resp- addHeader
response.addHeader("onOriginResponse-addHeader-resp-single", "single");
response.addHeader("onOriginResponse-addHeader-resp-multi", ["multi-1", "multi-2"]);
// Resp- removeHeader
if (!response.getHeader("onOriginResponse-removeHeader-resp-bye")) {
return;
}
response.removeHeader("onOriginResponse-removeHeader-resp-bye");
// Verify we set status
response.status = 189;
}
export function onClientResponse(request: EW.ImmutableRequest, response: EW.Response) {
if (request.getHeader("should-status")) {
response.status = 234;
return;
}
// Req - getHeader
let h = request.getHeader("onClientResponse-req-getHeader") || [];
response.setHeader("header-from-req", h);
// getVariable
const v = request.getVariable("req-var") || "";
response.setHeader("variable", v);
// Resp - getHeader
h = response.getHeader("onClientResponse-resp-getHeader") || [];
// Resp- setHeader
response.setHeader("onClientResponse-getHeader-resp-set", h);
// Resp- addHeader
response.addHeader("onClientResponse-addHeader-resp-single", "single");
response.addHeader("onClientResponse-addHeader-resp-multi", ["multi-1", "multi-2"]);
// Resp- removeHeader
if (!response.getHeader("onClientResponse-removeHeader-resp-bye")) {
return;
}
response.removeHeader("onClientResponse-removeHeader-resp-bye");
// Verify we set status
response.status = 123;
}

View File

@@ -0,0 +1,51 @@
import { Cookies, SetCookie } from 'cookies';
function onClientRequest(request: EW.MutableRequest & EW.HasRespondWith) {
// Verify parse constructor
const c = new Cookies(request.getHeader('cookies') || undefined);
// Verify toHeader()
const s: string = c.toHeader();
// Verify get()
let v = c.get('first');
if (v !== '1st value') {
request.respondWith(543, {}, "Wrong value for first cookie. Got " + v);
return;
}
v = c.get('m i s s i n g');
if (typeof v !== 'undefined') {
request.respondWith(544, {}, "Got a value for a missing header: " + v);
return;
}
// getAll()
const all: string[] = c.getAll('z');
// names()
const names: string[] = c.names();
c.add('name', 'value');
c.delete('name');
}
function onClientRequest2(request: EW.MutableRequest & EW.HasRespondWith) {
// The values passed to SetCookie can be ignored - they're just to verify the compiler.
const c = new SetCookie({
name: "n",
sameSite: true
});
c.name = "le name";
c.value = "le value";
c.maxAge = 5;
c.domain = "le domain";
c.path = "le path";
c.expires = new Date('2013-02-21T06:55:00');
c.httpOnly = true;
c.secure = false;
c.sameSite = "Lax";
request.respondWith(234, { SetCookie: c.toHeader() }, "");
}

View File

@@ -0,0 +1,63 @@
import URLSearchParams from 'url-search-params';
export function onClientRequest(request: EW.MutableRequest & EW.HasRespondWith) {
const params = new URLSearchParams(request.query);
params.append("from-script", "from-value");
params.delete("to-delete");
let gotten = params.get('m i s s i n g');
if (gotten != null) {
request.respondWith(404, {}, 'busted in get() null check');
return;
}
gotten = params.get('from-script');
if (gotten !== 'from-value') {
request.respondWith(404, {}, 'didn\'t get() value');
return;
}
if (params.has('nope')) {
request.respondWith(404, {}, 'has() found a non-existent value');
return;
}
if (!params.has('from-script')) {
request.respondWith(404, {}, 'has() didn\'t find an expected value');
return;
}
if (params.getAll('nope').length === 0) {
// excelsior
} else {
request.respondWith(404, {}, 'getAll() test failed');
return;
}
let entriesCount = 0;
for (const [k, v] of params.entries()) {
entriesCount++;
}
let keysCount = 0;
for (const [k, v] of params.keys()) {
keysCount++;
}
let valuesCount = 0;
for (const [k, v] of params.values()) {
valuesCount++;
}
if (entriesCount !== keysCount || keysCount !== valuesCount) {
request.respondWith(404, {}, 'iteration counts didn\'t add up');
return;
}
params.set("setted", "value-setted");
request.setHeader("foo", params.toString());
request.respondWith(282, {}, 'succeeded');
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"lib": [
"es6"
],
"noImplicitAny": false,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"types": [],
"forceConsistentCasingInFileNames": true,
"baseUrl": "../",
"typeRoots": [
"../"
],
"noEmit": true
},
"files": [
"index.d.ts",
"test/akamai-edgeworkers-global.test.ts",
"test/cookies-tests.ts",
"test/url-search-params-tests.ts"
]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "dtslint/dt.json"
}