Skip to content

Commit

Permalink
feat: Add edge compatibility for custom frameworks and Next.JS (#918)
Browse files Browse the repository at this point in the history
* Add support for polyfilling buffer and node for next

* Use querystringify instead of querystring for edge compatibility

* Add support for parse implementation using URLSearchParams

* Add tests for parseParams

* Run build-pretty on all files

* Add a workflow for testing Next.JS with edge runtime

* Add support for gzip compression

* Add support for br compression

* Disable brotli compression in edge

* Remove unnecessary log

* Replace parseParams with one URLSearchParams alternative

* Add support for throwing a proper error when brotli doesn't work

* Add support for CF workflow to test edge function compatibility

* Get rid of netflify edge test function

* Remove newly added conflicting route from netlify next test wf

* Rename api/auth dynamic variable in next app router

* Use ponyfilled process to refactor checking for test env

* Refactor process and expose an util function to make it accessible

* Add fallback implementation for accessing Buffer

* Refactor base64 encoding/decoding to use ponyfilled buffer

* Reuse ponyfilled buffer at more places

* Remove polyfill functionality for buffer and process

* Remove unused test file

* Update some functions according to comments in PR

* Update workflow to use more values from secrets

* Get rid of pages directory in next emailpassword example

* Remove use of getBuffer in loopback framework

* Add error handling for possible malformed body in lambda request

* Feat/hono api example repo (#2)

* Add init support for testing hono deployments on CF with edge

* Fix the directory path in hono workflow

* Fix the deploy command

* Fix tests to use assert instead

* Remove unused build command

* Update hono edge tests to test session endpoint as well

* Update README to contain proper details

* Drop brotli decompression support

* Remove brotli as a dependency

* Address requested changes

* Fix a typo regarding boxPrimitives check

* Update changelog with details of changes

---------

Co-authored-by: Rishabh Poddar <[email protected]>
  • Loading branch information
deepjyoti30Alt and rishabhpoddar authored Sep 9, 2024
1 parent 6420511 commit 8fabd09
Show file tree
Hide file tree
Showing 91 changed files with 4,370 additions and 226 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/test-cf-worker-hono.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: "Test edge function compatibility for Hono on Cloudflare Workers"
on: push
jobs:
test:
runs-on: ubuntu-latest
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
APP_URL: ${{ secrets.CLOUDFLARE_HONO_APP_URL }}
CLOUDFLARE_PROJECT_NAME: ${{ secrets.CLOUDFLARE_HONO_PROJECT_NAME }}
TEST_DEPLOYED_VERSION: true
defaults:
run:
working-directory: examples/cloudflare-workers/with-email-password-hono-be-only
steps:
- uses: actions/checkout@v2
- run: echo $GITHUB_SHA
- run: npm install git+https://github.com:supertokens/supertokens-node.git#$GITHUB_SHA
- run: npm install
- run: npm install [email protected] [email protected] puppeteer@^11.0.0 isomorphic-fetch@^3.0.0

- name: Replace APP_URL with deployed URL value
run: |
sed -i "s|process.env.REACT_APP_API_URL|\"${{ env.APP_URL }}\"|" config.ts
sed -i "s|process.env.REACT_APP_WEBSITE_URL|\"${{ env.APP_URL }}\"|" config.ts
- name: Deploy the changes
run: npx wrangler deploy --name ${{ env.CLOUDFLARE_PROJECT_NAME }} index.ts

- name: Run tests
run: |
( \
(echo "=========== Test attempt 1 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) || \
(echo "=========== Test attempt 2 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) || \
(echo "=========== Test attempt 3 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) \
)
- name: The job has failed
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: screenshots
path: |
./**/*screenshot.jpeg
72 changes: 72 additions & 0 deletions .github/workflows/test-cf-worker-next.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: "Test edge function compatibility for Next.js on Cloudflare Workers"
on: push
jobs:
test:
runs-on: ubuntu-latest
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
APP_URL: ${{ secrets.CLOUDFLARE_APP_URL }}
CLOUDFLARE_PROJECT_NAME: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
TEST_DEPLOYED_VERSION: true
defaults:
run:
working-directory: examples/next/with-emailpassword
steps:
- uses: actions/checkout@v2
- run: echo $GITHUB_SHA
- run: npm install git+https://github.com:supertokens/supertokens-node.git#$GITHUB_SHA
- run: npm install
- run: npm install [email protected] [email protected] puppeteer@^11.0.0 isomorphic-fetch@^3.0.0

# Step to update the runtime to edge to all files in app/api/
- name: Add runtime export to API files
run: |
find app/api -type f \( -name "*.js" -o -name "*.ts" \) -exec sed -i '1s/^/export const runtime = "edge";\n/' {} +
echo 'export const runtime = "edge";' >> app/auth/[[...path]]/page.tsx
# Install next on pages to build the app
- name: Install next-on-pages
run: npm install --save-dev @cloudflare/next-on-pages

# Setup the compatibility flag to make non edge functions run
- name: Create a wrangler.toml
run: echo "compatibility_flags = [ "nodejs_compat" ]" >> wrangler.toml

- name: Replace APP_URL with deployed URL value
run: |
sed -i "s|process.env.APP_URL|\"${{ env.APP_URL }}\"|" config/appInfo.ts
- name: Build using next-on-pages
run: npx next-on-pages

- name: Publish to Cloudflare Pages
id: deploy
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ env.CLOUDFLARE_API_TOKEN }}
accountId: ${{ env.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ env.CLOUDFLARE_PROJECT_NAME }}
directory: "./examples/next/with-emailpassword/.vercel/output/static"
wranglerVersion: "3"
branch: "master"

- name: Extract deployment info and save to JSON
id: extract_deploy_info
run: |
DEPLOY_ID=${{ steps.deploy.outputs.id }}
DEPLOY_URL=${{ steps.deploy.outputs.url }}
echo "{\"deploy_url\": \"$DEPLOY_URL\", \"deploy_id\": \"$DEPLOY_ID\"}" > deployInfo.json
- name: Run tests
run: |
( \
(echo "=========== Test attempt 1 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) || \
(echo "=========== Test attempt 2 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) || \
(echo "=========== Test attempt 3 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) \
)
- name: The job has failed
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: screenshots
path: |
./**/*screenshot.jpeg
1 change: 1 addition & 0 deletions .github/workflows/test-edge-function.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
- run: npm install git+https://github.com:supertokens/supertokens-node.git#$GITHUB_SHA
- run: npm install
- run: npm install [email protected] [email protected] puppeteer@^11.0.0 isomorphic-fetch@^3.0.0

- run: netlify deploy --alias 0 --build --json --auth=$NETLIFY_AUTH_TOKEN > deployInfo.json
- run: cat deployInfo.json
- run: |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [20.0.6] - 2024-09-05

- Add edge compatibility for custom frameworks and Next.JS

## [20.0.5] - 2024-09-02

- Optional form fields are now truly optional, can be omitted from the payload.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png)

# SuperTokens EmailPassword with Cloudflare Workers (HonoJS) on Edge Runtime

This demo app uses HonoJS with Cloudflare Workers for the backend server. We use [Wrangler](https://developers.cloudflare.com/workers/wrangler/) in the backend server to simulate the Cloudflare Worker runtime. This is a pure Edge runtime implementation (works without `nodejs_compat` flag).

## Project setup

Clone the repo, enter the directory, and use `npm` to install the project dependencies:

```bash
git clone https://github.com/supertokens/supertokens-node
cd supertokens-node/examples/cloudflare-workers/with-be-emailpassword
npm install
```

## Run the demo app

This compiles and serves the React app and starts the backend API server on port 3001.

```bash
npm run start
```

The app will start on `http://localhost:3000`
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Session from "supertokens-node/recipe/session";
import { TypeInput } from "supertokens-node/types";
import process from "process";

export const runtime = "edge";

export function getApiDomain() {
const apiPort = process.env.REACT_APP_API_PORT || 3001;
const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`;
return apiUrl;
}

export function getWebsiteDomain() {
const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3000;
const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
return websiteUrl;
}

export const SuperTokensConfig: TypeInput = {
supertokens: {
// this is the location of the SuperTokens core.
connectionURI: "https://try.supertokens.com",
},
appInfo: {
appName: "SuperTokens Demo App",
apiDomain: getApiDomain(),
websiteDomain: getWebsiteDomain(),
},
// recipeList contains all the modules that you want to
// use from SuperTokens. See the full list here: https://supertokens.com/docs/guides
recipeList: [EmailPassword.init(), Session.init()],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SessionContainer } from "supertokens-node/recipe/session";

declare module "hono" {
interface HonoRequest {
session?: SessionContainer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Hono } from "hono";
import { cors } from "hono/cors";
import supertokens from "supertokens-node";
import { middleware } from "./middleware";
import { getWebsiteDomain, SuperTokensConfig } from "./config";
import type { PageConfig } from "next";

export const config: PageConfig = {
runtime: "edge",
};

supertokens.init(SuperTokensConfig);

const app = new Hono();

app.use("*", async (c, next) => {
return await cors({
origin: getWebsiteDomain(),
credentials: true,
allowHeaders: ["Content-Type", ...supertokens.getAllCORSHeaders()],
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
})(c, next);
});

// This exposes all the APIs from SuperTokens to the client.
// and adds the session to the request object if one exists.
app.use("*", middleware());

// An example API that requires session verification
app.get("/sessioninfo", (c) => {
let session = c.req.session;
if (!session) {
return c.text("Unauthorized", 401);
}
return c.json({
sessionHandle: session!.getHandle(),
userId: session!.getUserId(),
accessTokenPayload: session!.getAccessTokenPayload(),
});
});

export default app;
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Context, Next } from "hono";
import { getCookie } from "hono/cookie";
import {
CollectingResponse,
PreParsedRequest,
middleware as customMiddleware,
} from "supertokens-node/framework/custom";
import Session from "supertokens-node/recipe/session";
import { HTTPMethod } from "supertokens-node/types";
import { serialize } from "cookie";

export const runtime = "edge";

function setCookiesInHeaders(headers: Headers, cookies: CollectingResponse["cookies"]) {
for (const cookie of cookies) {
headers.append(
"Set-Cookie",
serialize(cookie.key, cookie.value, {
domain: cookie.domain,
expires: new Date(cookie.expires),
httpOnly: cookie.httpOnly,
path: cookie.path,
sameSite: cookie.sameSite,
secure: cookie.secure,
})
);
}
}

function copyHeaders(source: Headers, destination: Headers): void {
for (const [key, value] of source.entries()) {
destination.append(key, value);
}
}

export const middleware = () => {
return async function (c: Context, next: Next) {
const request = new PreParsedRequest({
method: c.req.method as HTTPMethod,
url: c.req.url,
query: Object.fromEntries(new URL(c.req.url).searchParams.entries()),
cookies: getCookie(c),
headers: c.req.raw.headers as Headers,
getFormBody: () => c.req.formData(),
getJSONBody: () => c.req.json(),
});
const baseResponse = new CollectingResponse();

const stMiddleware = customMiddleware(() => request);

const { handled, error } = await stMiddleware(request, baseResponse);

if (error) {
throw error;
}

if (handled) {
setCookiesInHeaders(baseResponse.headers, baseResponse.cookies);
return new Response(baseResponse.body, {
status: baseResponse.statusCode,
headers: baseResponse.headers,
});
}

// Add session to c.req if it exists
try {
c.req.session = await Session.getSession(request, baseResponse, {
sessionRequired: false,
});

await next();

// Add cookies that were set by `getSession` to response
setCookiesInHeaders(c.res.headers, baseResponse.cookies);
// Copy headers that were set by `getSession` to response
copyHeaders(baseResponse.headers, c.res.headers);
return c.res;
} catch (err) {
if (Session.Error.isErrorFromSuperTokens(err)) {
if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.INVALID_CLAIMS) {
return new Response("Unauthorized", {
status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401,
});
}
}
}
};
};
Loading

0 comments on commit 8fabd09

Please sign in to comment.