Skip to content
Closed
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
21 changes: 17 additions & 4 deletions convex/downloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ 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: { 'Access-Control-Allow-Origin': '*' },
})
}

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: { 'Access-Control-Allow-Origin': '*' },
})
}

const skill = skillResult.skill
Expand All @@ -35,10 +41,16 @@ 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: { 'Access-Control-Allow-Origin': '*' },
})
}
if (version.softDeletedAt) {
return new Response('Version not available', { status: 410 })
return new Response('Version not available', {
status: 410,
headers: { 'Access-Control-Allow-Origin': '*' },
})
}

const entries: Array<{ path: string; bytes: Uint8Array }> = []
Expand All @@ -64,6 +76,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',
'Access-Control-Allow-Origin': '*',
},
})
})
Expand Down
7 changes: 7 additions & 0 deletions convex/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
usersListV1Http,
usersPostRouterV1Http,
whoamiV1Http,
preflightHandler,
} from './httpApiV1'

const http = httpRouter()
Expand Down Expand Up @@ -145,6 +146,12 @@ http.route({
handler: soulsDeleteRouterV1Http,
})

http.route({
pathPrefix: '/api/',
method: 'OPTIONS',
handler: preflightHandler,
})

// TODO: remove legacy /api routes after deprecation window.
http.route({
path: LegacyApiRoutes.download,
Expand Down
16 changes: 16 additions & 0 deletions convex/httpApiV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
// reading localStorage tokens on this origin.
'Content-Security-Policy':
"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
'Access-Control-Allow-Origin': '*',
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
})
return new Response(textContent, { status: 200, headers })
Expand Down Expand Up @@ -438,6 +439,18 @@ async function publishSkillV1Handler(ctx: ActionCtx, request: Request) {

export const publishSkillV1Http = httpAction(publishSkillV1Handler)

export const preflightHandler = httpAction(async (_ctx, _request) => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, PATCH',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Digest, X-Clawhub-Version',
'Access-Control-Max-Age': '86400',
},
})
})

type FileLike = {
name: string
size: number
Expand Down Expand Up @@ -864,6 +877,7 @@ function json(value: unknown, status = 200, headers?: HeadersInit) {
{
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Access-Control-Allow-Origin': '*',
},
headers,
),
Expand All @@ -877,6 +891,7 @@ function text(value: string, status: number, headers?: HeadersInit) {
{
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-store',
'Access-Control-Allow-Origin': '*',
},
headers,
),
Expand Down Expand Up @@ -1146,6 +1161,7 @@ async function soulsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
// reading localStorage tokens on this origin.
'Content-Security-Policy':
"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
'Access-Control-Allow-Origin': '*',
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
})
return new Response(textContent, { status: 200, headers })
Expand Down
8 changes: 6 additions & 2 deletions packages/clawdhub/src/cli/commands/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { getRegistry } from '../registry.js'
import type { GlobalOpts, ResolveResult } from '../types.js'
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
import { readGlobalConfig } from '../../config.js'

export async function cmdSearch(opts: GlobalOpts, query: string, limit?: number) {
if (!query) fail('Query required')
Expand Down Expand Up @@ -61,6 +62,9 @@ export async function cmdInstall(
const trimmed = slug.trim()
if (!trimmed) fail('Slug required')

const cfg = await readGlobalConfig()
const token = cfg?.token ?? undefined

const registry = await getRegistry(opts, { cache: true })
await mkdir(opts.dir, { recursive: true })
const target = join(opts.dir, trimmed)
Expand All @@ -78,15 +82,15 @@ export async function cmdInstall(
(
await apiRequest(
registry,
{ method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}` },
{ method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}`, token },
ApiV1SkillResponseSchema,
)
).latestVersion?.version ??
null
if (!resolvedVersion) fail('Could not resolve latest version')

spinner.text = `Downloading ${trimmed}@${resolvedVersion}`
const zip = await downloadZip(registry, { slug: trimmed, version: resolvedVersion })
const zip = await downloadZip(registry, { slug: trimmed, version: resolvedVersion, token })
await extractZipToDir(zip, target)

await writeSkillOrigin(target, {
Expand Down
24 changes: 20 additions & 4 deletions packages/clawdhub/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,19 +157,29 @@ export async function fetchText(registry: string, args: TextRequestArgs): Promis
)
}

export async function downloadZip(registry: string, args: { slug: string; version?: string }) {
export async function downloadZip(
registry: string,
args: { slug: string; version?: string; token?: string },
) {
const url = new URL(ApiRoutes.download, registry)
url.searchParams.set('slug', args.slug)
if (args.version) url.searchParams.set('version', args.version)
return pRetry(
async () => {
if (isBun) {
return await fetchBinaryViaCurl(url.toString())
return await fetchBinaryViaCurl(url.toString(), args.token)
}

const headers: Record<string, string> = {}
if (args.token) headers.Authorization = `Bearer ${args.token}`

const controller = new AbortController()
const timeout = setTimeout(() => controller.abort('Timeout'), REQUEST_TIMEOUT_MS)
const response = await fetch(url.toString(), { method: 'GET', signal: controller.signal })
const response = await fetch(url.toString(), {
method: 'GET',
headers,
signal: controller.signal,
})
clearTimeout(timeout)
if (!response.ok) {
const message = (await response.text().catch(() => '')) || `HTTP ${response.status}`
Expand Down Expand Up @@ -321,16 +331,22 @@ async function fetchTextViaCurl(url: string, args: { token?: string }) {
return body
}

async function fetchBinaryViaCurl(url: string) {
async function fetchBinaryViaCurl(url: string, token?: string) {
const tempDir = await mkdtemp(join(tmpdir(), 'clawhub-download-'))
const filePath = join(tempDir, 'payload.bin')
try {
const headers: string[] = []
if (token) {
headers.push('-H', `Authorization: Bearer ${token}`)
}

const curlArgs = [
'--silent',
'--show-error',
'--location',
'--max-time',
String(REQUEST_TIMEOUT_SECONDS),
...headers,
'-o',
filePath,
'--write-out',
Expand Down