feat(api-service, framework): wire email action buttons to onAction handler fixes NV-7422#11075
Conversation
Email cards from agents currently render `<Button>` action elements as links with `href="#"`, so clicks do nothing. Other channels (Slack/Teams/ in-app) already deliver clicks through `chat.processAction` → `onAction`, but email had no equivalent path. Each button now gets a unique signed URL pointing at a new public API endpoint. The flow is two-step to defeat email-client URL prefetchers (Outlook Safe Links, Mimecast, etc.): GET /v1/agents/email/actions/preview?t=<jwt> — verify, render confirm HTML POST /v1/agents/email/actions/execute — single-use claim, dispatch The execute endpoint calls `chat.processAction` with the email adapter, which fires the existing `chat.onAction` listener and flows through the unchanged `AgentInboundHandler.handleAction` → bridge → user's `onAction` handler with the original button's `id` and `value` intact. Security: - Audience-scoped JWT (`agent_email_action`) signed with JWT_SECRET - 30-day TTL (configurable via AGENT_EMAIL_ACTION_JWT_TTL) - Single-use enforced via Redis SET NX EX keyed on the token's jti - Two-step flow ensures Safe Links can't auto-execute actions UX: server-rendered HTML pages with inline CSS animations (confirm, animated checkmark success, already-submitted, expired/error). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for dashboard-v2-novu-staging canceled.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughImplements cache-backed opaque single-use email action tokens, signs action URLs via the Email adapter, resolves action links during card render, adds ChatSdk email-action processing, and provides GET /preview and POST /execute HTML endpoints to confirm and execute single-use actions. ChangesEmail Action Link Tokens & Click Processing
Sequence DiagramsequenceDiagram
participant User as User (Email Client)
participant Preview as GET /agents/email/actions/preview
participant Execute as POST /agents/email/actions/execute
participant TokenService as AgentEmailActionTokenService
participant ChatSdk as ChatSdkService
User->>Preview: Click email button (contains opaque token)
Preview->>TokenService: peekActionToken(token)
TokenService-->>Preview: claims or null
Preview-->>User: HTML confirmation page (hidden token)
User->>Execute: Submit confirmation form
Execute->>TokenService: consumeActionToken(token)
TokenService-->>Execute: consumed or null
Execute->>ChatSdk: processEmailAction(claims)
ChatSdk-->>Execute: action processed or throws
Execute-->>User: HTML success / error / already-submitted page
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/agent-email-actions.controller.ts`:
- Around line 90-110: The token is being irrevocably consumed by
tokenService.claimSingleUse(claims.jti, claims.exp) before the side-effecting
work in chatSdkService.processEmailAction(claims) completes, so transient
failures cause lost actions and false "Already submitted" responses; change the
flow to only claim the single-use token after processEmailAction succeeds (or
implement a claim-with-callback/atomic operation if tokenService supports it),
i.e., move or perform tokenService.claimSingleUse after a successful await of
chatSdkService.processEmailAction(claims), and if the claim itself can fail,
handle that error path to render the appropriate page
(renderAlreadySubmittedPage or renderErrorPage) to avoid losing retries.
- Around line 116-118: sendHtml currently returns HTML without cache headers;
update the sendHtml method (agent-email-actions.controller.ts) to set
Cache-Control: no-store, Pragma: no-cache and Expires: 0 (legacy no-cache) on
the Response before sending, e.g. call res.set(...) (or res.header(...)) with
those headers and then res.status(...).type(...).send(body) so token-bearing
HTML previews are never cached.
In `@packages/chat-adapter-email/src/adapter.ts`:
- Around line 158-169: The code mints a Message-ID via generateMessageId and
uses it in the action URL context passed to renderMessage, but later code
replaces or uses result.messageId (e.g., the value from send result) which may
differ if the provider rewrites Message-ID; preserve and use the originally
minted messageId for action-token correlation everywhere instead of
result.messageId — ensure functions/blocks that build action URLs or store
lookup keys (references: generateMessageId, messageId, renderMessage, and any
subsequent use of result.messageId or threadResolver.getReplyHeaders) continue
to use the minted messageId rather than replacing it with the provider-returned
result.messageId.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: aeac3af3-b872-463e-895e-05f022495a76
📒 Files selected for processing (10)
apps/api/src/app/agents/agent-email-actions.controller.tsapps/api/src/app/agents/agents.module.tsapps/api/src/app/agents/services/agent-email-action-token.service.tsapps/api/src/app/agents/services/chat-sdk.service.spec.tsapps/api/src/app/agents/services/chat-sdk.service.tspackages/chat-adapter-email/src/adapter.tspackages/chat-adapter-email/src/card-renderer.tsxpackages/chat-adapter-email/src/index.tspackages/chat-adapter-email/src/message-renderer.tspackages/chat-adapter-email/src/types.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/services/agent-email-action-token.service.ts`:
- Around line 10-31: The AgentEmailActionClaims interface currently includes
PII/internal identifiers in the JWT payload; change the token payload to be
opaque by only including a unique jti (and minimal non-identifying metadata like
exp if needed) in AgentEmailActionClaims and remove agentId, agentIdentifier,
integrationIdentifier, environmentId, organizationId, threadId, messageId, and
userIdentifier from the JWT. Persist the full action context server-side
(DB/cache) keyed by the jti when issuing the token in the token-creation
function(s) (e.g., the create/sign method in
agent-email-action-token.service.ts) and, in the token verification/consume path
(verify/validate method in the same service), load the action context from
storage by jti to perform authorization and routing. Update any callers that
read fields from the JWT (token consumers) to instead fetch the context using
the jti (e.g., resolveActionFromToken or equivalent handler) and ensure
single-use enforcement still clears the stored context on consume.
- Around line 61-65: Validate that the API_ROOT_URL scheme is HTTPS before
building the email action URL: after computing base (variable base) check its
URL scheme and throw an error if it is not "https:" except when the host is
explicitly allowed for local/dev (e.g., "localhost", "127.0.0.1" or a configured
dev host/port); update the logic in the function that builds url (`const base =
...` / `const url = ...`) to parse base, enforce the scheme rule, and only
permit "http" for those explicit local hosts, otherwise reject to prevent
issuing tokens over insecure HTTP.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 885915de-cd3a-4202-857e-bfa036c769a0
📒 Files selected for processing (3)
apps/api/src/app/agents/agent-email-actions.controller.tsapps/api/src/app/agents/services/agent-email-action-token.service.tspackages/chat-adapter-email/src/adapter.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/chat-adapter-email/src/adapter.ts
- apps/api/src/app/agents/agent-email-actions.controller.ts
The previous design embedded action context as JWT claims in the URL.
JWT payloads are signed but not encrypted, so any party that sees the
link (corporate email scanners, server logs, browser history, mail
archives) can decode and read the recipient address, internal Novu
identifiers (environmentId, organizationId, agentId), and any
agent-supplied button value (which may carry app data like order IDs).
Switch to opaque tokens: the URL carries only 32 bytes of base64url
randomness, and the full action context lives server-side in Redis,
keyed by that token. Nothing about the action is recoverable from the
URL alone.
- Drop @nestjs/jwt and JwtService — no longer needed.
- Drop AgentEmailActionTokenService.{claimSingleUse, releaseSingleUse,
verifyActionToken}; replace with peekActionToken (read-only, used by
GET preview) and consumeActionToken / releaseActionToken (atomic
GETDEL + transient-failure rollback, used by POST execute).
- TTL drops from 30 days to 3 days (env: AGENT_EMAIL_ACTION_TOKEN_TTL).
- Remove JwtModule.register from AgentsModule — no consumer remains.
- Single "Link expired" page covers both "expired" and "already used"
for the preview path; execute distinguishes "already submitted".
Single-use semantics are preserved by atomic Redis GETDEL: only one
caller wins per token; the rest see "already submitted". On transient
dispatch failure the entry is restored with the original remaining TTL
so a token can never outlive its natural 3-day expiry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
apps/api/src/app/agents/services/agent-email-action-token.service.ts (1)
78-82:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate HTTPS scheme in
API_ROOT_URLbefore minting action links.Lines 78-82 only verify that
API_ROOT_URLis non-empty, but do not enforce HTTPS. If misconfigured to usehttp://, the opaque token can be intercepted in transit (email scanner logs, proxy logs, network sniffing). Since these tokens grant access to execute actions on behalf of users, they must only be transmitted over encrypted connections.🔒 Proposed fix to enforce HTTPS
const base = (process.env.API_ROOT_URL ?? '').replace(/\/$/, ''); if (!base) { throw new Error('API_ROOT_URL is not configured — cannot build email action URL'); } +let parsedBase: URL; +try { + parsedBase = new URL(base); +} catch { + throw new Error('API_ROOT_URL is invalid'); +} +const isLocalHost = parsedBase.hostname === 'localhost' || parsedBase.hostname === '127.0.0.1'; +if (parsedBase.protocol !== 'https:' && !isLocalHost) { + throw new Error('API_ROOT_URL must use https in non-local environments'); +} const url = `${base}/v1/agents/email/actions/preview?t=${encodeURIComponent(token)}`;As per coding guidelines,
apps/api/**: "Review with focus on security, authentication, and authorization."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/services/agent-email-action-token.service.ts` around lines 78 - 82, The code only checks that API_ROOT_URL (read into variable base) is non-empty but does not enforce HTTPS; update the logic in agent-email-action-token.service.ts where base is computed to parse API_ROOT_URL (use the URL constructor or equivalent) and verify its protocol is exactly "https:" (or that it starts with "https://") before building the action URL (the url variable); if the scheme is missing or not HTTPS, throw a clear error (e.g., "API_ROOT_URL must use https://") so tokens are only ever minted for encrypted endpoints.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@apps/api/src/app/agents/services/agent-email-action-token.service.ts`:
- Around line 78-82: The code only checks that API_ROOT_URL (read into variable
base) is non-empty but does not enforce HTTPS; update the logic in
agent-email-action-token.service.ts where base is computed to parse API_ROOT_URL
(use the URL constructor or equivalent) and verify its protocol is exactly
"https:" (or that it starts with "https://") before building the action URL (the
url variable); if the scheme is missing or not HTTPS, throw a clear error (e.g.,
"API_ROOT_URL must use https://") so tokens are only ever minted for encrypted
endpoints.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 954f748e-583d-4fa9-afdc-7d80dba35c9d
📒 Files selected for processing (4)
.cursorignoreapps/api/src/app/agents/agent-email-actions.controller.tsapps/api/src/app/agents/agents.module.tsapps/api/src/app/agents/services/agent-email-action-token.service.ts
✅ Files skipped from review due to trivial changes (1)
- .cursorignore
API_ROOT_URL was only checked for non-emptiness; a misconfigured http:// base would leak the action token in cleartext on every click. Tokens are bearer authority, so transit confidentiality matters. Validate API_ROOT_URL with the URL constructor and require https:// except for loopback hosts (localhost / 127.0.0.1 / ::1), which keeps local development on http://127.0.0.1:3000 working as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round of design-quality improvements on the agent email action flow: - **Agent identity surfaced** — preview/success pages show the agent's display name + a deterministic colored monogram avatar (initials, one of 8 palette pairs hashed from the name). - **Style-aware destructive UI** — `<Button style="danger">` now renders a red Confirm button + a "This action cannot be undone" warning chip; non-destructive actions skip the warning entirely. - **Loading state on submit** — clicking Confirm immediately disables the button, swaps the label to "Submitting…" with an inline spinner, and prevents double-submits. - **Smooth in-place card swap** — inline fetch() intercepts the form submit, parses the server response, and replaces only the `.card` element with a fade-out → fade-in transition. No JS = graceful fallback to a plain form POST + full reload. - **Cancel affordance** — secondary button below the primary action; attempts window.close() and falls back to a visible hint. - **Dark mode** — full `@media (prefers-color-scheme: dark)` block with dark variants for every page state, including the avatar palette and danger styling. - **Footer with timestamp** — "Sent N min ago by <Agent Name>". - **Redesigned hierarchy** — action label becomes the headline; the redundant "Confirm <label>" button copy is collapsed to "Confirm". - **Improved close affordance on success page** — "← Close this tab" link with the same window.close() + fallback pattern. Data plumbing: - `agentName` added to ResolvedAgentConfig (populated from `agent.name`) - `style` added to ActionUrlBuilder params + extracted from button nodes in card-renderer.tsx - AgentEmailActionClaims gains `agentName` and `style?` - StoredEntry gains `mintedAt`; peek/consume return shapes updated to carry it through for the footer rendering - JwtModule import already removed in previous commit; nothing else in agents.module depended on it Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/agent-email-actions.controller.ts`:
- Around line 108-117: The current catch releases the action token for any
exception from chatSdkService.processEmailAction, which can permit replay after
partial side effects; change the logic to only re-release the token for errors
proven to be pre-dispatch by introducing and checking a specific retry signal
(e.g., a PreDispatchError class or an error property like err.preDispatch ===
true) returned/thrown from processEmailAction. Update
chatSdkService.processEmailAction (and its downstream handlers that call user
onAction) to throw or propagate that specific PreDispatchError when no side
effects have occurred, and in agent-email-actions.controller.ts only call
tokenService.releaseActionToken(token, consumed) when the caught error is that
PreDispatchError (otherwise rethrow or handle without releasing). Reference:
chatSdkService.processEmailAction, tokenService.releaseActionToken, consumed,
token, and the user onAction path.
In `@apps/api/src/app/agents/services/agent-email-action-token.service.ts`:
- Line 6: DEFAULT_TTL_SECONDS was changed to 3 days and the code only reads
AGENT_EMAIL_ACTION_TOKEN_TTL, breaking existing deployments that use
AGENT_EMAIL_ACTION_JWT_TTL and expect a 30-day default; restore backward
compatibility by setting DEFAULT_TTL_SECONDS to 30 days (60*60*24*30) and when
reading the TTL environment, check AGENT_EMAIL_ACTION_TOKEN_TTL first and
fallback to AGENT_EMAIL_ACTION_JWT_TTL before using the default (update the same
logic wherever AGENT_EMAIL_ACTION_TOKEN_TTL is read, e.g., in
AgentEmailActionTokenService and the code around the earlier referenced lines).
- Around line 152-158: The method consumeActionToken currently returns null when
cacheService.client is missing, which the caller treats the same as an
expired/consumed token; change this to return a distinguishable failure or throw
so callers can show a retryable error. Specifically, in consumeActionToken
(agent-email-action-token.service.ts) detect when cacheService.client is falsy
and either throw a dedicated error class (e.g., CacheUnavailableError) or return
a ConsumedActionToken-shaped object with an explicit status like { status:
'CACHE_UNAVAILABLE' }, then update the controller that consumes
consumeActionToken to handle that error/status and render a retryable error page
instead of the "Already submitted" page. Ensure the chosen approach uses the
existing ConsumedActionToken type or a documented error type so callers can
reliably branch on cache-unavailable vs expired/consumed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 839c8eb4-fda9-4ea6-a70d-bedc3aa359e3
📒 Files selected for processing (7)
apps/api/src/app/agents/agent-email-actions.controller.tsapps/api/src/app/agents/services/agent-config-resolver.service.tsapps/api/src/app/agents/services/agent-email-action-token.service.tsapps/api/src/app/agents/services/chat-sdk.service.tspackages/chat-adapter-email/src/card-renderer.tsxpackages/chat-adapter-email/src/index.tspackages/chat-adapter-email/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/chat-adapter-email/src/index.ts
- packages/chat-adapter-email/src/types.ts
- packages/chat-adapter-email/src/card-renderer.tsx
- apps/api/src/app/agents/services/chat-sdk.service.ts
Two of CodeRabbit's three new comments on #11075 (skipping the third with a documented reason): 1. Critical — release-on-failure could replay post-dispatch side effects `processEmailAction` ends with `chat.processAction(...)`, which routes into the agent author's `onAction` handler. If that throws after running partial non-idempotent work (Slack message sent, DB row updated, third-party API called), the previous catch handler would re-release the single-use token and the user could replay the same email click, duplicating that work. Fix: split `processEmailAction` into a pre-dispatch phase (config resolution, chat-instance lookup, adapter availability check) and the dispatch itself. Pre-dispatch failures are wrapped in a new `AgentActionPreDispatchError` — these are provably side-effect-free and safe to retry. Anything that escapes from `chat.processAction(...)` propagates as-is, and the controller no longer releases the token in that branch. The user sees a terminal error; recovery means a fresh email link, not a replay of a partially-executed action. 2. Major — cache outage masquerading as "Already submitted" When Redis is unreachable, `consumeActionToken` returned `null`, which the controller treated as "already used" and rendered the terminal "Already submitted" page — silently dropping a valid click. Same hazard on `peekActionToken` (cacheService.get throws → uncaught 500 from NestJS). Fix: new `AgentEmailActionCacheUnavailableError` typed error raised by both peek and consume on cache failures. Controller catches it and renders a new "We're having trouble right now" retry page (HTTP 503). The token is *not* consumed on this path, so reloading the page once service is restored just works. Skipped (with reason): - TTL backward-compat (`AGENT_EMAIL_ACTION_JWT_TTL` fallback + 30-day default). Both the old name and 30-day default lived in earlier commits on this same unmerged branch — no external deployment can possibly depend on them. The rename matches a real semantic change (JWT → opaque token); preserving the misleading name would only add noise. The 3-day default is an explicit product choice in this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
What changed? Why was the change needed?
Today, when a Novu Agent sends an email rendered as a
<Card>with<Button>action elements, the email HTML produces<a href="#">…</a>and clicks do nothing. Other channels (Slack/Teams/in-app) already deliver clicks throughchat.processAction→chat.onAction→AgentInboundHandler.handleAction→ bridge → user'sonActionhandler — but email had no equivalent path.Each action button now gets a unique opaque random token in its URL, pointing at a new public API endpoint. All action context (agent/environment/org IDs, recipient address, action id/value, button style) lives server-side in Redis keyed by that token — nothing about the action is recoverable from the URL alone, so corporate email scanners, browser history, proxy logs, and mail archives never see the recipient address or internal Novu identifiers.
The flow is intentionally two-step to defeat email-client URL prefetchers (Outlook Safe Links, Mimecast, etc.):
The existing
cached.chat.onAction(...)registration inChatSdkService.registerEventHandlerscarries the click intoAgentInboundHandler.handleActionwith the original button'sidandvalueintact — no changes to that handler are needed.Code areas
chat-adapter-email package
NovuEmailAdapterConfigaccepts an optionalactionUrlBuildercallback (per-button URL minting).card-renderer.tsxpre-walks the card tree, extractsid/value/label/stylefrom each<Button>, resolves URLs asynchronously, then renders the React tree synchronously.adapter.postMessagenow mints the localmessageIdbefore rendering so token claims bind to the exact email; also tracks the minted ID in the thread resolver when the provider rewrites Message-ID, so the JWT-stylesourceMessageIdcontract holds.API
AgentEmailActionTokenService— mints opaque 32-byte base64url tokens (randomBytes(32)), stores claims server-side viaCacheService, exposespeek/consume/release.consumeis atomic via RedisGETDEL.AgentEmailActionsController— public GET preview + POST execute, with inline server-rendered HTML pages.ChatSdkService.processEmailAction(claims)— public entrypoint that dispatches viachat.processActionso email flows reuse the existing onAction plumbing. Split into a pre-dispatch phase (config/adapter setup; throwsAgentActionPreDispatchError) and the dispatch phase, so only pre-dispatch failures are retryable.ResolvedAgentConfignow carriesagentName(populated fromagent.name) — used for the confirmation page header and monogram.Security
GETDEL— concurrent clicks resolve cleanly (one wins, the rest see "already submitted").API_ROOT_URLis parsed and validated at token-sign time; non-HTTPS schemes throw, except for loopback hosts (localhost / 127.0.0.1 / ::1) so local dev still works.Cache-Control: no-store, no-cache, must-revalidate, private,Pragma: no-cache,Expires: 0.AGENT_EMAIL_ACTION_TOKEN_TTL(seconds).chat.processAction(...)throws, the agent'sonActionmay have already executed partial work — re-releasing the token would risk replaying non-idempotent side effects. The release path is gated on a typedAgentActionPreDispatchErrorraised only by the config/adapter-setup phase.AgentEmailActionCacheUnavailableErroris thrown by peek/consume when Redis is unreachable; the controller renders an HTTP 503 "Try again" page that preserves the token.UX
agent.name, deterministic color from one of 8 palette pairs).<Button style="danger">renders a red Confirm button + "This action cannot be undone" warning chip. Non-destructive actions skip the warning entirely.fetch(); only the.cardelement is replaced with a fade-out → fade-in transition. Plain form-POST fallback when JS is disabled.window.close(), falls back to a visible hint.@media (prefers-color-scheme: dark), including dark variants of the avatar palette and the danger styling.<Agent Name>".Screenshots
Expand for optional sections
Special notes for your reviewer
send-test-emailagainst an agent whoseonMessagereturns a card with<Button id="approve" value="123">Approve</Button>. I'd like to confirm the bridge log shows the click landing inonActionwith{ id: 'approve', value: '123' }, and visually verify the success-screen animation.agentIdplumbing:createChatInstanceandbuildAdaptersnow takeagentIdso the email adapter'sactionUrlBuilderclosure can mint tokens bound to the right Mongo_id. This is the only signature change to existing internal methods; no public API onChatSdkServicewas renamed.chat-sdk.service.spec.tsbumps 5 manualnew ChatSdkService(...)calls to pass an extra{} as anyfor theAgentEmailActionTokenServiceconstructor argument.<meta name="robots" content="noindex,nofollow" />and<meta name="referrer" content="no-referrer" />, and the inline JS gracefully no-ops ifwindow.fetchis unavailable.GETDEL; the rest see "already submitted". Prefetchers don't POST, so this is robust in practice.AGENT_EMAIL_ACTION_JWT_TTL. The variable is renamed toAGENT_EMAIL_ACTION_TOKEN_TTLto match the JWT → opaque-token semantic change. No external deployment has ever depended on the old name.🤖 Generated with Claude Code