feat(revalidation): core WebhookSigner + SafeHttpClient for hooks (#15) #16

Merged
KofTwentyTwo merged 1 commits from feature/arch-core-signer into develop 2026-10-14 00:36:56 +00:00
KofTwentyTwo commented 2026-10-14 00:33:42 +00:00 (Migrated from github.com)

Summary

Closes #15. Architecture review QRun-IO/qqq#1150, finding F11: one webhook signer, in core.

  • Bumps to QQQ 4.1.0-RC.5.
  • RevalidationHookRunner signs with core WebhookSigner/WebhookSecret (Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature) and sends through core SafeHttpClient, which does the DNS checks and preserves the hostname.
  • New cdn_revalidation_hook fields:
    • signatureFormat (CdnSignatureFormat):
      • STANDARD_WEBHOOKS is the default for new rows.
      • LEGACY keeps X-Revalidate-Signature: sha256=<hex HMAC of body> byte-for-byte; golden vectors are tested.
      • A null format, as on existing rows after the column is added, stays LEGACY, so current receivers keep working.
    • allowPlainHttp and internalAllowlist: explicit exceptions to the SSRF-safe defaults.
  • Migration guide: docs/revalidation-hook-migration.md.

Host impact (kof22)

  • Liquibase: add three columns to cdn_revalidation_hook: signatureFormat (nullable), allowPlainHttp and internalAllowlist.

  • Internal HTTP receivers now need allowPlainHttp=true and the host in internalAllowlist. Without them, SafeHttpClient blocks the call. Set them in the same migration.

  • Moving a hook to Standard Webhooks needs a receiver change in Website-Frontend:

    • src/lib/revalidate.ts:14: verify id.timestamp.rawBody with a timestamp window.
    • src/app/api/revalidate/route.ts:40: pass the three headers.

    Then set signatureFormat=STANDARD_WEBHOOKS. A secret must have at least 16 bytes.

Tests

mvnw -B verify: 81 tests, 0 failures, 0 checkstyle violations. Coverage is 95.30% instructions and 100% classes.

  • core WebhookVerifier round trip;
  • legacy golden vectors, including the null-format-means-legacy case;
  • SSRF policy cases.

🤖 Generated with Claude Code

## Summary Closes #15. Architecture review QRun-IO/qqq#1150, finding F11: one webhook signer, in core. - Bumps to **QQQ 4.1.0-RC.5**. - `RevalidationHookRunner` signs with core `WebhookSigner`/`WebhookSecret` (Standard Webhooks: `webhook-id`, `webhook-timestamp`, `webhook-signature`) and sends through core `SafeHttpClient`, which does the DNS checks and preserves the hostname. - New `cdn_revalidation_hook` fields: - **`signatureFormat`** (`CdnSignatureFormat`): - `STANDARD_WEBHOOKS` is the default for **new** rows. - `LEGACY` keeps `X-Revalidate-Signature: sha256=<hex HMAC of body>` byte-for-byte; golden vectors are tested. - **A null format, as on existing rows after the column is added, stays `LEGACY`**, so current receivers keep working. - **`allowPlainHttp`** and **`internalAllowlist`**: explicit exceptions to the SSRF-safe defaults. - Migration guide: `docs/revalidation-hook-migration.md`. ## Host impact (kof22) - Liquibase: add three columns to `cdn_revalidation_hook`: `signatureFormat` (nullable), `allowPlainHttp` and `internalAllowlist`. - **Internal HTTP receivers now need `allowPlainHttp=true` and the host in `internalAllowlist`.** Without them, SafeHttpClient blocks the call. Set them in the same migration. - Moving a hook to Standard Webhooks needs a receiver change in Website-Frontend: - `src/lib/revalidate.ts:14`: verify `id.timestamp.rawBody` with a timestamp window. - `src/app/api/revalidate/route.ts:40`: pass the three headers. Then set `signatureFormat=STANDARD_WEBHOOKS`. A secret must have at least 16 bytes. ## Tests `mvnw -B verify`: 81 tests, 0 failures, 0 checkstyle violations. Coverage is 95.30% instructions and 100% classes. - core `WebhookVerifier` round trip; - legacy golden vectors, including the null-format-means-legacy case; - SSRF policy cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.