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 .codelit-platform.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"name": "SendLit",
"slug": "sendlit"
},
"cliVersion": "0.4.2",
"cliVersion": "0.6.0",
"capabilities": [
"auth",
"mcp",
"observability",
"billing"
],
"managedFiles": {
".github/workflows/platform-conformance.yml": "939bf5686a4433b93153ca2b919ba77251c7bf70b17b269cde02606be41535f4"
".github/workflows/platform-conformance.yml": "2f7ededb9ad4a18228873bf23e5607aaa3822fc236762af2eda1a18a82d35d8f"
},
"productOwnedGlobs": [
"apps/**",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/platform-conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
if: ${{ !hashFiles('bun.lock') }}
- run: bun install --frozen-lockfile
# Pinned to the CLI release that wrote this file. `sync` moves it.
- run: bunx @codelitdev/platform-cli@0.4.2 doctor
- run: bunx @codelitdev/platform-cli@0.6.0 doctor
- run: bun run --filter "*/api" check:drift
# Fails when the API package has no `test:conformance` script.
- run: bun run --filter "*/api" test:conformance
5 changes: 5 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ MEDIALIT_SERVER=https://api.medialit.cloud
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# Cloudflare Turnstile on the sign-in page (https://dash.cloudflare.com/?to=/:account/turnstile).
# Set both to turn it on; it checks the request for a sign-in code.
TURNSTILE_SITE_KEY=
TURNSTILE_SECRET_KEY=

# Hosted abuse controls (cloud only; see src/abuse). Review the queue with
# `bun scripts/abuse.ts queue`.
# ABUSE_CONTROLS=off # switch all of them off
Expand Down
12 changes: 6 additions & 6 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
"@anthropic-ai/sdk": "^0.135.0",
"@better-auth/cimd": "1.7.7",
"@better-auth/oauth-provider": "1.7.7",
"@codelitdev/billing": "0.4.2",
"@codelitdev/mcp-server-kit": "0.4.2",
"@codelitdev/oauth-server-kit": "0.4.2",
"@codelitdev/observability": "0.4.2",
"@codelitdev/platform": "0.4.2",
"@codelitdev/billing": "0.6.0",
"@codelitdev/mcp-server-kit": "0.6.0",
"@codelitdev/oauth-server-kit": "0.6.0",
"@codelitdev/observability": "0.6.0",
"@codelitdev/platform": "0.6.0",
"@modelcontextprotocol/node": "^2.0.0",
"@modelcontextprotocol/server": "^2.0.0",
"@sendlit/api-contract": "workspace:*",
Expand Down Expand Up @@ -64,7 +64,7 @@
},
"devDependencies": {
"@better-auth/core": "1.7.7",
"@codelitdev/platform-conformance": "0.4.2",
"@codelitdev/platform-conformance": "0.6.0",
"@electric-sql/pglite": "0.5.8",
"@modelcontextprotocol/client": "^2.0.0",
"@sendlit/test-utils": "workspace:*",
Expand Down
26 changes: 25 additions & 1 deletion apps/api/src/auth/better-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { cimd } from "@better-auth/cimd";
import { oauthProvider } from "@better-auth/oauth-provider";
import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client";
import { createOAuthProviderOptions } from "@codelitdev/oauth-server-kit/better-auth";
import type { HostedLoginMethod } from "@codelitdev/oauth-server-kit/express";
import type {
HostedLoginCaptcha,
HostedLoginMethod,
} from "@codelitdev/oauth-server-kit/express";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError, createAuthMiddleware } from "better-auth/api";
import { captcha } from "better-auth/plugins";
import { emailOTP } from "better-auth/plugins/email-otp";
import { jwt } from "better-auth/plugins/jwt";
import { createTransport } from "nodemailer";
Expand Down Expand Up @@ -113,6 +117,17 @@ export const mcpProtectedResourceMetadataUrl = `${authBaseUrl}/.well-known/oauth
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;

const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY;
const turnstileSecretKey = process.env.TURNSTILE_SECRET_KEY;
/** Cloudflare Turnstile on the hosted sign-in form, on only when both keys
* are set: the page sends the widget's token and the `captcha` plugin below
* verifies it, so one without the other would refuse every email sign-in.
* Only asking for a code is checked; signing in still needs that code. */
export const hostedLoginCaptcha: HostedLoginCaptcha | undefined =
turnstileSiteKey && turnstileSecretKey
? { provider: "cloudflare-turnstile", siteKey: turnstileSiteKey }
: undefined;

/** Codes of type `email-verification` only confirm deleting an account
* (`POST /account/deletion/code`); every other code signs in. */
function otpEmailCopy(type: string) {
Expand Down Expand Up @@ -294,6 +309,15 @@ export const auth = betterAuth({
max: 3,
},
}),
...(hostedLoginCaptcha && turnstileSecretKey
? [
captcha({
provider: "cloudflare-turnstile",
secretKey: turnstileSecretKey,
endpoints: ["/email-otp/send-verification-otp"],
}),
]
: []),
jwt(),
oauthProvider({
...createOAuthProviderOptions({
Expand Down
12 changes: 12 additions & 0 deletions apps/api/src/auth/oauth-pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mock.module("./better-auth", () => ({
webClientUrl: "http://localhost:3000",
authBasePath: "/api/auth",
hostedLoginMethods: [{ type: "email-otp" }],
hostedLoginCaptcha: { provider: "cloudflare-turnstile", siteKey: "0x4AAAAAAAtest" },
auth: { api: { getSession } },
oauthTeamSelectionAdapter: {
listTeamsForUser,
Expand Down Expand Up @@ -86,6 +87,17 @@ describe("SendLit OAuth pages", () => {
expect(response.headers.get("x-frame-options")).toBe("DENY");
});

it.each(["/login", "/oauth/login"])(
"renders the Turnstile widget on %s when it is configured",
async (path) => {
const response = await request(path);
expect(response.body).toContain(
'class="cf-turnstile" data-sitekey="0x4AAAAAAAtest"',
);
expect(response.body).toContain("var captcha=true");
},
);

it("routes an unauthenticated team selection through OAuth login", async () => {
const response = await request(
"/oauth/select-team?client_id=abc&scope=contacts:read",
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/auth/oauth-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Router } from "express";
import {
auth,
authBasePath,
hostedLoginCaptcha,
hostedLoginMethods,
oauthTeamSelectionAdapter,
webClientUrl,
Expand All @@ -21,6 +22,7 @@ router.use(
faviconUrl: new URL("/icon.svg", webClientUrl).toString(),
primaryColor: "oklch(0.47 0.14 150)",
loginMethods: hostedLoginMethods,
captcha: hostedLoginCaptcha,
legacyHostOnlySessionCookieNames: process.env.AUTH_COOKIE_DOMAIN
? [
"__Secure-better-auth.session_token",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"@codelitdev/design-system": "0.4.2",
"@codelitdev/design-system": "0.6.0",
"@sendlit/api-contract": "workspace:*",
"@sendlit/email-blocks": "workspace:*",
"@sendlit/email-editor": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@codelitdev/design-system": "0.4.2",
"@codelitdev/observability": "0.4.2",
"@codelitdev/design-system": "0.6.0",
"@codelitdev/observability": "0.6.0",
"@radix-ui/react-alert-dialog": "^1.1.18",
"@radix-ui/react-dialog": "^1.1.18",
"@radix-ui/react-label": "^2.1.11",
Expand Down
Loading
Loading