diff --git a/doc/design/COMPONENT-INVENTORY.md b/doc/design/COMPONENT-INVENTORY.md index 050e08fa2e..84bdccd4ab 100644 --- a/doc/design/COMPONENT-INVENTORY.md +++ b/doc/design/COMPONENT-INVENTORY.md @@ -428,3 +428,5 @@ Independently addressable examples live under `Connections/In-task connections` ## Shared setup wizard (2026-09-19) `ui/src/components/SetupWizard.tsx` extracts the Slack setup navigation into reusable numbered steps, a portal sidebar, an optional section-menu takeover outlet, and a single-row footer. `SetupWizardSidebarProvider` owns the portal target and takeover lifecycle. Chat exports retain their existing names and defaults for compatibility. The Design Guide demonstrates the components. The production routine trigger wizard and its Storybook previews share the sidebar takeover, navigation, and footer. `routine-triggers/TriggerWizard.tsx`, `TriggerCard.tsx`, and `WebhookFields.tsx` provide the shared trigger setup, compact editable cards, copyable credentials, and agent instructions. + +`routine-triggers/WebhookUrlWarning.tsx` uses `InlineBanner` for non-blocking localhost, private-network, Tailscale, and HTTP guidance. Setup and saved webhook editors share it; the Design Guide shows each warning. URL classification is heuristic, not a public reachability test. diff --git a/docs/api/routines.md b/docs/api/routines.md index b430d74a56..6ac4a2d5b4 100644 --- a/docs/api/routines.md +++ b/docs/api/routines.md @@ -298,3 +298,5 @@ Finish setup with `PATCH /api/routine-triggers/{id}` and `{ "setupPending": fals For compatibility, API-created triggers without `setupPending: true` are immediately live. Completed triggers cannot be returned to setup mode. Checking a previously enabled webhook observes real deliveries and can start the routine; the management UI explains this difference. Trigger cards support removal with Undo. `PATCH` with `{ "archived": true }` excludes a trigger from routine detail and scheduling, and rejects its webhook deliveries. Setting `archived` back to `false` restores the same URL and credentials. `DELETE` remains the permanent deletion API. + +The webhook wizard and saved trigger editor warn about localhost, private-network addresses, Tailscale hostnames, and HTTP URLs without blocking setup. HTTPS does not imply public access: Tailscale Serve is private, while Funnel can expose the same hostname publicly. These warnings inspect the URL only; they do not resolve DNS or test internet reachability. Use [the HTTPS setup guide](https://docs.paperclip.ing/reference/deploy/https/) to configure public access when the sender is outside your network. diff --git a/ui/src/components/routine-sections/editable-sections.test.tsx b/ui/src/components/routine-sections/editable-sections.test.tsx index b599399b30..d1fc38be14 100644 --- a/ui/src/components/routine-sections/editable-sections.test.tsx +++ b/ui/src/components/routine-sections/editable-sections.test.tsx @@ -95,6 +95,22 @@ describe("TriggersSection", () => { expect(container.textContent).not.toContain("one-time-secret"); }); + it("warns about private URLs without blocking webhook setup or completion", async () => { + routine.triggers = [{ id: "trigger-1", kind: "webhook", enabled: true, setupPending: true, signingMode: "bearer", webhookUrl: "https://paperclip.internal/webhook" }] as RoutineTrigger[]; + await render(); + await click("Resume setup"); + expect(container.textContent).toContain("This webhook URL appears to be private"); + expect(container.querySelector('a[href="https://docs.paperclip.ing/reference/deploy/https/"]')).not.toBeNull(); + expect(button("Check connection").disabled).toBe(false); + await click("Check connection"); + expect(container.textContent).toContain("This webhook URL appears to be private"); + expect(button("Finish without checking").disabled).toBe(false); + await click("Finish without checking"); + expect(api.updateTrigger).toHaveBeenCalledWith("trigger-1", { setupPending: false }); + await click("Edit webhook"); + expect(container.textContent).toContain("This webhook URL appears to be private"); + }); + it("shows polled connection results even when routine context is stale", async () => { routine.triggers = [{ id: "trigger-1", kind: "webhook", enabled: true, setupPending: true, signingMode: "bearer", webhookUrl: "https://paperclip.example/webhook" }] as RoutineTrigger[]; await render(undefined, routine); diff --git a/ui/src/components/routine-triggers/RoutineTriggers.tsx b/ui/src/components/routine-triggers/RoutineTriggers.tsx index a0ae876f36..d1c5f25468 100644 --- a/ui/src/components/routine-triggers/RoutineTriggers.tsx +++ b/ui/src/components/routine-triggers/RoutineTriggers.tsx @@ -26,6 +26,7 @@ import { type TriggerDraft, } from "./TriggerWizard"; import { AgentInstructions, CopyField } from "./WebhookFields"; +import { WebhookUrlWarning } from "./WebhookUrlWarning"; function readDraft(key: string): TriggerDraft | null { try { @@ -521,6 +522,7 @@ function WebhookSettings({ checkBaseline !== null && delivery && delivery.receivedAt !== checkBaseline; return (
{subtitle}