feat: migrate to Nuxt 4 (Nitro) and Tailwind CSS v3

This commit is contained in:
RhenCloud 2026-08-13 17:43:33 +08:00
parent f4959eebf8
commit b139712a91
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
166 changed files with 19790 additions and 5539 deletions

View file

@ -0,0 +1,20 @@
import type { Env, WebhookEvent } from "../../types";
import type { Provider } from "../types";
import { verifyGiteaSignature } from "./verify";
import { parseGiteaEvent } from "./parse";
export const giteaProvider: Provider = {
id: "gitea",
matches(headers) {
return headers["x-gitea-event"] !== undefined;
},
async verify(body, headers, env: Env) {
return verifyGiteaSignature(body, headers["x-gitea-signature"], env.GITEA_WEBHOOK_SECRET);
},
parse(body, headers): WebhookEvent | null {
return parseGiteaEvent(headers, body);
},
};

View file

@ -0,0 +1,76 @@
import type { WebhookEvent } from "../../types";
/**
* Gitea webhook events that map to a different internal event name. Everything
* else already uses the same name as GitHub (push, issues, release, ...).
*/
const EVENT_MAP: Record<string, string> = {
pull_request_comment: "pull_request_review_comment",
};
/**
* Normalize a Gitea webhook payload so the shared GitHub-shaped formatters can
* consume it. Gitea models its payloads on GitHub but with a few differences.
*/
function normalizePayload(
event: string,
payload: Record<string, unknown>,
): Record<string, unknown> {
if (event === "push") {
// Gitea sends `compare_url` (GitHub sends `compare`).
if (payload.compare_url && payload.compare === undefined) {
payload.compare = payload.compare_url;
}
// Gitea sends `pusher` (and `sender`); keep a `sender` for the formatters.
if (!payload.sender && payload.pusher) {
payload.sender = payload.pusher;
}
}
if (event === "pull_request_comment") {
// GitHub names this event pull_request_review_comment and always includes
// a top-level `pull_request`. Gitea may only carry the PR-as-issue.
if (!payload.pull_request && payload.issue) {
payload.pull_request = payload.issue;
}
// GitHub uses `comment.position` for the line number, Gitea uses `line`.
const comment = payload.comment as { line?: number; position?: number } | undefined;
if (comment && comment.position === undefined && comment.line !== undefined) {
comment.position = comment.line;
}
}
if (event === "commit_comment") {
// Gitea puts the commit id at the top level, GitHub on the comment object.
const comment = payload.comment as { commit_id?: string } | undefined;
if (comment && !comment.commit_id && typeof payload.commit_id === "string") {
comment.commit_id = payload.commit_id;
}
}
return payload;
}
export function parseGiteaEvent(
headers: Record<string, string>,
body: string,
): WebhookEvent | null {
const event = headers["x-gitea-event"];
const signature = headers["x-gitea-signature"];
const deliveryId = headers["x-gitea-delivery"];
if (!event) return null;
try {
const payload = normalizePayload(event, JSON.parse(body));
return {
provider: "gitea",
event: EVENT_MAP[event] ?? event,
payload,
signature,
deliveryId,
};
} catch {
return null;
}
}

View file

@ -0,0 +1,15 @@
import { hmacSha256Hex, timingSafeEqual } from "../hmac";
/**
* Gitea signs webhooks with the HMAC-SHA256 hex digest of the raw body in the
* `X-Gitea-Signature` header (no `sha256=` prefix, unlike GitHub).
*/
export async function verifyGiteaSignature(
payload: string,
signature: string | undefined,
secret: string | undefined,
): Promise<boolean> {
if (!signature || !secret) return false;
const expected = await hmacSha256Hex(secret, payload);
return timingSafeEqual(signature, expected);
}