Skip to content

Commit 5e463c2

Browse files
author
Faxbot Agent
committed
PR5.1: Trait-first gating (initial): dot-path traits in hook; remove inbound/provider name checks in Inbound, Diagnostics; trait-gate settings and scripts; TunnelSettings sinch detection by traits
1 parent 5fad192 commit 5e463c2

File tree

6 files changed

+46
-52
lines changed

6 files changed

+46
-52
lines changed

api/admin_ui/src/components/Diagnostics.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -571,16 +571,17 @@ function Diagnostics({ client, onNavigate, docsBase }: DiagnosticsProps) {
571571

572572
const { checks } = diagnostics;
573573

574-
if (active?.outbound === 'phaxio') {
575-
const phaxio = checks.phaxio || {};
574+
// Provider‑specific guidance based on available diagnostics keys
575+
if ((checks as any).phaxio) {
576+
const phaxio = (checks as any).phaxio || {};
576577
if (!phaxio.api_key_set) suggestions.push({ type: 'error', text: 'Set PHAXIO_API_KEY in .env' });
577578
if (!phaxio.api_secret_set) suggestions.push({ type: 'error', text: 'Set PHAXIO_API_SECRET in .env' });
578579
if (!phaxio.callback_url_set) suggestions.push({ type: 'warning', text: 'Set PHAXIO_STATUS_CALLBACK_URL (or PHAXIO_CALLBACK_URL)' });
579580
if (phaxio.public_url_https === false) suggestions.push({ type: 'warning', text: 'Use HTTPS for PUBLIC_API_URL' });
580581
}
581-
582-
if (active?.outbound === 'sip') {
583-
const sip = checks.sip || {};
582+
583+
if ((checks as any).sip) {
584+
const sip = (checks as any).sip || {};
584585
if (sip.ami_password_not_default === false) suggestions.push({ type: 'error', text: 'Change ASTERISK_AMI_PASSWORD from default "changeme"' });
585586
if (sip.ami_reachable === false) suggestions.push({ type: 'error', text: 'Verify Asterisk AMI host/port/credentials and network reachability' });
586587
}

api/admin_ui/src/components/Inbound.tsx

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ function Inbound({ client, docsBase }: InboundProps) {
419419
</Box>
420420
)}
421421

422-
{callbacks && active?.inbound === 'sip' && (
422+
{callbacks && hasTrait('inbound','requires_ami') && (
423423
<Box>
424424
<Typography variant="subtitle2" fontWeight={600} sx={{ mb: 1 }}>
425425
Asterisk Inbound Configuration
@@ -619,24 +619,9 @@ same => n,System(curl -s -X POST -H "Content-Type: application/json" -H "X-Inter
619619
<Typography variant="body2" sx={{ mb: 1 }}>
620620
Most failures are due to webhook configuration or auth. Use the links below for your active inbound provider.
621621
</Typography>
622-
{active?.inbound === 'sinch' && (
623-
<Stack direction="row" spacing={1} flexWrap="wrap">
624-
<Button size="small" variant="outlined" href={anchors['sinch-inbound-webhook'] || thirdParty['sinch-inbound-webhook']} target="_blank" rel="noreferrer">Sinch: Inbound webhook</Button>
625-
<Button size="small" variant="outlined" href={anchors['sinch-inbound-basic-auth'] || thirdParty['sinch-inbound-basic-auth']} target="_blank" rel="noreferrer">Sinch: Callback auth</Button>
626-
</Stack>
627-
)}
628-
{active?.inbound === 'phaxio' && (
629-
<Stack direction="row" spacing={1} flexWrap="wrap">
630-
<Button size="small" variant="outlined" href={anchors['phaxio-inbound-setup']} target="_blank" rel="noreferrer">Phaxio: Inbound setup</Button>
631-
<Button size="small" variant="outlined" href={anchors['phaxio-webhook-hmac'] || thirdParty['phaxio-webhook-hmac']} target="_blank" rel="noreferrer">Phaxio: Verify HMAC</Button>
632-
</Stack>
633-
)}
634-
{active?.inbound === 'sip' && (
635-
<Stack direction="row" spacing={1} flexWrap="wrap">
636-
<Button size="small" variant="outlined" href={anchors['sip-ami-setup'] || thirdParty['sip-ami-setup']} target="_blank" rel="noreferrer">Asterisk: AMI setup</Button>
637-
<Button size="small" variant="outlined" href={anchors['sip-ami-security'] || thirdParty['sip-ami-security']} target="_blank" rel="noreferrer">AMI security</Button>
638-
</Stack>
639-
)}
622+
<Stack direction="row" spacing={1} flexWrap="wrap">
623+
<Button size="small" variant="outlined" href={anchors['inbound-overview']} target="_blank" rel="noreferrer">Inbound docs</Button>
624+
</Stack>
640625
</Alert>
641626
</Box>
642627
)}

api/admin_ui/src/components/ScriptsTests.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const ConsoleBox: React.FC<{ lines: string[]; loading?: boolean; title?: string
142142
};
143143

144144
const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
145-
const { active } = useTraits();
145+
const { active, registry } = useTraits();
146146
const [error, setError] = useState<string>('');
147147
const [busyAuth, setBusyAuth] = useState<boolean>(false);
148148
const [busyInbound, setBusyInbound] = useState<boolean>(false);
@@ -508,7 +508,7 @@ const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
508508
)}
509509

510510
{/* Backend-specific helpers */}
511-
{active?.outbound === 'sip' && (
511+
{hasTrait('outbound','requires_ami') && (
512512
<Grid item xs={12} lg={6}>
513513
<ResponsiveFormSection
514514
title="SIP/Asterisk: Inbound Secret"
@@ -553,10 +553,11 @@ const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
553553
</Grid>
554554
)}
555555

556-
{active?.outbound === 'phaxio' && (
556+
{/* Show callback URL section when outbound supports status callbacks */}
557+
{Boolean(registry && active?.outbound && registry[active.outbound]?.traits?.status_callback) && (
557558
<Grid item xs={12} lg={6}>
558559
<ResponsiveFormSection
559-
title="Phaxio: Set Callback URL"
560+
title="Status Callback URL"
560561
subtitle="Configure webhook endpoint for status updates"
561562
icon={<SettingsIcon />}
562563
>
@@ -580,7 +581,7 @@ const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
580581
</Stack>
581582
<Alert severity="warning" sx={{ borderRadius: 2 }}>
582583
<Typography variant="caption">
583-
Ensure this is HTTPS and publicly reachable. Configure webhook verification if your provider supports signatures (Phaxio). For Sinch inbound, use Basic auth.
584+
Ensure this is HTTPS and publicly reachable. Configure verification according to the active providers traits.
584585
</Typography>
585586
</Alert>
586587
</Stack>
@@ -591,9 +592,7 @@ const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
591592
{/* Inbound Callbacks Info + Local Parser Tester */}
592593
<Grid item xs={12}>
593594
<ResponsiveFormSection
594-
title={active?.inbound === 'phaxio' ? 'Phaxio Inbound Callback' :
595-
active?.inbound === 'sinch' ? 'Sinch Inbound Callback' :
596-
active?.inbound === 'sip' ? 'Asterisk Inbound (internal)' : 'Inbound Callback'}
595+
title="Inbound Callback"
597596
subtitle="View current callback configuration"
598597
icon={<InfoIcon />}
599598
>

api/admin_ui/src/components/Settings.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function Settings({ client }: SettingsProps) {
5656
const [importingEnv, setImportingEnv] = useState<boolean>(false);
5757
const [importResult, setImportResult] = useState<{discovered:number; prefixes:string[]} | null>(null);
5858
const [lastGeneratedSecret, setLastGeneratedSecret] = useState<string>('');
59-
const { hasTrait, active } = useTraits();
59+
const { hasTrait, active, traitValue, registry } = useTraits();
6060
const handleForm = (field: string, value: any) => setForm((prev: any) => ({ ...prev, [field]: value }));
6161
const isSmall = useMediaQuery('(max-width:900px)');
6262
const ctlStyle: React.CSSProperties = { background: 'transparent', color: 'inherit', borderColor: '#444', padding: '6px', borderRadius: 6, width: isSmall ? '100%' : 'auto', maxWidth: isSmall ? '100%' : undefined };
@@ -179,11 +179,14 @@ function Settings({ client }: SettingsProps) {
179179

180180
// Provider-specific settings (traits-aware)
181181
const activeOutbound = active?.outbound;
182-
if (activeOutbound === 'phaxio') {
182+
const methods = (traitValue('outbound', 'auth.methods') || []) as string[];
183+
// Providers with basic-only auth (e.g., Phaxio-like)
184+
if (Array.isArray(methods) && methods.includes('basic') && !methods.includes('oauth2')) {
183185
if (form.phaxio_api_key) p.phaxio_api_key = form.phaxio_api_key;
184186
if (form.phaxio_api_secret) p.phaxio_api_secret = form.phaxio_api_secret;
185187
}
186-
if (activeOutbound === 'sinch') {
188+
// Providers supporting OAuth2 (e.g., Sinch-like)
189+
if (Array.isArray(methods) && methods.includes('oauth2')) {
187190
if (form.sinch_project_id) p.sinch_project_id = form.sinch_project_id;
188191
if (form.sinch_api_key) p.sinch_api_key = form.sinch_api_key;
189192
if (form.sinch_api_secret) p.sinch_api_secret = form.sinch_api_secret;
@@ -485,7 +488,7 @@ function Settings({ client }: SettingsProps) {
485488
</ResponsiveSettingSection>
486489

487490
{/* Backend-Specific Configuration */}
488-
{active?.outbound === 'phaxio' && (
491+
{(() => { const m = (traitValue('outbound','auth.methods') || []) as string[]; return Array.isArray(m) && m.includes('basic') && !m.includes('oauth2'); })() && (
489492
<>
490493
<Box id="settings-phaxio" />
491494
<ResponsiveSettingSection
@@ -540,7 +543,7 @@ function Settings({ client }: SettingsProps) {
540543
</>
541544
)}
542545

543-
{active?.outbound === 'sinch' && (
546+
{(() => { const m = (traitValue('outbound','auth.methods') || []) as string[]; return Array.isArray(m) && m.includes('oauth2'); })() && (
544547
<>
545548
<Box id="settings-sinch" />
546549
<ResponsiveSettingSection
@@ -688,7 +691,7 @@ function Settings({ client }: SettingsProps) {
688691
</ResponsiveSettingSection>
689692
)}
690693

691-
{active?.outbound === 'sip' && (
694+
{hasTrait('outbound','requires_ami') && (
692695
<>
693696
<Box id="settings-sip" />
694697
<ResponsiveSettingSection
@@ -847,7 +850,7 @@ function Settings({ client }: SettingsProps) {
847850
showCurrentValue={true}
848851
/>
849852

850-
{effectiveInbound === 'sip' && (
853+
{hasTrait('inbound','requires_ami') && (
851854
<Box sx={{ mt: 2 }}>
852855
<ResponsiveSettingItem
853856
icon={<SecurityIcon />}
@@ -908,7 +911,7 @@ function Settings({ client }: SettingsProps) {
908911
</Box>
909912
)}
910913

911-
{effectiveInbound === 'phaxio' && (
914+
{traitValue('inbound','webhook.verification') === 'hmac_sha256' && (
912915
<ResponsiveSettingItem
913916
icon={settings.inbound?.phaxio?.verify_signature ? <CheckCircleIcon color="success" /> : <WarningIcon color="warning" />}
914917
label="Verify Phaxio Inbound Signature"
@@ -925,7 +928,7 @@ function Settings({ client }: SettingsProps) {
925928
/>
926929
)}
927930

928-
{effectiveInbound === 'sinch' && (
931+
{traitValue('inbound','webhook.verification') === 'basic_auth' && (
929932
<Box sx={{ mt: 2 }}>
930933
<ResponsiveSettingItem
931934
icon={settings.inbound?.sinch?.basic_auth_configured ? <CheckCircleIcon color="success" /> : <WarningIcon color="warning" />}

api/admin_ui/src/components/TunnelSettings.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogT
33
import { Link as LinkIcon, Security, VpnKey, VpnLock } from '@mui/icons-material';
44
import AdminAPIClient from '../api/client';
55
import type { TunnelStatus } from '../api/types';
6+
import { useTraits } from '../hooks/useTraits';
67
import { ResponsiveFormSection, ResponsiveSelect, ResponsiveTextField, ResponsiveFileUpload } from './common/ResponsiveFormFields';
78
import { SmoothLoader, InlineLoader } from './common/SmoothLoader';
89

@@ -76,14 +77,15 @@ export default function TunnelSettings({ client, docsBase, hipaaMode }: Props) {
7677
(async () => {
7778
try {
7879
const s = await client.getSettings();
79-
const inbound = (s?.hybrid?.inbound_backend || s?.backend?.type || '').toLowerCase();
80-
setIsSinchActive(inbound === 'sinch');
80+
// Trait-gated detection: consider Sinch-like when OAuth2 is available for the active inbound provider
81+
const methods = (traitValue('inbound', 'auth.methods') || []) as string[];
82+
setIsSinchActive(Array.isArray(methods) && methods.includes('oauth2'));
8183
setInboundEnabled(Boolean(s?.inbound?.enabled));
8284
} catch {
8385
// no-op
8486
}
8587
})();
86-
}, [client]);
88+
}, [client, traitValue]);
8789

8890
// Auto-register Sinch webhook when tunnel URL changes (non-HIPAA only)
8991
useEffect(() => {
@@ -463,3 +465,4 @@ export default function TunnelSettings({ client, docsBase, hipaaMode }: Props) {
463465
</Box>
464466
);
465467
}
468+
const { traitValue } = useTraits();

api/admin_ui/src/hooks/useTraits.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,31 @@ export function useTraits(): TraitsHook {
9797
load();
9898
}, [fetchProviders]);
9999

100+
// Dot-path getter
101+
const dotGet = (obj: any, path: string) => {
102+
try {
103+
return path.split('.').reduce((acc: any, k: string) => (acc && acc[k] !== undefined ? acc[k] : undefined), obj);
104+
} catch {
105+
return undefined;
106+
}
107+
};
108+
100109
const hasTrait = useCallback((direction: 'outbound' | 'inbound', key: string): boolean => {
101110
if (!providers?.active || !providers?.registry) return false;
102-
103111
const activeProvider = providers.active[direction];
104112
if (!activeProvider) return false;
105-
106113
const providerInfo = providers.registry[activeProvider];
107114
if (!providerInfo?.traits) return false;
108-
109-
return key in providerInfo.traits;
115+
return dotGet(providerInfo.traits, key) !== undefined;
110116
}, [providers]);
111117

112118
const traitValue = useCallback((direction: 'outbound' | 'inbound', key: string): any => {
113119
if (!providers?.active || !providers?.registry) return undefined;
114-
115120
const activeProvider = providers.active[direction];
116121
if (!activeProvider) return undefined;
117-
118122
const providerInfo = providers.registry[activeProvider];
119123
if (!providerInfo?.traits) return undefined;
120-
121-
return providerInfo.traits[key];
124+
return dotGet(providerInfo.traits, key);
122125
}, [providers]);
123126

124127
const getWebhookUrl = useCallback((direction: 'inbound' | 'outbound'): string | null => {

0 commit comments

Comments
 (0)