console: sync the registration wizard and issue real API tokens
Mirror the Console registration feature: native hook catalog with activation plans, one-command install that embeds both credentials in exclusively created owner-only files (mktemp on POSIX, icacls on Windows), sh/PowerShell tabs, never-expiring default token, focus-visible styles, merged i18n keys, and the matching e2e specs. The server previously answered /v1/auth/token with the 7-day Better Auth session token and ignored ttl_seconds. It now issues opaque 32-byte API tokens stored as SHA-256 hashes (migration 003), honours ttl_seconds with null meaning no expiry, and resolves bearer values as API tokens before session validation. Issuance requires a session credential. Claude-Session: https://claude.ai/code/session_01QoUjdc7HGKTsgj5GhG8HvF
This commit is contained in:
1 parent
1bb9d4d49d
commit
edf9d73c3e
21 files changed
+967
-163
No files matched your search
@@ -12,7 +12,7 @@ import Modal from '@/components/ui/Modal';
|
||||
import Link from 'next/link';
|
||||
import type { AgentKey, Operation } from '@elydora/shared';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ADAPTER_INTEGRATION_IDS } from '@/features/agent-registration/integrations';
|
||||
import { HOOK_INTEGRATION_IDS } from '@/features/agent-registration/integrations';
|
||||
|
||||
export default function AgentDetailClient({ agentId }: { agentId: string }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -37,7 +37,7 @@ export default function AgentDetailClient({ agentId }: { agentId: string }) {
|
||||
|
||||
const agent = agentData?.agent;
|
||||
const supportsFreeze = agent
|
||||
? ADAPTER_INTEGRATION_IDS.has(agent.integration_type)
|
||||
? HOOK_INTEGRATION_IDS.has(agent.integration_type)
|
||||
: false;
|
||||
const keys = agentData?.keys ?? [];
|
||||
const operations = opsData?.operations ?? [];
|
||||
|
||||
@@ -7,7 +7,9 @@ import type { IntegrationCatalogItem } from '@/features/agent-registration/integ
|
||||
import {
|
||||
buildInstallInstructions,
|
||||
SDK_LANGUAGES,
|
||||
SHELLS,
|
||||
type SdkLanguage,
|
||||
type Shell,
|
||||
} from '@/features/agent-registration/install-instructions';
|
||||
import CopyControl from './CopyControl';
|
||||
|
||||
@@ -28,6 +30,7 @@ export default function ConnectStep({
|
||||
}: ConnectStepProps) {
|
||||
const { t } = useTranslation();
|
||||
const [language, setLanguage] = useState<SdkLanguage>('node');
|
||||
const [shell, setShell] = useState<Shell>('posix');
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787';
|
||||
const instructions = useMemo(
|
||||
() => buildInstallInstructions(language, {
|
||||
@@ -38,9 +41,12 @@ export default function ConnectStep({
|
||||
orgId: credentials.orgId,
|
||||
},
|
||||
baseUrl,
|
||||
}),
|
||||
[baseUrl, credentials.agentId, credentials.kid, credentials.orgId, integration, language],
|
||||
privateKey: credentials.privateKey,
|
||||
token,
|
||||
}, shell),
|
||||
[baseUrl, credentials, integration, language, shell, token],
|
||||
);
|
||||
const PostInstallList = instructions.postInstall?.kind === 'alternatives' ? 'ul' : 'ol';
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -66,10 +72,29 @@ export default function ConnectStep({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{integration.mode === 'hooks' && (
|
||||
<div className="flex border-b border-border" role="tablist" aria-label={t('agentRegistration.shell')}>
|
||||
{SHELLS.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={shell === item}
|
||||
onClick={() => setShell(item)}
|
||||
className={`px-4 py-2 font-mono text-[10px] tracking-wider border-b-2 -mb-px ${
|
||||
shell === item ? 'border-ink text-ink' : 'border-transparent text-ink-dim hover:text-ink'
|
||||
}`}
|
||||
>
|
||||
{item === 'posix' ? 'sh' : 'PowerShell'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section aria-labelledby="setup-command-label">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 id="setup-command-label" className="section-label">
|
||||
{integration.mode === 'adapter' ? t('agentRegistration.install') : t('agentRegistration.configure')}
|
||||
{integration.mode === 'hooks' ? t('agentRegistration.install') : t('agentRegistration.configure')}
|
||||
</h3>
|
||||
<CopyControl
|
||||
value={instructions.setup}
|
||||
@@ -138,14 +163,35 @@ export default function ConnectStep({
|
||||
)}
|
||||
|
||||
{instructions.postInstall && (
|
||||
<ol className="border-t border-border" aria-label={t('agentRegistration.postInstall')}>
|
||||
{instructions.postInstall.map((step, index) => (
|
||||
<li key={step} className="grid grid-cols-[2rem_1fr] gap-2 py-2 border-b border-border font-mono text-[11px] text-ink">
|
||||
<span className="text-ink-dim tabular-nums">{index + 1}</span>
|
||||
<span>{step}</span>
|
||||
<PostInstallList className="border-t border-border" aria-label={t('agentRegistration.postInstall')}>
|
||||
{instructions.postInstall.steps.map((step, index) => (
|
||||
<li
|
||||
key={`${step.key}:${step.command ?? ''}`}
|
||||
className="group grid grid-cols-[2rem_minmax(0,1fr)_auto] items-start gap-2 border-b border-border py-2 font-mono text-[11px] text-ink"
|
||||
>
|
||||
<span className="text-ink-dim tabular-nums">
|
||||
{step.variant ?? String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block">
|
||||
{t(`agentRegistration.postInstallSteps.${step.key}`, {
|
||||
integration: integration.name,
|
||||
})}
|
||||
</span>
|
||||
{step.command && (
|
||||
<code className="mt-1 block break-all text-ink-dim">{step.command}</code>
|
||||
)}
|
||||
</span>
|
||||
{step.command && (
|
||||
<CopyControl
|
||||
value={step.command}
|
||||
ariaLabel={t('agentRegistration.copyPostInstall', { command: step.command })}
|
||||
className="opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</PostInstallList>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
|
||||
@@ -35,6 +35,7 @@ function CredentialRow({ label, value }: { readonly label: string; readonly valu
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CredentialsStep({
|
||||
credentials,
|
||||
token,
|
||||
@@ -44,7 +45,7 @@ export default function CredentialsStep({
|
||||
onContinue,
|
||||
}: CredentialsStepProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expiration, setExpiration] = useState<ExpirationId>('24hours');
|
||||
const [expiration, setExpiration] = useState<ExpirationId>('neverExpire');
|
||||
const [customDays, setCustomDays] = useState('');
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
|
||||
@@ -32,11 +32,18 @@ function IntegrationGroup({
|
||||
key={integration.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(integration)}
|
||||
className="group flex items-center justify-between gap-3 px-3 py-3 text-left border-b border-border hover:bg-surface focus-visible:bg-surface focus-visible:outline-none"
|
||||
className="group flex items-center justify-between gap-3 px-3 py-3 text-left border-b border-border hover:bg-surface focus-visible:bg-surface"
|
||||
>
|
||||
<span className="font-mono text-[12px] text-ink">{integration.name}</span>
|
||||
<span className="font-mono text-[10px] text-ink-dim opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition-opacity" aria-hidden="true">
|
||||
→
|
||||
<span className="flex items-center gap-2" aria-hidden="true">
|
||||
{mode === 'hooks' && (
|
||||
<span className="font-mono text-[9px] tracking-wider text-ink-dim border border-border px-1.5 py-0.5">
|
||||
{t('agentRegistration.hookBadge')}
|
||||
</span>
|
||||
)}
|
||||
<span className="font-mono text-[10px] text-ink-dim opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition-opacity">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -44,10 +51,11 @@ function IntegrationGroup({
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IntegrationStep({ onSelect }: IntegrationStepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<IntegrationGroup mode="adapter" onSelect={onSelect} />
|
||||
<IntegrationGroup mode="hooks" onSelect={onSelect} />
|
||||
<IntegrationGroup mode="sdk" onSelect={onSelect} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user