From 3393d886fddb0733b9cfd7207a6c474ceba53f06 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Mon, 3 Aug 2026 15:11:07 +0330 Subject: [PATCH 1/3] add exist endpoint --- packages/app/server/api/repo/exists.get.ts | 31 ++++++++++++++++++++++ packages/app/server/utils/bucket.ts | 20 ++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 packages/app/server/api/repo/exists.get.ts diff --git a/packages/app/server/api/repo/exists.get.ts b/packages/app/server/api/repo/exists.get.ts new file mode 100644 index 00000000..304cbdcb --- /dev/null +++ b/packages/app/server/api/repo/exists.get.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { hasRepoReleases } from "../../utils/bucket"; + +const querySchema = z.object({ + owner: z.string().min(1), + repo: z.string().min(1), +}); + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, (data) => + querySchema.parse(data), + ); + const owner = query.owner.trim(); + const repo = query.repo.trim(); + + const exactMatch = await hasRepoReleases(event, owner, repo); + const normalizedMatch = + exactMatch || + (await hasRepoReleases(event, owner.toLowerCase(), repo.toLowerCase())); + + setHeader( + event, + "Cache-Control", + "public, max-age=60, s-maxage=300, stale-while-revalidate=600", + ); + + return { + id: `${owner}/${repo}`, + exists: normalizedMatch, + }; +}); diff --git a/packages/app/server/utils/bucket.ts b/packages/app/server/utils/bucket.ts index f962525f..c05ed490 100644 --- a/packages/app/server/utils/bucket.ts +++ b/packages/app/server/utils/bucket.ts @@ -111,6 +111,26 @@ export async function getRepoReleaseCount( } } +export async function hasRepoReleases( + event: Event, + owner: string, + repo: string, +): Promise { + try { + const binding = useBinding(event); + const prefix = `${usePackagesBucket.base}:${owner}:${repo}:`; + const response = await binding.list({ + limit: 1, + prefix, + } as any); + + return response.objects.length > 0; + } catch (error) { + console.error(`Error checking releases for ${owner}/${repo}:`, error); + return false; + } +} + export function useTemplatesBucket(event: Event) { const storage = useBucket(event); return prefixStorage(storage, useTemplatesBucket.key); From 0700e7df158d23cb34c1c419bccbc285dd7fdd5c Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Tue, 4 Aug 2026 17:23:39 +0330 Subject: [PATCH 2/3] HEAD method --- packages/app/server/api/repo/exists.get.ts | 31 ---------------- .../server/middleware/repo-release-head.ts | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 31 deletions(-) delete mode 100644 packages/app/server/api/repo/exists.get.ts create mode 100644 packages/app/server/middleware/repo-release-head.ts diff --git a/packages/app/server/api/repo/exists.get.ts b/packages/app/server/api/repo/exists.get.ts deleted file mode 100644 index 304cbdcb..00000000 --- a/packages/app/server/api/repo/exists.get.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod"; -import { hasRepoReleases } from "../../utils/bucket"; - -const querySchema = z.object({ - owner: z.string().min(1), - repo: z.string().min(1), -}); - -export default defineEventHandler(async (event) => { - const query = await getValidatedQuery(event, (data) => - querySchema.parse(data), - ); - const owner = query.owner.trim(); - const repo = query.repo.trim(); - - const exactMatch = await hasRepoReleases(event, owner, repo); - const normalizedMatch = - exactMatch || - (await hasRepoReleases(event, owner.toLowerCase(), repo.toLowerCase())); - - setHeader( - event, - "Cache-Control", - "public, max-age=60, s-maxage=300, stale-while-revalidate=600", - ); - - return { - id: `${owner}/${repo}`, - exists: normalizedMatch, - }; -}); diff --git a/packages/app/server/middleware/repo-release-head.ts b/packages/app/server/middleware/repo-release-head.ts new file mode 100644 index 00000000..47d6a283 --- /dev/null +++ b/packages/app/server/middleware/repo-release-head.ts @@ -0,0 +1,35 @@ +import { hasRepoReleases } from "../utils/bucket"; + +const REPO_HEAD_PATH = /^\/~\/([^/]+)\/([^/]+)\/?$/; + +export default eventHandler(async (event) => { + if (event.method !== "HEAD") return; + + let pathname: string; + try { + pathname = decodeURIComponent(getRequestURL(event).pathname); + } catch { + return; + } + + const match = pathname.match(REPO_HEAD_PATH); + if (!match) return; + + const owner = match[1]?.trim(); + const repo = match[2]?.trim(); + if (!owner || !repo) return; + + const exactMatch = await hasRepoReleases(event, owner, repo); + const normalizedMatch = + exactMatch || + (await hasRepoReleases(event, owner.toLowerCase(), repo.toLowerCase())); + + setHeader( + event, + "Cache-Control", + "public, max-age=60, s-maxage=300, stale-while-revalidate=600", + ); + setHeader(event, "x-has-releases", normalizedMatch ? "1" : "0"); + setResponseStatus(event, normalizedMatch ? 200 : 404); + return send(event, null); +}); From 404fa4b40f2b788c925eef72b74e792ecd99f52b Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Tue, 4 Aug 2026 20:27:46 +0330 Subject: [PATCH 3/3] use nitro head --- .../server/middleware/repo-release-head.ts | 35 ----------- .../server/routes/~/[owner]/[repo].head.ts | 60 +++++++++++++++++++ 2 files changed, 60 insertions(+), 35 deletions(-) delete mode 100644 packages/app/server/middleware/repo-release-head.ts create mode 100644 packages/app/server/routes/~/[owner]/[repo].head.ts diff --git a/packages/app/server/middleware/repo-release-head.ts b/packages/app/server/middleware/repo-release-head.ts deleted file mode 100644 index 47d6a283..00000000 --- a/packages/app/server/middleware/repo-release-head.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { hasRepoReleases } from "../utils/bucket"; - -const REPO_HEAD_PATH = /^\/~\/([^/]+)\/([^/]+)\/?$/; - -export default eventHandler(async (event) => { - if (event.method !== "HEAD") return; - - let pathname: string; - try { - pathname = decodeURIComponent(getRequestURL(event).pathname); - } catch { - return; - } - - const match = pathname.match(REPO_HEAD_PATH); - if (!match) return; - - const owner = match[1]?.trim(); - const repo = match[2]?.trim(); - if (!owner || !repo) return; - - const exactMatch = await hasRepoReleases(event, owner, repo); - const normalizedMatch = - exactMatch || - (await hasRepoReleases(event, owner.toLowerCase(), repo.toLowerCase())); - - setHeader( - event, - "Cache-Control", - "public, max-age=60, s-maxage=300, stale-while-revalidate=600", - ); - setHeader(event, "x-has-releases", normalizedMatch ? "1" : "0"); - setResponseStatus(event, normalizedMatch ? 200 : 404); - return send(event, null); -}); diff --git a/packages/app/server/routes/~/[owner]/[repo].head.ts b/packages/app/server/routes/~/[owner]/[repo].head.ts new file mode 100644 index 00000000..10a7b95c --- /dev/null +++ b/packages/app/server/routes/~/[owner]/[repo].head.ts @@ -0,0 +1,60 @@ +import { + createError, + defineEventHandler, + getRouterParams, + setHeader, +} from "h3"; +import { hasRepoReleases } from "../../../utils/bucket"; + +export default defineEventHandler(async (event) => { + const { owner, repo } = getRouterParams(event) as { + owner?: string; + repo?: string; + }; + + if (!owner || !repo) { + throw createError({ + statusCode: 400, + statusMessage: "Owner and repo are required", + }); + } + + const normalizedOwner = owner.trim(); + const normalizedRepo = repo.trim(); + + if (!normalizedOwner || !normalizedRepo) { + throw createError({ + statusCode: 400, + statusMessage: "Owner and repo are required", + }); + } + + const exactMatch = await hasRepoReleases( + event, + normalizedOwner, + normalizedRepo, + ); + const hasReleases = + exactMatch || + (await hasRepoReleases( + event, + normalizedOwner.toLowerCase(), + normalizedRepo.toLowerCase(), + )); + + setHeader( + event, + "Cache-Control", + "public, max-age=60, s-maxage=300, stale-while-revalidate=600", + ); + setHeader(event, "x-has-releases", hasReleases ? "1" : "0"); + + if (!hasReleases) { + throw createError({ + statusCode: 404, + statusMessage: "No releases found", + }); + } + + return null; +});