mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 02:40:39 +09:00
feat(server): add github_hmac and none webhook signing modes
Adds two new webhook trigger signing modes for external provider compatibility: - github_hmac: accepts X-Hub-Signature-256 header with HMAC-SHA256(secret, rawBody), no timestamp prefix. Compatible with GitHub, Sentry, and services following the same standard. - none: no authentication; the 24-char hex publicId in the URL acts as the shared secret. For services that cannot add auth headers. The replay window UI field is hidden when these modes are selected since neither uses timestamp-based replay protection. Closes #1892 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eefe9f39f1
commit
cd19834fab
5 changed files with 109 additions and 15 deletions
|
|
@ -61,7 +61,7 @@ import type { RoutineTrigger, RoutineVariable } from "@paperclipai/shared";
|
|||
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
||||
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
||||
const triggerKinds = ["schedule", "webhook"];
|
||||
const signingModes = ["bearer", "hmac_sha256"];
|
||||
const signingModes = ["bearer", "hmac_sha256", "github_hmac", "none"];
|
||||
const routineTabs = ["triggers", "runs", "activity"] as const;
|
||||
const concurrencyPolicyDescriptions: Record<string, string> = {
|
||||
coalesce_if_active: "Keep one follow-up run queued while an active run is still working.",
|
||||
|
|
@ -75,7 +75,10 @@ const catchUpPolicyDescriptions: Record<string, string> = {
|
|||
const signingModeDescriptions: Record<string, string> = {
|
||||
bearer: "Expect a shared bearer token in the Authorization header.",
|
||||
hmac_sha256: "Expect an HMAC SHA-256 signature over the request using the shared secret.",
|
||||
github_hmac: "Accept GitHub-style X-Hub-Signature-256 header (HMAC over raw body, no timestamp).",
|
||||
none: "No authentication — the webhook URL itself acts as a shared secret.",
|
||||
};
|
||||
const SIGNING_MODES_WITHOUT_REPLAY_WINDOW = new Set(["github_hmac", "none"]);
|
||||
|
||||
type RoutineTab = (typeof routineTabs)[number];
|
||||
|
||||
|
|
@ -198,13 +201,15 @@ function TriggerEditor({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Replay window (seconds)</Label>
|
||||
<Input
|
||||
value={draft.replayWindowSec}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, replayWindowSec: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
{!SIGNING_MODES_WITHOUT_REPLAY_WINDOW.has(draft.signingMode) && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Replay window (seconds)</Label>
|
||||
<Input
|
||||
value={draft.replayWindowSec}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, replayWindowSec: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -987,10 +992,12 @@ export function RoutineDetail() {
|
|||
</Select>
|
||||
<p className="text-xs text-muted-foreground">{signingModeDescriptions[newTrigger.signingMode]}</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Replay window (seconds)</Label>
|
||||
<Input value={newTrigger.replayWindowSec} onChange={(event) => setNewTrigger((current) => ({ ...current, replayWindowSec: event.target.value }))} />
|
||||
</div>
|
||||
{!SIGNING_MODES_WITHOUT_REPLAY_WINDOW.has(newTrigger.signingMode) && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Replay window (seconds)</Label>
|
||||
<Input value={newTrigger.replayWindowSec} onChange={(event) => setNewTrigger((current) => ({ ...current, replayWindowSec: event.target.value }))} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue