From 288692b272b695eda00a47f93339e1e16ee756b1 Mon Sep 17 00:00:00 2001 From: evan-hughes <58273622+evan-hughes@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:54:59 -0500 Subject: [PATCH] Add akamai-edgeworkers (#40716) * Add akamai-edgeworkers * PR feedback for Akamai-Edgeworkers --- types/akamai-edgeworkers/OTHER_FILES.txt | 1 + types/akamai-edgeworkers/README.md | 58 +++ types/akamai-edgeworkers/index.d.ts | 461 ++++++++++++++++++ .../test/akamai-edgeworkers-global.test.ts | 117 +++++ .../akamai-edgeworkers/test/cookies-tests.ts | 51 ++ .../test/url-search-params-tests.ts | 63 +++ types/akamai-edgeworkers/tsconfig.json | 26 + types/akamai-edgeworkers/tslint.json | 3 + 8 files changed, 780 insertions(+) create mode 100644 types/akamai-edgeworkers/OTHER_FILES.txt create mode 100644 types/akamai-edgeworkers/README.md create mode 100644 types/akamai-edgeworkers/index.d.ts create mode 100644 types/akamai-edgeworkers/test/akamai-edgeworkers-global.test.ts create mode 100644 types/akamai-edgeworkers/test/cookies-tests.ts create mode 100644 types/akamai-edgeworkers/test/url-search-params-tests.ts create mode 100644 types/akamai-edgeworkers/tsconfig.json create mode 100644 types/akamai-edgeworkers/tslint.json diff --git a/types/akamai-edgeworkers/OTHER_FILES.txt b/types/akamai-edgeworkers/OTHER_FILES.txt new file mode 100644 index 0000000000..42061c01a1 --- /dev/null +++ b/types/akamai-edgeworkers/OTHER_FILES.txt @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/types/akamai-edgeworkers/README.md b/types/akamai-edgeworkers/README.md new file mode 100644 index 0000000000..2066a52626 --- /dev/null +++ b/types/akamai-edgeworkers/README.md @@ -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 +/// + +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 +/// + +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 \ No newline at end of file diff --git a/types/akamai-edgeworkers/index.d.ts b/types/akamai-edgeworkers/index.d.ts new file mode 100644 index 0000000000..1690aca8d4 --- /dev/null +++ b/types/akamai-edgeworkers/index.d.ts @@ -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 +// 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; + + /** + * Iterate through the values. + */ + values(): IterableIterator; + + /** + * 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; + } +} diff --git a/types/akamai-edgeworkers/test/akamai-edgeworkers-global.test.ts b/types/akamai-edgeworkers/test/akamai-edgeworkers-global.test.ts new file mode 100644 index 0000000000..e1caca7180 --- /dev/null +++ b/types/akamai-edgeworkers/test/akamai-edgeworkers-global.test.ts @@ -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; +} diff --git a/types/akamai-edgeworkers/test/cookies-tests.ts b/types/akamai-edgeworkers/test/cookies-tests.ts new file mode 100644 index 0000000000..c44183cc6d --- /dev/null +++ b/types/akamai-edgeworkers/test/cookies-tests.ts @@ -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() }, ""); +} diff --git a/types/akamai-edgeworkers/test/url-search-params-tests.ts b/types/akamai-edgeworkers/test/url-search-params-tests.ts new file mode 100644 index 0000000000..c44233711e --- /dev/null +++ b/types/akamai-edgeworkers/test/url-search-params-tests.ts @@ -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'); +} diff --git a/types/akamai-edgeworkers/tsconfig.json b/types/akamai-edgeworkers/tsconfig.json new file mode 100644 index 0000000000..46738167db --- /dev/null +++ b/types/akamai-edgeworkers/tsconfig.json @@ -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" + ] +} diff --git a/types/akamai-edgeworkers/tslint.json b/types/akamai-edgeworkers/tslint.json new file mode 100644 index 0000000000..f93cf8562a --- /dev/null +++ b/types/akamai-edgeworkers/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "dtslint/dt.json" +}