Skip to content

feat: smart Slack link — preserve and auto-resume questions after account linking#2118

Merged
omar-inkeep merged 20 commits intomainfrom
feat/slack-smart-link
Feb 24, 2026
Merged

feat: smart Slack link — preserve and auto-resume questions after account linking#2118
omar-inkeep merged 20 commits intomainfrom
feat/slack-smart-link

Conversation

@nick-inkeep
Copy link
Copy Markdown
Collaborator

@nick-inkeep nick-inkeep commented Feb 18, 2026

Summary

  • Extend JWT link token with intent claims (question, entry point, channel context) so unlinked users' original questions are preserved when they're told to link their account
  • Auto-resume execution after linking completes — the verify-token handler detects intent in the JWT and fires a background callback via waitUntil to execute the original question
  • Replace two-step link flow with a single-step Block Kit ephemeral ("To get started, let's connect your Inkeep account with Slack." + [Link Account] button) across all 3 entry points (@mention, /inkeep <q>, /inkeep run "agent" <q>)
  • Add link page splash for unauthenticated users (sign-in CTA + "Don't have an account?" guidance) instead of silent redirect to /login
  • Add login page guidance ("Don't have an account? Ask your administrator for an invite.") for the invitation-only platform

How it works

User asks question → bot checks link → NOT LINKED →
  sign JWT with intent claims → send ephemeral Block Kit with [Link Account] →
  user clicks → /link splash → signs in → verify-token handler:
    1. create user mapping (existing)
    2. read intent from JWT
    3. waitUntil(resumeSmartLinkIntent()) — fire-and-forget
  → link page shows "Account Linked!" →
  background: resume resolves agent, executes question, posts response to Slack

Files changed (15 files, ~1200 lines including tests)

Area Files What
JWT schema agents-core/.../slack-link-token.ts Add SlackLinkIntentSchema + intent field to payload
Entry points app-mention.ts, commands/index.ts Capture intent in JWT, send Block Kit ephemeral
Block Kit blocks/index.ts createSmartLinkMessage() builder
Resume resume-intent.ts (new) Per-entry-point resume logic (~440 lines)
Verify-token routes/users.ts Wire waitUntil(resumeSmartLinkIntent(...)), normalize expired token errors
Link page link/page.tsx Splash page for unauthenticated users, improved expired token error
Login page login/page.tsx "Don't have an account?" guidance
Docs slack/commands.mdx, installation.mdx, overview.mdx Reflect smart link auto-prompt flow
Tests slack-link-token.test.ts, resume-intent.test.ts, app-mention.test.ts ~500 lines of unit tests
Changeset .changeset/satisfactory-coffee-cow.md Minor bump for agents-work-apps

Test plan

Manual QA scenarios verified via browser automation, direct function testing, and code review. Updated as tests complete.

Automated

  • Typecheck: pnpm typecheck passes
  • Lint: pnpm lint passes
  • Unit tests: All new tests pass (14 JWT schema tests, 8 resume-intent tests). Pre-existing form browser screenshot test failure (pixel dimension mismatch) is unrelated.

Visual / UI (browser automation)

  • Link page splash (expired JWT) — Navigated with real expired JWT (HS256, dev secret, exp in the past). Unauthenticated user sees "Connect your Inkeep account" splash: InkeepIcon, title, "Sign in to start using the bot in Slack." description, full-width Sign in button, "Don't have an account?" guidance. Token not validated client-side (correct behavior).
  • Link page splash (malformed token) — Same splash renders for not-a-valid-jwt-token. Token validation is server-side only.
  • Link page no-token — Unauthenticated user without token sees existing "Link Slack Account" / "run /inkeep link" message (unchanged)
  • Login page guidance — "Don't have an account?" in bold + "Ask your administrator for an invite." in muted text at bottom of CardContent, after sign-in form
  • Sign in redirect preserves token — Sign in button on link splash redirects to /login?returnUrl=/link?token=<full-expired-jwt>, preserving the complete return URL

Direct function testing (verifySlackLinkToken)

  • Expired tokenverifySlackLinkToken(expiredJWT) returns { valid: false, error: '"exp" claim timestamp check failed' }
  • Malformed tokenverifySlackLinkToken('not-a-valid-jwt-token') returns { valid: false, error: 'Invalid Compact JWS' }
  • Valid tokenverifySlackLinkToken(validJWT) returns { valid: true, payload: { ...intent, slack, tenantId } } with full intent preservation

Bug found and fixed during QA

  • Expired token error normalization — jose returns "exp" claim timestamp check failed but the link page UI checks error.includes('expired'). The word "expired" never appeared, so the expired-specific UI messaging (prompting users to run /inkeep link again) would never render. Fixed: verify-token handler now normalizes expired errors to 'Token expired. Please run /inkeep link in Slack to get a new one.'

Code review

  • Block Kit messagecreateSmartLinkMessage() produces correct structure: Section text + Actions with primary Link Account button + Context block
  • Resume error handlingresumeSmartLinkIntent() wrapped in top-level try/catch, never throws, logs structured events for success and failure

Not testable without live Slack workspace

  • E2E: @mention by unlinked user — Ephemeral Block Kit → complete linking → question auto-answered in thread · Skipped: requires live Slack workspace with bot installed
  • E2E: /inkeep <question> by unlinked user — Same flow → question answered in channel · Skipped: requires live Slack workspace
  • E2E: /inkeep run "agent" <question> — Same flow → answered with specified agent · Skipped: requires live Slack workspace
  • E2E: /inkeep link (no question) — Existing flow unchanged (no intent in JWT) · Skipped: requires live Slack workspace

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Feb 24, 2026 7:18pm
agents-docs Ready Ready Preview, Comment Feb 24, 2026 7:18pm
agents-manage-ui Ready Ready Preview, Comment Feb 24, 2026 7:18pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 18, 2026

🦋 Changeset detected

