mirror of
https://github.com/openclaw/clawhub.git
synced 2026-07-22 03:35:29 -04:00
feat(api): add CORS headers for browser clients
- Add convex/lib/cors.ts with shared CORS headers and preflight handler - Update httpApi.ts and httpApiV1.ts json/text helpers to include CORS headers - Update downloads.ts to include CORS headers on all responses - Add single OPTIONS handler for /api/* prefix in http.ts This enables browser-based clients (like Cove WebUI) to fetch skills directly from the ClawHub API without CORS errors. Allows: GET, POST, DELETE, OPTIONS from any origin (*)
This commit is contained in:
+6
-4
@@ -2,6 +2,7 @@ import { v } from 'convex/values'
|
||||
import { zipSync } from 'fflate'
|
||||
import { api } from './_generated/api'
|
||||
import { httpAction, mutation } from './_generated/server'
|
||||
import { CORS_HEADERS } from './lib/cors'
|
||||
import { insertStatEvent } from './skillStatEvents'
|
||||
|
||||
export const downloadZip = httpAction(async (ctx, request) => {
|
||||
@@ -11,12 +12,12 @@ export const downloadZip = httpAction(async (ctx, request) => {
|
||||
const tagParam = url.searchParams.get('tag')?.trim()
|
||||
|
||||
if (!slug) {
|
||||
return new Response('Missing slug', { status: 400 })
|
||||
return new Response('Missing slug', { status: 400, headers: CORS_HEADERS })
|
||||
}
|
||||
|
||||
const skillResult = await ctx.runQuery(api.skills.getBySlug, { slug })
|
||||
if (!skillResult?.skill) {
|
||||
return new Response('Skill not found', { status: 404 })
|
||||
return new Response('Skill not found', { status: 404, headers: CORS_HEADERS })
|
||||
}
|
||||
|
||||
const skill = skillResult.skill
|
||||
@@ -35,10 +36,10 @@ export const downloadZip = httpAction(async (ctx, request) => {
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
return new Response('Version not found', { status: 404 })
|
||||
return new Response('Version not found', { status: 404, headers: CORS_HEADERS })
|
||||
}
|
||||
if (version.softDeletedAt) {
|
||||
return new Response('Version not available', { status: 410 })
|
||||
return new Response('Version not available', { status: 410, headers: CORS_HEADERS })
|
||||
}
|
||||
|
||||
const files: Record<string, Uint8Array> = {}
|
||||
@@ -61,6 +62,7 @@ export const downloadZip = httpAction(async (ctx, request) => {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Disposition': `attachment; filename="${slug}-${version.version}.zip"`,
|
||||
'Cache-Control': 'private, max-age=60',
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,6 +30,11 @@ import {
|
||||
starsPostRouterV1Http,
|
||||
whoamiV1Http,
|
||||
} from './httpApiV1'
|
||||
import { corsPreflightResponse } from './lib/cors'
|
||||
import { httpAction } from './_generated/server'
|
||||
|
||||
// CORS preflight handler for browser clients
|
||||
const corsPreflightHttp = httpAction(async () => corsPreflightResponse())
|
||||
|
||||
const http = httpRouter()
|
||||
|
||||
@@ -191,4 +196,11 @@ http.route({
|
||||
handler: cliSkillUndeleteHttp,
|
||||
})
|
||||
|
||||
// CORS preflight handler for all API routes
|
||||
http.route({
|
||||
pathPrefix: '/api/',
|
||||
method: 'OPTIONS',
|
||||
handler: corsPreflightHttp,
|
||||
})
|
||||
|
||||
export default http
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { Id } from './_generated/dataModel'
|
||||
import type { ActionCtx } from './_generated/server'
|
||||
import { httpAction } from './_generated/server'
|
||||
import { requireApiTokenUser } from './lib/apiTokenAuth'
|
||||
import { CORS_HEADERS } from './lib/cors'
|
||||
import { publishVersionForUser } from './skills'
|
||||
|
||||
type SearchSkillEntry = {
|
||||
@@ -244,6 +245,7 @@ function json(value: unknown, status = 200) {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -254,6 +256,7 @@ function text(value: string, status: number) {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Doc, Id } from './_generated/dataModel'
|
||||
import type { ActionCtx } from './_generated/server'
|
||||
import { httpAction } from './_generated/server'
|
||||
import { requireApiTokenUser } from './lib/apiTokenAuth'
|
||||
import { CORS_HEADERS } from './lib/cors'
|
||||
import { hashToken } from './lib/tokens'
|
||||
import { publishVersionForUser } from './skills'
|
||||
import { publishSoulVersionForUser } from './souls'
|
||||
@@ -394,6 +395,7 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
'Content-Security-Policy':
|
||||
"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
||||
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
|
||||
...CORS_HEADERS,
|
||||
})
|
||||
return new Response(textContent, { status: 200, headers })
|
||||
}
|
||||
@@ -731,6 +733,7 @@ function json(value: unknown, status = 200, headers?: HeadersInit) {
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
headers,
|
||||
),
|
||||
@@ -744,6 +747,7 @@ function text(value: string, status: number, headers?: HeadersInit) {
|
||||
{
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
headers,
|
||||
),
|
||||
@@ -1014,6 +1018,7 @@ async function soulsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
'Content-Security-Policy':
|
||||
"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
||||
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
|
||||
...CORS_HEADERS,
|
||||
})
|
||||
return new Response(textContent, { status: 200, headers })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* CORS headers for public API endpoints.
|
||||
*
|
||||
* ClawHub is a public skill registry. Browser-based clients (like Cove WebUI)
|
||||
* need CORS headers to fetch skills directly from the API.
|
||||
*/
|
||||
|
||||
export const CORS_HEADERS = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400', // 24 hours
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Handle CORS preflight (OPTIONS) requests.
|
||||
*/
|
||||
export function corsPreflightResponse(): Response {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: CORS_HEADERS,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user