Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/obsidian/scripts/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ export const compile = ({
"process.env.SUPABASE_URL": dbEnv.SUPABASE_URL
? `"${dbEnv.SUPABASE_URL}"`
: "null",
"process.env.SUPABASE_ANON_KEY": dbEnv.SUPABASE_ANON_KEY
? `"${dbEnv.SUPABASE_ANON_KEY}"`
"process.env.SUPABASE_PUBLISHABLE_KEY": dbEnv.SUPABASE_PUBLISHABLE_KEY
? `"${dbEnv.SUPABASE_PUBLISHABLE_KEY}"`
: "null",
"process.env.NEXT_API_ROOT": `"${dbEnv.NEXT_API_ROOT || ""}"`,
},
Expand Down
4 changes: 2 additions & 2 deletions apps/roam/scripts/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@
"process.env.SUPABASE_URL": dbEnv.SUPABASE_URL
? `"${dbEnv.SUPABASE_URL}"`
: "null",
"process.env.SUPABASE_ANON_KEY": dbEnv.SUPABASE_ANON_KEY
? `"${dbEnv.SUPABASE_ANON_KEY}"`
"process.env.SUPABASE_PUBLISHABLE_KEY": dbEnv.SUPABASE_PUBLISHABLE_KEY

Check warning on line 165 in apps/roam/scripts/compile.ts

View workflow job for this annotation

GitHub Actions / eslint (apps/roam)

[eslint (apps/roam)] apps/roam/scripts/compile.ts#L165

Object Literal Property name `process.env.SUPABASE_PUBLISHABLE_KEY` must match one of the following formats: camelCase @typescript-eslint/naming-convention
Raw output
  165:9   warning  Object Literal Property name `process.env.SUPABASE_PUBLISHABLE_KEY` must match one of the following formats: camelCase                                              @typescript-eslint/naming-convention

Check warning on line 165 in apps/roam/scripts/compile.ts

View workflow job for this annotation

GitHub Actions / eslint (apps/roam)

[eslint (apps/roam)] apps/roam/scripts/compile.ts#L165

Unsafe member access .SUPABASE_PUBLISHABLE_KEY on an `any` value @typescript-eslint/no-unsafe-member-access
Raw output
  165:55  warning  Unsafe member access .SUPABASE_PUBLISHABLE_KEY on an `any` value                                                                                                    @typescript-eslint/no-unsafe-member-access
? `"${dbEnv.SUPABASE_PUBLISHABLE_KEY}"`

Check warning on line 166 in apps/roam/scripts/compile.ts

View workflow job for this annotation

GitHub Actions / eslint (apps/roam)

[eslint (apps/roam)] apps/roam/scripts/compile.ts#L166

Unsafe member access .SUPABASE_PUBLISHABLE_KEY on an `any` value @typescript-eslint/no-unsafe-member-access
Raw output
  166:23  warning  Unsafe member access .SUPABASE_PUBLISHABLE_KEY on an `any` value                                                                                                    @typescript-eslint/no-unsafe-member-access
: "null",
"process.env.NEXT_API_ROOT": `"${dbEnv.NEXT_API_ROOT || ""}"`,
"window.__DISCOURSE_GRAPH_VERSION__": `"${getVersion()}"`,
Expand Down
7 changes: 4 additions & 3 deletions apps/website/app/api/supabase/env/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {

export const GET = (request: NextRequest): NextResponse => {
try {
const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env;
if (!SUPABASE_URL || !SUPABASE_ANON_KEY)
const { SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_PUBLISHABLE_KEY } =
process.env;
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY)
return new NextResponse("Missing variables", { status: 500 });
return NextResponse.json(
// eslint-disable-next-line @typescript-eslint/naming-convention
{ SUPABASE_URL, SUPABASE_ANON_KEY },
{ SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_PUBLISHABLE_KEY },
{ status: 200 },
);
} catch (e: unknown) {
Expand Down
2 changes: 1 addition & 1 deletion apps/website/app/utils/supabase/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { envContents } from "@repo/database/dbDotEnv";
export const updateSession = async (request: NextRequest) => {
const dbEnv = envContents();
const supabaseUrl = dbEnv.SUPABASE_URL;
const supabaseKey = dbEnv.SUPABASE_ANON_KEY;
const supabaseKey = dbEnv.SUPABASE_PUBLISHABLE_KEY;

if (!supabaseUrl || !supabaseKey) {
throw new Error("Missing required Supabase environment variables");
Expand Down
2 changes: 1 addition & 1 deletion apps/website/app/utils/supabase/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const createClient = async () => {
const dbEnv = envContents();
const cookieStore = await cookies();
const supabaseUrl = dbEnv.SUPABASE_URL;
const supabaseKey = dbEnv.SUPABASE_ANON_KEY;
const supabaseKey = dbEnv.SUPABASE_PUBLISHABLE_KEY;

if (!supabaseUrl || !supabaseKey) {
throw new Error("Missing required Supabase environment variables");
Expand Down
8 changes: 8 additions & 0 deletions packages/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ This should be used with extreme caution, as there is not currently adequate sec
It may be appropriate if there is a problem in production that is due to corrupted data (vs schema issues), and it is somehow simpler to test code to repair it directly than to load the data locally.
Again, if all your code is running through Vercel API endpoints, the simplest way is to set `NEXT_API_ROOT` to the url of the API of the production Vercel branch (`https://discoursegraphs.com/api`).
But in most other cases, you will want your code to talk to the production database. set up vercel as above, and set `SUPABASE_USE_DB=production` in your console before running `turbo dev`.

## JWT token management

We are now using JWT Signing keys. See the Supabase [announcement](https://github.com/supabase/supabase/blob/037e5f90a5689c3d847bd2adf9c8ec3956a0e7a0/apps/docs/content/guides/functions/auth.mdx) and [documentation](https://supabase.com/docs/guides/auth/signing-keys).

This allows for better key management in general, including key deprecation. One small downside is that the value of `SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY`, generated in `https://supabase.com/dashboard/project/<project>/settings/jwt` has to be manually transferred into the edge function secrets, under slightly different names (since the `SUPABASE_` prefix is reserved, we replace it with `SB_`.) This is done in `https://supabase.com/dashboard/project/<project>/functions/secrets`. The announcement says this may get automated at some point.

We also need to transfer the `SUPABASE_PUBLISHABLE_KEY` to github secrets (without rename.) The vercel environment gets updated automaticaly.
6 changes: 3 additions & 3 deletions packages/database/features/step-definitions/stepdefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ if (getVariant() === "production") {
config();

const getAnonymousClient = () => {
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_PUBLISHABLE_KEY) {
throw new Error(
"Missing required environment variables: SUPABASE_URL and SUPABASE_ANON_KEY",
"Missing required environment variables: SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY",
);
}
return createClient<Database, "public">(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
process.env.SUPABASE_PUBLISHABLE_KEY,
);
};

Expand Down
22 changes: 20 additions & 2 deletions packages/database/scripts/createEnv.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execSync } from "node:child_process";
import { appendFileSync, writeFileSync } from "node:fs";
import { appendFileSync, writeFileSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import dotenv from "dotenv";
Expand Down Expand Up @@ -28,6 +28,10 @@ const getVercelToken = () => {
return process.env["VERCEL_TOKEN"];
};

const makeFnEnv = (envTxt: string): string => {
return envTxt.split('\n').filter(l=>l.match(/^SUPABASE_\w+_KEY/)).map((l)=> l.replace('SUPABASE_', 'SB_')).join('\n');
}

const makeLocalEnv = () => {
execSync("supabase start", {
cwd: projectRoot, stdio: "inherit"
Expand All @@ -48,6 +52,10 @@ const makeLocalEnv = () => {
join(projectRoot, ".env.local"),
prefixed + '\nNEXT_API_ROOT="http://localhost:3000/api"\n',
);
writeFileSync(
join(projectRoot, "supabase/functions/.env"),
makeFnEnv(prefixed)
)
};

const makeBranchEnv = async (vercel: Vercel, vercelToken: string) => {
Expand Down Expand Up @@ -86,6 +94,11 @@ const makeBranchEnv = async (vercel: Vercel, vercelToken: string) => {
throw err;
}
appendFileSync(".env.branch", `NEXT_API_ROOT="https://${url}/api"\n`);
const fromVercel = readFileSync('.env.branch').toString();
writeFileSync(
join(projectRoot, "supabase/functions/.env"),
makeFnEnv(fromVercel)
)
};

const makeProductionEnv = async (vercel: Vercel, vercelToken: string) => {
Expand All @@ -104,6 +117,11 @@ const makeProductionEnv = async (vercel: Vercel, vercelToken: string) => {
`vercel -t ${vercelToken} env pull --environment production .env.production`,
);
appendFileSync(".env.production", `NEXT_API_ROOT="https://${url}/api"\n`);
const fromVercel = readFileSync('.env.production').toString();
writeFileSync(
join(projectRoot, "supabase/functions/.env"),
makeFnEnv(fromVercel)
)
};

const main = async (variant: Variant) => {
Expand All @@ -118,7 +136,7 @@ const main = async (variant: Variant) => {
);
return;
} catch (e) {
if (process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY)
if (process.env.SUPABASE_URL && process.env.SUPABASE_PUBLISHABLE_KEY)
return;
throw new Error("Could not get environment from site");
}
Expand Down
4 changes: 2 additions & 2 deletions packages/database/src/dbDotEnv.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
variant = process.env["SUPABASE_USE_DB"];
}
const processHasVars =
!!process.env["SUPABASE_URL"] && !!process.env["SUPABASE_ANON_KEY"];
!!process.env["SUPABASE_URL"] && !!process.env["SUPABASE_PUBLISHABLE_KEY"];

Check warning on line 32 in packages/database/src/dbDotEnv.mjs

View workflow job for this annotation

GitHub Actions / eslint (packages/database)

[eslint (packages/database)] packages/database/src/dbDotEnv.mjs#L32

'process' is not defined no-undef
Raw output
  32:7   warning  'process' is not defined                                                                                    no-undef

Check warning on line 32 in packages/database/src/dbDotEnv.mjs

View workflow job for this annotation

GitHub Actions / eslint (packages/database)

[eslint (packages/database)] packages/database/src/dbDotEnv.mjs#L32

'process' is not defined no-undef
Raw output
  32:40  warning  'process' is not defined                                                                                    no-undef

if (
["local", "branch", "production", "none", "implicit", undefined].indexOf(
Expand Down Expand Up @@ -77,7 +77,7 @@
// Fallback to process.env when running in production environments
const raw = {
SUPABASE_URL: process.env.SUPABASE_URL,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY,

Check warning on line 80 in packages/database/src/dbDotEnv.mjs

View workflow job for this annotation

GitHub Actions / eslint (packages/database)

[eslint (packages/database)] packages/database/src/dbDotEnv.mjs#L80

Object Literal Property name `SUPABASE_PUBLISHABLE_KEY` must match one of the following formats: camelCase @typescript-eslint/naming-convention
Raw output
  80:7   warning  Object Literal Property name `SUPABASE_PUBLISHABLE_KEY` must match one of the following formats: camelCase  @typescript-eslint/naming-convention

Check warning on line 80 in packages/database/src/dbDotEnv.mjs

View workflow job for this annotation

GitHub Actions / eslint (packages/database)

[eslint (packages/database)] packages/database/src/dbDotEnv.mjs#L80

'process' is not defined no-undef
Raw output
  80:33  warning  'process' is not defined                                                                                    no-undef
NEXT_API_ROOT: process.env.NEXT_API_ROOT,
};
return Object.fromEntries(Object.entries(raw).filter(([, v]) => !!v));
Expand Down
2 changes: 1 addition & 1 deletion packages/database/src/lib/contextFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ let lastStorageKey: string | undefined = undefined;
// to ensure we never have conflict between multiple clients
const createSingletonClient = (uniqueKey: string): DGSupabaseClient | null => {
const url = process.env.SUPABASE_URL;
const key = process.env.SUPABASE_ANON_KEY;
const key = process.env.SUPABASE_PUBLISHABLE_KEY;

if (!url || !key) {
throw new FatalError("Missing required Supabase environment variables");
Expand Down
13 changes: 12 additions & 1 deletion packages/database/supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,22 @@ s3_secret_key = "env(S3_SECRET_KEY)"

[functions.create-space]
enabled = true
verify_jwt = true
verify_jwt = false
import_map = "./functions/create-space/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
entrypoint = "./functions/create-space/index.ts"
# Specifies static files to be bundled with the function. Supports glob patterns.
# For example, if you want to serve static HTML pages in your function:
# static_files = [ "./functions/create_space/*.html" ]

[functions.create-group]
enabled = true
verify_jwt = false
import_map = "./functions/create-group/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
entrypoint = "./functions/create-group/index.ts"
# Specifies static files to be bundled with the function. Supports glob patterns.
# For example, if you want to serve static HTML pages in your function:
# static_files = [ "./functions/create_group/*.html" ]
16 changes: 12 additions & 4 deletions packages/database/supabase/functions/create-group/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,26 @@ Deno.serve(async (req) => {
// @ts-ignore Deno is not visible to the IDE
const url = Deno.env.get("SUPABASE_URL");
// @ts-ignore Deno is not visible to the IDE
const service_key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
const service_key = Deno.env.get("SB_SECRET_KEY");
// @ts-ignore Deno is not visible to the IDE
const anon_key = Deno.env.get("SUPABASE_ANON_KEY");
const anon_key = Deno.env.get("SB_PUBLISHABLE_KEY");

if (!url || !anon_key || !service_key) {
return new Response("Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY", {
return new Response("Missing SUPABASE_URL or SB_SECRET_KEY or SB_PUBLISHABLE_KEY", {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
const supabase = createClient(url, anon_key)
const authHeader = req.headers.get('Authorization')!
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return Response.json(
{ msg: 'Missing authorization headers' },
{
status: 401,
}
)
}
const token = authHeader.replace('Bearer ', '')
const { data, error } = await supabase.auth.getClaims(token)

Expand Down
35 changes: 29 additions & 6 deletions packages/database/supabase/functions/create-space/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.

import "@supabase/functions-js/edge-runtime";
import {
createClient,
Expand Down Expand Up @@ -209,22 +208,46 @@ Deno.serve(async (req) => {
});
}

const input = await req.json();
// @ts-ignore Deno is not visible to the IDE
const url = Deno.env.get("SUPABASE_URL");
const url = Deno.env.get("SUPABASE_URL") as string | undefined;
// @ts-ignore Deno is not visible to the IDE
const key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
const key = Deno.env.get("SB_SECRET_KEY") as string | undefined;
if (!url || !key) {
return new Response("Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY", {
return new Response("Missing SUPABASE_URL or SB_SECRET_KEY", {
status: 500,
headers: { "Content-Type": "application/json" },
});
}

// check that we have at least a valid anonymous token with a dummy query.
// Unfortunately, this seems to be too permissive.
const authHeader = req.headers.get('Authorization') as string | undefined;
if (!authHeader) {
return Response.json(
{ msg: 'Missing authorization headers' },
{
status: 401,
}
)
}
const token = authHeader.replace('Bearer ', '');
const supabaseAnonClient: DGSupabaseClient = createClient(
url, token, { global: { headers: { Authorization: authHeader } } });
Comment on lines +234 to +235
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Bug: The createClient call is passing the user's JWT token as the second parameter, but this parameter expects the Supabase anon/publishable key, not a user token. This will fail to create a valid Supabase client.

The token is already being passed in the Authorization header via options. The second parameter should be the SB_PUBLISHABLE_KEY from environment variables.

const anonKey = Deno.env.get("SB_PUBLISHABLE_KEY");
if (!anonKey) {
  return Response.json(
    { msg: 'Missing SB_PUBLISHABLE_KEY' },
    { status: 500 }
  )
}
const supabaseAnonClient: DGSupabaseClient = createClient(
  url, anonKey, { global: { headers: { Authorization: authHeader } } });
Suggested change
const supabaseAnonClient: DGSupabaseClient = createClient(
url, token, { global: { headers: { Authorization: authHeader } } });
const anonKey = Deno.env.get("SB_PUBLISHABLE_KEY");
if (!anonKey) {
return Response.json(
{ msg: 'Missing SB_PUBLISHABLE_KEY' },
{ status: 500 }
)
}
const supabaseAnonClient: DGSupabaseClient = createClient(
url, anonKey, { global: { headers: { Authorization: authHeader } } });

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

{
const { error } = await supabaseAnonClient.from("Space").select("id").limit(1);
if (error?.code) return new Response(JSON.stringify(error), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}

// note: If we wanted this to be bound by permissions, we'd set the following options:
// { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
// { global: { headers: { Authorization: authHeader } } }
// But the point here is to bypass RLS
const supabase: DGSupabaseClient = createClient(url, key);

const input = await req.json();

const { data, error } = await processAndGetOrCreateSpace(supabase, input);
if (error) {
const status = error.code === "invalid space" ? 400 : 500;
Expand Down
1 change: 1 addition & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"RESEND_API_KEY",
"SUPABASE_ACCESS_TOKEN",
"SUPABASE_ANON_KEY",
"SUPABASE_PUBLISHABLE_KEY",
"SUPABASE_JWT_SECRET",
"SUPABASE_DB_PASSWORD",
"VERCEL_TOKEN"
Expand Down
Loading