Skip to content

feat(manage-ui): improve OAuth credentials CRUD with multi-app support#2708

Merged
omar-inkeep merged 4 commits intomainfrom
feat/improve-oauth-credentials-crud
Mar 16, 2026
Merged

feat(manage-ui): improve OAuth credentials CRUD with multi-app support#2708
omar-inkeep merged 4 commits intomainfrom
feat/improve-oauth-credentials-crud

Conversation

@omar-inkeep
Copy link
Copy Markdown
Contributor

@omar-inkeep omar-inkeep commented Mar 16, 2026

Summary

  • Multi-app support: Users can now configure multiple OAuth app configurations per provider per tenant, with full CRUD (create, update credentials, delete with confirmation dialog)
  • Integration visibility on credential detail page: The credential edit form now shows the linked Nango integration's masked credentials (Client ID, Secret) so users can distinguish which OAuth app backs each credential
  • Accurate auth type labels: Replaced raw "nango" credential type display with human-friendly auth mode names derived from retrievalParams.authMode (e.g., "OAuth 2.0", "API Key", "App authentication")
  • Update warning: When editing app credentials, a warning alerts users that existing connections will be invalidated
  • Provider setup interstitial: When existing app configurations exist, users see a selectable list of integrations with masked credentials and a three-dot menu for edit/delete actions

Changed files

File Change
nango.ts Added listNangoProviderIntegrations, deleteNangoIntegration, updateNangoIntegrationCredentials; refactored fetchNangoIntegration to return masked credentials
[providerId]/page.tsx Rewrote to support multi-integration discovery, CRUD operations, interstitial UI with selectable cards and dropdown menus
generic-auth-form.tsx Added mode (create/update) and onCancel props; update warning alert
edit-credential-form.tsx Added "App configuration" section showing linked Nango integration with masked credentials; auth mode label mapping
[credentialId]/page.tsx Fetch linked Nango integration in parallel and pass to form

Test plan

  • Create a new OAuth credential for a provider — verify the form flow works end-to-end
  • Return to the same provider — verify existing app configurations appear as selectable cards with masked credentials
  • Click the three-dot menu → Edit credentials — verify the update form shows with warning banner
  • Click the three-dot menu → Delete — verify confirmation dialog and successful deletion
  • Configure a second OAuth app for the same provider → create credentials from each
  • Open each credential's edit page — verify the "App configuration" section shows the correct masked Client ID/Secret
  • Verify credential type shows "OAuth 2.0" (not "nango" or "API Key") for OAuth credentials

Screenshots here:

Multiple apps configured:
Screenshot 2026-03-16 at 2 37 03 PM

In credentials page:
Screenshot 2026-03-16 at 2 33 41 PM

Made with Cursor

…t and integration visibility

Support multiple OAuth app configurations per provider per tenant with full
CRUD operations (create, update, delete). Surface linked Nango integration
details (masked client ID/secret) on the credential edit page so users can
distinguish credentials backed by different app configurations. Replace raw
"nango" credential type labels with human-friendly auth mode names (e.g.,
"OAuth 2.0", "API Key").

Made-with: Cursor
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: ffa1a02

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

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-manage-ui Patch
@inkeep/agents-api Patch
@inkeep/agents-cli Patch
@inkeep/agents-core Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents 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

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Mar 16, 2026

Error

agent completed without reporting progress

Pullfrog  | Rerun failed job ➔View workflow run | Triggered by Pullfrogpullfrog.com𝕏

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 16, 2026

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

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 16, 2026 9:30pm
agents-manage-ui Ready Ready Preview, Comment Mar 16, 2026 9:30pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
agents-docs Skipped Skipped Mar 16, 2026 9:30pm

Request Review

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) [providerId]/page.tsx Unsafe as any type assertions bypass credential type validation

files:

  • agents-manage-ui/src/app/[tenantId]/projects/[projectId]/credentials/new/providers/[providerId]/page.tsx:142-146
  • agents-manage-ui/src/app/[tenantId]/projects/[projectId]/credentials/new/providers/[providerId]/page.tsx:201-207

Issue: Both startConnectFlow (line 144) and handleUpdateCredentials (line 205) cast credentials to any before passing to Nango API functions. The ApiPublicIntegrationCredentials type from @nangohq/types is a discriminated union requiring a valid type discriminator, but the assertions skip validation that provider.auth_mode is actually a valid discriminator value.

