From 8a07d4d8396d712ad383ab1086fa68601301b8b9 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Tue, 19 May 2026 11:23:07 +0100 Subject: [PATCH] refactor: rename and enhance CORS path handling for public API and widgets --- apps/api/src/app.ts | 6 +++--- apps/api/src/middlewares/cors.ts | 15 +++++++++++---- apps/api/src/routes/widgets/widgets.ts | 3 ++- apps/website/src/lib/components/navigation.svelte | 4 ++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 4542f360..67980ed2 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -6,7 +6,7 @@ import { Hono } from '@api/utils/hono'; import { accountRouter } from '@api/routes/account'; import { auth } from '@cio/db/auth'; import { communityRouter } from '@api/routes/community'; -import { isPublicApiPath, sessionCors } from '@api/middlewares/cors'; +import { isPublicCorsPath, sessionCors } from '@api/middlewares/cors'; import { courseRouter } from '@api/routes/course'; import { dashAnalyticsRouter } from '@api/routes/dash'; import { domainRouter } from '@api/routes/domain/domain'; @@ -42,13 +42,13 @@ export const app = new Hono() .use('*', prettyJSON()) .use('*', secureHeaders()) .use('*', async (c, next) => { - if (isPublicApiPath(c.req.path)) return next(); + if (isPublicCorsPath(c.req.path)) return next(); return sessionCors(c, next); }) .use('*', rateLimiter) .use('*', async (c, next) => { - if (isPublicApiPath(c.req.path)) { + if (isPublicCorsPath(c.req.path)) { c.set('user', null); c.set('session', null); c.set('orgRoles', {}); diff --git a/apps/api/src/middlewares/cors.ts b/apps/api/src/middlewares/cors.ts index 360d113a..1eb7add2 100644 --- a/apps/api/src/middlewares/cors.ts +++ b/apps/api/src/middlewares/cors.ts @@ -28,9 +28,16 @@ export const sessionCors = cors({ credentials: true }); -/** Path prefix considered "public REST API" for CORS / session bypass. */ -export const PUBLIC_API_PATH_PREFIX = '/public-api/'; +/** + * Path prefixes that must accept requests from any third-party origin (no cookies): + * + * - `/public-api/`: Bearer-token REST API (automation keys). + * - `/widgets/`: Anonymous payload endpoint hit by the course-widget embed script on customer sites. + * + * Requests under these prefixes use `publicApiCors` and skip the Better Auth session lookup. + */ +export const PUBLIC_CORS_PATH_PREFIXES = ['/public-api/', '/widgets/'] as const; -export function isPublicApiPath(path: string): boolean { - return path.startsWith(PUBLIC_API_PATH_PREFIX); +export function isPublicCorsPath(path: string): boolean { + return PUBLIC_CORS_PATH_PREFIXES.some((prefix) => path.startsWith(prefix)); } diff --git a/apps/api/src/routes/widgets/widgets.ts b/apps/api/src/routes/widgets/widgets.ts index b3fa1d46..126803aa 100644 --- a/apps/api/src/routes/widgets/widgets.ts +++ b/apps/api/src/routes/widgets/widgets.ts @@ -5,6 +5,7 @@ import { ZWidgetPublicKeyParams } from '@cio/utils/validation/widget'; import { zValidator } from '@hono/zod-validator'; import { createRateLimiter } from '@api/middlewares/rate-limiter'; import { DEFAULT_WINDOW_MS } from '@api/constants/rate-limiter'; +import { publicApiCors } from '@api/middlewares/cors'; const widgetPayloadIpLimiter = createRateLimiter({ maxRequests: 100, @@ -19,7 +20,7 @@ const widgetPayloadKeyLimiter = createRateLimiter({ keyGenerator: (c) => `widget_payload_key:${c.req.param('publicKey')}` }); -export const publicWidgetsRouter = new Hono().get( +export const publicWidgetsRouter = new Hono().use('*', publicApiCors).get( '/:publicKey/payload', zValidator('param', ZWidgetPublicKeyParams), widgetPayloadIpLimiter, diff --git a/apps/website/src/lib/components/navigation.svelte b/apps/website/src/lib/components/navigation.svelte index 546735ec..628d05f0 100644 --- a/apps/website/src/lib/components/navigation.svelte +++ b/apps/website/src/lib/components/navigation.svelte @@ -251,8 +251,8 @@
- classroomio logo -

ClassroomIO

+ classroomio logo +

ClassroomIO