Skip to content

Public Actions — Improvement & Growth Plan

Package: capell-app/public-actions · Kind: package · Tier: premium · Product group: Capell Automation · Bundle: automation · Status: Draft

Public Actions is a framework package that lets Capell sites accept untrusted public submissions, run a registered server-side handler against the validated payload, and fan the resulting submission out to configured outbound destinations (HTTP webhooks for Zapier, Pipedream, n8n, Make, or generic). It exposes four surfaces: a public web form (GET/POST /actions/{action}), an authenticated Zapier-style JSON API (/api/public-actions/zapier/*), five Filament admin resources (actions, destinations, submissions, dispatch attempts, integration tokens), and a queue job for durable dispatch. Key Actions are SubmitPublicActionAction (the orchestrator: resolve → spam-check → validate → idempotency → persist → handler → dispatch), DispatchPublicActionDestinationAction, and the integration-token lifecycle actions; persistence is five tables (public_actions, public_action_destinations, public_action_submissions, public_action_dispatch_attempts, public_action_integration_tokens). Runtime deps are capell-app/{admin,core,frontend} plus lorisleiva/laravel-actions; it supports access-gate and form-builder. Current marketplace summary: “Turn any public form or API request into a signed, retried, fully-audited webhook to Zapier, Make, n8n, Pipedream, or your own endpoint — without exposing a single admin route.” Marketplace media now includes the extension card plus 7 committed Capell runner PNG captures covering the admin resources and public form. The Zapier discovery JSON capture remains runner evidence only, not buyer-facing product media.

  • 2026-06-05: Added real PublicActionsHealthCheck diagnostics, aligned cache-safety capability truth, hardened webhook dispatch against DNS rebinding/SSRF, and refreshed marketplace/composer copy for signed, retried, audited public-action webhooks.
  • 2026-06-06: Captured and committed all 8 docs/screenshots.json targets, promoted the 7 styled admin/frontend captures into capell.json marketplace media, and retained the Zapier discovery JSON capture as runner evidence.
  • Resolved: implemented the advertised health checksPublicActionsHealthCheck now returns the four manifest diagnostics (webhook-dispatch, webhook-security, provider-presets, and form-builder-integration) and focused coverage proves the checks pass/fail against tables, adapters, presets, and security config. — src/Health/PublicActionsHealthCheck.php, tests/Feature/PublicActionsHealthCheckTest.php — Closed.
  • Resolved: hardened the webhook SSRF guard against DNS rebinding (TOCTOU) — webhook dispatch resolves the host once through PublicActionWebhookHostResolver, rejects private/link-local/loopback addresses by default, and pins the validated address into cURL’s CURLOPT_RESOLVE entry before sending. IPv4 and IPv6 pinning are covered. — src/Support/Providers/HttpWebhookPublicActionAdapter.php, src/Data/ResolvedWebhookEndpointData.php, tests/Unit/Support/HttpWebhookPublicActionAdapterTest.php — Closed.
  • Resolved: pinned the resolved IP and disabled redirect-following on dispatch — the HTTP adapter calls withoutRedirecting(), sends the original Host header, and dispatches with cURL host pinning so unchecked redirects and second DNS lookups cannot reach internal hosts. Redirect and cURL option tests cover the contract. — src/Support/Providers/HttpWebhookPublicActionAdapter.php, tests/Unit/Support/HttpWebhookPublicActionAdapterTest.php — Closed.
  • Resolved: destination dispatch fan-out is prepared durably before delivery — successful submissions now persist one pending dispatch-attempt row per active destination in the same transaction as the handled submission status update. Sync delivery and queued jobs receive that attempt and update it in place, while async jobs are queued with afterCommit(), so a mid-loop failure leaves a recoverable pending row instead of silently dropping a destination. — src/Actions/SubmitPublicActionAction.php, src/Jobs/DispatchPublicActionDestinationJob.php, src/Support/Providers/HttpWebhookPublicActionAdapter.php — Closed.
  • Resolved: dispatch retries use configurable backoff and jitterDispatchPublicActionDestinationJob now reads capell-public-actions.dispatch_backoff_seconds (default [60, 300, 900]) by attempt number and adds bounded dispatch_retry_jitter_seconds so retryable provider failures do not hammer endpoints at a constant cadence. The legacy dispatch_retry_seconds remains a fallback if no valid curve is configured. — config/capell-public-actions.php, src/Jobs/DispatchPublicActionDestinationJob.php — Closed.
  • Resolved: canonical webhook payload encoding is reused per dispatchHttpWebhookPublicActionAdapter::dispatch() builds the body once, encodes it once through encodedBody(), reuses that canonical string for request_hash, HMAC signature headers, and the non-GET request body, and only keeps the structured body for GET query options. — src/Support/Providers/HttpWebhookPublicActionAdapter.php — Closed.
  • Resolved: schemaless public payloads require explicit opt-incapell-public-actions.allow_schemaless_payloads defaults false, so actions without payload_schema.fields now reject public submissions instead of silently accepting capped arbitrary payloads. If operators explicitly enable the escape hatch, the existing 16KB cap and warning remain, and PublicActionsHealthCheck::schemalessPayloadCheck() surfaces the relaxed boundary. — config/capell-public-actions.php, src/Actions/SubmitPublicActionAction.php, src/Health/PublicActionsHealthCheck.php — Closed.
  • Done/Shipped: filled in docs/overview.md. — The overview now documents package positioning, installed surfaces, public and Zapier routes, admin resources, screenshot coverage, public safety notes, and package verification guidance. — docs/overview.md — S
  • Inbound webhook signature verification — The package signs outbound payloads (X-Capell-Signature, HMAC-SHA256) but offers no way to verify a signed inbound submission. For a submit-actions capability that brands itself on “safe public submissions,” HMAC/Bearer-verified inbound posts (skip-honeypot trusted lane) is the obvious next tier. Ties to submit-actions, automation-webhooks. Differentiator.
  • Captcha breadth beyond Turnstile — Spam protection ships honeypot + Cloudflare Turnstile only. hCaptcha and reCAPTCHA v3 are table-stakes for a paid public-form product. The PublicActionSpamProtectionAdapter contract already makes this a small add. Ties to public-form. Table-stakes.
  • Resolved: rate-limit identity supports action and token overrides. Public submit throttles now read configurable defaults plus per-action overrides from config or action settings.rate_limit.per_minute; Zapier API routes authenticate before throttling so provider requests key by integration token, with per-token and provider-level override config. — src/Providers/PublicActionsServiceProvider.php, routes/web.php, config/capell-public-actions.php — Closed.
  • Idempotency on the Zapier submit pathSubmitPublicActionAction honours an Idempotency-Key header, but verify the Zapier controller forwards it; Zapier replays, so idempotency here is high-value. Ties to zapier-integration. Differentiator.
  • Resolved: dead-letter + manual replay for failed dispatches. ReplayPublicActionDispatchAttemptAction replays pending, retryable, or failed attempts through the registered destination adapter, and the dispatch-attempt Filament table exposes a confirmed Replay action with success/failure notifications. Successful attempts are not replayable, keeping the admin recovery lever scoped to dead-letter and stuck states. — src/Actions/ReplayPublicActionDispatchAttemptAction.php, src/Filament/Resources/DispatchAttempts/PublicActionDispatchAttemptResource.php — Closed.
  • Resolved: package no longer advertises cache-blocking — The shipped behaviour is route-level cache safety: public action GET/POST responses send no-store headers, but the package does not register page cache dependencies for embedded action buttons. capell.json now keeps cacheSafety.cacheable:false for those routes and drops the unsupported cache-blocking capability. — capell.json, tests/Unit/PublicActionsManifestTest.php — Closed.
  • First-class Form Builder binding UIform-builder integration works only via a hand-edited config('capell-public-actions.form_builder.mappings') array consumed by SubmitPublicActionFromFormSubmission. There is no admin UI to bind a form to an action. Ties to the supports: form-builder declaration. Differentiator.
  • Outbound destination adapters beyond raw HTTP — Only http_webhook exists; every preset (zapier/pipedream/n8n/make/generic) maps to it. Slack/Discord/email-relay/SQS adapters would justify the “outbound automation” positioning. Ties to automation-webhooks. Differentiator.
  • Resolved: submission retention / PII purge command. capell:public-actions:prune-submissions prunes submissions older than capell-public-actions.submission_retention_days (or --days) and their dispatch attempts through the existing cascade. It supports --dry-run for audit-only counts and --json for scheduled/ops output. — src/Actions/PrunePublicActionSubmissionsAction.php, src/Console/Commands/PrunePublicActionSubmissionsCommand.php — Closed.
  • Health diagnostics are now real (good): the manifest’s 4 declared health checks are implemented by PublicActionsHealthCheck, and package coverage exercises table presence, adapter registration, preset normalization, and relaxed webhook-guard failures.
  • DNS-rebinding SSRF window closed (good): dispatch now pins the validated address through cURL, keeps the original Host header, disables redirect-following, and covers private resolution, unchecked redirects, IPv4 pinning, and IPv6 pinning.
  • Closed: silent partial fan-out. Successful submissions now create pending dispatch-attempt rows before delivery starts, and the HTTP adapter updates those rows in place. Async jobs are queued after commit, leaving a durable pending audit row if a worker or request dies before delivery.
  • Closed: schemaless-accept path is opt-in only. Public Actions now rejects actions without payload_schema.fields unless capell-public-actions.allow_schemaless_payloads is explicitly true, and Diagnostics reports that relaxed mode.
  • Test gaps (medium): strong coverage exists for submit/idempotency/redaction/retry/honeypot/turnstile/redirect-safety/Zapier auth/policies/migrations, health checks, cache-capability truth, and webhook SSRF pinning. Remaining higher-value gaps are fan-out atomicity and performance budget assertions. tests/.
  • Performance budget unverified (low): capell.json sets frontendRenderBudgetMs:20 / adminQueryBudget:40. ShowPublicActionController reads only hydrated payload_schema (no N+1, safe), but no test asserts the budget; dispatchAttempts/submission admin lists over high-volume tables are the real query-budget risk and are unbenchmarked.
  • i18n (low): public/admin strings are translated (generic.php, filament.php), but webhook/SSRF InvalidArgumentException messages are hard-coded English. Acceptable (operator-facing, logged) but worth noting.
  • Public-output safety (good): verified clean. ShowPublicActionController passes only {key,label,type,required}; the Blade form leaks no model IDs, handler classes, selectors, or signed URLs; both controllers set no-store, private. Destination secret/endpoint_url/headers are encrypted at rest; tokens are stored hashed; webhook error/response summaries are redacted. This is the package’s strongest area.
  • Tech debt (low): The previously empty docs/overview.md is now filled with package surfaces, safety notes, and verification guidance; CHANGELOG.md records dated health, webhook security, canonical payload, overview, and screenshot slices; and webhook dispatch now reuses canonical payload encoding plus configurable retry backoff/jitter.

Critique. Marketplace and Composer copy now lead with the concrete, sellable hooks: Zapier/n8n/Make/Pipedream, signed webhooks, encrypted secrets, idempotent submissions, durable retry, and full audit. The static screenshot story is now reconciled for buyer-facing UI: capell.json.marketplace.screenshots lists the extension card plus the 7 committed runner captures for configured actions, action form, destinations, submissions, dispatch attempts, integration tokens, and the frontend form. The Zapier discovery JSON capture stays in the runner contract as technical API evidence until a styled integration explorer or admin UI explains it better.

Improved 1-sentence summary: Turn any public form or API request into a signed, retried, fully-audited webhook to Zapier, Make, n8n, Pipedream, or your own endpoint — without exposing a single admin route.

Improved 3–4 sentence description: Public Actions gives Capell a safe boundary for untrusted public input: submissions are validated against a per-action schema, screened for spam (honeypot + Turnstile), de-duplicated by idempotency key, and persisted before anything fires. Successful submissions fan out to configured destinations over signed HTTP webhooks (HMAC-SHA256) with SSRF protection, encrypted secrets, durable retry, and a complete per-attempt dispatch audit. Native presets cover Zapier, Make, n8n, Pipedream, and generic endpoints, and a registry lets other packages plug in their own handlers, destination adapters, and spam guards. An authenticated Zapier-style JSON API exposes discoverable actions and submissions for no-code automation builders.

Screenshot/media gaps: UI media is closed for the current contract: the 8 captures in docs/screenshots.json exist, and the 7 styled admin/frontend captures are promoted into marketplace media. The raw Zapier discovery JSON screenshot remains docs/runner evidence only. A future architecture diagram (form → validate → handler → fan-out → retry/audit) would still help explain the boundary quickly.

Pricing/tier/bundle positioning. Correctly premium in the automation bundle. In practice this is infrastructure / a dependency as much as a standalone seller — form-builder, access-gate, and future intake packages will all submit through it. Position it as the shared automation backbone of the Automation bundle (bundle pull-through), and cross-sell: Form Builder (no-code form → action binding), Access Gate (gated submissions), Email Studio (notify-on-submission). Extension Suites angle: publish the PublicActionHandler / PublicActionDestinationAdapter / PublicActionSpamProtectionAdapter contracts as the SDK other suites build on.

Differentiators / value props / target buyer. Differentiators: SSRF-guarded + signed + encrypted webhooks, idempotency, and a durable dispatch-attempt audit — most “form-to-webhook” plugins have none of these. Target buyer: agencies/site owners wiring CMS submissions into existing automation stacks (Zapier/Make/n8n) who need it to be safe and auditable, and package developers who want a vetted public-input boundary instead of rolling their own controllers.

Keywords/tags: webhooks, zapier, n8n, make, pipedream, automation, public-forms, server-side-actions, hmac-signed-webhooks, idempotency, ssrf-protection, integration-api, spam-protection, outbound-dispatch.

ItemBucketEffortImpactSection ref
Implement the 4 advertised health checks (truth-in-manifest)DoneMHigh2, 4 — closed by PublicActionsHealthCheck diagnostics and focused health coverage
Close DNS-rebinding TOCTOU + disable/guard webhook redirectsDoneMHigh2, 4 — closed by cURL host pinning, redirect blocking, and IPv4/IPv6 pinning coverage
Add SSRF rebinding + redirect-follow tests; add health-check testDoneMHigh4 — closed by webhook redirect/pinning tests and health-check diagnostics coverage
Ship the 8 specced screenshot targets; promote only the 7 styled UI capturesDoneSHigh5
Shipped: sharpen marketplace summary/descriptionDoneSHigh5
Resolve cache-blocking (wire real dependency blocking or drop capability)DoneSMed3, 4 — closed by dropping unsupported capability
Done/Shipped: Make destination fan-out durable/atomic (after-commit + pending rows)DoneMHigh2, 4 — successful submissions create pending dispatch-attempt rows before sync or queued delivery starts.
Gate schemaless submissions behind explicit opt-in configDoneSMed2, 4
Done/Shipped: Add manual dispatch replay + dead-letter admin surfaceDoneMHigh3 — pending, retryable, and failed dispatch attempts can be replayed from the admin dispatch-attempt table.
Configurable exponential backoff + jitter on dispatch jobDoneSMed2
Encode payload once per dispatch (hash/sign/send reuse)DoneSLow2
Done/Shipped: Per-token + per-action rate-limit overridesDoneMMed3 — public submit and Zapier API throttles now support configured defaults plus action, provider, and token overrides.
Done/Shipped: Submission retention / PII purge commandDoneMMed3 — capell:public-actions:prune-submissions supports configured retention, --days, --dry-run, and JSON output.
hCaptcha + reCAPTCHA v3 spam adaptersLaterMMed3
Form Builder → action binding admin UILaterMHigh3
Non-HTTP destination adapters (Slack, email-relay, SQS)LaterLHigh3
Inbound signed-webhook verification (trusted submission lane)LaterMHigh3
Shipped: Fill docs/overview.mdDoneSLow2, 4
Shipped: Add real CHANGELOG entriesDoneSLow4