Status: Deployed to production. Locking thread.
This thread covers the full mobieusCore Public REST API + Webhooks initiative — shipped over three phases (plus dual-signing follow-on) ending in commit a843eb4c.
Phase 1 — API foundation, keys, events read
api_keys + api_key_scopes + api_events per-tenant tables. Keys minted via CSPRNG (mc_live_… / mc_test_…); SHA-256-only at rest; constant-time compare; one-time plaintext display.
App\Services\PublicEventEmitter subscribes to internal EventBus kinds and translates them into the stable public taxonomy (post.created, user.joined, moderation.user_banned, …) — five new emit sites wired into ForumController / ZitadelCallback / AccountController to fill in the events the bus didn't already fire.
/api/v1/events, /api/v1/users, /api/v1/posts — cursor-paginated, scope-checked, per-key rate-limited, X-Request-Id + JSON envelope on every response.
- OpenAPI 3.1 spec at
/api/openapi.yaml, dark Redoc at /api/docs.
- Admin UI at
/admin/api-keys (Settings → API keys).
- Wiki pages:
/know/api-overview, /know/api-authentication, /know/api-events-reference.
Phase 2 — Webhook delivery
webhook_endpoints + webhook_deliveries + webhook_delivery_attempts per-tenant tables.
App\Services\SsrfGuard — HTTPS only, rejects loopback / RFC1918 / link-local / RFC4193 / multicast / 0.0.0.0 / 169.254.169.254 / metadata.* hostnames. Re-runs at every delivery (DNS-rebinding defence).
App\Services\WebhookSigner — HMAC-SHA256 with Mobieus-Signature: t=<unix>,v1=<hex>, replay-tolerance window.
App\Services\WebhookDispatcher — second EventBus listener; on every public event, enqueues one delivery row per matching endpoint.
bin/process-webhook-queue.php cron worker — runs every minute, SELECT … FOR UPDATE SKIP LOCKED, exponential backoff [0,1,5,30,120,360,1440] minutes, auto-disable after 20 consecutive failures (configurable), audit-logged.
- Six management endpoints under
/api/v1/webhooks/* (read with webhooks:read, write with webhooks:manage).
- Admin UI at
/admin/webhooks — register / test / disable / browse delivery log with payload + signature inspection.
- Wiki:
/know/api-webhooks-quickstart with Node + Python receiver snippets.
Phase 3 — Operability
- Per-delivery replay via API + per-row admin button.
- Edit endpoint config (label, events picker, active toggle) without delete+recreate.
- Rotate signing secret with show-once flow.
- Admin dashboard webhook-health card — last 24h success-rate, oldest pending age, dead-endpoint count, colour-coded by severity.
Follow-on: zero-downtime rotation (Stripe-style dual-signing)
WebhookEndpoint::rotateSecret($id, $graceSeconds = 86400) copies current → previous columns with expiry.
WebhookSigner::signDual($new, $prev, $body) emits Mobieus-Signature: t=…,v1=<new>,v1_prev=<old> during the grace window.
- Worker dual-signs during grace, single-signs after expiry, purges expired previous-secret material every tick.
- Receivers update at their own pace without dropped deliveries.
Follow-on: live cross-tenant isolation test
tests/Integration/Api/CrossTenantHttpTest.php proves over real HTTPS that a key minted on tenant A returns 401 invalid_api_key against tenants B and C. Verified live:
- support key → support: 200
- support key → fort-smith-live: 401
- support key → dev: 401
Security checklist (acceptance criteria) — every requirement satisfied:
- Hashed at rest, shown once, constant-time compare ✓
- TLS-only API + webhook targets ✓
- SSRF guard at register AND delivery ✓ (DNS rebinding defence)
- HMAC + timestamp replay protection ✓
- Per-key rate limiting + scope enforcement on every endpoint ✓
- Hard tenant isolation (per-tenant DB, connection-level scoping) ✓
- Audit log on key create/revoke + endpoint create/update/delete/rotate ✓
- No secrets in app logs ✓
Out-of-scope items (per spec) — explicitly not built:
- OAuth / per-user delegated auth, GraphQL, write APIs beyond webhook management, published Zapier/Make app.
Live on support, fort-smith-live, dev.