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
18 changes: 12 additions & 6 deletions .oxlintrc.effect.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
"!apps/cli/src/command-internal/branch-ref.*",
"!apps/cli/src/command-internal/branch-target.*",
"!apps/cli/src/command-internal/bundled-postgres-client.*",
"!apps/cli/src/command-internal/byte-quote.ts",
"!apps/cli/src/command-internal/colors.*",
"!apps/cli/src/command-internal/config-pull-run.*",
"!apps/cli/src/command-internal/config-validate.*",
"!apps/cli/src/command-internal/connect-errors.*",
"!apps/cli/src/command-internal/container-cli.*",
"!apps/cli/src/command-internal/db-*.ts",
Expand All @@ -39,35 +42,35 @@
"!apps/cli/src/command-internal/docker-*.ts",
"!apps/cli/src/command-internal/dotenv.*",
"!apps/cli/src/command-internal/drop-objects.ts",
"!apps/cli/src/command-internal/duration.*",
"!apps/cli/src/command-internal/edge-runtime-*.ts",
"!apps/cli/src/command-internal/ensure-login.*",
"!apps/cli/src/command-internal/error-message.ts",
"!apps/cli/src/command-internal/experimental-feature.ts",
"!apps/cli/src/command-internal/experimental-gate.*",
"!apps/cli/src/command-internal/fail-on.*",
"!apps/cli/src/command-internal/format-float.*",
"!apps/cli/src/command-internal/get-api-keys.*",
"!apps/cli/src/command-internal/get-tenant-api-keys.*",
"!apps/cli/src/command-internal/git-status.*",
"!apps/cli/src/command-internal/glob.*",
"!apps/cli/src/command-internal/global-flags.*",
"!apps/cli/src/command-internal/go-duration.*",
"!apps/cli/src/command-internal/go-float.*",
"!apps/cli/src/command-internal/go-json.*",
"!apps/cli/src/command-internal/go-output-flag.*",
"!apps/cli/src/command-internal/go-output.encoders.*",
"!apps/cli/src/command-internal/go-quote.ts",
"!apps/cli/src/command-internal/hostname.*",
"!apps/cli/src/command-internal/html-safe-json.*",
"!apps/cli/src/command-internal/http-*",
"!apps/cli/src/command-internal/identity-stitch.*",
"!apps/cli/src/command-internal/kong-*",
"!apps/cli/src/command-internal/link-services-core.*",
"!apps/cli/src/command-internal/linked-state.*",
"!apps/cli/src/command-internal/local-jwt.*",
"!apps/cli/src/command-internal/local-project-context.*",
"!apps/cli/src/command-internal/login-*",
"!apps/cli/src/command-internal/make-dir.*",
"!apps/cli/src/command-internal/management-api-runtime.*",
"!apps/cli/src/command-internal/migrate-and-seed.*",
"!apps/cli/src/command-internal/migration*",
"!apps/cli/src/command-internal/output-formats.*",
"!apps/cli/src/command-internal/output.encoders.*",
"!apps/cli/src/command-internal/param-introspection.*",
"!apps/cli/src/command-internal/parent-project-ref.*",
"!apps/cli/src/command-internal/parse-uint.*",
Expand Down Expand Up @@ -118,6 +121,9 @@
"!apps/cli/src/command-internal/vault.*",
"!apps/cli/src/command-internal/workdir-project.*",
"!apps/cli/src/command-internal/workdir-validation.ts",
"apps/cli/src/command-internal/config-validate.parity.unit.test.ts",
"apps/cli/src/command-internal/config-validate.ts",
"apps/cli/src/command-internal/config-validate.unit.test.ts",
"apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts",
"apps/cli/src/command-internal/db-bootstrap/local-db-running.ts"
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { join } from "node:path";
import * as BunPath from "@effect/platform-bun/BunPath";
import { Effect, Path } from "effect";
import { describe, expect, it, vi } from "vitest";