Latest commit: 0c759d0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@inkeep/agents-core Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-work-apps Patch
@inkeep/agents-api Patch
@inkeep/agents-cli Patch
@inkeep/agents-sdk Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch
@inkeep/agents-mcp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(8) Total Issues | Risk: Medium

🟠⚠️ Major (4) 🟠⚠️

🟠 1) resume-intent.ts Duplicate utility function: findAgentByIdentifierViaApi

Issue: The new findAgentByIdentifierViaApi function (lines 218-284) reimplements the same logic as the existing fetchAgentsFromManageApi + findAgentByIdentifier functions in commands/index.ts (lines 53-146). Both functions fetch projects, then agents for each project, then find by name/ID. The implementations are nearly identical but with minor differences.

Why: Code duplication increases maintenance burden. If the API contract changes or a bug is found, both implementations need updating. The ~66 lines of duplicated code could be a single shared utility.

Fix: Extract fetchAgentsFromManageApi to a shared utility module (e.g., slack/services/utils.ts or a new slack/services/agent-lookup.ts) and import it in both commands/index.ts and resume-intent.ts.

Refs:

🟠 2) resume-intent.ts Missing timeouts on external calls

Issue: Several external calls lack explicit timeouts: findWorkspaceConnectionByTeamId (line 29), signSlackUserToken (line 39-45), and resolveEffectiveAgent (line 147). In a waitUntil background context, these could hold resources indefinitely if the underlying service is slow.

Why: The file correctly uses timeouts for fetch calls (lines 225, 301), but other async operations that involve DB or Nango calls don't have timeout protection. Inconsistent timeout handling can lead to resource leaks in degraded conditions.

Fix: Either wrap these calls in a timeout (using AbortController or a Promise.race pattern), or use the existing timedOp helper from events/utils.ts. Also consider extracting the hardcoded timeout values (10_000, 30_000) into named constants like INTERNAL_FETCH_TIMEOUT_MS used in commands/index.ts.

Refs:

🟠 3) users.ts:270-273 waitUntil unavailable silently drops resume work

Issue: When waitUntil is not available (line 270-273), the resumeWork promise is created but never awaited or scheduled. The response is returned immediately at line 276, and the background work may be terminated before completion.

Why: In environments without waitUntil (some deployment contexts), the user's question will never be resumed after linking. This is a silent failure that depends on the deployment environment, with no indication to operators or users.

Fix: Either await the promise as a fallback, or log when waitUntil is unavailable:

const waitUntil = await getWaitUntil();
if (waitUntil) {
  waitUntil(resumeWork);
} else {
  logger.warn({ entryPoint: intent.entryPoint }, 'waitUntil not available, awaiting resume work synchronously');
  await resumeWork;
}

Refs:

🟠 4) slack-link-token.ts Type allows illegal states — consider discriminated union

Issue: The SlackLinkIntentSchema uses a flat structure with optional fields instead of a discriminated union. The entryPoint field acts as a discriminant but the schema does not enforce that entry-point-specific fields are present. For example, a mention intent can be constructed without agentId/projectId, and a run_command intent without agentIdentifier.

Why: The defensive runtime checks in resumeMention (lines 97-101) and resumeRunCommand (lines 184-188) indicate the type permits invalid data flow. TypeScript won't catch these errors at compile time.

Fix: Use a Zod discriminated union to enforce entry-point-specific required fields:

const MentionIntentSchema = z.object({
  entryPoint: z.literal('mention'),
  question: z.string().min(1).max(2000),
  channelId: z.string().min(1),
  threadTs: z.string().optional(),
  messageTs: z.string().optional(),
  agentId: z.string().min(1),  // required for mention
  projectId: z.string().min(1), // required for mention
});

// ... similar for QuestionCommandIntentSchema, RunCommandIntentSchema

export const SlackLinkIntentSchema = z.discriminatedUnion('entryPoint', [
  MentionIntentSchema,
  QuestionCommandIntentSchema,
  RunCommandIntentSchema,
]);

Refs:

Inline Comments:

  • 🟠 Major: resume-intent.ts:106 Silent failure when timestamps missing
  • 🟠 Major: resume-intent.ts:331 Silent failure when run API returns non-OK
  • 🟠 Major: resume-intent.ts:266-268 Empty catch swallows errors without logging
  • 🟠 Major: resume-intent.ts:279-281 Outer catch swallows all errors including timeouts

🟡 Minor (2) 🟡

Inline Comments:

  • 🟡 Minor: resume-intent.ts:98 Full intent object logged may expose user question content
  • 🟡 Minor: link/page.tsx:161-166 Circular guidance — user is already on this page from Slack

💭 Consider (2) 💭

💭 1) resume-intent.ts Missing tracing spans for observability

Issue: The resumeSmartLinkIntent function doesn't create OpenTelemetry spans, making it difficult to trace resume operations in production.
Why: Since these run in waitUntil background context where failures are harder to observe, tracing would help with debugging.
Fix: Add tracer.startActiveSpan() with relevant attributes (entryPoint, teamId, channelId).
Refs: handleAppMention uses spans

💭 2) resume-intent.ts:109-113 Acknowledgment message lacks resumption context

Issue: The "Preparing a response..." message shown when resuming lacks context that the user's original question is being answered after linking.
Why: The user has been away doing account linking — a message like "Answering your question..." would provide better continuity.
Fix: Change text to '_Answering your question..._' or similar.


💡 APPROVE WITH SUGGESTIONS

Summary: This is a well-designed feature that elegantly solves the friction in the account linking flow. The JWT-based stateless approach is clean and the test coverage is solid (34 new tests). The most actionable items are the silent failure paths in resume-intent.ts where users could complete linking but never receive their answer due to missing error notifications. The code duplication with findAgentByIdentifierViaApi should be consolidated, and the type design would benefit from a discriminated union to prevent illegal states at compile time.