Why: If provider.auth_mode contains an unexpected value (e.g., a new auth mode added by Nango), the credentials object will have an invalid type field. This could cause runtime errors in the Nango API or silent failures where credentials are not properly set. For update operations, this could corrupt existing integration configurations. The as any assertion silences TypeScript's type narrowing that would otherwise catch mismatches at compile time.

Fix: Create a shared validation helper that validates the auth mode before casting:

function buildCredentialsPayload(
  credentials: Record<string, any> | undefined,
  authMode: string | undefined
): ApiPublicIntegrationCredentials | undefined {
  if (!credentials || !authMode) return undefined;
  
  const supportedModes = ['OAUTH2', 'OAUTH1', 'TBA', 'APP', 'OAUTH2_CC', 'API_KEY', 'BASIC'];
  if (!supportedModes.includes(authMode)) {
    console.warn(`Unsupported auth mode: ${authMode}`);
    return undefined;
  }
  
  return { ...credentials, type: authMode } as ApiPublicIntegrationCredentials;
}

Then use it in both handlers with proper error handling if validation fails.

Refs:


🟠 2) [providerId]/page.tsx Race condition in delete confirmation dialog

file: agents-manage-ui/src/app/[tenantId]/projects/[projectId]/credentials/new/providers/[providerId]/page.tsx:438-440

Issue: The AlertDialogAction's onClick handler calls handleDeleteIntegration(deleteTarget) and then immediately sets setDeleteTarget(null), closing the dialog before the async delete operation completes.

Why: Users won't see the loading state or any error if the delete fails. The dialog dismisses optimistically, and the delete operation runs fire-and-forget from the user's perspective. If deletion fails, the user sees no feedback and may think it succeeded.

Fix: Make the onClick handler async and await the delete operation:

onClick={async () => {
  if (deleteTarget) {
    await handleDeleteIntegration(deleteTarget);
  }
  setDeleteTarget(null);
}}

Refs:


🟠 3) [providerId]/page.tsx Raw unique_key displayed as primary card label instead of human-readable name

file: agents-manage-ui/src/app/[tenantId]/projects/[projectId]/credentials/new/providers/[providerId]/page.tsx:371

Issue: The interstitial card displays the raw unique_key (e.g., google-tenant123-a1b2c3) as the primary label. This is an internal implementation detail that provides no semantic meaning to users managing multiple OAuth apps for the same provider.

Why: The display_name field is available in NangoIntegrationWithMaskedCredentials but not used. Users see opaque IDs instead of meaningful identifiers, making it difficult to distinguish between multiple app configurations. This hurts the mental model and increases cognitive load.

Fix: Display a human-readable name: use display_name if set, otherwise derive a friendlier label (e.g., show the provider name with a creation timestamp or sequential number). Keep the unique_key visible as secondary metadata if needed for advanced users.

<span className="text-sm font-medium truncate block">
  {integration.display_name || integration.provider}
</span>
<span className="text-xs text-muted-foreground truncate block">
  {integration.unique_key}
</span>

Refs:


🟠 4) multi-file Mixed terminology creates concept confusion: "integration" vs "app" vs "credentials"

scope: UI copy across multiple files

Issue: The PR introduces inconsistent terminology for the same concept:

  • Delete dialog: "Delete integration" (page.tsx:427)
  • Warning alert: "using this integration" (generic-auth-form.tsx:198)
  • Button: "Configure new app" (page.tsx:421)
  • Section header: "App configuration" (edit-credential-form.tsx:226)

The existing docs and UI consistently use "credential" throughout.

Why: This vocabulary split increases cognitive load and confuses users about whether "integration", "app configuration", and "credential" are distinct concepts or the same thing. Users may not understand that deleting an "integration" affects their "credentials".

Fix: Align terminology across all new UI surfaces:

  • Use "OAuth app" or "app configuration" consistently for the provider-level Client ID/Secret configuration
  • Use "credential" or "connection" for the user-specific token
  • Update the delete dialog title to "Delete OAuth app" instead of "Delete integration"
  • Update warning text to be consistent with the chosen terminology

Refs:


