Teraz z AI - twórz strony przez serwer MCP

Webhooki

Jedenaście zdarzeń, podpis HMAC po surowym body, osiem prób z backoffem - i jedno zdarzenie, które trzyma headlessowy frontend świeżym.

Webhook to sposób, w jaki cmssy mówi Twojej aplikacji, że coś się zmieniło - zamiast tego, żeby aplikacja pytała. Jest jedenaście zdarzeń, jeden kształt dostawy i jeden szczegół ważniejszy od reszty: content.changed zamienia publikację w żywą stronę na scache'owanym froncie.

Zdarzenia

list_webhook_event_types to autorytatywna lista - odczytaj ją, zamiast zgadywać nazwę. Dziś zwraca jedenaście:

  • Zamówienia - order.created, order.partially_paid, order.paid, order.partially_refunded, order.refunded, order.canceled, order.fulfilled, order.returned, order.edited, order.pipeline_changed.
  • Treść - content.changed, jedno zgrubne zdarzenie dla stron, rekordów, formularzy i ustawień workspace'u.

content.changed jest zgrubne celowo

Nie ma page.published. Jedno zdarzenie obejmuje każdą mutację treści, a payload mówi, co się stało:

{
  "kind": "page",          // page | record | form | settings
  "action": "published",   // published | unpublished | created | updated | deleted
  "ids": ["6a63..."],
  "slug": "/pricing",      // tylko strony
  "modelSlug": null         // tylko rekordy
}

Zasubskrybuj raz i traktuj każdą parę jako "unieważnij". W tym rzecz: nowy rodzaj podmiotu dodany później dotrze do Ciebie bez nowej subskrypcji.

Ustawienia są jednym z takich rodzajów. Zmiana brandingu albo włączonych języków emituje kind: "settings" z action: "updated", pustą tablicą ids i slug równym null - nic nie wskazuje na jedną stronę, więc unieważnij całe drzewo. Bez tego nowe logo albo świeżo włączony język czeka, aż wygaśnie Twój cache.

Dwa brzegi warte obsłużenia. Operacja masowa, której zbiór dotkniętych obiektów jest nieznany albo większy niż 100, wysyła pustą tablicę ids - potraktuj to jako "unieważnij wszystko". A przy wsadowym usunięciu stron slug to korzeń usuniętego poddrzewa; potomkowie z ids mieszkali pod innymi slugami.

Jak utrzymać świeżość scache'owanego frontu

Publikacja nie deployuje i nie omija też Twojego cache'u. Strona z revalidate = 3600 serwuje starą kopię nawet przez godzinę, dopóki coś jej nie unieważni. Tym czymś jest webhook content.changed wycelowany w route rewalidacji:

// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { NextResponse, type NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const secret = process.env.CMSSY_REVALIDATE_SECRET;
  const provided =
    request.nextUrl.searchParams.get("secret") ??
    request.headers.get("x-revalidate-secret");
  if (!secret || provided !== secret) {
    return NextResponse.json({ revalidated: false }, { status: 401 });
  }

  const { data } = (await request.json().catch(() => ({}))) as {
    data?: { subject?: { slug?: string | null } };
  };
  const slug = data?.subject?.slug;

  // Brak sluga znaczy, że zbiór jest nieznany - unieważnij całe drzewo.
  if (slug) revalidatePath(slug);
  else revalidatePath("/", "layout");

  return NextResponse.json({ revalidated: true, path: slug ?? "/" });
}

Jeśli Twoje URL-e niosą prefiks języka, rewaliduj też ścieżki zlokalizowane - każda jest cache'owana osobno.

Jak wygląda dostawa

Każda dostawa to POST z content-type: application/json i takim body:

{
  "id": "6a64...",                       // id endpointu webhooka
  "event": "content.changed",
  "createdAt": "2026-07-25T18:30:00.000Z",
  "data": { "workspaceId": "...", "subject": { } }
}

Towarzyszą jej trzy nagłówki: x-cmssy-event, x-cmssy-webhook-id i x-cmssy-signature.

Zweryfikuj podpis

Nagłówek podpisu ma postać t=<unix-ms>,v1=<hex>, gdzie hex to HMAC-SHA256 po <t>.<surowe body> z kluczem będącym sekretem endpointu. Podpisuj surowe body - ponowna serializacja sparsowanego JSON-a zmienia bajty i psuje porównanie:

import { createHmac, timingSafeEqual } from "crypto";

export function verifyCmssySignature(
  header: string | null,
  rawBody: string,
  secret: string,
): boolean {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((p) => p.split("=") as [string, string]),
  );
  if (!parts.t || !parts.v1) return false;

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  // Odrzuć też stary timestamp: przechwyconą dostawę da się odtworzyć.
  return a.length === b.length && timingSafeEqual(a, b);
}

Ponowienia

cmssy czeka na odpowiedź 5 sekund. Cokolwiek innego niż 2xx - albo timeout - jest ponawiane, do 8 prób, z backoffem 1 min, 5 min, 15 min, 30 min, 1 h, 2 h, 4 h. Trwała awaria kończy wcześniej.

Dwie konsekwencje dla Twojego handlera. Musi być szybki: odpowiedz 2xx i zrób robotę potem, bo wolny endpoint zamienia się w burzę ponowień. I musi być idempotentny: dostawa, którą już przetworzyłeś, może przyjść ponownie z tym samym id i createdAt.

list_webhook_deliveries pokazuje ostatnie próby ze statusem i kodem odpowiedzi; dostawy trzymane są 30 dni.

Zarządzanie endpointami

  • create_webhook - zwraca endpoint i jego sekret, raz. Zapisz go od razu; nigdy nie wróci.
  • rotate_webhook_secret - zwraca nowy sekret, też raz. Stare podpisy przestają się weryfikować natychmiast.
  • update_webhook - aktualizacja częściowa; przekaż enabled, żeby wstrzymać endpoint bez kasowania.
  • list_webhooks, delete_webhook, list_webhook_deliveries.

Do 20 endpointów na workspace. URL-e muszą być https na produkcji, a cele prywatne są odrzucane: localhost, *.local, *.internal, link-local i zakresy RFC 1918. Webhook, który może sięgnąć do Twojej sieci wewnętrznej, to powierzchnia SSRF, nie funkcja.

Następne kroki

  • Serwer MCP - narzędzia tworzące i podglądające endpointy.
  • Podgląd roboczy - publikacja, cache i co znaczy nieświeża strona.
  • Tokeny API - poświadczenie stojące za tymi narzędziami.