Discarded (12)
Location Issue Reason Discarded
commands.mdx:13 Ambiguous "it" pronoun LOW confidence, context makes meaning clear
installation.mdx:64 Sentence structure style LOW confidence, minor style preference
blocks/index.ts:221-231 Hardcoded strings vs i18n INFO level, matches existing pattern in PR
services/index.ts Barrel export missing Internal-only module, not consumer-facing
resume-intent.ts:67-86 New logging event pattern INFO level, reasonable new pattern
slack-link-token.ts:12-24 Schema follows patterns correctly Positive finding, no action needed
blocks/index.ts:221-230 Two message styles for linking INFO level, intentional design difference
resume-intent.ts:371 Generic error message Addressed by broader silent failure findings
link/page.tsx:124-171 Duplicate card layout MEDIUM confidence, reasonable for distinct states
link/page.tsx:125 isExpiredError reachability LOW confidence, likely defensive coding
link/page.tsx:151-159 Button vs Link for navigation SSR complexity makes onClick reasonable
login/page.tsx:279 Inconsistent Suspense fallback Minor styling inconsistency
Reviewers (11)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-sre 9 2 1 0 0 0 6
pr-review-errors 6 0 0 0 4 0 2
pr-review-tests 6 0 0 0 0 0 6
pr-review-consistency 6 1 0 0 0 0 5
pr-review-product 5 0 1 0 1 0 3
pr-review-frontend 4 0 0 0 1 0 3
pr-review-docs 2 0 0 0 0 0 2
pr-review-types 1 1 0 0 0 0 0
pr-review-security-iam 1 0 0 0 1 0 0
pr-review-standards 0 0 0 0 0 0 0
pr-review-breaking-changes 0 0 0 0 0 0 0
Total 40 4 2 0 7 0 27

Note: pr-review-breaking-changes confirmed backward compatibility is properly maintained via optional intent field.

Comment thread packages/agents-work-apps/src/slack/services/resume-intent.ts
Comment thread packages/agents-work-apps/src/slack/services/resume-intent.ts
Comment thread packages/agents-work-apps/src/slack/services/resume-intent.ts Outdated
Comment thread packages/agents-work-apps/src/slack/services/resume-intent.ts Outdated
Comment thread packages/agents-work-apps/src/slack/services/resume-intent.ts Outdated
Comment thread agents-manage-ui/src/app/link/page.tsx Outdated
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(5) Total Issues | Risk: Medium

🟠⚠️ Major (2) 🟠⚠️

Inline Comments:

  • 🟠 Major: slack-link-token.ts:12-22 Flat schema permits illegal states — discriminated union recommended
  • 🟠 Major: resume-intent.ts:241-324 Duplicate agent lookup function reimplements existing logic

🟡 Minor (1) 🟡

Inline Comments:

  • 🟡 Minor: .changeset/satisfactory-coffee-cow.md:3 Missing @inkeep/agents-work-apps in changeset

💭 Consider (1) 💭

Inline Comments:

  • 💭 Consider: link/page.tsx:131-139 Extract duplicate expired token check to helper

🕐 Pending Recommendations (2)

These issues were raised in the prior review and appear to still be relevant:

  • 🟠 resume-intent.ts:124 Silent failure when timestamps missing — user receives no feedback
  • 🟠 resume-intent.ts:388 Silent failure when run API returns non-OK response — user waits indefinitely

💡 APPROVE WITH SUGGESTIONS

Summary: The smart Slack link feature is well-structured with good test coverage and comprehensive error handling improvements from the prior review. However, two issues warrant attention before merge:

  1. Schema design: The flat SlackLinkIntentSchema permits invalid field combinations that require runtime validation. A discriminated union would make illegal states unrepresentable and simplify the resume-intent.ts switch logic.

  2. Code duplication: The new findAgentByIdentifierViaApi function duplicates ~84 lines of existing logic in commands/index.ts. Extracting to a shared utility would reduce maintenance burden.

The two Pending Recommendations from the prior review (silent failures at lines 124 and 388) also appear unresolved — the code logs errors but doesn't notify users when resume fails due to missing timestamps or API errors.

Discarded (12)
Location Issue Reason Discarded
resume-intent.ts Missing timeouts on findWorkspaceConnectionByTeamId These are internal DB/service calls with their own connection timeouts; adding explicit timeouts adds complexity without clear benefit
resume-intent.ts response_url 30-minute expiry window risk Documented Slack behavior; the code already falls back to bot channel post — this is defense-in-depth, not a bug
resume-intent.ts No retry logic for transient failures Fire-and-forget via waitUntil is intentional; adding retries would complicate the flow for marginal benefit
resume-intent.ts:152 agentName: intent.agentId passes ID as name Intentional fallback when agent name isn't available in JWT; acceptable UX tradeoff
resume-intent.ts Question content logged in error paths Reviewed the code — only questionLength is logged, not the full question. Finding was based on outdated code.
link/page.tsx Suspense fallback inconsistency The Suspense boundary wraps the entire page; the loader inside SlackLinkForm handles the auth loading state. Not a bug.
link/page.tsx Missing error boundary Pre-existing pattern across the app; not introduced by this PR
commands.mdx Ambiguous "you'll automatically receive" phrasing The docs are accurate — users do automatically receive the prompt. Minor stylistic preference.
installation.mdx Missing "What to tell users" section Documentation is adequate for the target audience; additional sections are nice-to-have
overview.mdx Feature list could use more detail Existing level of detail is consistent with sibling docs
resume-intent.ts Test coverage gaps Tests exist for core paths; additional edge case tests are nice-to-have but not blocking
link/page.tsx Success toast could be more specific UX preference; current "Account Linked!" message is clear and consistent
Reviewers (9)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-types 1 0 0 0 1 0 0
pr-review-consistency 5 0 0 0 2 0 3
pr-review-sre 8 0 0 0 0 0 8
pr-review-tests 7 0 0 0 0 0 7
pr-review-standards 4 0 0 0 0 2 2
pr-review-frontend 3 0 0 0 1 0 2
pr-review-errors 1 0 0 0 0 0 1
pr-review-docs 3 0 0 0 0 0 3
pr-review-product 4 0 0 0 0 0 4
Total 36 0 0 0 4 2 30