Inline Comments:

  • 🟠 Major: nango.ts:243 Missing URL encoding for path parameter
  • 🟠 Major: [providerId]/page.tsx:187-190 Empty catch block silently swallows list refresh errors
  • 🟠 Major: [providerId]/page.tsx:387-391 Icon-only button missing accessible name

🟡 Minor (3) 🟡

🟡 1) nango.ts:75-118 Test coverage gap for credential masking functions

Issue: The maskSecret() and buildMaskedCredentials() functions lack unit tests. These functions handle security-sensitive credential masking with specific boundary logic.

Why: If masking logic regresses, sensitive credentials could be exposed in the UI. The boundary condition at value.length <= visibleChars is error-prone. The type-specific branching (OAuth vs APP) in buildMaskedCredentials could regress independently.

Fix: Add unit tests covering:

  • maskSecret: null/undefined/empty inputs, short values (≤ visibleChars), long values, 12-bullet cap
  • buildMaskedCredentials: null credentials, OAuth type requirements, APP type requirements, unknown type fallback

Refs:

  • Test pattern: agents-manage-ui/src/lib/mcp-tools/__tests__/ (create if not exists)

🟡 2) [providerId]/page.tsx:366-368 Disabled integration cards lack explanation

Issue: Clicking an integration card with areCredentialsSet: false is silently disabled with disabled={loading || !integration.areCredentialsSet}. The user sees a dimmed card but has no indication why it cannot be selected.

Why: Users don't understand what action is needed to fix the disabled state. They may assume the card is broken rather than incomplete.

Fix: Add a visible indicator or tooltip explaining why the card is disabled (e.g., "Credentials not configured — click Edit to set up"). Alternatively, auto-open the edit flow for incomplete integrations when clicked.

Refs:


Inline Comments:

  • 🟡 Minor: [providerId]/page.tsx:392 DropdownMenuContent missing standard styling classes
  • 🟡 Minor: edit-credential-form.tsx:90-92 Unsafe type assertion on authMode

💭 Consider (1) 💭

💭 1) nango.ts:54-62 MaskedCredentials interface allows illegal field combinations

Issue: The MaskedCredentials interface uses optional fields for properties that are mutually exclusive based on credential type. OAuth credentials have client_id/client_secret, while APP credentials have app_id/app_link. The current type allows mixing both field sets.

Why: Permits constructing objects like { type: 'OAUTH2', app_id: '123' } which is semantically invalid.

Fix: Consider a discriminated union if stronger type safety is desired:

type MaskedCredentials = 
  | { type: 'OAUTH2' | 'OAUTH1' | 'TBA'; client_id?: string; client_secret?: string; scopes?: string | null }
  | { type: 'APP'; app_id?: string; app_link?: string; has_private_key?: boolean }
  | { type: string }; // fallback

Trade-off: More verbose access patterns but compile-time safety.

Refs:


🚫 REQUEST CHANGES

Summary: This is a well-structured feature PR that adds valuable multi-app OAuth support. The main concerns are: (1) type safety issues with as any assertions that could cause runtime failures with new Nango auth modes, (2) a race condition in the delete confirmation flow, and (3) UX issues around terminology consistency and card labeling. The accessibility gap on the menu button is straightforward to fix. After addressing the Major items, this will be ready to merge.

Discarded (8)
Location Issue Reason Discarded
nango.ts:97 Client ID exposed unmasked while client_secret is masked Client IDs are designed to be semi-public (used in OAuth redirects, etc.) — this is intentional design, not a bug
[providerId]/page.tsx:212-215 Empty catch after update Duplicate of inline comment at 187-190 — same pattern, same fix
[providerId]/page.tsx:75-84 Initial load falls back to empty array Acceptable graceful degradation — server-side logging exists, and showing create form for empty state is reasonable UX
[credentialId]/page.tsx:37-41 fetchLinkedIntegration swallows errors Acceptable graceful degradation for optional enhancement — core credential edit still works
generic-auth-form.tsx decorative icons Missing aria-hidden on decorative icons Very low impact — Radix handles accessible names from text content, icons are supplementary
[providerId]/page.tsx:419-422 Button Plus icon missing aria-hidden Very low impact — button text provides accessible name
edit-credential-form.tsx:224-258 No edit affordance for app config section Informational observation — read-only display is intentional to prevent accidental changes
nango.ts:58-73 NANGO_AUTH_MODE_LABELS incomplete coverage Fallback behavior (showing raw authMode) is acceptable for unknown modes
Reviewers (7)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 4 0 0 0 2 0 2
pr-review-frontend 5 0 0 0 1 0 4
pr-review-product 5 2 0 0 0 0 3
pr-review-consistency 7 0 0 0 1 0 6
pr-review-tests 6 1 0 0 0 0 5
pr-review-errors 4 0 0 0 1 0 3
pr-review-types 4 1 1 0 1 0 1
Total 35 4 1 0 6 0 24

