diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index 6fc7c3115..329233306 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -9,6 +9,7 @@ "decorate": "1.0.0-alpha.13", "duration": "1.0.0-alpha.13", "eslint-config": "1.0.0-alpha.13", + "headers": "1.0.0-alpha.13", "ip": "1.0.0-alpha.13", "logger": "1.0.0-alpha.13", "protocol": "1.0.0-alpha.13", diff --git a/.github/release-please-config.json b/.github/release-please-config.json index be0def3f6..93b40f1ca 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -65,6 +65,10 @@ "component": "@arcjet/eslint-config", "skip-github-release": true }, + "headers": { + "component": "@arcjet/headers", + "skip-github-release": true + }, "ip": { "component": "@arcjet/ip", "skip-github-release": true @@ -105,6 +109,7 @@ "@arjcet/decorate", "@arjcet/duration", "@arcjet/eslint-config", + "@arcjet/headers", "@arcjet/ip", "@arcjet/logger", "@arcjet/protocol", diff --git a/arcjet-bun/index.ts b/arcjet-bun/index.ts index 24f501e8e..d487e3806 100644 --- a/arcjet-bun/index.ts +++ b/arcjet-bun/index.ts @@ -5,7 +5,6 @@ import core, { ArcjetOptions, Primitive, Product, - ArcjetHeaders, Runtime, ArcjetRequest, ExtraProps, @@ -16,6 +15,7 @@ import core, { Arcjet, } from "arcjet"; import findIP from "@arcjet/ip"; +import ArcjetHeaders from "@arcjet/headers"; import type { Server } from "bun"; // Re-export all named exports from the generic SDK diff --git a/arcjet-bun/package.json b/arcjet-bun/package.json index 1eec9e48a..283d50a78 100644 --- a/arcjet-bun/package.json +++ b/arcjet-bun/package.json @@ -38,6 +38,7 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests" }, "dependencies": { + "@arcjet/headers": "1.0.0-alpha.13", "@arcjet/ip": "1.0.0-alpha.13", "@connectrpc/connect-node": "1.4.0", "arcjet": "1.0.0-alpha.13" diff --git a/arcjet-next/index.ts b/arcjet-next/index.ts index 96ebbb30e..deffbaec5 100644 --- a/arcjet-next/index.ts +++ b/arcjet-next/index.ts @@ -12,7 +12,6 @@ import arcjet, { ArcjetOptions, Primitive, Product, - ArcjetHeaders, Runtime, ArcjetRequest, ExtraProps, @@ -23,6 +22,7 @@ import arcjet, { Arcjet, } from "arcjet"; import findIP from "@arcjet/ip"; +import ArcjetHeaders from "@arcjet/headers"; // Re-export all named exports from the generic SDK export * from "arcjet"; diff --git a/arcjet-next/package.json b/arcjet-next/package.json index 8b63b10de..1bb08bceb 100644 --- a/arcjet-next/package.json +++ b/arcjet-next/package.json @@ -40,6 +40,7 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests" }, "dependencies": { + "@arcjet/headers": "1.0.0-alpha.13", "@arcjet/ip": "1.0.0-alpha.13", "@connectrpc/connect-web": "1.4.0", "arcjet": "1.0.0-alpha.13" diff --git a/arcjet-node/index.ts b/arcjet-node/index.ts index 70a0c9ba1..79933996e 100644 --- a/arcjet-node/index.ts +++ b/arcjet-node/index.ts @@ -4,7 +4,6 @@ import core, { ArcjetOptions, Primitive, Product, - ArcjetHeaders, Runtime, ArcjetRequest, ExtraProps, @@ -15,6 +14,7 @@ import core, { Arcjet, } from "arcjet"; import findIP from "@arcjet/ip"; +import ArcjetHeaders from "@arcjet/headers"; // Re-export all named exports from the generic SDK export * from "arcjet"; diff --git a/arcjet-node/package.json b/arcjet-node/package.json index 6052274f9..9acf02963 100644 --- a/arcjet-node/package.json +++ b/arcjet-node/package.json @@ -40,6 +40,7 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests" }, "dependencies": { + "@arcjet/headers": "1.0.0-alpha.13", "@arcjet/ip": "1.0.0-alpha.13", "@connectrpc/connect-node": "1.4.0", "arcjet": "1.0.0-alpha.13" diff --git a/arcjet-sveltekit/index.ts b/arcjet-sveltekit/index.ts index 09b70f7c9..23e4fb786 100644 --- a/arcjet-sveltekit/index.ts +++ b/arcjet-sveltekit/index.ts @@ -5,7 +5,6 @@ import core, { ArcjetOptions, Primitive, Product, - ArcjetHeaders, Runtime, ArcjetRequest, ExtraProps, @@ -16,6 +15,7 @@ import core, { Arcjet, } from "arcjet"; import findIP from "@arcjet/ip"; +import ArcjetHeaders from "@arcjet/headers"; // Re-export all named exports from the generic SDK export * from "arcjet"; diff --git a/arcjet-sveltekit/package.json b/arcjet-sveltekit/package.json index 29887faef..88b58b05e 100644 --- a/arcjet-sveltekit/package.json +++ b/arcjet-sveltekit/package.json @@ -40,6 +40,7 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests" }, "dependencies": { + "@arcjet/headers": "1.0.0-alpha.13", "@arcjet/ip": "1.0.0-alpha.13", "@connectrpc/connect-node": "1.4.0", "@connectrpc/connect-web": "1.4.0", diff --git a/arcjet/index.ts b/arcjet/index.ts index 68acbf1c1..3a2b04cec 100644 --- a/arcjet/index.ts +++ b/arcjet/index.ts @@ -40,6 +40,7 @@ import { import * as analyze from "@arcjet/analyze"; import * as duration from "@arcjet/duration"; import logger from "@arcjet/logger"; +import ArcjetHeaders from "@arcjet/headers"; export * from "@arcjet/protocol"; @@ -49,10 +50,6 @@ function assert(condition: boolean, msg: string) { } } -function isIterable(val: any): val is Iterable { - return typeof val?.[Symbol.iterator] === "function"; -} - function nowInSeconds(): number { return Math.floor(Date.now() / 1000); } @@ -557,65 +554,6 @@ export type EmailOptions = { allowDomainLiteral?: boolean; }; -export class ArcjetHeaders extends Headers { - constructor( - init?: HeadersInit | Record, - ) { - super(); - if (typeof init !== "undefined") { - if (isIterable(init)) { - for (const [key, value] of init) { - this.append(key, value); - } - } else { - for (const [key, value] of Object.entries( - init as Record, - )) { - if (typeof value === "undefined") { - continue; - } - - if (Array.isArray(value)) { - for (const singleValue of value) { - this.append(key, singleValue); - } - } else { - this.append(key, value); - } - } - } - } - } - - /** - * Append a key and value to the headers, while filtering any key named - * `cookie`. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) - * - * @param key The key to append in the headers - * @param value The value to append for the key in the headers - */ - append(key: string, value: string): void { - if (key.toLowerCase() !== "cookie") { - super.append(key, value); - } - } - /** - * Set a key and value in the headers, but filtering any key named `cookie`. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) - * - * @param key The key to set in the headers - * @param value The value to set for the key in the headers - */ - set(key: string, value: string): void { - if (key.toLowerCase() !== "cookie") { - super.set(key, value); - } - } -} - const Priority = { Shield: 1, RateLimit: 2, diff --git a/arcjet/package.json b/arcjet/package.json index 5cb7aa448..ff504c84b 100644 --- a/arcjet/package.json +++ b/arcjet/package.json @@ -42,6 +42,7 @@ "dependencies": { "@arcjet/analyze": "1.0.0-alpha.13", "@arcjet/duration": "1.0.0-alpha.13", + "@arcjet/headers": "1.0.0-alpha.13", "@arcjet/logger": "1.0.0-alpha.13", "@arcjet/protocol": "1.0.0-alpha.13" }, diff --git a/arcjet/test/index.node.test.ts b/arcjet/test/index.node.test.ts index 9ed81a8ba..3c4d539b7 100644 --- a/arcjet/test/index.node.test.ts +++ b/arcjet/test/index.node.test.ts @@ -33,7 +33,6 @@ import arcjet, { rateLimit, ArcjetRule, defaultBaseUrl, - ArcjetHeaders, Runtime, validateEmail, protectSignup, @@ -1223,54 +1222,6 @@ describe("createRemoteClient", () => { }); }); -describe("ArcjetHeaders", () => { - test("can be constructed no initializer", () => { - const headers = new ArcjetHeaders(); - expect(headers).toBeInstanceOf(ArcjetHeaders); - expect(headers).toBeInstanceOf(Headers); - }); - - test("can be constructed with a Headers instance", () => { - const init = new Headers(); - init.set("foobar", "baz"); - const headers = new ArcjetHeaders(init); - expect(headers.get("foobar")).toEqual("baz"); - }); - - test("can be constructed with an ArcjetHeaders instance", () => { - const init = new ArcjetHeaders(); - init.set("foobar", "baz"); - const headers = new ArcjetHeaders(init); - expect(headers.get("foobar")).toEqual("baz"); - }); - - test("can be constructed with an array of tuples", () => { - const headers = new ArcjetHeaders([["foobar", "baz"]]); - expect(headers.get("foobar")).toEqual("baz"); - }); - - test("can be constructed with an object", () => { - const headers = new ArcjetHeaders({ - foobar: "baz", - }); - expect(headers.get("foobar")).toEqual("baz"); - }); - - test("filters undefined values in an object", () => { - const headers = new ArcjetHeaders({ - foobar: undefined, - }); - expect(headers.has("foobar")).toEqual(false); - }); - - test("combines array values in an object", () => { - const headers = new ArcjetHeaders({ - foo: ["bar", "baz"], - }); - expect(headers.get("foo")).toEqual("bar, baz"); - }); -}); - describe("ArcjetDecision", () => { test("will default the `id` property if not specified", () => { const decision = new ArcjetAllowDecision({ diff --git a/headers/.eslintignore b/headers/.eslintignore new file mode 100644 index 000000000..9cfa2cae7 --- /dev/null +++ b/headers/.eslintignore @@ -0,0 +1,6 @@ +/.turbo/ +/coverage/ +/node_modules/ +*.d.ts +*.js +!*.config.js diff --git a/headers/.eslintrc.cjs b/headers/.eslintrc.cjs new file mode 100644 index 000000000..abe4cd7b4 --- /dev/null +++ b/headers/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ["@arcjet/eslint-config"], +}; diff --git a/headers/.gitignore b/headers/.gitignore new file mode 100644 index 000000000..35b162da3 --- /dev/null +++ b/headers/.gitignore @@ -0,0 +1,135 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Generated files +index.js +index.d.ts +test/*.js diff --git a/headers/LICENSE b/headers/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/headers/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/headers/README.md b/headers/README.md new file mode 100644 index 000000000..5d95d119f --- /dev/null +++ b/headers/README.md @@ -0,0 +1,47 @@ + + + + Arcjet Logo + + + +# `@arcjet/headers` + +

+ + + + npm badge + + +

+ +[Arcjet][arcjet] extension of the Headers class. + +## Installation + +```shell +npm install -S @arcjet/headers +``` + +## Example + +```ts +import ArcjetHeaders from "@arcjet/headers"; + +const headers = new ArcjetHeaders({ abc: "123" }); + +console.log(headers.get("abc")); +``` + +## Considerations + +This package will filter the `cookie` header and all headers with keys or values +that are not strings, such as `{ "abc": undefined }`. + +## License + +Licensed under the [Apache License, Version 2.0][apache-license]. + +[arcjet]: https://arcjet.com +[apache-license]: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/headers/index.ts b/headers/index.ts new file mode 100644 index 000000000..c99d62bc6 --- /dev/null +++ b/headers/index.ts @@ -0,0 +1,87 @@ +function isIterable(val: any): val is Iterable { + return typeof val?.[Symbol.iterator] === "function"; +} + +/** + * This Fetch API interface allows you to perform various actions on HTTP + * request and response headers. These actions include retrieving, setting, + * adding to, and removing. A Headers object has an associated header list, + * which is initially empty and consists of zero or more name and value pairs. + * + * You can add to this using methods like `append()`. + * + * In all methods of this interface, header names are matched by + * case-insensitive byte sequence. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers) + */ +export default class ArcjetHeaders extends Headers { + constructor( + init?: HeadersInit | Record, + ) { + super(); + if ( + typeof init !== "undefined" && + typeof init !== "string" && + init !== null + ) { + if (isIterable(init)) { + for (const [key, value] of init) { + this.append(key, value); + } + } else { + for (const [key, value] of Object.entries( + init as Record, + )) { + if (typeof value === "undefined") { + continue; + } + + if (Array.isArray(value)) { + for (const singleValue of value) { + this.append(key, singleValue); + } + } else { + this.append(key, value); + } + } + } + } + } + + /** + * Append a key and value to the headers, while filtering any key named + * `cookie`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) + * + * @param key The key to append in the headers + * @param value The value to append for the key in the headers + */ + append(key: string, value: string): void { + if (typeof key !== "string" || typeof value !== "string") { + return; + } + + if (key.toLowerCase() !== "cookie") { + super.append(key, value); + } + } + /** + * Set a key and value in the headers, but filtering any key named `cookie`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) + * + * @param key The key to set in the headers + * @param value The value to set for the key in the headers + */ + set(key: string, value: string): void { + if (typeof key !== "string" || typeof value !== "string") { + return; + } + + if (key.toLowerCase() !== "cookie") { + super.set(key, value); + } + } +} diff --git a/headers/jest.config.js b/headers/jest.config.js new file mode 100644 index 000000000..6d5656840 --- /dev/null +++ b/headers/jest.config.js @@ -0,0 +1,16 @@ +/** @type {import('jest').Config} */ +const config = { + // We only test JS files once compiled with TypeScript + moduleFileExtensions: ["js"], + coverageDirectory: "coverage", + collectCoverage: true, + // If this is set to default (babel) rather than v8, tests fail with the edge + // runtime and the error "EvalError: Code generation from strings disallowed + // for this context". Tracking in + // https://github.com/vercel/edge-runtime/issues/250 + coverageProvider: "v8", + verbose: true, + testEnvironment: "node", +}; + +export default config; diff --git a/headers/package.json b/headers/package.json new file mode 100644 index 000000000..fd3660709 --- /dev/null +++ b/headers/package.json @@ -0,0 +1,57 @@ +{ + "name": "@arcjet/headers", + "version": "1.0.0-alpha.13", + "description": "Arcjet extension of the Headers class", + "license": "Apache-2.0", + "homepage": "https://arcjet.com", + "repository": { + "type": "git", + "url": "git+https://github.com/arcjet/arcjet-js.git", + "directory": "headers" + }, + "bugs": { + "url": "https://github.com/arcjet/arcjet-js/issues", + "email": "support@arcjet.com" + }, + "author": { + "name": "Arcjet", + "email": "support@arcjet.com", + "url": "https://arcjet.com" + }, + "engines": { + "node": ">=18" + }, + "type": "module", + "main": "./index.js", + "types": "./index.d.ts", + "files": [ + "LICENSE", + "README.md", + "*.js", + "*.d.ts", + "*.ts", + "!*.config.js" + ], + "scripts": { + "prepublishOnly": "npm run build", + "build": "rollup --config rollup.config.js", + "lint": "eslint .", + "pretest": "npm run build", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "dependencies": {}, + "devDependencies": { + "@arcjet/eslint-config": "1.0.0-alpha.13", + "@arcjet/rollup-config": "1.0.0-alpha.13", + "@arcjet/tsconfig": "1.0.0-alpha.13", + "@jest/globals": "29.7.0", + "@rollup/wasm-node": "4.17.2", + "@types/node": "18.18.0", + "jest": "29.7.0", + "typescript": "5.4.5" + }, + "publishConfig": { + "access": "public", + "tag": "latest" + } +} diff --git a/headers/rollup.config.js b/headers/rollup.config.js new file mode 100644 index 000000000..79177f236 --- /dev/null +++ b/headers/rollup.config.js @@ -0,0 +1,3 @@ +import { createConfig } from "@arcjet/rollup-config"; + +export default createConfig(import.meta.url); diff --git a/headers/test/headers.test.ts b/headers/test/headers.test.ts new file mode 100644 index 000000000..faec33bf8 --- /dev/null +++ b/headers/test/headers.test.ts @@ -0,0 +1,154 @@ +/** + * @jest-environment node + */ +import { describe, expect, test } from "@jest/globals"; +import ArcjetHeaders from "../index"; + +describe("ArcjetHeaders", () => { + test("can be constructed with no initializer", () => { + const headers = new ArcjetHeaders(); + expect(Array.from(headers.entries())).toEqual([]); + }); + + test("can be constructed with Headers", () => { + const headers = new ArcjetHeaders(new Headers([["abc", "xyz"]])); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz"]]); + }); + + test("can be constructed with ArcjetHeaders", () => { + const headers = new ArcjetHeaders(new ArcjetHeaders([["abc", "xyz"]])); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz"]]); + }); + + test("can be constructed with a plain object", () => { + const headers = new ArcjetHeaders({ abc: "xyz" }); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz"]]); + }); + + test("can be constructed with a plain object", () => { + const headers = new ArcjetHeaders({ abc: "xyz" }); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz"]]); + }); + + test("can be constructed with a plain object containing multiple entries", () => { + const headers = new ArcjetHeaders({ abc: ["xyz", "123"] }); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz, 123"]]); + }); + + test("does not error if null is used as init", () => { + // @ts-expect-error + const headers = new ArcjetHeaders(null); + expect(Array.from(headers.entries())).toEqual([]); + }); + + test("does not initialize header values if string used as init", () => { + // @ts-expect-error + const headers = new ArcjetHeaders("abc123"); + expect(Array.from(headers.entries())).toEqual([]); + }); + + test("filters undefined values from plain objects", () => { + const headers = new ArcjetHeaders({ abc: undefined }); + expect(Array.from(headers.entries())).toEqual([]); + }); + + describe("#append(key, value)", () => { + test("sets a header if not already set", () => { + const headers = new ArcjetHeaders(); + headers.append("abc", "xyz"); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz"]]); + }); + + test("adds another value to header if already set", () => { + const headers = new ArcjetHeaders({ abc: "xyz" }); + headers.append("abc", "123"); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz, 123"]]); + }); + + test("does NOT add cookie header", () => { + const headers = new ArcjetHeaders(); + headers.append("cookie", "abc"); + headers.append("COOKIE", "123"); + headers.append("cOoKiE", "xyz"); + expect(Array.from(headers.entries())).toEqual([]); + }); + + test("does NOT add non-string keys", () => { + const headers = new ArcjetHeaders(); + // @ts-expect-error + headers.append(123, "abc"); + // @ts-expect-error + headers.append({}, "abc"); + // @ts-expect-error + headers.append([], "abc"); + // @ts-expect-error + headers.append(function () {}, "abc"); + expect(Array.from(headers.entries())).toEqual([]); + }); + + test("does NOT add non-string values", () => { + const headers = new ArcjetHeaders(); + // @ts-expect-error + headers.append("abc", undefined); + // @ts-expect-error + headers.append("abc", 123); + // @ts-expect-error + headers.append("abc", {}); + // @ts-expect-error + headers.append("abc", []); + // @ts-expect-error + headers.append("abc", function () {}); + expect(Array.from(headers.entries())).toEqual([]); + }); + }); + + describe("#set(key, value)", () => { + test("sets a header if not already set", () => { + const headers = new ArcjetHeaders(); + headers.set("abc", "xyz"); + expect(Array.from(headers.entries())).toEqual([["abc", "xyz"]]); + }); + + test("overrides a header if already set", () => { + const headers = new ArcjetHeaders({ abc: "xyz" }); + headers.set("abc", "123"); + expect(Array.from(headers.entries())).toEqual([["abc", "123"]]); + }); + + test("does NOT add cookie header", () => { + const headers = new ArcjetHeaders(); + headers.set("cookie", "abc"); + headers.set("COOKIE", "123"); + headers.set("cOoKiE", "xyz"); + expect(Array.from(headers.entries())).toEqual([]); + }); + + test("does NOT add non-string keys", () => { + const headers = new ArcjetHeaders(); + // @ts-expect-error + headers.set(123, "abc"); + // @ts-expect-error + headers.set({}, "abc"); + // @ts-expect-error + headers.set([], "abc"); + // @ts-expect-error + headers.set(function () {}, "abc"); + expect(Array.from(headers.entries())).toEqual([]); + }); + + test("does NOT add non-string values", () => { + const headers = new ArcjetHeaders(); + // @ts-expect-error + headers.set("abc", undefined); + // @ts-expect-error + headers.set("abc", 123); + // @ts-expect-error + headers.set("abc", {}); + // @ts-expect-error + headers.set("abc", []); + // @ts-expect-error + headers.set("abc", function () {}); + expect(Array.from(headers.entries())).toEqual([]); + }); + }); +}); diff --git a/headers/tsconfig.json b/headers/tsconfig.json new file mode 100644 index 000000000..95929e097 --- /dev/null +++ b/headers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@arcjet/tsconfig/base", + "include": ["index.ts", "test/*.ts"] +} diff --git a/package-lock.json b/package-lock.json index e583c8a33..401dbb06c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -223,6 +223,23 @@ "eslint": "^8" } }, + "headers": { + "version": "1.0.0-alpha.13", + "license": "Apache-2.0", + "devDependencies": { + "@arcjet/eslint-config": "1.0.0-alpha.13", + "@arcjet/rollup-config": "1.0.0-alpha.13", + "@arcjet/tsconfig": "1.0.0-alpha.13", + "@jest/globals": "29.7.0", + "@rollup/wasm-node": "4.17.2", + "@types/node": "18.18.0", + "jest": "29.7.0", + "typescript": "5.4.5" + }, + "engines": { + "node": ">=18" + } + }, "ip": { "name": "@arcjet/ip", "version": "1.0.0-alpha.13", @@ -300,6 +317,10 @@ "resolved": "eslint-config", "link": true }, + "node_modules/@arcjet/headers": { + "resolved": "headers", + "link": true + }, "node_modules/@arcjet/ip": { "resolved": "ip", "link": true