Comment thread .changeset/satisfactory-coffee-cow.md
Comment thread packages/agents-core/src/utils/slack-link-token.ts
Comment thread agents-manage-ui/src/app/link/page.tsx
Comment thread packages/agents-work-apps/src/slack/services/resume-intent.ts
@github-actions github-actions Bot deleted a comment from claude Bot Feb 18, 2026
nick-inkeep and others added 6 commits February 18, 2026 13:23
Add SlackLinkIntentSchema and SlackLinkIntent type to slack-link-token.ts.
Intent carries entry point, question, channel context, and agent info
so the user's original question can be resumed after account linking.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…nked path

Add createSmartLinkMessage() to blocks/index.ts with Section + Actions
(primary Link Account button) + Context ("This only needs to happen once.").
Replace plain text ephemeral in app-mention.ts unlinked path with JWT
signed with intent (mention entry point) and new Block Kit message.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
handleQuestionCommand now signs JWT with question_command intent.
handleRunCommand now signs JWT with run_command intent + agentIdentifier.
Both paths truncate questions to 2000 chars and use createSmartLinkMessage.
generateLinkCodeWithIntent accepts optional intent parameter.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(1) Total Issues | Risk: Medium

Delta Review Scope

This re-review covers changes since the last automated review (feb547e49):

  • 1 commit (ef96c3e78 fix test)
  • 2 lines changed — test fixture adding grantAccessToMembers: true

The delta does not address the blocking issue from the previous review.

🟠⚠️ Major (1) 🟠⚠️

Inline Comments:

  • 🟠 Major: resume-intent.ts:39-45 Missing channel auth context in resumed execution JWT

🟡 Minor (1) 🟡

Inline Comments:

  • 🟡 Minor: users.ts:306-308 Inconsistent response shape in concurrent conflict handler

🚫 REQUEST CHANGES

Summary: The blocking issue from the previous review remains unresolved. The signSlackUserToken call in resume-intent.ts (lines 39-45) still does not include channel authorization context (slackAuthorized, slackAuthSource, slackChannelId, slackAuthorizedProjectId).

Impact: Users who have access via channel-level authorization (grantAccessToMembers: true) but lack explicit project membership will:

  1. ✅ Get their question captured correctly when unlinked
  2. ✅ Successfully complete account linking
  3. Fail with 403 when the resumed question executes (SpiceDB denies access)
  4. ✅ Succeed if they ask the same question again directly after linking

This creates a confusing UX where the "smart link" feature appears broken for a specific (but common) authorization configuration.

Recommended fix: See the inline comment at resume-intent.ts:45 for implementation guidance. The pattern already exists in app-mention.ts and commands/index.ts — it just needs to be applied to the resume path.

Discarded (0)

No findings discarded in this delta review.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
Delta review — no subagents dispatched

Note: This is a delta re-review. The delta (1 commit, 2 lines) consists of test fixture updates — no substantive logic changes requiring full subagent review. The blocking issue from the previous review was identified by pr-review-security-iam and pr-review-consistency.

slackTeamId: teamId,
slackUserId,
slackEnterpriseId,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR: Missing channel auth context in resumed execution JWT

Issue: The signSlackUserToken call here does NOT include the channel authorization context parameters (slackAuthorized, slackAuthSource, slackChannelId, slackAuthorizedProjectId) that direct execution paths now include.

Why: This creates an authorization inconsistency:

  1. User asks question in channel with grantAccessToMembers enabled (but user lacks explicit project membership)
  2. User is prompted to link account, intent is stored in JWT
  3. User links account, resumeSmartLinkIntent fires
  4. Execution fails with 403 'insufficient permissions' because the token lacks slackAuthorized: true
  5. If user asks the same question again after linking, it succeeds because direct path includes channel auth

Direct paths (app-mention.ts:272-281, commands/index.ts:530-536) include these fields — resumed paths should too for consistent authorization.

Fix: For entry points that already have agent context (like mention with intent.agentId/intent.projectId), include channel auth. For question_command and run_command, resolveEffectiveAgent() is already called — use its result:

// For resumeMention - re-resolve agent config to get fresh auth state
const agentConfig = await resolveEffectiveAgent({
  tenantId,
  teamId,
  channelId: intent.channelId,
});

const slackUserToken = await signSlackUserToken({
  inkeepUserId,
  tenantId,
  slackTeamId: teamId,
  slackUserId,
  slackEnterpriseId,
  slackAuthorized: agentConfig?.grantAccessToMembers ?? false,
  slackAuthSource: agentConfig?.source === 'none' ? undefined : agentConfig?.source,
  slackChannelId: intent.channelId,
  slackAuthorizedProjectId: agentConfig?.projectId,
});

For resumeCommand and resumeRunCommand, pass the resolved agent's auth context to a shared token-signing call.

Refs:

@github-actions github-actions Bot deleted a comment from claude Bot Feb 23, 2026
@itoqa
Copy link
Copy Markdown

itoqa Bot commented Feb 24, 2026

Ito Test Report ✅

30 test cases ran. 30 passed.