Note: High discard rate reflects effective filtering — many findings were duplicate observations across reviewers (e.g., empty catch blocks flagged by both standards and errors reviewers) or positive observations confirming patterns are correct.

process.env.NANGO_SERVER_URL ||
'https://api.nango.dev';

const response = await fetch(`${host}/integrations/${uniqueKey}`, {
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 URL encoding for path parameter

Issue: The uniqueKey path parameter is interpolated directly into the URL without encoding.

Why: If uniqueKey contains special characters (e.g., /, ?, #, or URL-reserved characters), this will cause malformed HTTP requests or routing to unintended endpoints. While current key generation uses safe characters, this is a latent bug that could surface if key formats change.

Fix: (1-click apply)

Suggested change
const response = await fetch(`${host}/integrations/${uniqueKey}`, {
const response = await fetch(`${host}/integrations/${encodeURIComponent(uniqueKey)}`, {

Refs:

Comment on lines +187 to +190
try {
const result = await listNangoProviderIntegrations(provider.name, tenantId);
setIntegrations(result);
} catch {}
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: Empty catch block silently swallows list refresh errors

Issue: After startConnectFlow succeeds, the integration list refresh silently discards any errors. The same pattern appears at lines 212-215 after credential updates.

Why: If the refresh fails (network issue, auth problem, Nango outage), the user sees stale data with no indication why. They may think their integration wasn't created, leading to duplicate attempts. Debugging is impossible since no error is logged.

Fix: Add warning-level logging to preserve diagnostic info without disrupting the success flow:

Suggested change
try {
const result = await listNangoProviderIntegrations(provider.name, tenantId);
setIntegrations(result);
} catch {}
try {
const result = await listNangoProviderIntegrations(provider.name, tenantId);
setIntegrations(result);
} catch (refreshError) {
console.warn('Failed to refresh integrations list after create:', refreshError);
}

Refs:

Comment on lines +387 to +391
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button size="icon-sm" variant="ghost" className="shrink-0 ml-4" disabled={loading}>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
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: Icon-only button missing accessible name

Issue: The three-dot menu trigger button has no aria-label to describe its purpose to screen reader users.

Why: Screen reader users hear only "button" with no context. With multiple integration cards rendered via .map(), users cannot distinguish between different menu triggers.

Fix: (1-click apply)

Suggested change
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button size="icon-sm" variant="ghost" className="shrink-0 ml-4" disabled={loading}>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button size="icon-sm" variant="ghost" className="shrink-0 ml-4" disabled={loading} aria-label={`Actions for ${integration.unique_key}`}>
<MoreVertical className="h-4 w-4" />
</Button>

Refs:

<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
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: DropdownMenuContent missing standard styling classes

Issue: This dropdown lacks the custom styling classes used consistently by peer components in the credentials UI.

Why: Visual inconsistency with other dropdown menus in the same domain (e.g., credential-item.tsx, mcp-tool-item.tsx).

Fix: (1-click apply)

Suggested change
<DropdownMenuContent align="end">
<DropdownMenuContent align="end" className="w-48 shadow-lg border border-border bg-popover/95 backdrop-blur-sm">

Refs:

Comment on lines +90 to +92
if (credential.type === CredentialStoreType.nango && credential.retrievalParams?.authMode) {
const authMode = credential.retrievalParams.authMode as string;
return NANGO_AUTH_MODE_LABELS[authMode] ?? authMode;
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: Unsafe type assertion on authMode

Issue: The authMode value is asserted as string without validation. If retrievalParams.authMode is not a string (e.g., null, undefined, or unexpected type), the fallback ?? authMode would display a non-string value.

Why: Could cause runtime rendering issues if data shape changes or corruption occurs.

Fix: Add type guard:

Suggested change
if (credential.type === CredentialStoreType.nango && credential.retrievalParams?.authMode) {
const authMode = credential.retrievalParams.authMode as string;
return NANGO_AUTH_MODE_LABELS[authMode] ?? authMode;
if (credential.type === CredentialStoreType.nango && credential.retrievalParams?.authMode) {
const authMode = credential.retrievalParams.authMode;
if (typeof authMode === 'string') {
return NANGO_AUTH_MODE_LABELS[authMode] ?? authMode;
}

Refs:

  • TypeScript type narrowing best practices

@github-actions github-actions Bot deleted a comment from claude Bot Mar 16, 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

(1) Total Issues | Risk: Low

Re-review scoped to delta — Commit 81d1fbd4d ("feedback") addresses 6 of 8 issues from the prior review.

✅ Addressed in This Delta

Prior Finding Resolution
🟠 Major #1: Unsafe as any type assertions ✅ Added buildCredentialsPayload() with proper isCredentialAuthMode() type guard at nango.ts:86-102
🟠 Major #3: Raw unique_key as primary label ✅ Now shows display_name || provider as primary, unique_key as secondary at page.tsx:373-378
🟠 Major #4: Mixed terminology ✅ "OAuth app" terminology now used consistently in dialog, toasts, and error messages
🟠 Inline: Missing URL encoding encodeURIComponent() added at nango.ts:272 and nango.ts:314
🟠 Inline: Empty catch blocks ✅ Added console.error logging at page.tsx:185-186 and page.tsx:215-216
🟠 Inline: Missing accessible name ✅ Added aria-label="OAuth app actions" at page.tsx:400

🕐 Pending Recommendations (1)

Issues from the prior review that remain unaddressed. The specific code causing these issues was not modified in this delta — they remain valid feedback for a future iteration.

  • 🟠 page.tsx:451-453 Race condition in delete confirmation dialog — handleDeleteIntegration(deleteTarget) is not awaited before setDeleteTarget(null) closes the dialog, preventing users from seeing loading state or error feedback if deletion fails.

💭 Lower Priority (Not Blocking)

These items from the prior review are valid but lower impact — reasonable to address in a follow-up:

  • 🟡 Test coverage gap for maskSecret() and buildMaskedCredentials() functions
  • 🟡 Disabled integration cards lack explanation for why they cannot be selected
  • 🟡 DropdownMenuContent missing standard styling classes used in peer components
  • 💭 MaskedCredentials interface could use discriminated union for stronger type safety

💡 APPROVE WITH SUGGESTIONS

Summary: The delta effectively addresses the critical type safety and UX issues from the prior review. The buildCredentialsPayload() helper with proper type guard is a clean solution that eliminates the unsafe as any casts. Terminology is now consistent, accessibility is improved, and error handling is no longer silent. The one remaining Major issue (delete dialog race condition) wasn't touched in this delta and can be addressed in a follow-up. Ship it! 🚀

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

Note: Sub-reviewers not dispatched for this delta re-review since changes were targeted feedback fixes with clear scope.

@github-actions github-actions Bot deleted a comment from claude Bot Mar 16, 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

Re-review scoped to delta — Commit ffa1a02d9 ("feedback") addresses the final pending issue from the prior review.

✅ Addressed in This Delta

Prior Finding Resolution
🟠 Delete dialog race condition ✅ Handler is now async and properly awaits handleDeleteIntegration(deleteTarget) before closing the dialog at page.tsx:451-456

Additional Changes

Change Analysis
buildCredentialsPayload now async Forward-compatible signature change. Call sites at page.tsx:141 and page.tsx:198 properly await the result.

📊 Review History Summary

This PR went through 3 review iterations:

  1. Initial review: 8 issues identified (4 Major, 3 Minor, 1 Consider)
  2. Delta review #1 (81d1fbd4d): 6 of 8 issues addressed
  3. Delta review #2 (ffa1a02d9): Final issue addressed ✅

All originally identified issues have been resolved:

  • ✅ Type safety with buildCredentialsPayload() helper
  • ✅ Human-readable labels (display_name || provider)
  • ✅ Consistent "OAuth app" terminology
  • ✅ URL encoding for path parameters
  • ✅ Error logging in catch blocks
  • ✅ Accessible aria-label on menu button
  • ✅ Delete dialog race condition (this delta)

✅ APPROVE

Summary: All feedback from prior reviews has been addressed. The multi-app OAuth credentials feature is well-implemented with proper type safety, consistent UX terminology, accessibility, and correct async handling. Ship it! 🚀

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

Note: Sub-reviewers not dispatched — delta was a targeted fix for the single remaining pending issue.

@github-actions github-actions Bot deleted a comment from claude Bot Mar 16, 2026
@omar-inkeep omar-inkeep added this pull request to the merge queue Mar 16, 2026
Merged via the queue into main with commit 5394245 Mar 16, 2026
11 checks passed
@omar-inkeep omar-inkeep deleted the feat/improve-oauth-credentials-crud branch March 16, 2026 22:35
@itoqa
Copy link
Copy Markdown

itoqa Bot commented Mar 16, 2026

Ito Test Report ❌

12 test cases ran. 10 passed, 2 failed.

✅ Core credential management flows were stable for baseline, detail-page, resilience, and mobile behaviors. 🔍 Code-first verification identified two actionable defects: URL protocol validation is bypassed in auth-form submission, and deep-linked credential-management surfaces rely on client-side redirect timing instead of enforced server-side authorization checks.

✅ Passed (10)
Test Case Summary Timestamp Screenshot
ROUTE-1 Provider route loaded, seeded integration card rendered, and Configure new app CTA was visible for mock-integration-provider. 12:50 ROUTE-1_12-50.png
ROUTE-5 Credential detail rendered App configuration for linked integration with masked client and secret values while keeping edit form usable. 40:10 ROUTE-5_40-10.png
LOGIC-1 Detail pages displayed mapped labels OAuth 2.0 and API Key rather than raw internal auth mode values. 40:17 LOGIC-1_40-17.png
LOGIC-3 Cancel returned to interstitial for provider with existing integrations and to providers list for provider without integrations. 13:27 LOGIC-3_13-27.png
EDGE-1 Confirmed unknown provider route renders Provider not found state and Back to providers recovery navigation works. 0:00 EDGE-1_0-00.png
EDGE-2 After a forced provider-data failure, the provider setup page stayed functional without hard crash and remained usable. 43:53 EDGE-2_43-53.png
EDGE-3 Incomplete OAuth App card remained disabled and non-interactive for mouse and keyboard attempts; no connect flow started. 24:20 EDGE-3_24-20.png
EDGE-4 When linked integration fetch returned unauthorized, credential edit form still rendered and remained usable with App configuration omitted. 40:58 EDGE-4_40-58.png
EDGE-5 In iPhone 13 viewport, card actions opened successfully and delete confirmation dialog remained readable and clickable. 45:35 EDGE-5_45-35.png
ADV-6 After repeated history navigation and refresh during mode switches, the provider interstitial recovered and remained usable. 46:42 ADV-6_46-42.png
❌ Failed (2)
Test Case Summary Timestamp Screenshot
ADV-4 Unsafe URL payload (javascript:) was accepted through form submission path, indicating missing or insufficient client-side protocol rejection. 44:51 ADV-4_44-51.png
ADV-5 Authorization boundary failed: authenticated view-only user (Viewer User) could access provider setup and credential deep-link edit surfaces with active write controls (Create Credential, Save, Delete Credential) instead of being redirected or restricted. 1:11:45 ADV-5_1-11-45.png
Input injection and unsafe URL payload rejection in auth forms – Failed
  • Where: Provider credential setup/update auth form (/credentials/new/providers/[providerId]) for URL-based fields.

  • Steps to reproduce: Open a provider form with app_link (or similar URL field), enter javascript:alert(1), submit.

  • What failed: The form accepted an unsafe protocol and proceeded with submission instead of blocking non-HTTP(S) URLs.

  • Code analysis: I checked the dynamic form validation pipeline and found protocol validation is defined but then dropped when required-field rules are applied.

  • Relevant code:

    agents-manage-ui/src/components/credentials/views/generic-auth-form.tsx (lines 39-58)

    if (field.validate) {
      fieldSchema = fieldSchema.refine(
        (value: unknown) => {
          const stringValue = String(value || '');
          if (!stringValue && !field.required) return true;
          const error = field.validate ? field.validate(stringValue) : undefined;
          return !error;
        },
        { message: `Invalid ${field.label.toLowerCase()}` }
      );
    }
    
    if (field.required) {
      fieldSchema = z.string().min(1, `${field.label} is required`);
    }

    agents-manage-ui/src/components/credentials/views/auth-form-config.ts (lines 50-57)

    const validateUrl = (value: string): string | undefined => {
      if (!value.trim()) return undefined;
      const url = new URL(value.trim());
      if (!['http:', 'https:'].includes(url.protocol)) {
        return 'Only HTTP and HTTPS URLs are allowed';
      }
      return undefined;
    };

    agents-manage-ui/src/components/credentials/views/auth-form-config.ts (lines 101-110)

    app_link: {
      key: 'app_link',
      type: 'url',
      required: true,
      validate: validateUrl,
    },
  • Why this is likely a bug: URL protocol allowlisting exists by design, but schema construction overwrites that validator for required fields, creating a real validation bypass in production form logic.

  • Introduced by this PR: Yes – this PR modified the relevant code.

  • Timestamp: 44:51

Edit permission bypass attempt via deep-link is blocked – Failed
  • Where: Deep-linked provider setup and credential-edit routes under project credentials.

  • Steps to reproduce: Authenticate as a view-only project user, open deep links to provider setup and credential edit pages, observe whether write controls/actions are available.

  • What failed: Write-capable controls remained accessible on deep-linked surfaces instead of being strictly denied by route-level authorization.

  • Code analysis: I checked permission enforcement across credential routes. The parent providers route has a server-side permission gate, but the deep-linked provider page only performs a client-side redirect in useEffect and still renders create/update/delete actions.

  • Relevant code:

    agents-manage-ui/src/app/[tenantId]/projects/[projectId]/credentials/new/providers/page.tsx (lines 39-44)

    await checkProjectPermissionOrRedirect(
      tenantId,
      projectId,
      'edit',
      `/${tenantId}/projects/${projectId}/credentials`
    );

    agents-manage-ui/src/app/[tenantId]/projects/[projectId]/credentials/new/providers/[providerId]/page.tsx (lines 68-72)

    useEffect(() => {
      if (!canEdit) {
        router.replace(`/${tenantId}/projects/${projectId}/credentials`);
      }
    }, [canEdit, router, tenantId, projectId]);

    agents-manage-ui/src/app/[tenantId]/projects/[projectId]/credentials/new/providers/[providerId]/page.tsx (lines 363-370)

    {integrations.map((integration) => (
      <button
        type="button"
        onClick={() => startConnectFlow(integration.unique_key)}
        disabled={loading || !integration.areCredentialsSet}
      >
  • Why this is likely a bug: Security-sensitive write flows depend on client-side redirect timing instead of guaranteed server-side authorization at the deep-link route, allowing unauthorized access to active write surfaces.

  • Introduced by this PR: Yes – this PR modified the relevant code.

  • Timestamp: 1:11:45

📋 View Recording

Screen Recording

omar-inkeep added a commit that referenced this pull request Mar 17, 2026
The `buildMaskedCredentials` refactor in #2708 removed the catch-all
`areCredentialsSet = true` for credential types other than OAUTH/TBA/APP.
MCP-generic integrations have no client credentials, so `areCredentialsSet`
became `false`, causing `createProviderConnectSession` to skip reuse and
attempt to create a duplicate integration — triggering "already exists".

Fix: check `existingIntegration` instead of `existingIntegration?.areCredentialsSet`.
Made-with: Cursor
github-merge-queue Bot pushed a commit that referenced this pull request Mar 17, 2026
…2729)

The `buildMaskedCredentials` refactor in #2708 removed the catch-all
`areCredentialsSet = true` for credential types other than OAUTH/TBA/APP.
MCP-generic integrations have no client credentials, so `areCredentialsSet`
became `false`, causing `createProviderConnectSession` to skip reuse and
attempt to create a duplicate integration — triggering "already exists".

Fix: check `existingIntegration` instead of `existingIntegration?.areCredentialsSet`.
Made-with: Cursor
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.

1 participant