├── .node-version ├── .github ├── CODEOWNERS └── workflows │ ├── semgrep.yml │ └── release.yml ├── example ├── static │ ├── style.css │ ├── thank-you.html │ ├── blog │ │ └── hello-world.html │ ├── contact.html │ └── index.html ├── functions │ ├── throw.ts │ ├── date.ts │ ├── image.tsx │ ├── stytch-admin │ │ └── _middleware.ts │ ├── hcaptcha.ts │ ├── graphql.ts │ ├── _middleware.ts │ ├── blog │ │ └── _middleware.tsx │ └── admin │ │ └── _middleware.ts ├── package.json └── tsconfig.json ├── packages ├── vercel-og │ ├── src │ │ └── api │ │ │ └── index.ts │ ├── functions │ │ ├── tsconfig.json │ │ └── _middleware.ts │ ├── tsconfig.json │ ├── README.md │ ├── CHANGELOG.md │ ├── index.d.ts │ └── package.json ├── sentry │ ├── functions │ │ ├── tsconfig.json │ │ └── _middleware.ts │ ├── index.d.ts │ ├── README.md │ ├── CHANGELOG.md │ └── package.json ├── stytch │ ├── functions │ │ ├── tsconfig.json │ │ └── _middleware.ts │ ├── src │ │ └── api │ │ │ └── index.ts │ ├── tsconfig.json │ ├── README.md │ ├── CHANGELOG.md │ ├── index.d.ts │ └── package.json ├── google-chat │ ├── functions │ │ ├── tsconfig.json │ │ └── index.ts │ ├── tsconfig.json │ ├── index.d.ts │ ├── README.md │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ └── api │ │ └── index.ts ├── graphql │ ├── functions │ │ ├── tsconfig.json │ │ └── index.ts │ ├── index.d.ts │ ├── README.md │ ├── CHANGELOG.md │ ├── package.json │ └── static │ │ └── index.html ├── hcaptcha │ ├── functions │ │ ├── tsconfig.json │ │ └── index.ts │ ├── README.md │ ├── CHANGELOG.md │ ├── index.d.ts │ └── package.json ├── headers │ ├── functions │ │ ├── tsconfig.json │ │ └── _middleware.ts │ ├── index.d.ts │ ├── README.md │ └── package.json ├── honeycomb │ ├── functions │ │ ├── tsconfig.json │ │ └── _middleware.ts │ ├── README.md │ ├── index.d.ts │ ├── CHANGELOG.md │ └── package.json ├── turnstile │ ├── functions │ │ ├── tsconfig.json │ │ └── index.ts │ ├── README.md │ ├── CHANGELOG.md │ ├── index.d.ts │ └── package.json ├── static-forms │ ├── functions │ │ ├── tsconfig.json │ │ └── _middleware.ts │ ├── index.d.ts │ ├── README.md │ ├── CHANGELOG.md │ └── package.json └── cloudflare-access │ ├── functions │ ├── tsconfig.json │ └── _middleware.ts │ ├── tsconfig.json │ ├── README.md │ ├── CHANGELOG.md │ ├── src │ └── api │ │ └── index.ts │ ├── index.d.ts │ └── package.json ├── .vscode ├── settings.json └── extensions.json ├── tsconfig.src.json ├── .changeset ├── config.json └── README.md ├── tsconfig.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── patches └── @vercel+og+0.4.1.patch └── .gitignore /.node-version: -------------------------------------------------------------------------------- 1 | v20.11.0 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cloudflare/pages -------------------------------------------------------------------------------- /example/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #94b68f; 3 | } 4 | -------------------------------------------------------------------------------- /packages/vercel-og/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export { ImageResponse } from "@vercel/og"; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/sentry/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/stytch/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/google-chat/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/graphql/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/hcaptcha/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/headers/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/honeycomb/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/turnstile/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/vercel-og/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/static-forms/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/cloudflare-access/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /example/functions/throw.ts: -------------------------------------------------------------------------------- 1 | export const onRequest: PagesFunction = () => { 2 | throw new Error("Waaa"); 3 | }; 4 | -------------------------------------------------------------------------------- /example/functions/date.ts: -------------------------------------------------------------------------------- 1 | export const onRequest: PagesFunction = () => 2 | new Response(new Date().toISOString()); 3 | -------------------------------------------------------------------------------- /example/static/thank-you.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Thank you for your submission!

