fix(deploy): wire Phase 3 services + GitHub git-sync webhook
CI / typecheck + build (turbo) (push) Canceled after 0s

- apps/api: assertWebhookAuth now verifies GitHub X-Hub-Signature-256 HMAC
  (raw-body HMAC-SHA256) AND the manual x-webhook-secret header. GitHub does
  not send a custom header, so only the HMAC path made the push webhook work.
- root package.json: api/worker scripts use absolute bun path + direct-file
  form (bun --cwd apps/api run dev errored in bun 1.3.14).
- deploy/*.service: ExecStart uses /home/code/.bun/bin/bun (systemd PATH lacks bun).
- GitHub push webhook created -> https://wiki.asepharyana.my.id/hooks/reindex
  (verified: ping + push deliveries return 200, worker drains, 0 failed).
- Caddy: expose /hooks/* + /health on wiki.asepharyana.my.id -> :4020.
Services mcpedia-api + mcpedia-worker now enabled + active on host.
This commit is contained in:
asepharyana
2026-08-20 09:38:32 +07:00
parent 2fac2b5230
commit ec0ab4dbb3
5 changed files with 53 additions and 8 deletions
+23 -4
View File
@@ -1,5 +1,7 @@
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import type { Context as HonoContext } from "hono";
import { createHmac, timingSafeEqual } from "node:crypto";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { db } from "@mcpedia/db";
import { appRouter } from "./router";
@@ -22,8 +24,25 @@ app.get("/health", (c) => c.json({ ok: true }));
// Shared guard for the git-sync webhooks: require `x-webhook-secret` header to
// match the configured secret. Reject anything else with 401.
function assertWebhookAuth(c: { req: { header: (k: string) => string | undefined } }): boolean {
const provided = c.req.header("x-webhook-secret");
// Verify a git-provider webhook. Supports GitHub's native HMAC signature
// (X-Hub-Signature-256 = HMAC-SHA256 of the raw body with the webhook secret) and a
// plain `x-webhook-secret` header for manual/local triggers. GitHub does NOT send a
// custom header, so the HMAC path is what a real GitHub delivery will hit.
async function assertWebhookAuth(c: HonoContext): Promise<boolean> {
if (!WEBHOOK_SECRET) return false;
const raw = c.req.raw;
const ghSig = raw.headers.get("x-hub-signature-256");
if (ghSig && ghSig.startsWith("sha256=")) {
try {
const body = await raw.text();
const mac = createHmac("sha256", WEBHOOK_SECRET).update(body).digest("hex");
const expected = `sha256=${mac}`;
return timingSafeEqual(Buffer.from(ghSig), Buffer.from(expected));
} catch {
return false;
}
}
const provided = raw.headers.get("x-webhook-secret");
return provided != null && provided === WEBHOOK_SECRET;
}
@@ -32,13 +51,13 @@ function assertWebhookAuth(c: { req: { header: (k: string) => string | undefined
// POST /hooks/index?slug=... -> enqueue a single document reindex
// Returns the created job id(s). The worker processes them asynchronously.
app.post("/hooks/reindex", async (c) => {
if (!assertWebhookAuth(c)) return c.json({ ok: false, error: "unauthorized" }, 401);
if (!(await assertWebhookAuth(c))) return c.json({ ok: false, error: "unauthorized" }, 401);
const job = await enqueueFullIndex("git-push");
return c.json({ ok: true, jobId: job.id, kind: "full" });
});
app.post("/hooks/index", async (c) => {
if (!assertWebhookAuth(c)) return c.json({ ok: false, error: "unauthorized" }, 401);
if (!(await assertWebhookAuth(c))) return c.json({ ok: false, error: "unauthorized" }, 401);
const slug = c.req.query("slug");
if (!slug) return c.json({ ok: false, error: "slug query param required" }, 400);
// slug is the relative path without extension, e.g. docs/websocket/contract