/**
Expand All @@ -25,22 +26,22 @@ vi.mock("node:fs", () => ({
}));

const FAKE_EXISTING_BASE = "/fake/project-root";
const path = Effect.runSync(Effect.provide(Path.Path, BunPath.layer));

describe("canonicalPathForContainment (via resolveEmailTemplateContentPath)", () => {
it("resolves a 5,000-component-deep missing content_path without blowing the call stack", async () => {
const { resolveEmailTemplateContentPath } = await import("./config-validate.ts");
it("resolves a 5,000-component-deep missing content_path without blowing the call stack", () =>
import("./config-validate.ts").then(({ resolveEmailTemplateContentPath }) => {
const missingSegments = Array.from({ length: 5000 }, (_, i) => `missing-${i}`);
const contentPath = `${missingSegments.join("/")}/invite.html`;

const missingSegments = Array.from({ length: 5000 }, (_, i) => `missing-${i}`);
const contentPath = `${missingSegments.join("/")}/invite.html`;
const resolved = resolveEmailTemplateContentPath({
section: "template",
name: "invite",
contentPath,
contentPresent: false,
base: FAKE_EXISTING_BASE,
});

const resolved = resolveEmailTemplateContentPath({
section: "template",
name: "invite",
contentPath,
contentPresent: false,
base: FAKE_EXISTING_BASE,
});

expect(resolved).toBe(join(FAKE_EXISTING_BASE, contentPath));
});
expect(resolved).toBe(path.join(FAKE_EXISTING_BASE, contentPath));
}));
});
5 changes: 4 additions & 1 deletion apps/cli/src/command-internal/local-jwt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createHmac, createPrivateKey, createSign } from "node:crypto";
import { DateTime } from "effect";
import { encodeHtmlSafeJsonCompact } from "./html-safe-json.ts";

export { DEFAULT_SIGNING_KEY } from "@supabase/stack/defaults";
Expand Down Expand Up @@ -249,6 +250,8 @@ export function signJwtWithJwk(jwk: Jwk, payloadJson: string): string {
* shape as {@link generateLocalJwt} (`iss`/`role`/`exp`), except the expiry is 10 years from now.
*/
export function generateAsymmetricLocalJwt(jwk: Jwk, role: "anon" | "service_role"): string {
const expiresAt = Math.floor(Date.now() / 1000) + LOCAL_JWT_ASYMMETRIC_EXPIRY_SECONDS;
const expiresAt =
Math.floor(DateTime.toEpochMillis(DateTime.nowUnsafe()) / 1000) +
LOCAL_JWT_ASYMMETRIC_EXPIRY_SECONDS;
return signJwtWithJwk(jwk, JSON.stringify({ iss: LOCAL_JWT_ISSUER, role, exp: expiresAt }));
}
49 changes: 29 additions & 20 deletions apps/cli/src/command-internal/local-jwt.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHmac, generateKeyPairSync } from "node:crypto";
import { importJWK, jwtVerify } from "jose";
import { DateTime } from "effect";
import { describe, expect, it } from "vitest";

