├── .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 |
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 |
--------------------------------------------------------------------------------