Skip to content

feat(v2.0d): webhooks + public REST API — closes #35, completes v2.0#46

Merged
thunpisit merged 1 commit into
mainfrom
feat/v2.0d-webhooks-api
May 2, 2026
Merged

feat(v2.0d): webhooks + public REST API — closes #35, completes v2.0#46
thunpisit merged 1 commit into
mainfrom
feat/v2.0d-webhooks-api

Conversation

@thunpisit

Copy link
Copy Markdown
Contributor

Summary

Last v2.0 PR. Closes #35. Completes the engagement-and-growth milestone and the entire roadmap through v2.0.

Schema (migration 0010)

  • `webhooks` (id, label, url, secret 48-char nanoid, events JSON, enabled)
  • `webhook_deliveries` (per-attempt log: payload, status, excerpt, durationMs, attempt, nextAttemptAt, ok)
  • `api_keys` (key_hash UNIQUE = SHA-256 hex, prefix kp_live_xxxx for display, scopes JSON, expiresAt, revokedAt, lastUsedAt)

Webhook dispatcher

  • HMAC-SHA256 signed body (`X-Khaopad-Signature: sha256=`)
  • Headers: `X-Khaopad-Event`, `X-Khaopad-Delivery` UUID
  • 5s timeout, 3 inline attempts, 250ms / 1500ms backoff
  • Best-effort logs every attempt; `dispatchEvent()` is fire-and-forget at the call site

Wired into 5 events

`article.publish` / `article.unpublish` / `article.delete` / `comment.approve` / `form.submit` / `subscriber.confirm`

Public REST API

  • `GET /api/public/articles` (paginated, `?locale=`)
  • `GET /api/public/articles/[slug]`
  • `GET /api/public/categories`
  • `GET /api/public/tags`
  • `GET /api/public/pages`
  • Bearer auth via `Authorization: Bearer kp_live_…`
  • Per-key scopes: `articles:read`, `categories:read`, `tags:read`, `pages:read`, `*:read`
  • Drafts / future-dated published articles 404

CMS

  • `/cms/webhooks` (admin+): list, create, edit, rotate-secret, delete
  • `/cms/api-keys` (admin+): list, create (one-time secret display + copy button), revoke (soft), delete (hard)
  • Sidebar entries

i18n

39 new keys (EN + TH).

Migration

0010 already applied to live D1.

Roadmap state after this merges

v2.0 complete. Every version v1.0 → v2.0 is shipped.

Version Theme Status
v1.0 → v1.9 Foundation through performance/trust
v2.0a Forms
v2.0b Newsletter (optional)
v2.0c Comments
v2.0d Webhooks + REST API (this PR)

Backlog (not committed): OAuth providers, block editor, AI authoring, multi-site, A/B, gated content.

Test plan

  • `pnpm build` succeeds
  • `paraglide compile` succeeds
  • `svelte-check` clean
  • After deploy: `/cms/webhooks` admin can register a hook against e.g. webhook.site, publish an article → delivery log shows 200
  • `/cms/api-keys` create one with `articles:read` → `curl -H "Authorization: Bearer kp_live_…" /api/public/articles` returns 200; without header returns 401; with key but wrong scope returns 403; revoke → next request returns 401

🤖 Generated with Claude Code

Last of four v2.0 PRs. Closes the engagement-and-growth milestone
and the entire v2.0 roadmap.

Schema (Drizzle migration 0010)
-------------------------------
- webhooks: id, label, url, secret (48-char nanoid), events JSON,
  enabled, audit fields.
- webhook_deliveries: per-attempt log. webhookId CASCADE, event,
  payload, responseStatus, responseExcerpt (256-char cap),
  durationMs, attempt, nextAttemptAt, ok.
- api_keys: id, label, key_hash UNIQUE (SHA-256 hex of raw key),
  prefix (kp_live_xxxx — kept for display only), scopes JSON,
  expiresAt, revokedAt, lastUsedAt, audit fields.

Webhook dispatcher
------------------
- $lib/server/webhooks/index.ts.
- WebhookEvent union: article.{publish,unpublish,delete} /
  comment.approve / form.submit / subscriber.confirm.
- HMAC-SHA256 sign body with webhook secret. Headers:
  X-Khaopad-Signature: sha256=<hex>
  X-Khaopad-Event: <event>
  X-Khaopad-Delivery: <uuid>
- 5s fetch timeout, 3 inline attempts, 250ms / 1500ms backoff.
- Best-effort writes a webhook_deliveries row for every attempt,
  success or fail. Operator debugs from CMS.
- dispatchEvent() is fire-and-forget at every call site so the
  originating action never pays the network round-trip.

Wired into 5 events
-------------------
- article.publish / article.unpublish (article edit save +
  togglePublish)
- article.delete (article delete action)
- comment.approve (only on approve — spam/archive don't fire)
- form.submit (public POST /api/forms/[key])
- subscriber.confirm (the email click target)

Public REST API
---------------
- $lib/server/api-auth/index.ts: parses Authorization: Bearer …,
  delegates to provider.authenticateApiKey(), enforces scopes.
- /api/public/articles (paginated, ?locale=en|th, ?limit, ?page)
- /api/public/articles/[slug]
- /api/public/categories
- /api/public/tags
- /api/public/pages
- All routes require Authorization: Bearer kp_live_…. Per-key
  scopes: articles:read, categories:read, tags:read, pages:read,
  or *:read for the bundle. Drafts and future-dated published
  articles 404 — consumers never see unpublished content.
- lastUsedAt bumped fire-and-forget on every successful auth.

CMS UI
------
- /cms/webhooks: list, create, edit, rotate-secret, delete. Subscribe
  to specific events via checkbox grid. Show signing secret in a
  collapsible details for verification setup.
- /cms/api-keys: list, create with one-time secret display (copy
  button + clear "won't be shown again" warning), revoke (soft —
  rejected at auth), delete (hard).
- Both pages admin+ gated. Sidebar entries under Admin group.

i18n: 39 new cms_webhooks_* / cms_api_keys_* keys (EN + TH).

Migration 0010 already applied to live D1.

Closes #35. Completes v2.0 (engagement-and-growth) and the entire
roadmap. Backlog (OAuth, block editor, AI authoring, multi-site,
A/B, gated content) stays explicitly uncommitted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@thunpisit thunpisit merged commit 228807b into main May 2, 2026
@thunpisit thunpisit deleted the feat/v2.0d-webhooks-api branch May 2, 2026 08:36
thunpisit added a commit to codustry/khaopad-example that referenced this pull request May 2, 2026
) (#14)

Cherry-picks upstream PR codustry#46. Migration 0010 already applied to live D1.
Field-merged i18n: 39 new cms_webhooks_* / cms_api_keys_* keys (EN + TH).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v2.0 — Engagement and growth

1 participant