4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/headers/index.d.ts: -------------------------------------------------------------------------------- 1 | export type PluginArgs = HeadersInit; 2 | 3 | export default function (args: PluginArgs): PagesFunction; 4 | -------------------------------------------------------------------------------- /packages/stytch/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export const envs = { 2 | live: "https://api.stytch.com/v1/", 3 | test: "https://test.stytch.com/v1/", 4 | }; 5 | -------------------------------------------------------------------------------- /tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDeclarationOnly": true, 4 | "noEmit": false 5 | }, 6 | "extends": "./tsconfig.json" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "github.vscode-github-actions", 6 | "bierner.markdown-checkbox" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/stytch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/types/", 4 | "rootDir": "./src/" 5 | }, 6 | "extends": "../../tsconfig.src.json", 7 | "include": ["./src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /example/functions/image.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api"; 2 | 3 | export const onRequestGet = async () => { 4 | return new ImageResponse(
Hello, world!
); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/google-chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/types/", 4 | "rootDir": "./src/" 5 | }, 6 | "extends": "../../tsconfig.src.json", 7 | "include": ["./src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/vercel-og/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/types/", 4 | "rootDir": "./src/" 5 | }, 6 | "extends": "../../tsconfig.src.json", 7 | "include": ["./src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/cloudflare-access/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/types/", 4 | "rootDir": "./src/" 5 | }, 6 | "extends": "../../tsconfig.src.json", 7 | "include": ["./src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/graphql/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { graphql, GraphQLSchema } from "graphql"; 2 | 3 | export type PluginArgs = { schema: GraphQLSchema; graphql: typeof graphql }; 4 | 5 | export default function (args: PluginArgs): PagesFunction; 6 | -------------------------------------------------------------------------------- /packages/google-chat/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { chat_v1 } from "@googleapis/chat"; 2 | 3 | export type PluginArgs = ( 4 | event: chat_v1.Schema$DeprecatedEvent, 5 | ) => Promise; 6 | 7 | export default function (args: PluginArgs): PagesFunction; 8 | -------------------------------------------------------------------------------- /packages/static-forms/index.d.ts: -------------------------------------------------------------------------------- 1 | export type PluginArgs = { 2 | respondWith: ({ 3 | formData, 4 | name, 5 | }: { 6 | formData: FormData; 7 | name: string; 8 | }) => Response | Promise; 9 | }; 10 | 11 | export default function (args: PluginArgs): PagesFunction; 12 | -------------------------------------------------------------------------------- /packages/sentry/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Toucan } from "toucan-js"; 2 | import type { Options } from "toucan-js/dist/types"; 3 | 4 | export type PluginArgs = Omit; 5 | 6 | export type PluginData = { sentry: Toucan }; 7 | 8 | export default function (args: PluginArgs): PagesFunction; 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", 3 | "access": "public", 4 | "baseBranch": "main", 5 | "changelog": "@changesets/cli/changelog", 6 | "commit": false, 7 | "fixed": [], 8 | "ignore": [], 9 | "linked": [], 10 | "updateInternalDependencies": "patch" 11 | } 12 | -------------------------------------------------------------------------------- /packages/stytch/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # Stytch Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-stytch 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/stytch/). 14 | -------------------------------------------------------------------------------- /packages/graphql/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # GraphQL Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-graphql 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/graphql/). 14 | -------------------------------------------------------------------------------- /packages/hcaptcha/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # hCaptcha Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-hcaptcha 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/hcaptcha/). 14 | -------------------------------------------------------------------------------- /packages/honeycomb/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # Honeycomb Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-honeycomb 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/honeycomb/). 14 | -------------------------------------------------------------------------------- /packages/honeycomb/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Config, 3 | RequestTracer, 4 | } from "@cloudflare/workers-honeycomb-logger"; 5 | 6 | export type PluginArgs = Config & { apiKey: string; dataset: string }; 7 | 8 | export type PluginData = { honeycomb: { tracer: RequestTracer } }; 9 | 10 | export default function (args: PluginArgs): PagesFunction; 11 | -------------------------------------------------------------------------------- /packages/stytch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-stytch 2 | 3 | ## 1.0.3 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.2 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | -------------------------------------------------------------------------------- /packages/turnstile/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # Turnstile Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-turnstile 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/turnstile/). 14 | -------------------------------------------------------------------------------- /packages/turnstile/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-turnstile 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.1 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | -------------------------------------------------------------------------------- /packages/vercel-og/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # `@vercel/og` Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-vercel-og 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/vercel-og/). 14 | -------------------------------------------------------------------------------- /packages/google-chat/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # Google Chat Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-google-chat 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/google-chat/). 14 | -------------------------------------------------------------------------------- /packages/static-forms/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # Static Forms Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-static-forms 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/static-forms/). 14 | -------------------------------------------------------------------------------- /example/functions/stytch-admin/_middleware.ts: -------------------------------------------------------------------------------- 1 | import stytchPlugin from "@cloudflare/pages-plugin-stytch"; 2 | import { envs } from "@cloudflare/pages-plugin-stytch/api"; 3 | 4 | export const onRequest = stytchPlugin({ 5 | project_id: "project-test-747d6ca9-b21e-4c44-a245-28df7451f1da", 6 | secret: "secret-test-XlSSLKr8Yf5UrY26Gj9Ln61CMVwqaYoSd0E=", 7 | env: envs.test, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/cloudflare-access/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # Cloudflare Access Pages Plugin 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save @cloudflare/pages-plugin-cloudflare-access 9 | ``` 10 | 11 | ## Usage 12 | 13 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/cloudflare-access/). 14 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pages-plugins-example", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build:prod": "npx wrangler pages functions build --outdir ./static/_worker.js/", 7 | "clean": "rm -rf static/_worker.js", 8 | "start": "npx wrangler pages dev static" 9 | }, 10 | "dependencies": { 11 | "graphql": "^16.3.0", 12 | "react": "^18.2.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/static/blog/hello-world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blog | Hello, world! 5 | 6 | 7 | 8 | 9 | 10 |

Hello world from a blog post!

11 | 12 | 13 | -------------------------------------------------------------------------------- /example/functions/hcaptcha.ts: -------------------------------------------------------------------------------- 1 | import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; 2 | 3 | export const onRequest: PagesFunction[] = [ 4 | hCaptchaPlugin({ 5 | // nosemgrep 6 | secret: "0x0000000000000000000000000000000000000000", 7 | sitekey: "10000000-ffff-ffff-ffff-000000000001", 8 | response: "10000000-aaaa-bbbb-cccc-000000000001", 9 | }), 10 | () => new Response("Validated your humanity!"), 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/static-forms/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-static-forms 2 | 3 | ## 1.0.3 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.2 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | - 9bff7b9: fix: Only modify forms with the `data-static-form-name` property 15 | -------------------------------------------------------------------------------- /packages/graphql/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-graphql 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.3 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | 15 | ## 1.0.1 16 | 17 | ### Patch Changes 18 | 19 | - 5d29e41: chore: Upgrade dependencies 20 | -------------------------------------------------------------------------------- /packages/honeycomb/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-honeycomb 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.3 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | 15 | ## 1.0.1 16 | 17 | ### Patch Changes 18 | 19 | - 5d29e41: chore: Upgrade dependencies 20 | -------------------------------------------------------------------------------- /packages/google-chat/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-google-chat 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.3 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | 15 | ## 1.0.1 16 | 17 | ### Patch Changes 18 | 19 | - 5d29e41: chore: Upgrade dependencies 20 | -------------------------------------------------------------------------------- /packages/hcaptcha/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-hcaptcha 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.3 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | 15 | ## 1.0.2 16 | 17 | ### Patch Changes 18 | 19 | - f445f39: Fix missing-input-secret error code 20 | -------------------------------------------------------------------------------- /example/static/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Contact

5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/sentry/README.md: -------------------------------------------------------------------------------- 1 | _Sentry now provides official support for Cloudflare Workers and Pages. Refer to the [Sentry documentation](https://docs.sentry.io/platforms/javascript/guides/cloudflare/) for more details._ 2 | 3 | ## Pages Plugins 4 | 5 | # Sentry Pages Plugin 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm install --save @cloudflare/pages-plugin-sentry 11 | ``` 12 | 13 | ## Usage 14 | 15 | Documentation available on [Cloudflare's Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/sentry/). 16 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/vercel-og/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-vercel-og 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | - 47f7a33: chore: Fix `@cloudflare/pages-plugin-vercel-og/api`. 15 | 16 | Previously, the API wasn't built and the Wasm/binary files weren't being brought along. This change now produces a bundle with these files. 17 | -------------------------------------------------------------------------------- /example/functions/graphql.ts: -------------------------------------------------------------------------------- 1 | import graphQLPlugin from "@cloudflare/pages-plugin-graphql"; 2 | import { 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | GraphQLString, 6 | graphql, 7 | } from "graphql"; 8 | 9 | const schema = new GraphQLSchema({ 10 | query: new GraphQLObjectType({ 11 | name: "RootQueryType", 12 | fields: { 13 | hello: { 14 | type: GraphQLString, 15 | resolve() { 16 | return "Hello, world!"; 17 | }, 18 | }, 19 | }, 20 | }), 21 | }); 22 | 23 | export const onRequest: PagesFunction = graphQLPlugin({ schema, graphql }); 24 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "resolveJsonModule": true, 7 | "moduleDetection": "force", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "noUncheckedIndexedAccess": true, 11 | "lib": ["ESNext"], 12 | "incremental": true, 13 | "newLine": "lf", 14 | "moduleResolution": "NodeNext", 15 | "module": "NodeNext", 16 | "target": "ESNext", 17 | "types": ["@cloudflare/workers-types"], 18 | "customConditions": ["workerd"], 19 | "noEmit": true, 20 | "jsx": "react-jsx" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import headersPlugin from "@cloudflare/pages-plugin-headers"; 2 | import honeycombPlugin from "@cloudflare/pages-plugin-honeycomb"; 3 | 4 | export const onRequest: PagesFunction[] = [ 5 | honeycombPlugin({ 6 | apiKey: "", 7 | dataset: "pages-plugin-example", 8 | }), 9 | ({ next }) => { 10 | try { 11 | return next(); 12 | } catch (thrown) { 13 | return new Response(`${thrown}`, { status: 500 }); 14 | } 15 | }, 16 | // sentryPlugin({ 17 | // // dsn: "https://sentry.io/xyz", 18 | // }), 19 | headersPlugin({ 20 | "Access-Control-Allow-Origin": "*", 21 | }), 22 | ]; 23 | -------------------------------------------------------------------------------- /example/functions/blog/_middleware.tsx: -------------------------------------------------------------------------------- 1 | import vercelOGPagesPlugin from "@cloudflare/pages-plugin-vercel-og"; 2 | 3 | interface Props { 4 | ogTitle: string; 5 | } 6 | 7 | export const onRequest = vercelOGPagesPlugin({ 8 | imagePathSuffix: "/social-image.png", 9 | component: ({ ogTitle, pathname }) => { 10 | return
{ogTitle}
; 11 | }, 12 | extractors: { 13 | on: { 14 | 'meta[property="og:title"]': (props) => ({ 15 | element(element) { 16 | props.ogTitle = element.getAttribute("content"); 17 | }, 18 | }), 19 | }, 20 | }, 21 | autoInject: { 22 | openGraph: true, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/headers/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import type { PluginArgs } from "@cloudflare/pages-plugin-headers"; 2 | 3 | type HeadersPagesPluginFunction< 4 | Env = unknown, 5 | Params extends string = any, 6 | Data extends Record = Record, 7 | > = PagesPluginFunction; 8 | 9 | export const onRequest: HeadersPagesPluginFunction = async ({ 10 | next, 11 | pluginArgs, 12 | }) => { 13 | const headers = new Headers(pluginArgs); 14 | 15 | const response = await next(); 16 | 17 | for (const [name, value] of headers.entries()) { 18 | response.headers.set(name, value); 19 | } 20 | 21 | return response; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/turnstile/index.d.ts: -------------------------------------------------------------------------------- 1 | export type PluginArgs = { 2 | secret: string; 3 | response?: string; 4 | remoteip?: string; 5 | idempotency_key?: string; 6 | onError?: PagesFunction; 7 | }; 8 | 9 | interface TurnstileSuccess { 10 | success: true; 11 | challenge_ts: string; 12 | hostname: string; 13 | "error-codes"?: string[]; 14 | action?: string; 15 | cdata?: string; 16 | } 17 | 18 | interface TurnstileFailure { 19 | success: false; 20 | "error-codes": string[]; 21 | } 22 | 23 | export type PluginData = { 24 | turnstile: TurnstileSuccess | TurnstileFailure; 25 | }; 26 | 27 | export default function (args: PluginArgs): PagesFunction; 28 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | - cron: '0 0 * * *' 9 | name: Semgrep config 10 | jobs: 11 | semgrep: 12 | name: semgrep/ci 13 | runs-on: ubuntu-latest 14 | env: 15 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 16 | SEMGREP_URL: https://cloudflare.semgrep.dev 17 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 19 | container: 20 | image: semgrep/semgrep 21 | steps: 22 | - uses: actions/checkout@v4 23 | - run: semgrep ci 24 | -------------------------------------------------------------------------------- /packages/hcaptcha/index.d.ts: -------------------------------------------------------------------------------- 1 | export type PluginArgs = { 2 | secret: string; 3 | response?: string; 4 | remoteip?: string; 5 | sitekey?: string; 6 | onError?: PagesFunction; 7 | }; 8 | 9 | interface hCaptchaSuccess { 10 | success: true; 11 | challenge_ts: string; 12 | hostname: string; 13 | credit?: boolean; 14 | "error-codes"?: string[]; 15 | score?: number; 16 | score_reason?: string[]; 17 | } 18 | 19 | interface hCaptchaFailure { 20 | success: false; 21 | "error-codes": string[]; 22 | } 23 | 24 | export type PluginData = { 25 | hCaptcha: hCaptchaSuccess | hCaptchaFailure; 26 | }; 27 | 28 | export default function (args: PluginArgs): PagesFunction; 29 | -------------------------------------------------------------------------------- /example/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pages Plugins 5 | 6 | 7 |

Pages Plugins

8 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/cloudflare-access/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-cloudflare-access 2 | 3 | ## 1.0.5 4 | 5 | ### Patch Changes 6 | 7 | - 567f8c5: Add MIT license 8 | 9 | ## 1.0.4 10 | 11 | ### Patch Changes 12 | 13 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 14 | - 9ba89fd: chore: Improve conversion from string to char array 15 | 16 | This code was a bit hard to read and got typescript errors when copying to a Workers project. This improves the speed by 1.8X and makes the code easier to understand. 17 | 18 | ## 1.0.2 19 | 20 | ### Patch Changes 21 | 22 | - 90281ad: fix: Cloudflare Access failing to validate JWTs 23 | -------------------------------------------------------------------------------- /packages/sentry/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import type { PluginArgs, PluginData } from "@cloudflare/pages-plugin-sentry"; 2 | import { Toucan } from "toucan-js"; 3 | 4 | type SentryPagesPluginFunction< 5 | Env = unknown, 6 | Params extends string = any, 7 | Data extends Record = Record, 8 | > = PagesPluginFunction; 9 | 10 | export const onRequest: SentryPagesPluginFunction = async (context) => { 11 | context.data.sentry = new Toucan({ 12 | context, 13 | ...context.pluginArgs, 14 | }); 15 | 16 | try { 17 | return await context.next(); 18 | } catch (thrown) { 19 | context.data.sentry.captureException(thrown); 20 | throw thrown; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /packages/sentry/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cloudflare/pages-plugin-sentry 2 | 3 | ## 1.1.4 4 | 5 | ### Patch Changes 6 | 7 | - 12c0932: chore: Deprecate @cloudflare/pages-plugin-sentry in favor of official Sentry plugin instead 8 | 9 | ## 1.1.3 10 | 11 | ### Patch Changes 12 | 13 | - 567f8c5: Add MIT license 14 | 15 | ## 1.1.2 16 | 17 | ### Patch Changes 18 | 19 | - 6089299: maintenance: Build Plugins with newer `--outdir` option, actually build the APIs of Plugins, and update `package.json`s to be more complete. 20 | - 34b1707: fix: Correctly type the PluginData (`context.data.sentry`) as a Toucan instance 21 | 22 | ## 1.1.0 23 | 24 | ### Minor Changes 25 | 26 | - 5d29e41: chore: Bump to using toucan-js@3.0.0 27 | 28 | https://github.com/robertcepa/toucan-js/releases/tag/toucan-js%403.0.0 29 | -------------------------------------------------------------------------------- /packages/vercel-og/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "@vercel/og"; 2 | import { FunctionComponent } from "react"; 3 | 4 | type ProvidedProps = { 5 | pathname: string; 6 | }; 7 | 8 | export type PluginArgs = { 9 | imagePathSuffix: string; 10 | component: FunctionComponent; 11 | extractors?: { 12 | on?: Record HTMLRewriterElementContentHandlers>; 13 | onDocument?: (props: Props) => HTMLRewriterDocumentContentHandlers; 14 | }; 15 | options?: ConstructorParameters[1]; 16 | onError?: () => Response | Promise; 17 | autoInject?: { 18 | openGraph?: boolean; 19 | }; 20 | }; 21 | 22 | export default function ( 23 | args: PluginArgs, 24 | ): PagesFunction; 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Node.js 16.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16.x 22 | 23 | - name: Install Dependencies 24 | run: npm ci 25 | 26 | - name: Create Release Pull Request or Publish to npm 27 | id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | publish: npm run publish 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | -------------------------------------------------------------------------------- /packages/stytch/index.d.ts: -------------------------------------------------------------------------------- 1 | export type PluginArgs = { 2 | project_id: string; 3 | secret: string; 4 | env: string; 5 | session_token?: string; 6 | session_jwt?: string; 7 | session_duration_minutes?: number; 8 | }; 9 | 10 | export type PluginData = { 11 | stytch: { 12 | session: { 13 | status_code: number; 14 | request_id: string; 15 | session: { 16 | attributes: { 17 | ip_address: string; 18 | user_agent: string; 19 | }; 20 | authentication_factors: Record[]; 21 | expires_at: string; 22 | last_accessed_at: string; 23 | session_id: string; 24 | started_at: string; 25 | user_id: string; 26 | }; 27 | session_jwt: string; 28 | session_token: string; 29 | }; 30 | }; 31 | }; 32 | 33 | export default function (args: PluginArgs): PagesFunction; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "resolveJsonModule": true, 7 | "moduleDetection": "force", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "noUncheckedIndexedAccess": true, 11 | "lib": ["ESNext"], 12 | "incremental": true, 13 | "newLine": "lf", 14 | "moduleResolution": "NodeNext", 15 | "module": "NodeNext", 16 | "sourceMap": true, 17 | "declaration": true, 18 | "composite": true, 19 | "declarationMap": true, 20 | "target": "ESNext", 21 | "types": ["@cloudflare/workers-types"], 22 | "customConditions": ["workerd"], 23 | "noEmit": true 24 | }, 25 | "exclude": [ 26 | "**/node_modules/**/*", 27 | "**/*.test.ts", 28 | "**/*.test.tsx", 29 | "**/__tests__/**/*", 30 | "**/dist/**/*", 31 | "./example/**/*" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Filing a feature request or issue 2 | 3 | Please file any feature requests or issues on the [Issues](https://github.com/cloudflare/pages-plugins/issues) page for this repository. 4 | 5 | # Development 6 | 7 | If an issue has been labelled as "validated", this indicates that we intend to work on it. 8 | 9 | If you would like to contribute to this repository, for any non-trivial changes, please first open an issue to discuss the proposed change. 10 | 11 | ## Getting Started 12 | 13 | 1. Clone the repository 14 | 1. `npm i` 15 | 1. `npm run build` 16 | 17 | ## Changesets 18 | 19 | When making a change, [add and commit a changeset as part of work](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md#adding-changesets). This allows us to automatically generate a CHANGELOG and to publish the updated package to npm. 20 | 21 | `npx changeset` in the root of the monorepo and follow the prompts. 22 | -------------------------------------------------------------------------------- /packages/headers/README.md: -------------------------------------------------------------------------------- 1 | ## Pages Plugins 2 | 3 | # Headers 4 | 5 | This headers Plugin adds headers to all responses which occur below it in the execution chain. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm install --save @cloudflare/pages-plugin-headers 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```typescript 16 | // ./functions/api/_middleware.ts 17 | 18 | import headersPlugin from "@cloudflare/pages-plugin-headers"; 19 | 20 | export const onRequest: PagesFunction = headersPlugin({ 21 | "Access-Control-Allow-Origin": "*", 22 | }); 23 | ``` 24 | 25 | The Plugin takes [the same argument as the `new Headers()` constructor](https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers#parameters): 26 | 27 | - a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) instance, 28 | - an object of header names mapping to header values (i.e. `Record`), or 29 | - an array of header name, header value pairs (i.e. `[string, string][]`). 30 | -------------------------------------------------------------------------------- /packages/headers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-headers", 3 | "private": true, 4 | "bugs": { 5 | "url": "https://github.com/cloudflare/pages-plugins/issues" 6 | }, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 10 | "directory": "./packages/headers" 11 | }, 12 | "license": "MIT", 13 | "type": "module", 14 | "exports": { 15 | ".": { 16 | "types": "./index.d.ts", 17 | "default": "./dist/functions/index.js" 18 | } 19 | }, 20 | "main": "./dist/functions/index.js", 21 | "types": "./index.d.ts", 22 | "files": [ 23 | "./index.d.ts", 24 | "./dist/" 25 | ], 26 | "scripts": { 27 | "build": "npm run build:functions", 28 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap", 29 | "prepack": "npm run build" 30 | }, 31 | "volta": { 32 | "node": "20.11.0", 33 | "npm": "10.2.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/functions/admin/_middleware.ts: -------------------------------------------------------------------------------- 1 | import type { PluginData as CloudflareAccessPluginData } from "@cloudflare/pages-plugin-cloudflare-access"; 2 | import cloudflareAccessPlugin from "@cloudflare/pages-plugin-cloudflare-access"; 3 | import type { PluginData as SentryPluginData } from "@cloudflare/pages-plugin-sentry"; 4 | 5 | export const onRequest = [ 6 | cloudflareAccessPlugin({ 7 | domain: "https://test.cloudflareaccess.com", 8 | aud: "97e2aae120121f902df8bc99fc345913ab186d174f3079ea729236766b2e7c4a", 9 | }), 10 | (async ({ data, next }) => { 11 | const identity = await data.cloudflareAccess.JWT.getIdentity(); 12 | 13 | data.sentry.setUser({ 14 | id: 15 | data.cloudflareAccess.JWT.payload.sub || 16 | data.cloudflareAccess.JWT.payload.common_name, 17 | username: identity.name, 18 | ip_address: identity.ip, 19 | email: data.cloudflareAccess.JWT.payload.email, 20 | }); 21 | 22 | return next(); 23 | }) as PagesFunction< 24 | unknown, 25 | any, 26 | SentryPluginData & CloudflareAccessPluginData 27 | >, 28 | ]; 29 | -------------------------------------------------------------------------------- /packages/hcaptcha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-hcaptcha", 3 | "version": "1.0.4", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/hcaptcha/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/hcaptcha" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | } 20 | }, 21 | "main": "./dist/functions/index.js", 22 | "types": "./index.d.ts", 23 | "files": [ 24 | "./CHANGELOG.md", 25 | "./index.d.ts", 26 | "./dist/" 27 | ], 28 | "scripts": { 29 | "build": "npm run build:functions", 30 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap", 31 | "prepack": "npm run build" 32 | }, 33 | "volta": { 34 | "node": "20.11.0", 35 | "npm": "10.2.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/turnstile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-turnstile", 3 | "version": "1.0.2", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/turnstile/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/turnstile" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | } 20 | }, 21 | "main": "./dist/functions/index.js", 22 | "types": "./index.d.ts", 23 | "files": [ 24 | "./CHANGELOG.md", 25 | "./index.d.ts", 26 | "./dist/" 27 | ], 28 | "scripts": { 29 | "build": "npm run build:functions", 30 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap", 31 | "prepack": "npm run build" 32 | }, 33 | "volta": { 34 | "node": "20.11.0", 35 | "npm": "10.2.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cloudflare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/static-forms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-static-forms", 3 | "version": "1.0.3", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/static-forms/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/static-forms" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | } 20 | }, 21 | "main": "./dist/functions/index.js", 22 | "types": "./index.d.ts", 23 | "files": [ 24 | "./CHANGELOG.md", 25 | "./index.d.ts", 26 | "./dist/" 27 | ], 28 | "scripts": { 29 | "build": "npm run build:functions", 30 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap", 31 | "prepack": "npm run build" 32 | }, 33 | "volta": { 34 | "node": "20.11.0", 35 | "npm": "10.2.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/sentry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-sentry", 3 | "version": "1.1.4", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/sentry/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/sentry" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | } 20 | }, 21 | "main": "./dist/functions/index.js", 22 | "types": "./index.d.ts", 23 | "files": [ 24 | "./CHANGELOG.md", 25 | "./index.d.ts", 26 | "./dist/" 27 | ], 28 | "scripts": { 29 | "build": "npm run build:functions", 30 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap", 31 | "prepack": "npm run build" 32 | }, 33 | "devDependencies": { 34 | "toucan-js": "^3.0.0" 35 | }, 36 | "volta": { 37 | "node": "20.11.0", 38 | "npm": "10.2.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-graphql", 3 | "version": "1.0.4", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/graphql/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/graphql" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | } 20 | }, 21 | "main": "./dist/functions/index.js", 22 | "types": "./index.d.ts", 23 | "files": [ 24 | "./CHANGELOG.md", 25 | "./index.d.ts", 26 | "./dist/", 27 | "./static/" 28 | ], 29 | "scripts": { 30 | "build": "npm run build:functions", 31 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap", 32 | "prepack": "npm run build" 33 | }, 34 | "devDependencies": { 35 | "graphql": "^16.6.0" 36 | }, 37 | "volta": { 38 | "node": "20.11.0", 39 | "npm": "10.2.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/graphql/functions/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginArgs } from "@cloudflare/pages-plugin-graphql"; 2 | 3 | type GraphQLPagesPluginFunction< 4 | Env = unknown, 5 | Params extends string = any, 6 | Data extends Record = Record, 7 | > = PagesPluginFunction; 8 | 9 | const extractGraphQLQueryFromRequest = async (request: Request) => { 10 | if (/application\/graphql/i.test(request.headers.get("Content-Type"))) { 11 | return { source: await request.text() }; 12 | } 13 | 14 | const { query, variables, operationName } = await request.json(); 15 | 16 | return { 17 | source: query, 18 | variableValues: variables, 19 | operationName, 20 | }; 21 | }; 22 | 23 | export const onRequestPost: GraphQLPagesPluginFunction = async ({ 24 | request, 25 | pluginArgs, 26 | }) => { 27 | const { schema, graphql } = pluginArgs; 28 | 29 | const result = await graphql({ 30 | schema, 31 | ...(await extractGraphQLQueryFromRequest(request)), 32 | }); 33 | 34 | return new Response(JSON.stringify(result), { 35 | headers: { "Content-Type": "application/json" }, 36 | }); 37 | }; 38 | 39 | export { onRequest } from "assets:../static"; 40 | -------------------------------------------------------------------------------- /packages/honeycomb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-honeycomb", 3 | "version": "1.0.4", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/honeycomb/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/honeycomb" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | } 20 | }, 21 | "main": "./dist/functions/index.js", 22 | "types": "./index.d.ts", 23 | "files": [ 24 | "./CHANGELOG.md", 25 | "./index.d.ts", 26 | "./dist/" 27 | ], 28 | "scripts": { 29 | "build": "npm run build:functions", 30 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap", 31 | "prepack": "npm run build" 32 | }, 33 | "devDependencies": { 34 | "@cloudflare/workers-honeycomb-logger": "^2.3.3" 35 | }, 36 | "volta": { 37 | "node": "20.11.0", 38 | "npm": "10.2.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cloudflare-access/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Identity } from "@cloudflare/pages-plugin-cloudflare-access"; 2 | 3 | export const getIdentity = async ({ 4 | jwt, 5 | domain, 6 | }: { 7 | jwt: string; 8 | domain: string; 9 | }): Promise => { 10 | const identityURL = new URL("/cdn-cgi/access/get-identity", domain); 11 | const response = await fetch(identityURL.toString(), { 12 | headers: { Cookie: `CF_Authorization=${jwt}` }, 13 | }); 14 | if (response.ok) return await response.json(); 15 | }; 16 | 17 | export const generateLoginURL = ({ 18 | redirectURL: redirectURLInit, 19 | domain, 20 | aud, 21 | }: { 22 | redirectURL: string | URL; 23 | domain: string; 24 | aud: string; 25 | }): string => { 26 | const redirectURL = 27 | typeof redirectURLInit === "string" 28 | ? new URL(redirectURLInit) 29 | : redirectURLInit; 30 | const { hostname } = redirectURL; 31 | const loginPathname = `/cdn-cgi/access/login/${hostname}?`; 32 | const searchParams = new URLSearchParams({ 33 | kid: aud, 34 | redirect_url: redirectURL.pathname + redirectURL.search, 35 | }); 36 | return new URL(loginPathname + searchParams.toString(), domain).toString(); 37 | }; 38 | 39 | export const generateLogoutURL = ({ domain }: { domain: string }) => 40 | new URL(`/cdn-cgi/access/logout`, domain).toString(); 41 | -------------------------------------------------------------------------------- /packages/static-forms/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import { PluginArgs } from "@cloudflare/pages-plugin-static-forms"; 2 | 3 | type StaticFormPagesPluginFunction< 4 | Env = unknown, 5 | Params extends string = any, 6 | Data extends Record = Record, 7 | > = PagesPluginFunction; 8 | 9 | export const onRequestPost: StaticFormPagesPluginFunction = async ({ 10 | request, 11 | next, 12 | pluginArgs, 13 | }) => { 14 | let formData: FormData, name: string; 15 | try { 16 | formData = await request.formData(); 17 | name = formData.get("static-form-name").toString(); 18 | } catch {} 19 | 20 | if (name) { 21 | formData.delete("static-form-name"); 22 | return pluginArgs.respondWith({ formData, name }); 23 | } 24 | 25 | return next(); 26 | }; 27 | 28 | export const onRequestGet: StaticFormPagesPluginFunction = async ({ next }) => { 29 | const response = await next(); 30 | 31 | return new HTMLRewriter() 32 | .on("form[data-static-form-name]", { 33 | element(form) { 34 | const formName = form.getAttribute("data-static-form-name"); 35 | form.setAttribute("method", "POST"); 36 | form.removeAttribute("action"); 37 | form.append( 38 | ``, 39 | { html: true }, 40 | ); 41 | }, 42 | }) 43 | .transform(response); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/cloudflare-access/index.d.ts: -------------------------------------------------------------------------------- 1 | export type Identity = { 2 | id: string; 3 | name: string; 4 | email: string; 5 | groups: string[]; 6 | amr: string[]; 7 | idp: { id: string; type: string }; 8 | geo: { country: string }; 9 | user_uuid: string; 10 | account_id: string; 11 | ip: string; 12 | auth_status: string; 13 | common_name: string; 14 | service_token_id: string; 15 | service_token_status: boolean; 16 | is_warp: boolean; 17 | is_gateway: boolean; 18 | version: number; 19 | device_sessions: Record; 20 | iat: number; 21 | }; 22 | 23 | export type JWTPayload = { 24 | aud: string | string[]; 25 | common_name?: string; // Service token client ID 26 | country?: string; 27 | custom?: unknown; 28 | email?: string; 29 | exp: number; 30 | iat: number; 31 | nbf?: number; 32 | iss: string; // https://.cloudflareaccess.com 33 | type?: string; // Always just 'app'? 34 | identity_nonce?: string; 35 | sub: string; // Empty string for service tokens or user ID otherwise 36 | }; 37 | 38 | export type PluginArgs = { 39 | aud: string; 40 | domain: `https://${string}.cloudflareaccess.com`; 41 | }; 42 | 43 | export type PluginData = { 44 | cloudflareAccess: { 45 | JWT: { 46 | payload: JWTPayload; 47 | getIdentity: () => Promise; 48 | }; 49 | }; 50 | }; 51 | 52 | export default function (args: PluginArgs): PagesFunction; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pages Plugins 2 | 3 | ## Features 4 | 5 | - 🥞 **Completely composable** 6 | 7 | You can include multiple Plugins, Plugins can rely on other Plugins, and they all share the same loading interface. 8 | 9 | - ✍️ **Author a Plugin as a folder of Functions** 10 | 11 | The straight-forward syntax and intuitive file-based routing we've developed for Functions can be used to write Plugins. 12 | 13 | - 📥 **Simple loading mechanism for including Plugins in projects** 14 | 15 | Mount the Plugin wherever you want and optionally pass it data. 16 | 17 | - ⚡️ **Plugins can bring static assets** 18 | 19 | We hide static assets behind an inaccessible URL so they'll only be available in user-land where the Plugin exposes them. 20 | 21 | ## Usage 22 | 23 | Check out our [Developer Docs](https://developers.cloudflare.com/pages/platform/functions/plugins/) for an example of creating and mounting a Pages Plugin. 24 | 25 | ## Plugins 26 | 27 | Check out these examples: 28 | 29 | - [Cloudflare Access Pages Plugin](./packages/cloudflare-access) 30 | - [Google Chat Pages Plugin](./packages/google-chat) 31 | - [GraphQL Pages Plugin](./packages/graphql) 32 | - [hCaptcha Pages Plugin](./packages/hcaptcha) 33 | - [Headers Pages Plugin](./packages/headers) 34 | - [Honeycomb Pages Plugin](./packages/honeycomb) 35 | - [Sentry Pages Plugin](./packages/sentry) 36 | - [Static Forms Pages Plugin](./packages/static-forms) 37 | - [Stytch Pages Plugin](./packages/stytch) 38 | - [Turnstile Pages Plugin](./packages/turnstile) 39 | - [`@vercel/og` Pages Plugin](./packages/vercel-og) 40 | -------------------------------------------------------------------------------- /packages/cloudflare-access/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-cloudflare-access", 3 | "version": "1.0.5", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/cloudflare-access/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/cloudflare-access" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | }, 20 | "./api": { 21 | "types": "./dist/types/api/index.d.ts", 22 | "default": "./dist/src/api/index.js" 23 | } 24 | }, 25 | "main": "./dist/functions/index.js", 26 | "types": "./index.d.ts", 27 | "files": [ 28 | "./CHANGELOG.md", 29 | "./index.d.ts", 30 | "./dist/" 31 | ], 32 | "scripts": { 33 | "build": "npm run build:src && npm run build:functions", 34 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap --external=@cloudflare/pages-plugin-cloudflare-access/api", 35 | "prebuild:src": "npm run build:src:types", 36 | "build:src": "npx esbuild ./src/**/* --outdir=./dist/src --bundle --platform=neutral --format=esm --main-fields=module,browser,main --conditions=workerd --outbase=./src --sourcemap", 37 | "build:src:types": "npx tsc", 38 | "prepack": "npm run build" 39 | }, 40 | "volta": { 41 | "node": "20.11.0", 42 | "npm": "10.2.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/stytch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-stytch", 3 | "version": "1.0.3", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/stytch/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/stytch" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | }, 20 | "./api": { 21 | "types": "./dist/types/api/index.d.ts", 22 | "default": "./dist/src/api/index.js" 23 | } 24 | }, 25 | "main": "./dist/functions/index.js", 26 | "types": "./index.d.ts", 27 | "files": [ 28 | "./CHANGELOG.md", 29 | "./index.d.ts", 30 | "./dist/" 31 | ], 32 | "scripts": { 33 | "build": "npm run build:src && npm run build:functions", 34 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap --external=@cloudflare/pages-plugin-stytch/api", 35 | "prebuild:src": "npm run build:src:types", 36 | "build:src": "npx esbuild ./src/**/* --outdir=./dist/src --bundle --platform=neutral --format=esm --main-fields=module,browser,main --conditions=workerd --outbase=./src --sourcemap", 37 | "build:src:types": "npx tsc", 38 | "prepack": "npm run build" 39 | }, 40 | "devDependencies": { 41 | "cookie": "^0.5.0" 42 | }, 43 | "volta": { 44 | "node": "20.11.0", 45 | "npm": "10.2.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/google-chat/functions/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginArgs } from "@cloudflare/pages-plugin-google-chat"; 2 | import { KJUR } from "jsrsasign"; 3 | 4 | type GoogleChatPagesPluginFunction< 5 | Env = unknown, 6 | Params extends string = any, 7 | Data extends Record = Record, 8 | > = PagesPluginFunction; 9 | 10 | const extractJWTFromRequest = (request: Request) => { 11 | return request.headers.get("Authorization").split("Bearer ")[1]; 12 | }; 13 | 14 | const isAuthorized = async (request: Request) => { 15 | const jwt = extractJWTFromRequest(request); 16 | 17 | const { kid } = KJUR.jws.JWS.parse(jwt) 18 | .headerObj as KJUR.jws.JWS.JWSResult["headerObj"] & { kid: string }; 19 | 20 | const keysResponse = await fetch( 21 | "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com", 22 | ); 23 | const keys = (await keysResponse.json()) as Record; 24 | const cert = Object.entries(keys).find(([id, cert]) => id === kid)[1]; 25 | 26 | return KJUR.jws.JWS.verifyJWT(jwt, cert, { alg: ["RS256"] }); 27 | }; 28 | 29 | export const onRequestPost: GoogleChatPagesPluginFunction = async ({ 30 | request, 31 | pluginArgs, 32 | }) => { 33 | let authorized = false; 34 | try { 35 | authorized = await isAuthorized(request); 36 | } catch {} 37 | 38 | if (!authorized) { 39 | return new Response(null, { status: 403 }); 40 | } 41 | 42 | const message = await pluginArgs(await request.json()); 43 | 44 | if (message !== undefined) { 45 | return new Response(JSON.stringify(message), { 46 | headers: { "Content-Type": "application/json" }, 47 | }); 48 | } 49 | 50 | return new Response(null); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/google-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-google-chat", 3 | "version": "1.0.4", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/google-chat/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/google-chat" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | }, 20 | "./api": { 21 | "types": "./dist/types/api/index.d.ts", 22 | "default": "./dist/src/api/index.js" 23 | } 24 | }, 25 | "main": "./dist/functions/index.js", 26 | "types": "./index.d.ts", 27 | "files": [ 28 | "./CHANGELOG.md", 29 | "./index.d.ts", 30 | "./dist/" 31 | ], 32 | "scripts": { 33 | "build": "npm run build:src && npm run build:functions", 34 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap --external=@cloudflare/pages-plugin-google-chat/api", 35 | "prebuild:src": "npm run build:src:types", 36 | "build:src": "npx esbuild ./src/**/* --outdir=./dist/src --bundle --platform=neutral --format=esm --main-fields=module,browser,main --conditions=workerd --outbase=./src --sourcemap", 37 | "build:src:types": "npx tsc", 38 | "prepack": "npm run build" 39 | }, 40 | "devDependencies": { 41 | "@googleapis/chat": "^9.0.0", 42 | "@types/jsrsasign": "^10.5.4", 43 | "jsrsasign": "^10.6.1" 44 | }, 45 | "volta": { 46 | "node": "20.11.0", 47 | "npm": "10.2.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/stytch/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "cookie"; 2 | import type { PluginArgs, PluginData } from "@cloudflare/pages-plugin-stytch"; 3 | 4 | type StytchPagesPluginFunction< 5 | Env = unknown, 6 | Params extends string = any, 7 | Data extends Record = Record, 8 | > = PagesPluginFunction; 9 | 10 | type Payload = ( 11 | | { 12 | session_token: string; 13 | } 14 | | { session_jwt: string } 15 | ) & { session_duration_minutes?: number }; 16 | 17 | export const onRequest: StytchPagesPluginFunction = async ({ 18 | request, 19 | pluginArgs, 20 | data, 21 | next, 22 | }) => { 23 | const url = `${pluginArgs.env}sessions/authenticate`; 24 | 25 | const cookies = parse(request.headers.get("Cookie")); 26 | 27 | let payload: Payload = { 28 | session_token: cookies.session_token, 29 | }; 30 | 31 | if (pluginArgs.session_token) { 32 | payload = { session_token: pluginArgs.session_token }; 33 | } else if (pluginArgs.session_jwt) { 34 | payload = { session_jwt: pluginArgs.session_jwt }; 35 | } 36 | 37 | if (pluginArgs.session_duration_minutes) { 38 | payload.session_duration_minutes = pluginArgs.session_duration_minutes; 39 | } 40 | 41 | const response = await fetch(url, { 42 | method: "POST", 43 | headers: { 44 | "Content-Type": "application/json", 45 | Authorization: `Basic ${btoa( 46 | `${pluginArgs.project_id}:${pluginArgs.secret}`, 47 | )}`, 48 | }, 49 | body: JSON.stringify(payload), 50 | }); 51 | 52 | if (response.ok) { 53 | data.stytch = { 54 | session: await response.json(), 55 | }; 56 | 57 | return next(); 58 | } 59 | 60 | return new Response(null, { status: 403 }); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/vercel-og/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugin-vercel-og", 3 | "version": "0.1.2", 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/vercel-og/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git", 11 | "directory": "./packages/vercel-og" 12 | }, 13 | "license": "MIT", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./index.d.ts", 18 | "default": "./dist/functions/index.js" 19 | }, 20 | "./api": { 21 | "types": "./dist/types/api/index.d.ts", 22 | "default": "./dist/src/api/index.js" 23 | } 24 | }, 25 | "main": "./dist/functions/index.js", 26 | "types": "./index.d.ts", 27 | "files": [ 28 | "./CHANGELOG.md", 29 | "./index.d.ts", 30 | "./dist/" 31 | ], 32 | "scripts": { 33 | "build": "npm run build:src && npm run build:functions", 34 | "build:functions": "npx wrangler pages functions build --plugin --outdir=./dist/functions --sourcemap --external=@cloudflare/pages-plugin-vercel-og/api", 35 | "prebuild:src": "npm run build:src:types", 36 | "build:src": "npx esbuild ./src/**/* --outdir=./dist/src --bundle --platform=neutral --format=esm --main-fields=module,browser,main --conditions=workerd --outbase=./src --sourcemap --external:*.bin --external:*.wasm && cp ../../node_modules/@vercel/og/dist/resvg.wasm ./dist/src/api/resvg.wasm && cp ../../node_modules/@vercel/og/dist/yoga.wasm ./dist/src/api/yoga.wasm && cp ../../node_modules/@vercel/og/dist/noto-sans-v27-latin-regular.ttf.bin ./dist/src/api/noto-sans-v27-latin-regular.ttf.bin", 37 | "build:src:types": "npx tsc", 38 | "prepack": "npm run build" 39 | }, 40 | "devDependencies": { 41 | "@vercel/og": "0.4.1" 42 | }, 43 | "volta": { 44 | "node": "20.11.0", 45 | "npm": "10.2.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/graphql/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | GraphQL Playground 10 | 14 | 18 | 19 | 20 | 21 | 22 |
23 | 56 | 60 |
61 | Loading 62 | GraphQL Playground 63 |
64 |
65 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /packages/hcaptcha/functions/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginArgs, PluginData } from "@cloudflare/pages-plugin-hcaptcha"; 2 | 3 | type hCaptchaPagesPluginFunction< 4 | Env = unknown, 5 | Params extends string = any, 6 | Data extends Record = Record, 7 | > = PagesPluginFunction; 8 | 9 | const errorStringMap = { 10 | "missing-input-secret": "Your secret key is missing.", 11 | 12 | "invalid-input-secret": "Your secret key is invalid or malformed.", 13 | 14 | "missing-input-response": 15 | "The response parameter (verification token) is missing.", 16 | 17 | "invalid-input-response": 18 | "The response parameter (verification token) is invalid or malformed.", 19 | 20 | "bad-request": "The request is invalid or malformed.", 21 | 22 | "invalid-or-already-seen-response": 23 | "The response parameter has already been checked, or has another issue.", 24 | 25 | "not-using-dummy-passcode": 26 | "You have used a testing sitekey but have not used its matching secret.", 27 | 28 | "sitekey-secret-mismatch": 29 | "The sitekey is not registered with the provided secret.", 30 | }; 31 | 32 | export const onRequest: hCaptchaPagesPluginFunction = async (context) => { 33 | const { 34 | secret, 35 | response: hCaptchaResponse = (await context.request.clone().formData()) 36 | .get("h-captcha-response") 37 | .toString(), 38 | remoteip = context.request.headers.get("CF-Connecting-IP"), 39 | sitekey, 40 | onError, 41 | } = context.pluginArgs; 42 | 43 | const formData = new FormData(); 44 | formData.set("secret", secret); 45 | formData.set("response", hCaptchaResponse); 46 | if (remoteip) formData.set("remoteip", remoteip); 47 | if (sitekey) formData.set("sitekey", sitekey); 48 | 49 | const response = await fetch("https://hcaptcha.com/siteverify", { 50 | method: "POST", 51 | body: formData, 52 | }); 53 | context.data.hCaptcha = await response.json(); 54 | 55 | if (!context.data.hCaptcha.success) { 56 | if (onError) { 57 | return onError(context); 58 | } else { 59 | const descriptions = context.data.hCaptcha["error-codes"].map( 60 | (errorCode) => 61 | errorStringMap[errorCode] || "An unexpected error has occurred.", 62 | ); 63 | 64 | return new Response( 65 | `Could not confirm your humanity. 66 | 67 | ${descriptions.join("\n")}. 68 | 69 | Please try again.`, 70 | { status: 400 }, 71 | ); 72 | } 73 | } 74 | 75 | return context.next(); 76 | }; 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/pages-plugins-root", 3 | "private": true, 4 | "homepage": "https://developers.cloudflare.com/pages/platform/functions/plugins/", 5 | "bugs": { 6 | "url": "https://github.com/cloudflare/pages-plugins/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/cloudflare/pages-plugins.git" 11 | }, 12 | "license": "MIT", 13 | "type": "module", 14 | "workspaces": [ 15 | "example", 16 | "packages/*" 17 | ], 18 | "scripts": { 19 | "build": "npm run build --workspace=./packages/static-forms && npm run build --workspaces --if-present", 20 | "prebuild:prod": "npm run build", 21 | "build:prod": "npm run build:prod --workspace=./example", 22 | "postinstall": "patch-package", 23 | "publish": "npm run build && npx changeset publish", 24 | "prestart": "npm run build", 25 | "start": "npm run start --workspace=./example", 26 | "pretest": "npm run build", 27 | "test": "npx vitest" 28 | }, 29 | "prettier": { 30 | "plugins": [ 31 | "prettier-plugin-organize-imports", 32 | "prettier-plugin-packagejson", 33 | "prettier-plugin-sort-json" 34 | ] 35 | }, 36 | "eslintConfig": { 37 | "parser": "@typescript-eslint/parser", 38 | "plugins": [ 39 | "@typescript-eslint", 40 | "eslint-plugin-isaacscript", 41 | "eslint-plugin-unicorn" 42 | ], 43 | "extends": [ 44 | "eslint:recommended", 45 | "plugin:@typescript-eslint/eslint-recommended", 46 | "plugin:@typescript-eslint/recommended" 47 | ], 48 | "rules": { 49 | "isaacscript/no-template-curly-in-string-fix": "error", 50 | "unicorn/expiring-todo-comments": "error" 51 | }, 52 | "root": true 53 | }, 54 | "devDependencies": { 55 | "@changesets/cli": "^2.27.1", 56 | "@cloudflare/workers-types": "^4.20240222.0", 57 | "@types/react": "^18.2.61", 58 | "@typescript-eslint/eslint-plugin": "7.1.0", 59 | "@typescript-eslint/parser": "7.1.0", 60 | "esbuild": "^0.20.1", 61 | "eslint": "8.57.0", 62 | "eslint-plugin-isaacscript": "3.12.2", 63 | "eslint-plugin-unicorn": "51.0.1", 64 | "patch-package": "^8.0.0", 65 | "prettier": "3.2.5", 66 | "prettier-plugin-organize-imports": "3.2.4", 67 | "prettier-plugin-packagejson": "2.4.12", 68 | "prettier-plugin-sort-json": "3.1.0", 69 | "typescript": "5.3.3", 70 | "vitest": "1.3.1", 71 | "wrangler": "3.78.8" 72 | }, 73 | "engines": { 74 | "node": "20.11.0", 75 | "npm": "10.2.4" 76 | }, 77 | "volta": { 78 | "node": "20.11.0", 79 | "npm": "10.2.4" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /patches/@vercel+og+0.4.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@vercel/og/dist/index.edge.d.ts b/node_modules/@vercel/og/dist/index.edge.d.ts 2 | index eab3ed7..3a5f9ad 100644 3 | --- a/node_modules/@vercel/og/dist/index.edge.d.ts 4 | +++ b/node_modules/@vercel/og/dist/index.edge.d.ts 5 | @@ -1,5 +1,5 @@ 6 | import type { ReactElement } from 'react'; 7 | import type { ImageResponseOptions } from './types'; 8 | -export declare class ImageResponse { 9 | +export declare class ImageResponse extends Response { 10 | constructor(element: ReactElement, options?: ImageResponseOptions); 11 | } 12 | diff --git a/node_modules/@vercel/og/dist/index.edge.js b/node_modules/@vercel/og/dist/index.edge.js 13 | index 33895c5..5862810 100644 14 | --- a/node_modules/@vercel/og/dist/index.edge.js 15 | +++ b/node_modules/@vercel/og/dist/index.edge.js 16 | @@ -6,11 +6,11 @@ import { 17 | import satori, { init as initSatori } from "satori/wasm"; 18 | import initYoga from "yoga-wasm-web"; 19 | import * as resvg from "@resvg/resvg-wasm"; 20 | -import resvg_wasm from "./resvg.wasm?module"; 21 | -import yoga_wasm from "./yoga.wasm?module"; 22 | +import resvg_wasm from "./resvg.wasm"; 23 | +import yoga_wasm from "./yoga.wasm"; 24 | +import fallbackFont from './noto-sans-v27-latin-regular.ttf.bin'; 25 | var initializedResvg = resvg.initWasm(resvg_wasm); 26 | var initializedYoga = initYoga(yoga_wasm).then((yoga) => initSatori(yoga)); 27 | -var fallbackFont = fetch(new URL("./noto-sans-v27-latin-regular.ttf", import.meta.url)).then((res) => res.arrayBuffer()); 28 | var _a, _b; 29 | var isDev = ((_b = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env) == null ? void 0 : _b.NODE_ENV) === "development"; 30 | var ImageResponse = class { 31 | diff --git a/node_modules/@vercel/og/dist/index.node.d.ts b/node_modules/@vercel/og/dist/index.node.d.ts 32 | index 66edc70..22b42ba 100644 33 | --- a/node_modules/@vercel/og/dist/index.node.d.ts 34 | +++ b/node_modules/@vercel/og/dist/index.node.d.ts 35 | @@ -2,7 +2,7 @@ 36 | import type { ReactElement } from 'react'; 37 | import type { ImageResponseNodeOptions, ImageResponseOptions } from './types'; 38 | import { Readable } from 'stream'; 39 | -export declare class ImageResponse { 40 | +export declare class ImageResponse extends Response { 41 | constructor(element: ReactElement, options?: ImageResponseOptions); 42 | } 43 | /** 44 | diff --git a/node_modules/@vercel/og/dist/noto-sans-v27-latin-regular.ttf b/node_modules/@vercel/og/dist/noto-sans-v27-latin-regular.ttf.bin 45 | similarity index 100% 46 | rename from node_modules/@vercel/og/dist/noto-sans-v27-latin-regular.ttf 47 | rename to node_modules/@vercel/og/dist/noto-sans-v27-latin-regular.ttf.bin 48 | -------------------------------------------------------------------------------- /packages/turnstile/functions/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PluginArgs, 3 | PluginData, 4 | } from "@cloudflare/pages-plugin-turnstile"; 5 | 6 | type turnstilePagesPluginFunction< 7 | Env = unknown, 8 | Params extends string = any, 9 | Data extends Record = Record, 10 | > = PagesPluginFunction; 11 | 12 | const errorStringMap = { 13 | "missing-input-secret": "The secret parameter was not passed.", 14 | 15 | "invalid-input-secret": "The secret parameter was invalid or did not exist.", 16 | 17 | "missing-input-response": "The response parameter was not passed.", 18 | 19 | "invalid-input-response": "The response parameter is invalid or has expired.", 20 | 21 | "invalid-widget-id": 22 | "The widget ID extracted from the parsed site secret key was invalid or did not exist.", 23 | 24 | "invalid-parsed-secret": 25 | "The secret extracted from the parsed site secret key was invalid.", 26 | 27 | "bad-request": "The request was rejected because it was malformed.", 28 | 29 | "timeout-or-duplicate": 30 | "The response parameter has already been validated before.", 31 | 32 | "invalid-idempotency-key": "The provided idempotendy key was malformed.", 33 | 34 | "internal-error": 35 | "An internal error happened while validating the response. The request can be retried.", 36 | }; 37 | 38 | const SITEVERIFY_URL = 39 | "https://challenges.cloudflare.com/turnstile/v0/siteverify"; 40 | 41 | export const onRequest: turnstilePagesPluginFunction = async (context) => { 42 | const { 43 | secret, 44 | response: turnstileResponse = (await context.request.clone().formData()) 45 | .get("cf-turnstile-response") 46 | .toString(), 47 | remoteip = context.request.headers.get("CF-Connecting-IP"), 48 | idempotency_key: idempotencyKey, 49 | onError, 50 | } = context.pluginArgs; 51 | 52 | const formData = new FormData(); 53 | formData.set("secret", secret); 54 | formData.set("response", turnstileResponse); 55 | if (remoteip) formData.set("remoteip", remoteip); 56 | if (idempotencyKey) formData.set("idempotency_key", idempotencyKey); 57 | 58 | const response = await fetch(SITEVERIFY_URL, { 59 | method: "POST", 60 | body: formData, 61 | }); 62 | context.data.turnstile = await response.json(); 63 | 64 | if (!context.data.turnstile.success) { 65 | if (onError) { 66 | return onError(context); 67 | } else { 68 | const descriptions = context.data.turnstile["error-codes"].map( 69 | (errorCode) => 70 | errorStringMap[errorCode] || "An unexpected error has occurred.", 71 | ); 72 | 73 | return new Response( 74 | `Could not confirm your humanity. 75 | 76 | ${descriptions.join("\n")}. 77 | 78 | Please try again.`, 79 | { status: 400 }, 80 | ); 81 | } 82 | } 83 | 84 | return context.next(); 85 | }; 86 | -------------------------------------------------------------------------------- /packages/vercel-og/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import type { PluginArgs } from "@cloudflare/pages-plugin-vercel-og"; 2 | import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api"; 3 | 4 | type vercelOGPagesPluginFunction< 5 | Env = unknown, 6 | Params extends string = any, 7 | Data extends Record = Record, 8 | > = PagesPluginFunction; 9 | 10 | function escapeRegex(string: string) { 11 | return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&"); 12 | } 13 | 14 | const responseIsValidHTML = (response: Response) => 15 | response.ok && response.headers.get("Content-Type")?.includes("text/html"); 16 | 17 | export const onRequestGet: vercelOGPagesPluginFunction = async ({ 18 | request, 19 | pluginArgs, 20 | next, 21 | }) => { 22 | const { 23 | imagePathSuffix, 24 | extractors: htmlRewriterHandlers, 25 | component: Component, 26 | options, 27 | onError = () => new Response(null, { status: 404 }), 28 | autoInject, 29 | } = pluginArgs; 30 | const url = new URL(request.url); 31 | 32 | const match = url.pathname.match(`(.*)${escapeRegex(imagePathSuffix)}`); 33 | if (match) { 34 | const props = { 35 | pathname: match[1], 36 | }; 37 | 38 | if (htmlRewriterHandlers) { 39 | const response = await next(match[1]); 40 | 41 | if (!responseIsValidHTML(response)) { 42 | return onError(); 43 | } 44 | 45 | let htmlRewriter = new HTMLRewriter(); 46 | 47 | if (htmlRewriterHandlers.onDocument) { 48 | htmlRewriter = htmlRewriter.onDocument( 49 | htmlRewriterHandlers.onDocument(props), 50 | ); 51 | } 52 | 53 | if (htmlRewriterHandlers.on) { 54 | for (const [selector, handlerGenerators] of Object.entries( 55 | htmlRewriterHandlers.on, 56 | )) { 57 | htmlRewriter = htmlRewriter.on(selector, handlerGenerators(props)); 58 | } 59 | } 60 | 61 | await htmlRewriter.transform(response).arrayBuffer(); 62 | } 63 | 64 | return new ImageResponse({ type: Component, props, key: null }, options); 65 | } else if (autoInject) { 66 | const response = await next(); 67 | 68 | if (!responseIsValidHTML(response)) { 69 | return response; 70 | } 71 | 72 | return new HTMLRewriter() 73 | .on("head", { 74 | element(element) { 75 | if (autoInject.openGraph) { 76 | element.append( 77 | ``, 84 | { html: true }, 85 | ); 86 | } 87 | }, 88 | }) 89 | .transform(response); 90 | } 91 | 92 | return next(); 93 | }; 94 | -------------------------------------------------------------------------------- /packages/honeycomb/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PluginArgs, 3 | PluginData, 4 | } from "@cloudflare/pages-plugin-honeycomb"; 5 | import { RequestTracer, resolve } from "@cloudflare/workers-honeycomb-logger"; 6 | 7 | type HoneycombPagesPluginFunction< 8 | Env = unknown, 9 | Params extends string = any, 10 | Data extends Record = Record, 11 | > = PagesPluginFunction; 12 | 13 | type OutgoingFetcher = { fetch: typeof fetch }; 14 | 15 | function proxyFetch( 16 | obj: OutgoingFetcher, 17 | tracer: RequestTracer, 18 | name: string, 19 | ): OutgoingFetcher { 20 | obj.fetch = new Proxy(obj.fetch, { 21 | apply: (target, thisArg, argArray) => { 22 | const info = argArray[0] as Request; 23 | const input = argArray[1] as RequestInit; 24 | const request = new Request(info, input); 25 | const childSpan = tracer.startChildSpan(request.url, name); 26 | 27 | const traceHeaders = childSpan.eventMeta.trace.getHeaders(); 28 | request.headers.set("traceparent", traceHeaders.traceparent); 29 | if (traceHeaders.tracestate) 30 | request.headers.set("tracestate", traceHeaders.tracestate); 31 | 32 | childSpan.addRequest(request); 33 | const promise = Reflect.apply(target, thisArg, [ 34 | request, 35 | ]) as Promise; 36 | promise 37 | .then((response) => { 38 | childSpan.addResponse(response); 39 | childSpan.finish(); 40 | }) 41 | .catch((reason) => { 42 | childSpan.addData({ exception: reason }); 43 | childSpan.finish(); 44 | }); 45 | return promise; 46 | }, 47 | }); 48 | return obj; 49 | } 50 | 51 | function proxyGet(fn: Function, tracer: RequestTracer, do_name: string) { 52 | return new Proxy(fn, { 53 | apply: (target, thisArg, argArray) => { 54 | const obj = Reflect.apply(target, thisArg, argArray); 55 | return proxyFetch(obj, tracer, do_name); 56 | }, 57 | }); 58 | } 59 | 60 | function proxyNS( 61 | dns: DurableObjectNamespace, 62 | tracer: RequestTracer, 63 | do_name: string, 64 | ) { 65 | return new Proxy(dns, { 66 | get: (target, prop, receiver) => { 67 | const value = Reflect.get(target, prop, receiver); 68 | if (prop === "get") { 69 | return proxyGet(value, tracer, do_name).bind(dns); 70 | } else { 71 | return value ? value.bind(dns) : undefined; 72 | } 73 | }, 74 | }); 75 | } 76 | 77 | function proxyEnv(env: any, tracer: RequestTracer): any { 78 | return new Proxy(env, { 79 | get: (target, prop, receiver) => { 80 | const value = Reflect.get(target, prop, receiver); 81 | if (value && value.idFromName) { 82 | return proxyNS(value, tracer, prop.toString()); 83 | } else if (value && value.fetch) { 84 | return proxyFetch(value, tracer, prop.toString()); 85 | } else { 86 | return value; 87 | } 88 | }, 89 | }); 90 | } 91 | 92 | export const onRequest: HoneycombPagesPluginFunction = async ({ 93 | request, 94 | env, 95 | next, 96 | pluginArgs, 97 | data, 98 | waitUntil, 99 | }) => { 100 | const config = resolve(pluginArgs); 101 | data.honeycomb = { tracer: new RequestTracer(request, config) }; 102 | 103 | proxyEnv(env, data.honeycomb.tracer); 104 | 105 | try { 106 | const response = await next(); 107 | data.honeycomb.tracer.finishResponse(response); 108 | waitUntil(data.honeycomb.tracer.sendEvents()); 109 | return response; 110 | } catch (thrown) { 111 | data.honeycomb.tracer.finishResponse(undefined, thrown); 112 | waitUntil(data.honeycomb.tracer.sendEvents()); 113 | throw thrown; 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode,macos 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Node ### 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | lerna-debug.log* 42 | .pnpm-debug.log* 43 | 44 | # Diagnostic reports (https://nodejs.org/api/report.html) 45 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 46 | 47 | # Runtime data 48 | pids 49 | *.pid 50 | *.seed 51 | *.pid.lock 52 | 53 | # Directory for instrumented libs generated by jscoverage/JSCover 54 | lib-cov 55 | 56 | # Coverage directory used by tools like istanbul 57 | coverage 58 | *.lcov 59 | 60 | # nyc test coverage 61 | .nyc_output 62 | 63 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 64 | .grunt 65 | 66 | # Bower dependency directory (https://bower.io/) 67 | bower_components 68 | 69 | # node-waf configuration 70 | .lock-wscript 71 | 72 | # Compiled binary addons (https://nodejs.org/api/addons.html) 73 | build/Release 74 | 75 | # Dependency directories 76 | node_modules/ 77 | jspm_packages/ 78 | 79 | # Snowpack dependency directory (https://snowpack.dev/) 80 | web_modules/ 81 | 82 | # TypeScript cache 83 | *.tsbuildinfo 84 | 85 | # Optional npm cache directory 86 | .npm 87 | 88 | # Optional eslint cache 89 | .eslintcache 90 | 91 | # Optional stylelint cache 92 | .stylelintcache 93 | 94 | # Microbundle cache 95 | .rpt2_cache/ 96 | .rts2_cache_cjs/ 97 | .rts2_cache_es/ 98 | .rts2_cache_umd/ 99 | 100 | # Optional REPL history 101 | .node_repl_history 102 | 103 | # Output of 'npm pack' 104 | *.tgz 105 | 106 | # Yarn Integrity file 107 | .yarn-integrity 108 | 109 | # dotenv environment variable files 110 | .env 111 | .env.development.local 112 | .env.test.local 113 | .env.production.local 114 | .env.local 115 | 116 | # parcel-bundler cache (https://parceljs.org/) 117 | .cache 118 | .parcel-cache 119 | 120 | # Next.js build output 121 | .next 122 | out 123 | 124 | # Nuxt.js build / generate output 125 | .nuxt 126 | dist 127 | 128 | # Gatsby files 129 | .cache/ 130 | # Comment in the public line in if your project uses Gatsby and not Next.js 131 | # https://nextjs.org/blog/next-9-1#public-directory-support 132 | # public 133 | 134 | # vuepress build output 135 | .vuepress/dist 136 | 137 | # vuepress v2.x temp and cache directory 138 | .temp 139 | 140 | # Docusaurus cache and generated files 141 | .docusaurus 142 | 143 | # Serverless directories 144 | .serverless/ 145 | 146 | # FuseBox cache 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | .dynamodb/ 151 | 152 | # TernJS port file 153 | .tern-port 154 | 155 | # Stores VSCode versions used for testing VSCode extensions 156 | .vscode-test 157 | 158 | # yarn v2 159 | .yarn/cache 160 | .yarn/unplugged 161 | .yarn/build-state.yml 162 | .yarn/install-state.gz 163 | .pnp.* 164 | 165 | ### Node Patch ### 166 | # Serverless Webpack directories 167 | .webpack/ 168 | 169 | # Optional stylelint cache 170 | 171 | # SvelteKit build / generate output 172 | .svelte-kit 173 | 174 | ### VisualStudioCode ### 175 | .vscode/* 176 | !.vscode/settings.json 177 | !.vscode/tasks.json 178 | !.vscode/launch.json 179 | !.vscode/extensions.json 180 | !.vscode/*.code-snippets 181 | 182 | # Local History for Visual Studio Code 183 | .history/ 184 | 185 | # Built Visual Studio Code Extensions 186 | *.vsix 187 | 188 | ### VisualStudioCode Patch ### 189 | # Ignore all local history of files 190 | .history 191 | .ionide 192 | 193 | # Support for Project snippet scope 194 | 195 | # End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,macos 196 | 197 | ### FUNCTIONS ### 198 | _worker.js 199 | cdn-cgi/ 200 | 201 | ### WRANGLER ### 202 | !/vendor/wrangler*.tgz -------------------------------------------------------------------------------- /packages/cloudflare-access/functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import type { PluginArgs } from "@cloudflare/pages-plugin-cloudflare-access"; 2 | import { 3 | generateLoginURL, 4 | getIdentity, 5 | } from "@cloudflare/pages-plugin-cloudflare-access/api"; 6 | 7 | type CloudflareAccessPagesPluginFunction< 8 | Env = unknown, 9 | Params extends string = any, 10 | Data extends Record = Record, 11 | > = PagesPluginFunction; 12 | 13 | const extractJWTFromRequest = (request: Request) => 14 | request.headers.get("Cf-Access-Jwt-Assertion"); 15 | 16 | // Adapted slightly from https://github.com/cloudflare/workers-access-external-auth-example 17 | const base64URLDecode = (s: string) => { 18 | s = s.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, ""); 19 | return new Uint8Array( 20 | Array.from(atob(s)).map((c: string) => c.charCodeAt(0)), 21 | ); 22 | }; 23 | 24 | const asciiToUint8Array = (s: string) => { 25 | const chars = []; 26 | for (let i = 0; i < s.length; ++i) { 27 | chars.push(s.charCodeAt(i)); 28 | } 29 | return new Uint8Array(chars); 30 | }; 31 | 32 | const generateValidator = 33 | ({ domain, aud }: { domain: string; aud: string }) => 34 | async ( 35 | request: Request, 36 | ): Promise<{ 37 | jwt: string; 38 | payload: object; 39 | }> => { 40 | const jwt = extractJWTFromRequest(request); 41 | 42 | const parts = jwt.split("."); 43 | if (parts.length !== 3) { 44 | throw new Error("JWT does not have three parts."); 45 | } 46 | const [header, payload, signature] = parts; 47 | 48 | const textDecoder = new TextDecoder("utf-8"); 49 | const { kid, alg } = JSON.parse( 50 | textDecoder.decode(base64URLDecode(header)), 51 | ); 52 | if (alg !== "RS256") { 53 | throw new Error("Unknown JWT type or algorithm."); 54 | } 55 | 56 | const certsURL = new URL("/cdn-cgi/access/certs", domain); 57 | const certsResponse = await fetch(certsURL.toString()); 58 | const { keys } = (await certsResponse.json()) as { 59 | keys: ({ 60 | kid: string; 61 | } & JsonWebKey)[]; 62 | public_cert: { kid: string; cert: string }; 63 | public_certs: { kid: string; cert: string }[]; 64 | }; 65 | if (!keys) { 66 | throw new Error("Could not fetch signing keys."); 67 | } 68 | const jwk = keys.find((key) => key.kid === kid); 69 | if (!jwk) { 70 | throw new Error("Could not find matching signing key."); 71 | } 72 | if (jwk.kty !== "RSA" || jwk.alg !== "RS256") { 73 | throw new Error("Unknown key type of algorithm."); 74 | } 75 | 76 | const key = await crypto.subtle.importKey( 77 | "jwk", 78 | jwk, 79 | { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, 80 | false, 81 | ["verify"], 82 | ); 83 | 84 | const unroundedSecondsSinceEpoch = Date.now() / 1000; 85 | 86 | const payloadObj = JSON.parse(textDecoder.decode(base64URLDecode(payload))); 87 | 88 | if (payloadObj.iss && payloadObj.iss !== certsURL.origin) { 89 | throw new Error("JWT issuer is incorrect."); 90 | } 91 | if (payloadObj.aud && !payloadObj.aud.includes(aud)) { 92 | throw new Error("JWT audience is incorrect."); 93 | } 94 | if ( 95 | payloadObj.exp && 96 | Math.floor(unroundedSecondsSinceEpoch) >= payloadObj.exp 97 | ) { 98 | throw new Error("JWT has expired."); 99 | } 100 | if ( 101 | payloadObj.nbf && 102 | Math.ceil(unroundedSecondsSinceEpoch) < payloadObj.nbf 103 | ) { 104 | throw new Error("JWT is not yet valid."); 105 | } 106 | 107 | const verified = await crypto.subtle.verify( 108 | "RSASSA-PKCS1-v1_5", 109 | key, 110 | base64URLDecode(signature), 111 | asciiToUint8Array(`${header}.${payload}`), 112 | ); 113 | if (!verified) { 114 | throw new Error("Could not verify JWT."); 115 | } 116 | 117 | return { jwt, payload: payloadObj }; 118 | }; 119 | 120 | export const onRequest: CloudflareAccessPagesPluginFunction = async ({ 121 | request, 122 | pluginArgs: { domain, aud }, 123 | data, 124 | next, 125 | }) => { 126 | try { 127 | const validator = generateValidator({ domain, aud }); 128 | 129 | const { jwt, payload } = await validator(request); 130 | 131 | data.cloudflareAccess = { 132 | JWT: { 133 | payload, 134 | getIdentity: () => getIdentity({ jwt, domain }), 135 | }, 136 | }; 137 | 138 | return next(); 139 | } catch {} 140 | 141 | return new Response(null, { 142 | status: 302, 143 | headers: { 144 | Location: generateLoginURL({ redirectURL: request.url, domain, aud }), 145 | }, 146 | }); 147 | }; 148 | -------------------------------------------------------------------------------- /packages/google-chat/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { chat_v1 } from "@googleapis/chat"; 2 | import { KJUR } from "jsrsasign"; 3 | 4 | const ONE_MINUTE = 60; 5 | 6 | export class GoogleChatAPI { 7 | private emailAddress: string; 8 | private privateKey: string; 9 | 10 | private tokenExpiration: number; 11 | 12 | constructor({ 13 | credentials: { client_email, private_key }, 14 | tokenExpiration = ONE_MINUTE * 15, 15 | }: { 16 | credentials: { client_email: string; private_key: string }; 17 | tokenExpiration?: number; 18 | }) { 19 | this.emailAddress = client_email; 20 | this.privateKey = private_key; 21 | 22 | this.tokenExpiration = tokenExpiration; 23 | } 24 | 25 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 26 | // @ts-ignore 27 | private _token: string; 28 | 29 | get token(): Promise { 30 | return (async () => { 31 | if (this._token) return this._token; 32 | 33 | const oHeader = { alg: "RS256", typ: "JWT" }; 34 | const tNow = KJUR.jws.IntDate.get("now"); 35 | const tEnd = tNow + this.tokenExpiration; 36 | const oPayload = { 37 | iss: this.emailAddress, 38 | scope: "https://www.googleapis.com/auth/chat.bot", 39 | aud: "https://oauth2.googleapis.com/token", 40 | iat: tNow, 41 | exp: tEnd, 42 | }; 43 | 44 | const sHeader = JSON.stringify(oHeader); 45 | const sPayload = JSON.stringify(oPayload); 46 | const sJWT = KJUR.jws.JWS.sign( 47 | "RS256", 48 | sHeader, 49 | sPayload, 50 | this.privateKey, 51 | ); 52 | 53 | const response = await fetch("https://oauth2.googleapis.com/token", { 54 | method: "POST", 55 | headers: { 56 | "Content-Type": "application/x-www-form-urlencoded", 57 | }, 58 | body: new URLSearchParams({ 59 | grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", 60 | assertion: sJWT, 61 | }).toString(), 62 | }); 63 | 64 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 65 | // @ts-ignore 66 | const { access_token } = await response.json(); 67 | 68 | this._token = access_token; 69 | return access_token; 70 | })(); 71 | } 72 | 73 | private api = async (...args: Parameters) => { 74 | const request = new Request(new Request(...args).clone()); 75 | 76 | request.headers.set("Authorization", `Bearer ${await this.token}`); 77 | request.headers.set("Content-Type", "application/json"); 78 | 79 | const response = await fetch(request); 80 | 81 | return (await response.json()) as Promise; 82 | }; 83 | 84 | downloadMedia = async ({ 85 | resourceName, 86 | }: { 87 | resourceName: string; 88 | }): Promise => { 89 | const url = `https://chat.googleapis.com/v1/media/${resourceName}`; 90 | 91 | return this.api(url); // TODO: Check response 92 | }; 93 | 94 | getSpace = async ( 95 | args: 96 | | { 97 | name: string; 98 | } 99 | | { space: string }, 100 | ): Promise => { 101 | const url = 102 | "name" in args 103 | ? `https://chat.googleapis.com/v1/${args.name}` 104 | : `https://chat.googleapis.com/v1/spaces/${args.space}`; 105 | 106 | return this.api(url); 107 | }; 108 | 109 | listSpaces = async ( 110 | _: undefined, 111 | { pageSize, pageToken }: { pageSize?: number; pageToken?: string } = {}, 112 | ): Promise<{ nextPageToken?: string; spaces: chat_v1.Schema$Space[] }> => { 113 | const urlSearchParams = new URLSearchParams({ 114 | ...(pageSize !== undefined ? { pageSize: pageSize.toString() } : {}), 115 | ...(pageToken !== undefined ? { pageToken } : {}), 116 | }); 117 | const url = new URL("https://chat.googleapis.com/v1/spaces"); 118 | url.search = urlSearchParams.toString(); 119 | 120 | return this.api(url.toString()); 121 | }; 122 | 123 | getMember = async ( 124 | args: { name: string } | { space: string; member: string }, 125 | ): Promise => { 126 | const url = 127 | "name" in args 128 | ? `https://chat.googleapis.com/v1/${args.name}` 129 | : `https://chat.googleapis.com/v1/spaces/${args.space}/members/${args.member}`; 130 | 131 | return this.api(url); 132 | }; 133 | 134 | listMembers = async ( 135 | args: { parent: string } | { space: string }, 136 | { pageSize, pageToken }: { pageSize?: number; pageToken?: string }, 137 | ): Promise<{ 138 | nextPageToken: string; 139 | memberships: chat_v1.Schema$Membership[]; 140 | }> => { 141 | const urlSearchParams = new URLSearchParams({ 142 | ...(pageSize !== undefined ? { pageSize: pageSize.toString() } : {}), 143 | ...(pageToken !== undefined ? { pageToken } : {}), 144 | }); 145 | const url = new URL( 146 | "parent" in args 147 | ? `https://chat.googleapis.com/v1/${args.parent}/members` 148 | : `https://chat.googleapis.com/v1/spaces/${args.space}/members`, 149 | ); 150 | url.search = urlSearchParams.toString(); 151 | 152 | return this.api(url.toString()); 153 | }; 154 | 155 | createMessage = async ( 156 | args: 157 | | { 158 | parent: string; 159 | } 160 | | { space: string }, 161 | { threadKey, requestId }: { threadKey?: string; requestId?: string } = {}, 162 | message: chat_v1.Schema$Message, 163 | ): Promise => { 164 | const urlSearchParams = new URLSearchParams({ 165 | ...(threadKey !== undefined ? { threadKey } : {}), 166 | ...(requestId !== undefined ? { requestId } : {}), 167 | }); 168 | const url = new URL( 169 | "parent" in args 170 | ? `https://chat.googleapis.com/v1/${args.parent}` 171 | : `https://chat.googleapis.com/v1/spaces/${args.space}/messages`, 172 | ); 173 | url.search = urlSearchParams.toString(); 174 | 175 | return this.api(url.toString(), { 176 | method: "POST", 177 | body: JSON.stringify(message), 178 | }); 179 | }; 180 | 181 | deleteMessage = async ( 182 | args: 183 | | { 184 | name: string; 185 | } 186 | | { space: string; message: string }, 187 | ): Promise<{}> => { 188 | const url = 189 | "name" in args 190 | ? `https://chat.googleapis.com/v1/${args.name}` 191 | : `https://chat.googleapis.com/v1/spaces/${args.space}/messages/${args.message}`; 192 | 193 | return this.api(url, { method: "DELETE" }); 194 | }; 195 | 196 | getMessage = async ( 197 | args: 198 | | { 199 | name: string; 200 | } 201 | | { space: string; message: string }, 202 | ): Promise => { 203 | const url = 204 | "name" in args 205 | ? `https://chat.googleapis.com/v1/${args.name}` 206 | : `https://chat.googleapis.com/v1/spaces/${args.space}/messages/${args.message}`; 207 | 208 | return this.api(url); 209 | }; 210 | 211 | updateMessage = async ( 212 | args: // 'message.name' is almost certainly meant to be just 'name', so we handle both cases to be nice 213 | | { 214 | "message.name": string; 215 | } 216 | | { name: string } 217 | | { space: string; message: string }, 218 | { updateMask }: { updateMask?: string } = {}, 219 | message: chat_v1.Schema$Message, 220 | ): Promise => { 221 | const urlSearchParams = new URLSearchParams({ 222 | ...(updateMask !== undefined ? { updateMask } : {}), 223 | }); 224 | const url = new URL( 225 | "message.name" in args 226 | ? `https://chat.googleapis.com/v1/${args["message.name"]}` 227 | : "name" in args 228 | ? `https://chat.googleapis.com/v1/${args.name}` 229 | : `https://chat.googleapis.com/v1/spaces/${args.space}/messages/${args.message}`, 230 | ); 231 | url.search = urlSearchParams.toString(); 232 | 233 | return this.api(url.toString(), { 234 | method: "PUT", 235 | body: JSON.stringify(message), 236 | }); 237 | }; 238 | 239 | getAttachment = async ( 240 | args: 241 | | { 242 | name: string; 243 | } 244 | | { 245 | space: string; 246 | message: string; 247 | attachment: string; 248 | }, 249 | ): Promise => { 250 | const url = 251 | "name" in args 252 | ? `https://chat.googleapis.com/v1/${args.name}` 253 | : `https://chat.googleapis.com/v1/spaces/${args.space}/messages/${args.message}/attachments/${args.attachment}`; 254 | 255 | return this.api(url); 256 | }; 257 | } 258 | --------------------------------------------------------------------------------