├── .dockerignore ├── .env.defaults ├── .env.example ├── .github └── workflows │ ├── mirror.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .releaserc ├── Dockerfile ├── LICENSE ├── README.md └── src └── index.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.env.defaults: -------------------------------------------------------------------------------- 1 | JWT_SECRET=secret 2 | DENO_ORIGIN=http://host.docker.internal:8082 3 | VERIFY_JWT=true 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | JWT_SECRET=secret 2 | DENO_ORIGIN=http://localhost:8082 3 | VERIFY_JWT=true 4 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yml: -------------------------------------------------------------------------------- 1 | name: Mirror Image 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Image tag" 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | mirror: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | id-token: write 18 | steps: 19 | - name: configure aws credentials 20 | uses: aws-actions/configure-aws-credentials@v1 21 | with: 22 | role-to-assume: ${{ secrets.PROD_AWS_ROLE }} 23 | aws-region: us-east-1 24 | - uses: docker/login-action@v2 25 | with: 26 | registry: public.ecr.aws 27 | - uses: docker/login-action@v2 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | - uses: akhilerm/tag-push-action@v2.1.0 33 | with: 34 | src: docker.io/supabase/deno-relay:${{ inputs.version }} 35 | dst: | 36 | public.ecr.aws/supabase/deno-relay:${{ inputs.version }} 37 | ghcr.io/supabase/deno-relay:${{ inputs.version }} 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Image Registry 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_call: 8 | inputs: 9 | version: 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | id-token: write 20 | steps: 21 | - id: meta 22 | uses: docker/metadata-action@v4 23 | with: 24 | images: | 25 | supabase/deno-relay 26 | public.ecr.aws/supabase/deno-relay 27 | ghcr.io/supabase/deno-relay 28 | flavor: latest=false 29 | tags: | 30 | type=raw,value=${{ inputs.version }} 31 | - uses: docker/setup-qemu-action@v2 32 | with: 33 | platforms: amd64,arm64 34 | - uses: docker/setup-buildx-action@v2 35 | - name: Login to DockerHub 36 | uses: docker/login-action@v2 37 | with: 38 | username: ${{ secrets.DOCKER_USERNAME }} 39 | password: ${{ secrets.DOCKER_PASSWORD }} 40 | - name: configure aws credentials 41 | uses: aws-actions/configure-aws-credentials@v1 42 | with: 43 | role-to-assume: ${{ secrets.PROD_AWS_ROLE }} 44 | aws-region: us-east-1 45 | - name: Login to ECR 46 | uses: docker/login-action@v2 47 | with: 48 | registry: public.ecr.aws 49 | - name: Login to GHCR 50 | uses: docker/login-action@v2 51 | with: 52 | registry: ghcr.io 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | - uses: docker/build-push-action@v3 56 | with: 57 | push: true 58 | platforms: linux/amd64,linux/arm64 59 | tags: ${{ steps.meta.outputs.tags }} 60 | cache-from: type=gha 61 | cache-to: type=gha,mode=max 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: docker-publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-20.04 11 | outputs: 12 | published: ${{ steps.semantic.outputs.new_release_published }} 13 | version: ${{ steps.semantic.outputs.new_release_version }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Semantic Release 17 | id: semantic 18 | uses: cycjimmy/semantic-release-action@v3 19 | with: 20 | semantic_version: 18 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | publish: 25 | needs: 26 | - release 27 | if: needs.release.outputs.published == 'true' 28 | # Call publish explicitly because events from actions cannot trigger more actions 29 | uses: ./.github/workflows/publish.yml 30 | with: 31 | version: v${{ needs.release.outputs.version }} 32 | secrets: inherit 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "plugins": [ 6 | "@semantic-release/commit-analyzer", 7 | "@semantic-release/release-notes-generator", 8 | "@semantic-release/github" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukechannings/deno:v1.30.3 2 | 3 | EXPOSE 8081 4 | WORKDIR /app 5 | USER deno 6 | 7 | COPY . ./ 8 | RUN deno cache src/index.ts 9 | 10 | CMD ["run", "--allow-read", "--allow-net", "--allow-env", "src/index.ts"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Supabase 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Functions Relay (Archived) 2 | 3 | ### Update (07/07/2023) 4 | 5 | This repo is no longer used. For any issues related to running Edge Functions via Supabase CLI, refer to [Edge Runtime](https://github.com/supabase/edge-runtime) repo 6 | 7 | Supabase Functions Relay is the API gateway which is run before any edge function is invoked. For more details see our Functions blog post [here](https://supabase.com/blog/2022/03/31/supabase-edge-functions) 8 | 9 | ### CONTRIBUTING 10 | 11 | * This repo uses [semantic release](https://github.com/semantic-release/semantic-release) for releases. Follow the semantic release commit guide when making commits. 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | Request, 4 | Status, 5 | Context, 6 | } from "https://deno.land/x/oak@v10.3.0/mod.ts"; 7 | import * as jose from "https://deno.land/x/jose@v4.3.7/index.ts"; 8 | import { config } from "https://deno.land/x/dotenv@v3.2.0/mod.ts"; 9 | 10 | const app = new Application(); 11 | 12 | const X_FORWARDED_HOST = "x-forwarded-host"; 13 | 14 | const JWT_SECRET = 15 | Deno.env.get("JWT_SECRET") ?? config({ safe: true }).JWT_SECRET; 16 | const DENO_ORIGIN = 17 | Deno.env.get("DENO_ORIGIN") ?? config({ safe: true }).DENO_ORIGIN; 18 | const VERIFY_JWT = 19 | (Deno.env.get("VERIFY_JWT") ?? config({ safe: true }).VERIFY_JWT) === "true"; 20 | 21 | function getAuthToken(ctx: Context) { 22 | const authHeader = ctx.request.headers.get("authorization"); 23 | if (!authHeader) { 24 | ctx.throw(Status.Unauthorized, "Missing authorization header"); 25 | } 26 | const [bearer, token] = authHeader.split(" "); 27 | if (bearer !== "Bearer") { 28 | ctx.throw(Status.Unauthorized, `Auth header is not 'Bearer {token}'`); 29 | } 30 | return token; 31 | } 32 | 33 | async function verifyJWT(jwt: string): Promise { 34 | const encoder = new TextEncoder(); 35 | const secretKey = encoder.encode(JWT_SECRET); 36 | try { 37 | await jose.jwtVerify(jwt, secretKey); 38 | } catch (err) { 39 | console.error(err); 40 | return false; 41 | } 42 | return true; 43 | } 44 | 45 | function sanitizeHeaders(headers: Headers): Headers { 46 | const sanitizedHeaders = new Headers(); 47 | const headerDenyList = ["set-cookie"]; 48 | 49 | headers.forEach((value, key) => { 50 | if (!headerDenyList.includes(key.toLowerCase())) { 51 | sanitizedHeaders.set(key, value); 52 | } 53 | }); 54 | return sanitizedHeaders; 55 | } 56 | 57 | function patchedReq(req: Request): [URL, RequestInit] { 58 | // Parse & patch URL (preserve path and querystring) 59 | const url = req.url; 60 | const denoOrigin = new URL(DENO_ORIGIN); 61 | url.host = denoOrigin.host; 62 | url.port = denoOrigin.port; 63 | url.protocol = denoOrigin.protocol; 64 | // Patch Headers 65 | const xHost = url.hostname; 66 | 67 | return [ 68 | url, 69 | { 70 | headers: { 71 | ...Object.fromEntries(req.headers.entries()), 72 | [X_FORWARDED_HOST]: xHost, 73 | }, 74 | body: (req.hasBody 75 | ? req.body({ type: "stream" }).value 76 | : undefined) as unknown as BodyInit, 77 | method: req.method, 78 | }, 79 | ]; 80 | } 81 | 82 | async function relayTo(req: Request): Promise { 83 | const [url, init] = patchedReq(req); 84 | return await fetch(url, init); 85 | } 86 | 87 | app.use(async (ctx: Context, next: () => Promise) => { 88 | try { 89 | await next(); 90 | } catch (err) { 91 | console.error(err); 92 | ctx.response.body = err.message; 93 | ctx.response.headers.append("x-relay-error", "true"); 94 | ctx.response.status = err.status || 500; 95 | } 96 | }); 97 | 98 | app.use(async (ctx: Context, next: () => Promise) => { 99 | const { request, response } = ctx; 100 | 101 | const supportedVerbs = ['POST', 'GET','PUT', 'PATCH', 'DELETE', 'OPTIONS']; 102 | if (!(supportedVerbs.includes(request.method))) { 103 | console.error(`${request.method} not supported`); 104 | return ctx.throw( 105 | Status.MethodNotAllowed, 106 | `HTTP request method not supported (supported: ${supportedVerbs.join(' ')})` 107 | ); 108 | } 109 | 110 | 111 | if (request.method !== "OPTIONS" && VERIFY_JWT) { 112 | const token = getAuthToken(ctx); 113 | const isValidJWT = await verifyJWT(token); 114 | 115 | if (!isValidJWT) { 116 | return ctx.throw(Status.Unauthorized, "Invalid JWT"); 117 | } 118 | } 119 | 120 | const resp = await relayTo(request); 121 | 122 | const sanitizedHeaders = sanitizeHeaders(resp.headers); 123 | if (request.method === "GET") { 124 | const contentTypeHeader = sanitizedHeaders.get('Content-Type'); 125 | if (contentTypeHeader?.includes('text/html')) { 126 | sanitizedHeaders.set('Content-Type', 'text/plain'); 127 | } 128 | } 129 | 130 | response.body = resp.body; 131 | response.status = resp.status; 132 | response.headers = sanitizedHeaders; 133 | response.type = resp.type; 134 | 135 | await next(); 136 | }); 137 | 138 | if (import.meta.main) { 139 | const port = parseInt(Deno.args?.[0] ?? 8081); 140 | const hostname = "0.0.0.0"; 141 | 142 | console.log(`Listening on http://${hostname}:${port}`); 143 | await app.listen({ port, hostname }); 144 | } 145 | --------------------------------------------------------------------------------