Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions packages/frontend/src/app/connectors/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@
}

fetchConnector();
}, [token, id]);

Check warning on line 251 in packages/frontend/src/app/connectors/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (lint, typecheck, build)

React Hook useEffect has missing dependencies: 'fetchConnector' and 'searchParams'. Either include them or remove the dependency array

const resetOauth1Fields = () => {
setEditOauth1Key('');
Expand Down Expand Up @@ -1147,29 +1147,29 @@
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium mb-1">Header Name</label>
<input type="text" value={editAuthKey} onChange={(e) => setEditAuthKey(e.target.value)} placeholder="X-API-Key" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="text" autoComplete="off" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editAuthKey} onChange={(e) => setEditAuthKey(e.target.value)} placeholder="X-API-Key" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
<div>
<label className="block text-sm font-medium mb-1">API Key</label>
<input type="password" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="password" autoComplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
</div>
)}
{editAuthType === 'BEARER_TOKEN' && (
<div>
<label className="block text-sm font-medium mb-1">Bearer Token</label>
<input type="password" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="password" autoComplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
)}
{editAuthType === 'BASIC_AUTH' && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium mb-1">Username</label>
<input type="text" value={editAuthKey} onChange={(e) => setEditAuthKey(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="text" autoComplete="off" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editAuthKey} onChange={(e) => setEditAuthKey(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
<div>
<label className="block text-sm font-medium mb-1">Password</label>
<input type="password" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="password" autoComplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
</div>
)}
Expand All @@ -1178,11 +1178,11 @@
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium mb-1">Client ID</label>
<input type="text" value={editAuthKey} onChange={(e) => setEditAuthKey(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="text" autoComplete="off" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editAuthKey} onChange={(e) => setEditAuthKey(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
<div>
<label className="block text-sm font-medium mb-1">Client Secret</label>
<input type="password" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="password" autoComplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editAuthValue} onChange={(e) => setEditAuthValue(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
</div>
<div>
Expand Down Expand Up @@ -1266,11 +1266,11 @@
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium mb-1">Username / Access key</label>
<input type="text" value={editLtUsername} onChange={(e) => setEditLtUsername(e.target.value)} placeholder="access key" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm font-mono bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="text" autoComplete="off" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editLtUsername} onChange={(e) => setEditLtUsername(e.target.value)} placeholder="access key" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm font-mono bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
<div>
<label className="block text-sm font-medium mb-1">Password / Secret</label>
<input type="password" value={editLtPassword} onChange={(e) => setEditLtPassword(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm font-mono bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
<input type="password" autoComplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore="true" data-form-type="other" value={editLtPassword} onChange={(e) => setEditLtPassword(e.target.value)} placeholder="Leave empty to keep current" className="w-full border border-[var(--border)] rounded-[9px] px-3 py-2 text-sm font-mono bg-[var(--surface)] focus:outline-none focus:border-[var(--border-strong)]" />
</div>
</div>
<div>
Expand Down
18 changes: 14 additions & 4 deletions packages/frontend/src/app/connectors/store/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { Card } from '@/components/ui/card';
import { Button, buttonVariants } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { authTypeLabel, cn } from '@/lib/utils';
import { adapterAuthLabel, adapterNeedsCredentials, cn } from '@/lib/utils';
import { McpAssignModal } from '@/components/mcp-assign-modal';
import { matchesSearch } from '@/lib/marketplace-search';
import { isTrialLimitMessage, TrialLimitNotice } from '@/lib/trial-limit';
Expand Down Expand Up @@ -384,7 +384,7 @@
if (!adapter) return;
autoInstallTriggered.current = true;
handleImportClick(adapter);
}, [loading, list, token, searchParams]);

Check warning on line 387 in packages/frontend/src/app/connectors/store/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (lint, typecheck, build)

React Hook useEffect has a missing dependency: 'handleImportClick'. Either include it or remove the dependency array

const handleConfigSubmit = () => {
if (!configAdapter) return;
Expand Down Expand Up @@ -606,7 +606,7 @@
) : (
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((adapter) => {
const isPublic = adapter.authType === 'NONE';
const isPublic = !adapterNeedsCredentials(adapter);
const isImporting = importing === adapter.slug;
/* log-ish 1..10 segment scale, same as the marketing-site card */
const fillCount = Math.max(
Expand Down Expand Up @@ -666,7 +666,7 @@
className="max-w-full min-w-0 gap-1 truncate font-mono uppercase tracking-wider"
>
{isPublic ? <SparklesIcon /> : <LockIcon />}
{authTypeLabel(adapter.authType)}
{adapterAuthLabel(adapter)}
</Badge>
)}
{adapter.docsUrl && (
Expand Down Expand Up @@ -734,7 +734,7 @@

<div className="mb-3 flex items-center gap-2 text-xs text-[var(--text-3)]">
<LockIcon />
<span>Auth type: {authTypeLabel(configAdapter.connector?.authType)}</span>
<span>Auth type: {adapterAuthLabel({ authType: configAdapter.connector?.authType, requiredEnvVars: configAdapter.requiredEnvVars })}</span>
</div>

{/* Setup instructions — collapsible details block, default open
Expand Down Expand Up @@ -784,6 +784,16 @@
<div className="relative">
<input
id={`cred-${envVar}`}
// These are API credentials, never the person's own
// login: without this, a browser that sees a text field
// followed by a password field fills in the saved
// AnythingMCP e-mail and password (seen on live installs).
name={`amcp-connector-var-${envVar}`}
autoComplete={isSecret ? 'new-password' : 'off'}
data-1p-ignore
data-lpignore="true"
data-bwignore="true"
data-form-type="other"
type={isSecret && !visible ? 'password' : 'text'}
value={credentialValues[envVar] || ''}
onChange={(e) =>
Expand Down Expand Up @@ -920,7 +930,7 @@
);
}

function DownloadIcon() {

Check warning on line 933 in packages/frontend/src/app/connectors/store/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (lint, typecheck, build)

'DownloadIcon' is defined but never used. Allowed unused vars must match /^_/u
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
Expand Down
25 changes: 25 additions & 0 deletions packages/frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,28 @@ export function authTypeLabel(authType: string | null | undefined): string {
if (!authType) return 'None';
return AUTH_TYPE_LABELS[authType] ?? authType;
}

/** A variable that holds a credential, judged by its name. */
const SECRET_VARIABLE = /KEY|TOKEN|SECRET|PASSWORD/i;

/**
* Whether an adapter needs a credential from the user. Some carry the key in
* the request body (Odoo's JSON-RPC) or a URL, so their authType is NONE
* although an API key is required: the variables decide too.
*/
export function adapterNeedsCredentials(adapter: {
authType?: string | null;
requiredEnvVars?: string[] | null;
}): boolean {
if (adapter.authType && adapter.authType !== 'NONE') return true;
return (adapter.requiredEnvVars ?? []).some((v) => SECRET_VARIABLE.test(v));
}

/** The auth chip of a catalog adapter: "API Key" rather than "Public API" when a key is needed. */
export function adapterAuthLabel(adapter: {
authType?: string | null;
requiredEnvVars?: string[] | null;
}): string {
if (adapter.authType === 'NONE' && adapterNeedsCredentials(adapter)) return 'API Key';
return authTypeLabel(adapter.authType);
}
57 changes: 57 additions & 0 deletions packages/frontend/tests/e2e/install-form-no-autofill.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test } from '@playwright/test';

/**
* The install form asks for API credentials. A browser that sees a text field
* followed by a password field takes it for a login and fills in the saved
* AnythingMCP e-mail and password: seen on live installs, where a user's own
* login ended up as a connector's Basic Auth or Odoo user id. The fields must
* opt out of autofill and password managers.
*
* And an adapter whose key travels in the body (authType NONE, Odoo's
* JSON-RPC) is not a "Public API".
*/

const USER = { id: 'u1', email: 'test@example.com', name: 'Test User', role: 'ADMIN', organizationId: 'o1', emailVerified: true };
const ODOO = {
slug: 'odoo-jsonrpc', name: 'Odoo 14-18 (JSON-RPC)', description: 'Odoo over JSON-RPC.', region: 'intl', category: 'erp',
icon: 'odoo', docsUrl: null, requiredEnvVars: ['ODOO_URL', 'ODOO_DB', 'ODOO_UID', 'ODOO_API_KEY'], toolCount: 11, authType: 'NONE',
};
const HN = { slug: 'hackernews', name: 'Hacker News', description: 'Public stories.', region: 'intl', category: 'news', icon: 'hackernews', docsUrl: null, requiredEnvVars: [], toolCount: 5, authType: 'NONE' };

test('credential fields opt out of autofill, and a body-key adapter is not labelled public', async ({ page, baseURL }) => {
await page.context().addCookies([{ name: 'amcp_token', value: 't', url: baseURL! }]);
await page.addInitScript((u) => {
localStorage.setItem('amcp_token', 't');
localStorage.setItem('amcp_user', JSON.stringify(u));
}, USER);
await page.route(/\/api\//, async (route) => {
const url = route.request().url();
const json = (b: unknown) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(b) });
if (/\/api\/adapters\/odoo-jsonrpc$/.test(url)) {
return json({ ...ODOO, instructions: 'Four values.', connector: { authType: 'NONE', baseUrl: '{{ODOO_URL}}' }, tools: [] });
}
if (/\/api\/adapters(\?|$)/.test(url)) return json([ODOO, HN]);
if (url.includes('/api/users/me/onboarding-state')) return json({ onboardingCompletedAt: '2026-01-01T00:00:00Z' });
if (url.includes('/api/users/me')) return json(USER);
if (url.includes('/api/license/status')) return json({ plan: 'community', status: 'active' });
if (url.includes('/api/connectors')) return json([]);
return json({});
});

await page.goto('/connectors/store');
await expect(page.getByText('Odoo 14-18 (JSON-RPC)', { exact: true })).toBeVisible({ timeout: 15_000 });
await expect(page.getByText('API Key', { exact: true }).first()).toBeVisible();
await expect(page.getByText('Public API', { exact: true })).toHaveCount(1); // Hacker News only

await page.getByRole('button', { name: 'Install' }).first().click();
const uid = page.locator('#cred-ODOO_UID');
const key = page.locator('#cred-ODOO_API_KEY');
await expect(uid).toBeVisible({ timeout: 10_000 });
await expect(uid).toHaveAttribute('autocomplete', 'off');
await expect(key).toHaveAttribute('autocomplete', 'new-password');
for (const field of [uid, key]) {
await expect(field).toHaveAttribute('data-1p-ignore');
await expect(field).toHaveAttribute('data-lpignore', 'true');
await expect(field).toHaveAttribute('name', /^amcp-connector-var-/);
}
});
Loading