import {
Expand Down Expand Up @@ -85,32 +86,38 @@ describe("generateLocalJwt", () => {
});

describe("generateAsymmetricLocalJwt", () => {
it("signs and verifies an RS256 token from an RSA JWK", async () => {
it("signs and verifies an RS256 token from an RSA JWK", () => {
const jwk = generateRsaJwk("rsa-kid");
const token = generateAsymmetricLocalJwt(jwk, "anon");
const publicKey = await importJWK(publicJwkOf(jwk), "RS256");
const { payload, protectedHeader } = await jwtVerify(token, publicKey);
expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" });
expect(protectedHeader).toEqual({ alg: "RS256", kid: "rsa-kid", typ: "JWT" });
return importJWK(publicJwkOf(jwk), "RS256")
.then((publicKey) => jwtVerify(token, publicKey))
.then(({ payload, protectedHeader }) => {
expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" });
expect(protectedHeader).toEqual({ alg: "RS256", kid: "rsa-kid", typ: "JWT" });
});
});

it("signs an RS256 token from an RSA JWK missing CRT exponents (dp/dq/qi)", async () => {
it("signs an RS256 token from an RSA JWK missing CRT exponents (dp/dq/qi)", () => {
const jwk = generateRsaJwk("rsa-kid");
const { dp: _dp, dq: _dq, qi: _qi, ...jwkWithoutCrtParams } = jwk;
const token = generateAsymmetricLocalJwt(jwkWithoutCrtParams, "anon");
const publicKey = await importJWK(publicJwkOf(jwk), "RS256");
const { payload, protectedHeader } = await jwtVerify(token, publicKey);
expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" });
expect(protectedHeader).toEqual({ alg: "RS256", kid: "rsa-kid", typ: "JWT" });
return importJWK(publicJwkOf(jwk), "RS256")
.then((publicKey) => jwtVerify(token, publicKey))
.then(({ payload, protectedHeader }) => {
expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" });
expect(protectedHeader).toEqual({ alg: "RS256", kid: "rsa-kid", typ: "JWT" });
});
});

it("signs and verifies an ES256 token from an EC JWK", async () => {
it("signs and verifies an ES256 token from an EC JWK", () => {
const jwk = generateEcJwk("ec-kid");
const token = generateAsymmetricLocalJwt(jwk, "service_role");
const publicKey = await importJWK(publicJwkOf(jwk), "ES256");
const { payload, protectedHeader } = await jwtVerify(token, publicKey);
expect(payload).toMatchObject({ iss: "supabase-demo", role: "service_role" });
expect(protectedHeader).toEqual({ alg: "ES256", kid: "ec-kid", typ: "JWT" });
return importJWK(publicJwkOf(jwk), "ES256")
.then((publicKey) => jwtVerify(token, publicKey))
.then(({ payload, protectedHeader }) => {
expect(payload).toMatchObject({ iss: "supabase-demo", role: "service_role" });
expect(protectedHeader).toEqual({ alg: "ES256", kid: "ec-kid", typ: "JWT" });
});
});

it("omits the kid header entirely when the JWK has no kid", () => {
Expand All @@ -123,7 +130,7 @@ describe("generateAsymmetricLocalJwt", () => {

it("sets a ~10-year expiry computed from the current time, not a fixed timestamp", () => {
const jwk = generateRsaJwk();
const before = Math.floor(Date.now() / 1000);
const before = Math.floor(DateTime.toEpochMillis(DateTime.nowUnsafe()) / 1000);
const token = generateAsymmetricLocalJwt(jwk, "anon");
const [, payload] = token.split(".");
const decoded = JSON.parse(Buffer.from(payload ?? "", "base64url").toString());
Expand Down Expand Up @@ -204,7 +211,7 @@ describe("generateAsymmetricLocalJwt", () => {
});

describe("signJwtWithJwk", () => {
it("signs the caller's exact pre-encoded payload string verbatim (no re-serialization)", async () => {
it("signs the caller's exact pre-encoded payload string verbatim (no re-serialization)", () => {
const jwk = generateEcJwk("ec-kid");
// Unsorted and containing `&`, which the encoder would normally HTML-escape: this function
// must sign exactly the bytes it's given.
Expand All @@ -213,9 +220,11 @@ describe("signJwtWithJwk", () => {
const [, payload] = token.split(".");
expect(decodeSegment(payload ?? "")).toBe(payloadJson);

const publicKey = await importJWK(publicJwkOf(jwk), "ES256");
const { payload: verified } = await jwtVerify(token, publicKey);
expect(verified).toEqual({ role: "postgres", "sb-role": "mgmt-api & co" });
return importJWK(publicJwkOf(jwk), "ES256")
.then((publicKey) => jwtVerify(token, publicKey))
.then(({ payload: verified }) => {
expect(verified).toEqual({ role: "postgres", "sb-role": "mgmt-api & co" });
});
});

it("HTML-escapes the kid in the header, unlike JSON.stringify", () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/shared/services/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor

FROM supabase/gotrue:v2.197.0 AS gotrue

FROM supabase/realtime:v2.144.2 AS realtime
FROM supabase/realtime:v2.144.3 AS realtime

FROM supabase/storage-api:v1.83.5 AS storage

Expand Down
18 changes: 9 additions & 9 deletions packages/stack/src/Artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,23 +241,23 @@ const definitions: Readonly<Record<ArtifactKind, ArtifactDefinition>> = {
realtime: definition(
"realtime",
{
upstreamVersion: "v2.144.2",
upstreamVersion: "v2.144.3",
revision: 0,
image:
"ghcr.io/supabase/cli/realtime:v2.144.2-r0@sha256:1286f5ef0cb3277153505655173b3bb56eb5b10b22eaf783b5a7b132a0535da4",
upstreamImage: "supabase/realtime:v2.144.2",
"ghcr.io/supabase/cli/realtime:v2.144.3-r0@sha256:06902c6205b9f8a8105d274ff806376f6463a717893f40a4c48e7c6642a892b0",
upstreamImage: "supabase/realtime:v2.144.3",
natives: {
"darwin-arm64": {
archive: "49bf73501f8725394f90a6b97110267d698a9dd9470474793ff88f8de2194280",
manifest: "ff21521edc8b66fc9703e843f54945dc1f29d128011c0c24a0801f23b07f8667",
archive: "b89bb6f01af36639fdbc0cdee176a330f43103f99d03470ba4c13367a26167ba",
manifest: "ecc29ce5e3802c2dc7b9fa494e597d5ffe89a8656650bf73226cfe5207704a43",
},
"linux-amd64": {
archive: "8e0c024828bfb0ba835ae5d78907529edc84a911c7699bf0efa05bf78c7c391f",
manifest: "91f8365ad24506f6bf37be964263f0caa01554b1f4550371891334997e772384",
archive: "5601297a2523b60cd5902ffda214d66a31c9b921d05d0a39704f4411bf05df58",
manifest: "fb8c4028ebeb8aac4abcef8eb6a73a6828668d225845dd70bbb8644a102575ed",
},
"linux-arm64": {
archive: "39134ec990b5c0469c79b69c1c1969f538f8d07edc7e2956afe0654b6b01d1f7",
manifest: "b447d793963a9051ab59435d298f1f627e598e397467e1817d5fd9de6c5ab0f9",
archive: "15f411c9fc4ecfe724571ccc016f0276d953af93e92a6a9173ffdd06f83e5079",
manifest: "2b171e3a171baa7444c79f582c104a6cac7430a605d5c61a151a70c099e0840d",
},
},
},
Expand Down
Loading