All test cases for the Smart Slack Link feature (PR #2118) passed verification. The tests comprehensively covered the /link page behavior for both authenticated and unauthenticated users, the /login page guidance text, returnUrl preservation through authentication flows, token handling (expired, invalid, empty, long tokens), security protections (XSS, open redirect, session-only auth), and the complete end-to-end account linking flow. The application correctly renders splash screens, handles errors appropriately, prevents duplicate API calls, and sanitizes malicious input.

✅ Passed (30)
Test Case Summary Timestamp Screenshot
ROUTE-1 Splash screen renders correctly with InkeepIcon, 'Connect your Inkeep account' heading, 'Sign in' button, and account guidance text. URL stays on /link without redirect. 2:53 ROUTE-1_2-53.png
ROUTE-2 Clicking 'Sign in' button navigates to /login with returnUrl preserving the token. Login form renders with email/password fields. 3:56 ROUTE-2_3-56.png
ROUTE-3 Authenticated user navigating to /link with token triggers auto-link API call. Error state shown confirming token processing occurred. 6:39 ROUTE-3_6-39.png
ROUTE-4 Without token parameter, page shows 'Link Slack Account' heading with '/inkeep link' instructions. No Sign in button displayed. 4:10 ROUTE-4_4-10.png
ROUTE-5 Login page displays 'Don't have an account?' and 'Ask your administrator for an invite.' guidance text below sign-in form. 12:14 ROUTE-5_12-14.png
ROUTE-6 returnUrl from /link is preserved through login. After sign-in, redirected to /link with token showing linking state. 13:52 ROUTE-6_13-52.png
ROUTE-7 Success state verified with green CheckCircle icon, 'Account Linked!' heading, @testuser slack username, connected text, and countdown timer. 16:35 ROUTE-7_16-35.png
EDGE-1 Expired token with unauthenticated state shows splash screen normally. Error only displays post-auth when API validates token. 4:46 EDGE-1_4-46.png
EDGE-2 Authenticated user with expired token shows alert 'This link has expired. Try /inkeep link on Slack to get a new link.' 7:09 EDGE-2_7-09.png
EDGE-3 Generic (non-expired) token error correctly shows 'Invalid token signature' instead of expired message. isExpiredTokenError differentiates error types. 7:37 EDGE-3_7-37.png
EDGE-4 Loading spinner (Loader2 animate-spin) detected during auth check phase before splash screen renders. 3:32 EDGE-4_3-32.png
EDGE-5 Login page with encoded returnUrl preserves the /link path through OAuth flows. After auth, correctly redirects to /link with token. 14:28 EDGE-5_14-28.png
EDGE-6 Authenticated user on /link without token sees 'Link Slack Account' heading with instructions in waiting state. 6:11 EDGE-6_6-11.png
EDGE-7 verify-token API returns appropriate error messages. Expired errors show expired-specific alert, generic errors show raw message. 19:07 EDGE-7_19-07.png
EDGE-8 Tokens with URL-encoded characters (+, /, =) correctly preserved through encoding/decoding. Token sent to API matches original exactly. 20:11 EDGE-8_20-11.png
EDGE-9 Suspense fallback with Loader2 spinner renders during lazy load before SlackLinkForm mounts. 3:33 EDGE-9_3-33.png
EDGE-10 Complete E2E flow verified: unauthenticated splash → Sign in → login with returnUrl → redirect back to /link → token verification. 15:08 EDGE-10_15-08.png
EDGE-11 Concurrent verify-token requests both resolved gracefully with 400 status. No 500 server errors, unique constraint handled correctly. 21:08 EDGE-11_21-08.png
EDGE-12 Real API call with garbage token shows error alert with raw API error message, does not show expired message. 8:19 EDGE-12_8-19.png
ADV-1 XSS attempts in token parameter (script tag and javascript: URI) are safely handled. No script execution, splash screen shown safely. 23:19 ADV-1_23-19.png
ADV-2 Malicious returnUrl parameters (https://evil.com, //evil.com, javascript:alert) all redirect to / instead of malicious URLs. 24:32 ADV-2_24-32.png
ADV-3 verify-token API rejects all non-session authentication: x-dev-user-id (401), Bearer token (401), x-api-key (401). Session-based auth required. 22:09 ADV-3_22-09.png
ADV-4 Double-click on Sign in button results in single clean redirect to login with returnUrl preserved. No duplicate navigation. 23:37 ADV-4_23-37.png
ADV-5 Back/forward navigation preserves token state. Splash preserved on back, returnUrl preserved on forward. 24:02 ADV-5_24-02.png
ADV-6 Empty token parameter shows fallback instructions state (same as no token). No console errors, no API call made. 4:24 ADV-6_4-24.png
ADV-7 Page handles 10000-character token gracefully. Splash screen renders, no browser crash, HTTP 200 response. 5:10 ADV-7_5-10.png
LOGIC-1 Block Kit smart link documentation verified: overview.mdx, commands.mdx, installation.mdx all mention auto-prompt and Link Account button. 27:24 LOGIC-1_27-24.png
LOGIC-2 linkingRef guard prevents duplicate API calls. After navigating to /link with token and waiting, exactly 1 API call was made. 6:40 LOGIC-2_6-40.png
LOGIC-3 Authenticated user navigating to /login auto-redirects to dashboard. With returnUrl, redirects to the specified path. 12:50 LOGIC-3_12-50.png
LOGIC-4 isExpiredTokenError helper correctly identifies expired vs non-expired errors. Displays appropriate messages for each. 7:11 LOGIC-4_7-11.png
📋 View Recording

Screen Recording

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(1) Total Issues | Risk: Low

Delta Review Scope

This re-review covers changes since the last automated review (ef96c3e7884f):

  • 1 commit (d8c64e656 Update smart-link so it works with google and sso methods)
  • Key changes: Login page auto-signin support for Google/SSO, auth method propagation through invitation flow

✅ Prior Blocking Issue: RESOLVED

The MAJOR blocking issue from the previous review ("Missing channel auth context in resumed execution JWT") has been fully addressed:

Component Status
getChannelAuthClaims helper added ✅ Lines 22-30
resumeMention uses channel auth ✅ Lines 147-156
resumeCommand uses channel auth ✅ Lines 215-218
resumeRunCommand uses channel auth ✅ Lines 256-259
Test coverage for channel auth claims ✅ All three entry points verified

The implementation correctly re-resolves resolveEffectiveAgent for each entry point and includes slackAuthorized, slackAuthSource, slackChannelId, and slackAuthorizedProjectId in the signed JWT token — matching the pattern used in direct execution paths.

🟡 Minor (1) 🟡

Inline Comments:

  • 🟡 Minor: commands/index.ts:73-74 Leftover debug console.log statement

💭 Consider (1) 💭

💭 1) login/page.tsx:161-223 Auto-signin effect complexity

Issue: The useEffect for auto-signin has many dependencies (11 items) and complex conditional logic with multiple auth method checks.

Why: This is manageable complexity for the use case, but the effect could benefit from early-return guards being consolidated. The current implementation is correct but could be slightly cleaner.

Fix: Consider extracting the auto-signin logic into a separate hook (e.g., useAutoSignIn) for better testability and separation of concerns. Not blocking — the current implementation works correctly.


💡 APPROVE WITH SUGGESTIONS

Summary: The prior blocking issue about missing channel auth context has been fully resolved. The new delta adds support for Google/SSO auto-signin when users follow invitation links with email hints, which improves the onboarding UX for organizations using OAuth providers.

The only actionable item is removing the debug console.log statement in commands/index.ts:73. This is a quick 1-click fix via the inline suggestion.

🎉 The smart Slack link feature is ready to ship after removing the debug statement!

Discarded (0)

No findings discarded in this delta review.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator-only 0 1 0 1 0 0

Note: This was a focused delta re-review. No sub-agent reviewers were dispatched as the delta was small and the prior blocking issue was the primary focus.


const message = createJwtLinkMessage(linkUrl, LINK_CODE_TTL_MINUTES);
console.log('linkResult', linkResult);
const message = buildLinkPromptMessage(linkResult);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: Leftover debug statement

Issue: console.log('linkResult', linkResult) is a debug statement that should be removed before merge.

Why: Console logs in production code are unprofessional, can leak sensitive data (the linkResult contains URLs with JWT tokens), and add noise to server logs.

Fix: (1-click apply)

Suggested change
const message = buildLinkPromptMessage(linkResult);
const message = buildLinkPromptMessage(linkResult);

Refs:

@github-actions github-actions Bot deleted a comment from claude Bot Feb 24, 2026
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(0) Total Issues | Risk: Low

Delta Review Scope

This re-review covers changes since the last automated review (d8c64e6561ae):

  • 1 commit (9248df82e remove log)
  • 1 line deleted — removed debug console.log('linkResult', linkResult) statement

✅ Prior Suggestion: RESOLVED

The only remaining suggestion from the previous review was the debug console.log statement in commands/index.ts:73. This has been removed in the delta commit.

Issue Status
Debug console.log in commands/index.ts ✅ Removed in 9248df82e

Prior Review Summary

All prior issues have now been addressed:

Category Status
Silent failure when timestamps missing ✅ Resolved
Silent failure when run API returns non-OK ✅ Resolved
Empty catch blocks swallowing errors ✅ Resolved
Missing @inkeep/agents-work-apps in changeset ✅ Resolved
Duplicate expired token check ✅ Resolved (helper extracted)
Circular guidance on link page ✅ Resolved
Full intent object logged ✅ Resolved
Channel auth context in resumed execution JWT ✅ Resolved
Debug console.log statement ✅ Resolved (this delta)
Flat schema design Declined with valid rationale
Duplicate agent lookup function Declined with valid rationale

✅ APPROVE

Summary: The smart Slack link feature is ready to merge. 🎉

This delta cleanly removes the debug statement that was the only remaining suggestion. The feature provides an excellent UX improvement — unlinked Slack users now have their questions preserved in JWT intent claims and automatically resumed after account linking. The implementation includes:

  • Stateless JWT-based intent preservation across all 3 entry points (@mention, /inkeep <q>, /inkeep run "agent" <q>)
  • Comprehensive error handling with user-facing feedback via postErrorToChannel
  • Channel auth context correctly propagated through resumed execution paths
  • Solid test coverage (~500 lines of new tests)
  • Updated documentation reflecting the new auto-prompt flow

The declined suggestions (discriminated union schema, agent lookup deduplication) have reasonable justifications — the flat schema keeps JWT payloads simple, and the agent lookup implementations serve different authorization models.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator-only 0 0 0 0 0 0

Note: This was a trivial delta re-review (1 line removed). No sub-agent reviewers were dispatched.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 24, 2026

Claude encountered an error —— View job


PR Review in Progress

Reviewing PR #2118: feat: smart Slack link — preserve and auto-resume questions after account linking

Review Scope: Delta review (review_scope=delta) — scoped to changes since last automated review (9248df82e)

  • Phase 1: Analyze PR context and explore codebase
  • Phase 1.5: Generate PR TLDR brief
  • Phase 2: Select appropriate reviewers
  • Phase 3: Dispatch reviewer subagents
  • Phase 4: Judge and filter findings
  • Phase 5: Add inline comments via pending review
  • Phase 6: Submit review with summary

Delta Analysis:

  • 15 commits since last review
  • 59 files changed (most from main merge)
  • Key PR-specific changes: Merge from main only (commit 0c759d0a0)

The prior review on 2026-02-24T18:01:14Z gave ✅ APPROVE with no remaining issues. All prior feedback has been addressed or declined with valid rationale.

Analyzing the delta to determine if any substantive PR-specific changes require re-review...

@github-actions
Copy link
Copy Markdown
Contributor

🔎💬 Inkeep AI search and chat service is syncing content for source 'Inkeep Agent Framework Docs'

@itoqa
Copy link
Copy Markdown

itoqa Bot commented Feb 24, 2026

Ito Test Report ✅

35 test cases ran. 35 passed.

This test run validated the Smart Slack Link feature from PR #2118, which introduces automatic account linking prompts with intent preservation and auto-resume functionality. All route behaviors, edge cases, security measures, and end-to-end redirect chains passed verification. The new link page splash for unauthenticated users, returnUrl preservation through OAuth flows, expired token handling, open redirect prevention, and Block Kit message formatting all work as expected.

✅ Passed (35)
Test Case Summary Timestamp Screenshot
ROUTE-1 Verified unauthenticated splash page shows 'Connect your Inkeep account' title, 'Sign in to start using the bot in Slack.' description, Sign in button, InkeepIcon, 'Don't have an account?' and 'Ask your administrator for an invite.' guidance text 6:52 ROUTE-1_6-52.png
ROUTE-2 Verified Sign-in button redirects to /login?returnUrl=%2Flink%3Ftoken%3Dsome-test-token. Decoded returnUrl equals /link?token=some-test-token, confirming the full return path is preserved 7:18 ROUTE-2_7-18.png
ROUTE-3 Verified link page without token shows 'Link Slack Account' heading, 'Use /inkeep link in Slack to link your account.' description, instruction text with /inkeep link code element, and no Sign-in button 0:13 ROUTE-3_0-13.png
ROUTE-4 Authenticated user navigating to /link with valid JWT token triggers auto-linking. Page shows 'Linking your accounts...' spinner, then transitions to success state with green checkmark, 'Account Linked' text, '@testuser' username, and 'You can close this window and return to Slack.' instruction. 9:40 ROUTE-4_9-40.png
ROUTE-5 Authenticated user navigating to /link without token sees 'Link Slack Account' heading with 'Use /inkeep link in Slack to link your account.' description and instruction text with code element. No linking process triggered. 9:22 ROUTE-5_9-22.png
ROUTE-6 Login page renders correctly with email field, password field, Sign in button, 'Don't have an account?' text, and 'Ask your administrator for an invite.' text 13:13 ROUTE-6_13-13.png
ROUTE-7 Auto-sign-in correctly triggered when all three params (email, invitation, authMethod=google) provided. Form showed 'Signing in...' with disabled fields, then redirected to Google OAuth (accounts.google.com). OAuth failed with invalid_client due to test client ID, confirming the auto-sign-in mechanism works. 44:27 ROUTE-7_44-27.png
ROUTE-8 Auto-sign-in correctly triggered when email+invitation+authMethod=sso params provided. Browser redirected to test-domain.auth0.com/authorize with login_hint=[email protected], proper scope and PKCE code_challenge. Auth0 showed error due to test domain, confirming SSO auto-sign-in mechanism works correctly. 46:47 ROUTE-8_46-47.png
ROUTE-9 Verified returnUrl is preserved in OAuth callbackURL. Captured POST to /api/auth/sign-in/social showing callbackURL='http://localhost:3000/?returnUrl=%2Flink%3Ftoken%3Dabc123'. After OAuth completes, home page would redirect to /link?token=abc123. OAuth redirect to Google confirmed. 47:04 ROUTE-9_47-04.png
ROUTE-10 Navigated to http://localhost:3000/?invitation=inv_789&returnUrl=%2Flink%3Ftoken%3Dtest while authenticated. Page redirected to /accept-invitation/inv_789?returnUrl=%2Flink%3Ftoken%3Dtest, confirming returnUrl is preserved through invitation redirect. 21:03 ROUTE-10_21-03.png
EDGE-1 Navigated to /link with an expired JWT token (exp set 30 min in the past). After linking attempt, page showed destructive alert with 'This link has expired. Try /inkeep link on Slack to get a new link.' with /inkeep link in a code element. Page state shows 'An error occurred.' heading. 23:39 EDGE-1_23-39.png
EDGE-2 Navigated to /link with invalid-not-a-jwt token. Page showed 'Linking your accounts...' then displayed error state with 'An error occurred.' and alert showing raw error 'Invalid Compact JWS'. Not the expired-specific message. 23:12 EDGE-2_23-12.png
EDGE-3 Verified clean unauthenticated splash page displays no error alerts. The expired token alert only appears after server-side token verification which requires authentication, so unauthenticated users see the clean splash page regardless of token validity. 6:58 EDGE-3_6-58.png
EDGE-4 Network monitoring during page load with valid JWT token confirms exactly ONE POST request to verify-token endpoint (http://localhost:3002/work-apps/slack/users/link/verify-token). The linkingRef guard successfully prevents React Strict Mode from triggering duplicate API calls. Linking completed successfully with @testuser2. 11:06 EDGE-4_11-06.png
EDGE-5 Login form renders normally without auto-sign-in when either invitation or email params are missing. Tested with email+authMethod=google (no invitation) and invitation+authMethod=google (no email) - both showed interactive form without loading spinner 13:33 EDGE-5_13-33.png
EDGE-6 Login form renders normally with all auto-sign-in params (email, invitation, authMethod=email-password) but auto-sign-in does NOT trigger. Button shows 'Sign in' (not loading), is not disabled, has no spinner. Form is fully interactive. 13:54 EDGE-6_13-54.png
EDGE-7 Verified three scenarios: (1) /login redirects to /default/projects, (2) /login?returnUrl=/link?token=xyz redirects to /link?token=xyz, (3) /login?invitation=inv_123 redirects to /accept-invitation/inv_123. All redirect behaviors correct for authenticated users. 17:18 EDGE-7_17-18.png
EDGE-8 Navigated to http://localhost:3000/?returnUrl=%2Flink%3Ftoken%3Dabc123 while authenticated. Page redirected to /link?token=abc123, confirming returnUrl without invitation is followed directly. 20:43 EDGE-8_20-43.png
EDGE-9 Success state after linking shows personalized message with '@testuser' in bold: 'Your Slack account @testuser is now connected. You can close this window and return to Slack.' The username comes from the JWT token's slack.username field. 10:07 EDGE-9_10-07.png
EDGE-10 After successful account linking, waited 6+ seconds. Page remains open showing success state with 'Account Linked' message. No countdown timer (3, 2, 1) appeared. Window did not auto-close. User must manually close as indicated by 'You can close this window' text. 10:27 EDGE-10_10-27.png
EDGE-11 Intercepted session API to return user.id=null while maintaining authenticated state. The useEffect guard (isAuthenticated && user?.id) prevented handleLinkWithToken from being called. Page showed safe waiting state with 'Link Slack Account' and 'Use /inkeep link in Slack to link your account.' No crash, no API call, no error. The defensive guard inside handleLinkWithToken is unreachable via normal flow but provides safety. 25:44 EDGE-11_25-44.png
EDGE-12 Verified autoSignInTriggered guard exists and functions correctly. Source code at login/page.tsx:73 declares useState(false), line 167 checks the guard in useEffect (returns early if true), and line 183 sets it to true before triggering OAuth. UI testing confirmed: navigating with auto-sign-in params (invitation+email+authMethod=google) renders interactive form without stuck loading spinner, and repeated navigations produce the same interactive state. 15:29 EDGE-12_15-29.png
EDGE-13 Cleared cookies to establish unauthenticated state. Navigated to http://localhost:3000/?returnUrl=%2Flink%3Ftoken%3Dabc. Page redirected to /login?returnUrl=%2Flink%3Ftoken%3Dabc, confirming unauthenticated users are sent to login with returnUrl preserved. 21:30 EDGE-13_21-30.png
ADV-1 All open redirect vectors blocked: https://evil.com in login returnUrl redirected to /default/projects, https://evil.com in home returnUrl redirected to /default/projects, protocol-relative //evil.com blocked and redirected safely, javascript:alert(1) blocked with no JS execution. All cases redirected to safe default /default/projects. 28:00 ADV-1_28-00.png
ADV-2 Navigated to /link with a JWT token signed using a wrong secret (different from the dev secret used by the application). Server rejected the token with 'signature verification failed' error. Page showed error state with 'An error occurred.' heading and error alert. 26:26 ADV-2_26-26.png
ADV-3 Tested three invalid token scenarios: (1) 'not-a-jwt-at-all' string shows error 'Invalid Compact JWS' gracefully; (2) empty token value shows instruction page 'Use /inkeep link in Slack'; (3) XSS attempt <script>alert(1)</script> in token param - no script execution, shows error gracefully. All cases handled safely without crashes or security issues. 26:48 ADV-3_26-48.png
ADV-4 Authenticated user navigated to /login?returnUrl=%2Flink%3Ftoken%3Dabc%26extra%3Dvalue was redirected to /link?token=abc&extra=value. URL-encoded characters (%2F, %3F, %3D, %26) were properly decoded. Both token=abc and extra=value query parameters preserved in final URL. Page loaded correctly showing link page with expected error for invalid token. 29:03 ADV-4_29-03.png
ADV-5 Verified that POST /work-apps/slack/users/link/verify-token returns 403 Forbidden with 'Session authentication required for account linking' for both system sessions (bypass secret Bearer auth) and dev-user sessions (localhost requests without session cookies). Both non-real session types are correctly rejected. 33:15 ADV-5_33-15.png
LOGIC-1 Verified complete redirect chain: unauthenticated user at /link?token sees splash, clicks Sign in, redirected to /login with returnUrl preserved, signs in with email/password, redirected back to /link?token with auto-linking triggered. Verify-token returned error (expected - no real Slack workspace) but redirect chain integrity confirmed. 36:13 LOGIC-1_36-13.png
LOGIC-2 Verified full redirect chain: /link?token=... (splash) -> Sign in -> /login?returnUrl=... -> Continue with Google -> OAuth with callbackURL containing returnUrl. Each step preserves the returnUrl correctly. OAuth redirect to Google confirmed. Full chain verified up to Google OAuth (which fails with test client ID but confirms all URL handling is correct). 49:07 LOGIC-2_49-07.png
LOGIC-3 Verified auto-invite OAuth chain. Auto-sign-in triggered automatically with email+invitation+authMethod=google params. Captured POST showing callbackURL='http://localhost:3000/?invitation=inv_test123&returnUrl=%2Flink%3Ftoken%3Dchain-token' with loginHint='[email protected]'. Both invitation and returnUrl preserved for post-OAuth redirect through home page. 50:19 LOGIC-3_50-19.png
LOGIC-4 Verified that the same authenticated user can re-link with a different valid JWT token for the same Slack user. First link succeeded showing Account Linked with @idemptest. Second link with a new token for the same Slack user also succeeded with Account Linked and @idemptest, confirming idempotent behavior with no 'already linked' error. 33:49 LOGIC-4_33-49.png
LOGIC-5 Verified createSmartLinkMessage produces correct Block Kit structure by executing the function directly via bun. Output contains: (1) section block with text 'To get started, let's connect your Inkeep account with Slack.', (2) actions block with 'Link Account' button having actionId 'smart_link_account' and style 'primary' with URL parameter passed through, (3) context block with 'This only needs to happen once.' text. All three expected elements confirmed present. 34:52 LOGIC-5_34-52.png
LOGIC-6 Navigated to /login?returnUrl=%2Flink%3Ftoken%3Dabc123, filled admin credentials, clicked Sign in. Successfully redirected to /link?token=abc123, confirming returnUrl preservation through email/password login flow. 16:41 LOGIC-6_16-41.png
LOGIC-7 Verified both invitation and returnUrl params are preserved in OAuth callbackURL. Captured POST to /api/auth/sign-in/social showing callbackURL='http://localhost:3000/?invitation=inv_abc&returnUrl=%2Flink%3Ftoken%3Dxyz'. Code confirmed at getFullCallbackURL function (login/page.tsx:55-70). 48:37 LOGIC-7_48-37.png
📋 View Recording

Screen Recording

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants