Files
PaperClipAI/ui/src/components/routine-triggers/RoutineTriggers.tsx
T
DottaandPaperclip 24429024e7 feat: add Fireflies connector and summary-ready routines (#13890)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Apps gives agents governed access to external tools through stored
credentials.
> - Routines start work when an external service sends an event.
> - Fireflies provides meeting transcripts and summaries through an
official hosted MCP server.
> - This PR adds that connection and accepts signed meeting events
through the shared app webhook flow.
> - Agents can review completed meetings with the same permissions and
audit records as other work.

## Linked Issues or Issue Description

**Problem or motivation**

Operators need agents to read Fireflies meetings and start follow-up
work when a summary is ready. The Apps catalog lacks Fireflies. The
shared app webhook flow needs to accept its signed deliveries.

**Proposed solution**

Use the official Fireflies MCP endpoint with OAuth or a vaulted bearer
API key. Extend the existing Another app or script flow with signed
webhook support. Verify the raw-body signature and pass the JSON payload
as external data. Select Meeting Summarized in Fireflies. Deduplicate
identical signed deliveries, including setup deliveries.

**Alternatives considered**

A separate REST connector would duplicate the governed MCP path.
Polling, legacy V1 payloads, and automatic provider-side webhook
registration are outside this change.

**Roadmap alignment**

This extends the existing MCP Tool Gateway & Apps and Scheduled Routines
surfaces. It adds a provider to those systems. It does not introduce a
second integration framework.

**Additional context**

A GitHub search found no existing Fireflies issues or PRs. Provider
references and verification limits are in
`doc/connections/FIREFLIES.md`.

## What Changed

- Add the official Fireflies catalog definition, generated registry,
provider evidence, and branded artwork.
- Reuse Access → Connect, dynamic discovery, Permissions, vault storage,
policy, and audit behavior.
- Classify Fireflies sharing, movement, and access revocation as writes.
- Preserve Off and Ask first restrictions during OAuth reauthorization
and API-key replacement. New actions retain normal defaults.
- Add `app_webhook` authentication to the shared Another app or script
flow. Accept bearer tokens or raw-body HMAC-SHA256. Preserve earlier
`fireflies_hmac` triggers and revision snapshots for compatibility.
Existing text columns need no migration.
- Verify `X-Hub-Signature` or `X-Hub-Signature-256` against the exact
request body. Preserve generic event payloads and deduplicate identical
signed requests.
- Keep the routine wizard generic. Show one webhook URL and secret in
Another app or script. Keep all new app webhook event names
provider-neutral. Keep provider setup instructions in the connector
documentation.
- Pass generic webhook JSON to the task in an explicit external-data
block, capped at 16,384 characters. Keep strict meeting validation for
existing legacy Fireflies triggers.

## Verification

- Feature implementation commit `0882dc8a1`: all 54 CI checks passed;
two conditional Storybook checks skipped. This includes full tests,
typecheck, build, browser E2E, canary dry run, and security checks.
Greptile rated this commit 5/5; all review threads are resolved.
- Full local `pnpm -r typecheck`, `pnpm build`, and token gates passed
on the final code. Targeted connector, gateway, webhook, revision, and
UI suites passed during implementation. After the provider-neutral
follow-up, all 84 app-webhook and routine-service tests passed; the
final payload-to-task assertion also passed in the 72-test routine suite
and a clean-config rerun.
- The long local `pnpm test:run` invocation started before the final
edits and was stopped after the final-commit CI suites passed. It
reported one generic webhook test failure while those files were
changing; that test and the entire routine suite passed on the final
source, including a clean-config reproduction. The interrupted local run
is not counted as a full-suite pass.
- In the embedded browser, completed official OAuth consent and
discovered 20 live actions. Real meeting listing, transcript retrieval,
and summary/action-item retrieval succeeded as the selected agent.
Turning a live read Off blocked its test; catalog refresh preserved the
restriction.
- Embedded-browser Another app or script setup, back/save/resume, narrow
layout, and a signed synthetic Fireflies delivery succeeded. The UI
reported authentication passed without creating a task. Fixtures cover
signature tampering, malformed requests, ordinary app event names,
duplicate/setup deliveries, rotation, revisions, pause/archive, and
company isolation.
- Existing MCP browser suite: 8 passed and 2 provider-dependent cases
skipped. Branding checks passed; connector artwork and webhook setup
were checked at desktop/mobile widths and in light/dark modes.
- An unauthenticated POST to a correctly formatted public webhook URL
reached the staging tenant verifier through the existing Cloud gateway.
- A real Fireflies webhook delivery remains unverified. A staging
callback is available for the operator walkthrough. Live API-key
authorization, credential expiry, and a new meeting's summary completion
were not tested against the provider. Fixtures cover these protocol and
lifecycle paths where applicable.

- Storybook follow-up `c54174faa`: 27 production-component stories cover
every UI change, with a source-to-story map in the connector
documentation. Static Storybook build, UI typecheck, token gates, and
Playwright checks for all stories and the mobile footer pass. All PR
checks passed for this Storybook follow-up; Greptile reviewed
`c54174faa` at 5/5.

## Risks

- Fireflies may change its hosted MCP tools or OAuth behavior. Tool
discovery stays dynamic. Experimental search/fetch tools are not
required.
- Public webhook setup requires HTTPS and a separate signing secret.
Fireflies normally emits events for meetings owned by the configuring
account.
- Reauthorization touches shared MCP permission code. Regression tests
cover existing restrictions, new actions, connection removal, and other
gateway callers.
- Webhook receipt grants no tool access. The routine agent still needs
an authorized Fireflies connection.

- New generic triggers rely on provider event subscriptions. Without a
sender-supplied idempotency key, changed request bytes count as a new
event. Existing legacy Fireflies triggers retain summary-only filtering
and per-meeting deduplication.

## Model Used

OpenAI Codex, model `gpt-6-astra`. Used reasoning, repository editing,
code execution, and embedded-browser testing. The runtime did not expose
a context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-23 17:11:16 -05:00

655 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useRef, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { CalendarClock, Plus, Webhook } from "lucide-react";
import type { RoutineTrigger } from "@paperclipai/shared";
import { useSearchParams } from "@/lib/router";
import { routinesApi } from "@/api/routines";
import { queryKeys } from "@/lib/queryKeys";
import { describeCron } from "@/lib/cron-readable";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { ScheduleEditor } from "@/components/ScheduleEditor";
import { useRoutineDetail } from "@/components/routine-sections/context";
import { RoutineTriggerCard } from "./TriggerCard";
import {
RoutineTriggerWizard,
defaultTriggerDraft,
webhookAgentInstructions,
type TriggerDraft,
} from "./TriggerWizard";
import { AgentInstructions, CopyField } from "./WebhookFields";
import { WebhookUrlWarning } from "./WebhookUrlWarning";
function readDraft(key: string): TriggerDraft | null {
try {
const draft = JSON.parse(sessionStorage.getItem(key) ?? "null");
return draft && ["choose", "schedule", "webhook"].includes(draft.kind)
? { ...defaultTriggerDraft, ...draft, sender: draft.sender === "github" ? "github" : "custom" }
: null;
} catch {
return null;
}
}
function saveDraft(key: string, draft: TriggerDraft) {
// Only setup choices are persisted. The one-time key stays in component memory.
try {
sessionStorage.setItem(key, JSON.stringify(draft));
} catch {
throw new Error(
"Couldn’t save your draft in this browser. Keep this page open and try again.",
);
}
}
function scheduleCron(draft: TriggerDraft) {
const [hour, minute] = draft.time.split(":").map(Number);
const day = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
].indexOf(draft.weekday);
return `${minute} ${hour} * * ${draft.frequency === "daily" ? "*" : draft.frequency === "weekly" ? day : "1-5"}`;
}
export function RoutineTriggers() {
const ctx = useRoutineDetail();
const [params, setParams] = useSearchParams();
const setupId = params.get("triggerSetup");
const client = useQueryClient();
const { data, error: statusError } = useQuery({
queryKey: queryKeys.routines.detail(ctx.routineId),
queryFn: () => routinesApi.get(ctx.routineId),
refetchInterval: 3000,
});
const routine = data ?? ctx.routine;
const [expanded, setExpanded] = useState<string | null>(null);
const [removed, setRemoved] = useState<RoutineTrigger[]>([]);
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const refresh = useCallback(async () => {
await client.invalidateQueries({ queryKey: ["routines"] });
}, [client]);
const change = async (action: () => Promise<void>) => {
if (busy) return;
setBusy(true);
setError("");
try {
await action();
await refresh();
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: "Couldn’t save the trigger. Please try again.",
);
} finally {
setBusy(false);
}
};
const startSetup = (id: string) => setParams({ triggerSetup: id });
const closeSetup = useCallback(() => setParams({}), [setParams]);
if (setupId) {
const trigger = routine.triggers.find((item) => item.id === setupId);
if (setupId !== "new" && !trigger)
return (
<p role="alert">
This trigger is no longer available.{" "}
<Button variant="link" onClick={closeSetup}>
Back to triggers
</Button>
</p>
);
if (trigger && !trigger.setupPending)
return (
<p>
This trigger is ready.{" "}
<Button variant="link" onClick={closeSetup}>
Back to triggers
</Button>
</p>
);
return (
<div className="space-y-4">
{statusError && (
<p role="alert" className="text-sm text-destructive">
Connection status is unavailable. Retrying…
</p>
)}
<TriggerSetup
key={routine.id}
routineId={routine.id}
companyId={routine.companyId}
routineTitle={routine.title}
trigger={trigger}
onExit={closeSetup}
onRefresh={refresh}
/>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{routine.triggers.length}{" "}
{routine.triggers.length === 1 ? "trigger" : "triggers"}
</p>
<Button size="sm" onClick={() => startSetup("new")}>
<Plus className="mr-1.5 h-3.5 w-3.5" />
Add trigger
</Button>
</div>
{(error || statusError) && (
<p role="alert" className="text-sm text-destructive">
{error || "Connection status is unavailable. Retrying…"}
</p>
)}
{removed.map((trigger) => (
<div
key={trigger.id}
role="status"
className="flex items-center gap-3 rounded-md bg-muted/40 px-4 py-3 text-sm"
>
<span className="flex-1">
{trigger.kind === "schedule"
? "Schedule"
: trigger.kind === "api"
? "API trigger"
: "Webhook"}{" "}
removed.
</span>
<Button
variant="ghost"
size="sm"
disabled={busy}
onClick={() =>
void change(async () => {
await routinesApi.updateTrigger(trigger.id, {
archived: false,
});
setRemoved((items) =>
items.filter((item) => item.id !== trigger.id),
);
})
}
>
Undo
</Button>
</div>
))}
{ctx.secretMessage && (
<div className="space-y-3 rounded-md border p-4">
<p className="text-sm font-medium">{ctx.secretMessage.title}</p>
{ctx.secretMessage.entries.map((entry) => (
<div key={entry.webhookUrl} className="space-y-3">
<CopyField label="Webhook URL" value={entry.webhookUrl} />
<CopyField label="Secret key" value={entry.webhookSecret} />
</div>
))}
<Button variant="outline" onClick={() => ctx.setSecretMessage(null)}>
Done
</Button>
</div>
)}
{routine.triggers.length === 0 && (
<p className="py-6 text-sm text-muted-foreground">
Run this routine on a schedule or when another app sends a webhook.
</p>
)}
<fieldset disabled={busy} className="min-w-0 space-y-3">
{routine.triggers.map((trigger) => (
<RoutineTriggerCard
key={trigger.id}
kind={
trigger.kind === "schedule"
? "schedule"
: trigger.kind === "api"
? "api"
: "webhook"
}
icon={
trigger.kind === "schedule" ? (
<CalendarClock className="h-4 w-4" />
) : (
<Webhook className="h-4 w-4" />
)
}
title={
trigger.kind === "schedule"
? (describeCron(trigger.cronExpression) ?? "Schedule")
: trigger.kind === "api"
? "API trigger"
: trigger.signingMode === "github_hmac"
? "GitHub webhook"
: "Webhook"
}
summary={
trigger.setupPending
? "Setup unfinished · Events only test the connection"
: !trigger.enabled
? "Paused"
: trigger.kind === "schedule"
? (trigger.timezone ?? "UTC")
: trigger.kind === "api"
? "Run through the API"
: trigger.lastWebhookDelivery?.status === "rejected"
? "Authentication failed · Check the key in your app"
: trigger.lastWebhookDelivery?.status === "received" &&
!trigger.lastWebhookDelivery.test
? "Receiving events"
: "Ready · Waiting for an event"
}
expanded={expanded === trigger.id}
editLabel={trigger.setupPending ? "Resume setup" : undefined}
onEdit={() =>
trigger.setupPending
? startSetup(trigger.id)
: setExpanded((id) => (id === trigger.id ? null : trigger.id))
}
onRemove={() =>
void change(async () => {
await routinesApi.updateTrigger(trigger.id, { archived: true });
setRemoved((items) => [...items, trigger]);
})
}
>
{trigger.setupPending ? (
<Button onClick={() => startSetup(trigger.id)}>
Resume setup
</Button>
) : trigger.kind === "schedule" ? (
<ScheduleSettings
trigger={trigger}
onSave={async (patch) => {
await routinesApi.updateTrigger(trigger.id, patch);
await refresh();
setExpanded(null);
}}
onCancel={() => setExpanded(null)}
/>
) : trigger.kind === "api" ? (
<p className="text-sm text-muted-foreground">
This trigger starts the routine through the API.
</p>
) : (
<WebhookSettings
trigger={trigger}
routineTitle={routine.title}
onRefresh={refresh}
/>
)}
{!trigger.setupPending && (
<Button
variant="ghost"
size="sm"
onClick={() =>
void change(async () => {
await routinesApi.updateTrigger(trigger.id, {
enabled: !trigger.enabled,
});
})
}
>
{trigger.enabled ? "Pause trigger" : "Enable trigger"}
</Button>
)}
</RoutineTriggerCard>
))}
</fieldset>
</div>
);
}
function TriggerSetup({
routineId,
companyId,
routineTitle,
trigger,
onExit,
onRefresh,
}: {
routineId: string;
companyId: string;
routineTitle: string;
trigger?: RoutineTrigger;
onExit: () => void;
onRefresh: () => Promise<void>;
}) {
const [, setParams] = useSearchParams();
const storagePrefix = `routine-trigger-draft:${companyId}:${routineId}:`;
const createdRef = useRef<RoutineTrigger | undefined>(trigger);
const [created, setCreated] = useState(trigger);
const [secret, setSecret] = useState("");
const currentTrigger = trigger ?? created;
const [initialDraft] = useState<TriggerDraft>(() => {
const saved = readDraft(storagePrefix + (trigger?.id ?? "new"));
if (trigger)
return {
...defaultTriggerDraft,
...saved,
kind: "webhook",
sender: trigger.signingMode === "github_hmac" ? "github" : "custom",
signingMode: trigger.signingMode === "bearer" ? "bearer" : trigger.signingMode === "fireflies_hmac" ? "fireflies_hmac" : "app_webhook",
created: true,
step: saved?.step ?? 1,
availableStep: Math.max(1, saved?.availableStep ?? 1),
};
return (
saved ?? {
...defaultTriggerDraft,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
created: false,
}
);
});
const createWebhook = useCallback(
async (draft: TriggerDraft) => {
if (createdRef.current) return;
const response = await routinesApi.createTrigger(routineId, {
kind: "webhook",
signingMode: draft.sender === "github" ? "github_hmac" : "app_webhook",
setupPending: true,
});
createdRef.current = response.trigger;
setCreated({
...response.trigger,
webhookUrl:
response.secretMaterial?.webhookUrl ?? response.trigger.webhookUrl,
});
setSecret(response.secretMaterial?.webhookSecret ?? "");
saveDraft(storagePrefix + response.trigger.id, {
...draft,
created: true,
step: 1,
availableStep: 1,
});
sessionStorage.removeItem(storagePrefix + "new");
await onRefresh();
setParams({ triggerSetup: response.trigger.id }, { replace: true });
},
[routineId, onRefresh, storagePrefix, setParams],
);
const saveExit = useCallback(
(draft: TriggerDraft) => {
saveDraft(storagePrefix + (createdRef.current?.id ?? "new"), draft);
onExit();
},
[onExit, storagePrefix],
);
const finish = useCallback(
async (draft: TriggerDraft) => {
if (draft.kind === "schedule")
await routinesApi.createTrigger(routineId, {
kind: "schedule",
cronExpression: scheduleCron(draft),
timezone: draft.timezone,
});
else {
if (!createdRef.current)
throw new Error("Create the webhook before finishing setup.");
await routinesApi.updateTrigger(createdRef.current.id, {
setupPending: false,
});
}
try {
sessionStorage.removeItem(
storagePrefix + (createdRef.current?.id ?? "new"),
);
} catch {
// The server mutation succeeded; draft cleanup must not cause a retry.
}
await onRefresh();
onExit();
},
[routineId, onRefresh, onExit, storagePrefix],
);
const { routine: currentRoutine } = useRoutineDetail();
return (
<RoutineTriggerWizard
initialDraft={initialDraft}
routineTitle={routineTitle}
routineId={routineId}
routineActive={currentRoutine.status === "active"}
webhookUrl={currentTrigger?.webhookUrl ?? ""}
webhookSecret={secret}
onCreateWebhook={createWebhook}
onSaveExit={saveExit}
onFinish={finish}
onRotateKey={async () => {
if (!createdRef.current) return;
const response = await routinesApi.rotateTriggerSecret(
createdRef.current.id,
);
setSecret(response.secretMaterial.webhookSecret);
await onRefresh();
}}
checkResult={currentTrigger?.lastWebhookDelivery?.status ?? "waiting"}
/>
);
}
function ScheduleSettings({
trigger,
onSave,
onCancel,
}: {
trigger: RoutineTrigger;
onSave: (patch: Record<string, unknown>) => Promise<void>;
onCancel: () => void;
}) {
const [cronExpression, setCron] = useState(
trigger.cronExpression ?? "0 9 * * *",
);
const [timezone, setTimezone] = useState(trigger.timezone ?? "UTC");
const [valid, setValid] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
return (
<fieldset disabled={busy} className="min-w-0 space-y-4">
<ScheduleEditor
value={cronExpression}
onChange={setCron}
onValidityChange={setValid}
/>
<Label>
Time zone
<Input
value={timezone}
onChange={(event) => setTimezone(event.target.value)}
/>
</Label>
{error && (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
)}
<div className="flex gap-2">
<Button
disabled={!valid}
onClick={async () => {
setBusy(true);
setError("");
try {
await onSave({ cronExpression, timezone });
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: "Couldn’t save schedule.",
);
} finally {
setBusy(false);
}
}}
>
Save schedule
</Button>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
</div>
</fieldset>
);
}
function WebhookSettings({
trigger,
routineTitle,
onRefresh,
}: {
trigger: RoutineTrigger;
routineTitle: string;
onRefresh: () => Promise<void>;
}) {
const [secret, setSecret] = useState("");
const [replace, setReplace] = useState(false);
const [checkBaseline, setCheckBaseline] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const github = trigger.signingMode === "github_hmac";
const delivery = trigger.lastWebhookDelivery;
const checked =
checkBaseline !== null && delivery && delivery.receivedAt !== checkBaseline;
return (
<div className="space-y-4">
<WebhookUrlWarning url={trigger.webhookUrl ?? ""} />
{secret && (github || trigger.signingMode === "bearer" || trigger.signingMode === "app_webhook" || trigger.signingMode === "fireflies_hmac") && (
<AgentInstructions
value={webhookAgentInstructions(
github ? "github" : "custom",
routineTitle,
trigger.webhookUrl ?? "",
secret,
false,
trigger.signingMode === "bearer" ? "bearer" : trigger.signingMode === "fireflies_hmac" ? "fireflies_hmac" : "app_webhook",
)}
/>
)}
<CopyField label="Webhook URL" value={trigger.webhookUrl ?? ""} />
{trigger.signingMode !== "none" && (
<div className="space-y-2">
{secret ? (
<CopyField
label={
trigger.signingMode === "bearer"
? "Authorization header value"
: "Secret key"
}
value={
trigger.signingMode === "bearer" ? `Bearer ${secret}` : secret
}
/>
) : (
<p className="text-sm text-muted-foreground">
The secret key is hidden.
</p>
)}
<Button variant="outline" size="sm" onClick={() => setReplace(true)}>
Replace key
</Button>
{secret && (
<Button variant="ghost" size="sm" onClick={() => setSecret("")}>
Hide key
</Button>
)}
</div>
)}
{trigger.signingMode === "none" && (
<p className="text-sm text-muted-foreground">
This webhook uses its URL as the shared secret.
</p>
)}
{trigger.signingMode === "hmac_sha256" && (
<p className="text-sm text-muted-foreground">
Sign the timestamp, a period, and the exact JSON body with
HMAC-SHA256. Send X-Paperclip-Timestamp and X-Paperclip-Signature:
sha256=&lt;signature&gt;.
</p>
)}
<Button
variant="outline"
onClick={() =>
setCheckBaseline(trigger.lastWebhookDelivery?.receivedAt ?? "")
}
>
Check connection
</Button>
<Dialog open={replace} onOpenChange={setReplace}>
<DialogContent>
<DialogHeader>
<DialogTitle>Replace the webhook key?</DialogTitle>
<DialogDescription>
The old key will stop working. Update your sending app with the
new key.
</DialogDescription>
</DialogHeader>
{error && (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
)}
<Button
disabled={busy}
onClick={async () => {
setBusy(true);
setError("");
try {
const response = await routinesApi.rotateTriggerSecret(
trigger.id,
);
setSecret(response.secretMaterial.webhookSecret);
setReplace(false);
await onRefresh();
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: "Couldn’t replace the key.",
);
} finally {
setBusy(false);
}
}}
>
Replace key
</Button>
</DialogContent>
</Dialog>
<Dialog
open={checkBaseline !== null}
onOpenChange={(open) => {
if (!open) setCheckBaseline(null);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Check connection</DialogTitle>
<DialogDescription>
This webhook is already enabled. Events sent now can start the
routine. Send an event from your app to check delivery.
</DialogDescription>
</DialogHeader>
<p role="status" className="text-sm">
{checked
? delivery.status === "received"
? "Event received · Authentication passed"
: "Event arrived, but authentication failed. Check the key in your app."
: "Waiting for an event from your app…"}
</p>
</DialogContent>
</Dialog>
</div>
);
}