feat: provider config management (#193)
* feat: support scoped removal of provider config (auth, user, project, custom) * feat: implement UI session token management with cookies for window visibility control
This commit is contained in:
committed by
GitHub
parent
d4f1d8abbf
commit
05caf4cc58
@@ -2234,6 +2234,55 @@ async fn handle_config_routes(
|
||||
));
|
||||
}
|
||||
|
||||
// Handle provider source lookup: GET /api/provider/:providerId/source
|
||||
if let Some(rest) = path.strip_prefix("/api/provider/") {
|
||||
if let Some(provider_id) = rest.strip_suffix("/source") {
|
||||
if method == Method::GET {
|
||||
let trimmed = provider_id.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(config_error_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Provider ID is required",
|
||||
));
|
||||
}
|
||||
|
||||
let requested_directory = extract_directory_from_request(&req);
|
||||
let working_directory = if let Some(ref value) = requested_directory {
|
||||
match resolve_project_directory(&state, Some(value.to_string())).await {
|
||||
Ok(directory) => Some(directory),
|
||||
Err(resp) => return Ok(resp),
|
||||
}
|
||||
} else {
|
||||
resolve_project_directory(&state, None).await.ok()
|
||||
};
|
||||
|
||||
match opencode_config::get_provider_sources(trimmed, working_directory.as_deref()).await {
|
||||
Ok(mut sources) => {
|
||||
let auth = opencode_auth::get_provider_auth(trimmed).await;
|
||||
sources.auth.exists = auth.ok().flatten().is_some();
|
||||
return Ok(json_response(
|
||||
StatusCode::OK,
|
||||
serde_json::json!({
|
||||
"providerId": trimmed,
|
||||
"sources": sources
|
||||
}),
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"[desktop:config] Failed to get provider sources {}: {}",
|
||||
trimmed, err
|
||||
);
|
||||
return Ok(config_error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle provider auth removal: DELETE /api/provider/:providerId/auth
|
||||
if let Some(rest) = path.strip_prefix("/api/provider/") {
|
||||
if let Some(provider_id) = rest.strip_suffix("/auth") {
|
||||
@@ -2246,28 +2295,100 @@ async fn handle_config_routes(
|
||||
));
|
||||
}
|
||||
|
||||
match opencode_auth::remove_provider_auth(trimmed).await {
|
||||
Ok(removed) => {
|
||||
if let Err(resp) = refresh_opencode_after_config_change(
|
||||
&state,
|
||||
&format!("provider {} disconnected", trimmed),
|
||||
let scope = req
|
||||
.uri()
|
||||
.query()
|
||||
.and_then(|query| {
|
||||
query
|
||||
.split('&')
|
||||
.find(|pair| pair.starts_with("scope="))
|
||||
.and_then(|pair| pair.split('=').nth(1))
|
||||
})
|
||||
.unwrap_or("auth");
|
||||
|
||||
let requested_directory = extract_directory_from_request(&req);
|
||||
let working_directory = if scope == "project" {
|
||||
match resolve_project_directory(&state, requested_directory.clone()).await {
|
||||
Ok(directory) => Some(directory),
|
||||
Err(resp) => return Ok(resp),
|
||||
}
|
||||
} else if let Some(ref value) = requested_directory {
|
||||
match resolve_project_directory(&state, Some(value.to_string())).await {
|
||||
Ok(directory) => Some(directory),
|
||||
Err(resp) => return Ok(resp),
|
||||
}
|
||||
} else {
|
||||
resolve_project_directory(&state, None).await.ok()
|
||||
};
|
||||
|
||||
let removal_result = if scope == "auth" {
|
||||
opencode_auth::remove_provider_auth(trimmed).await
|
||||
} else if scope == "user" {
|
||||
opencode_config::remove_provider_config(trimmed, working_directory.as_deref(), opencode_config::ProviderScope::User).await
|
||||
} else if scope == "project" {
|
||||
opencode_config::remove_provider_config(trimmed, working_directory.as_deref(), opencode_config::ProviderScope::Project).await
|
||||
} else if scope == "custom" {
|
||||
opencode_config::remove_provider_config(trimmed, working_directory.as_deref(), opencode_config::ProviderScope::Custom).await
|
||||
} else if scope == "all" {
|
||||
let auth_removed = opencode_auth::remove_provider_auth(trimmed).await.unwrap_or(false);
|
||||
let user_removed = opencode_config::remove_provider_config(
|
||||
trimmed,
|
||||
working_directory.as_deref(),
|
||||
opencode_config::ProviderScope::User,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let project_removed = if let Some(ref directory) = working_directory {
|
||||
opencode_config::remove_provider_config(
|
||||
trimmed,
|
||||
Some(directory),
|
||||
opencode_config::ProviderScope::Project,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(resp);
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let custom_removed = opencode_config::remove_provider_config(
|
||||
trimmed,
|
||||
working_directory.as_deref(),
|
||||
opencode_config::ProviderScope::Custom,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(auth_removed || user_removed || project_removed || custom_removed)
|
||||
} else {
|
||||
return Ok(config_error_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid scope",
|
||||
));
|
||||
};
|
||||
|
||||
match removal_result {
|
||||
Ok(removed) => {
|
||||
if removed {
|
||||
if let Err(resp) = refresh_opencode_after_config_change(
|
||||
&state,
|
||||
&format!("provider {} disconnected", trimmed),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(json_response(
|
||||
StatusCode::OK,
|
||||
ConfigActionResponse {
|
||||
success: true,
|
||||
requires_reload: true,
|
||||
requires_reload: removed,
|
||||
message: if removed {
|
||||
"Provider disconnected successfully".to_string()
|
||||
} else {
|
||||
"Provider was not connected".to_string()
|
||||
},
|
||||
reload_delay_ms: CLIENT_RELOAD_DELAY_MS,
|
||||
reload_delay_ms: if removed { CLIENT_RELOAD_DELAY_MS } else { 0 },
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -2406,11 +2527,16 @@ async fn proxy_to_opencode(
|
||||
&& origin_path.ends_with("/auth")
|
||||
&& origin_path != "/api/provider/auth"; // Exclude GET /api/provider/auth
|
||||
|
||||
let is_provider_source_get = method == Method::GET
|
||||
&& origin_path.starts_with("/api/provider/")
|
||||
&& origin_path.ends_with("/source");
|
||||
|
||||
let is_desktop_config_route = origin_path.starts_with("/api/config/agents/")
|
||||
|| origin_path.starts_with("/api/config/commands/")
|
||||
|| origin_path.starts_with("/api/config/skills")
|
||||
|| origin_path == "/api/config/reload"
|
||||
|| is_provider_auth_delete;
|
||||
|| is_provider_auth_delete
|
||||
|| is_provider_source_get;
|
||||
|
||||
if is_desktop_config_route {
|
||||
return handle_config_routes(state, &origin_path, method, req).await;
|
||||
|
||||
@@ -68,6 +68,19 @@ pub async fn write_auth(auth: &Value) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get provider auth entry from auth.json
|
||||
pub async fn get_provider_auth(provider_id: &str) -> Result<Option<Value>> {
|
||||
if provider_id.is_empty() {
|
||||
return Err(anyhow!("Provider ID is required"));
|
||||
}
|
||||
|
||||
let auth = read_auth().await?;
|
||||
let auth_obj = auth
|
||||
.as_object()
|
||||
.ok_or_else(|| anyhow!("Auth file is not a valid JSON object"))?;
|
||||
Ok(auth_obj.get(provider_id).cloned())
|
||||
}
|
||||
|
||||
/// Remove provider auth entry from auth.json
|
||||
pub async fn remove_provider_auth(provider_id: &str) -> Result<bool> {
|
||||
if provider_id.is_empty() {
|
||||
|
||||
@@ -36,6 +36,13 @@ pub enum Scope {
|
||||
Project,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProviderScope {
|
||||
User,
|
||||
Project,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl From<AgentScope> for Scope {
|
||||
fn from(scope: AgentScope) -> Self {
|
||||
match scope {
|
||||
@@ -151,6 +158,23 @@ struct ConfigLayers {
|
||||
paths: ConfigPaths,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderSources {
|
||||
pub auth: ProviderSourceInfo,
|
||||
pub user: ProviderSourceInfo,
|
||||
pub project: ProviderSourceInfo,
|
||||
pub custom: ProviderSourceInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderSourceInfo {
|
||||
pub exists: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
fn get_config_paths(working_directory: Option<&Path>) -> ConfigPaths {
|
||||
ConfigPaths {
|
||||
user: get_config_file(),
|
||||
@@ -310,6 +334,135 @@ fn get_config_for_path<'a>(layers: &'a mut ConfigLayers, target_path: &Path) ->
|
||||
&mut layers.user
|
||||
}
|
||||
|
||||
pub async fn get_provider_sources(
|
||||
provider_id: &str,
|
||||
working_directory: Option<&Path>,
|
||||
) -> Result<ProviderSources> {
|
||||
if provider_id.trim().is_empty() {
|
||||
return Err(anyhow!("Provider ID is required"));
|
||||
}
|
||||
|
||||
let layers = read_config_layers(working_directory).await?;
|
||||
|
||||
let custom_exists = layers
|
||||
.custom
|
||||
.get("provider")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|p| p.get(provider_id))
|
||||
.is_some()
|
||||
|| layers
|
||||
.custom
|
||||
.get("providers")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|p| p.get(provider_id))
|
||||
.is_some();
|
||||
let project_exists = layers
|
||||
.project
|
||||
.get("provider")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|p| p.get(provider_id))
|
||||
.is_some()
|
||||
|| layers
|
||||
.project
|
||||
.get("providers")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|p| p.get(provider_id))
|
||||
.is_some();
|
||||
let user_exists = layers
|
||||
.user
|
||||
.get("provider")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|p| p.get(provider_id))
|
||||
.is_some()
|
||||
|| layers
|
||||
.user
|
||||
.get("providers")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|p| p.get(provider_id))
|
||||
.is_some();
|
||||
|
||||
Ok(ProviderSources {
|
||||
auth: ProviderSourceInfo { exists: false, path: None },
|
||||
user: ProviderSourceInfo { exists: user_exists, path: Some(layers.paths.user.to_string_lossy().to_string()) },
|
||||
project: ProviderSourceInfo {
|
||||
exists: project_exists,
|
||||
path: layers.paths.project.as_ref().map(|p| p.to_string_lossy().to_string()),
|
||||
},
|
||||
custom: ProviderSourceInfo {
|
||||
exists: custom_exists,
|
||||
path: layers.paths.custom.as_ref().map(|p| p.to_string_lossy().to_string()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remove_provider_config(
|
||||
provider_id: &str,
|
||||
working_directory: Option<&Path>,
|
||||
scope: ProviderScope,
|
||||
) -> Result<bool> {
|
||||
if provider_id.trim().is_empty() {
|
||||
return Err(anyhow!("Provider ID is required"));
|
||||
}
|
||||
|
||||
let mut layers = read_config_layers(working_directory).await?;
|
||||
let target_path = match scope {
|
||||
ProviderScope::Project => layers
|
||||
.paths
|
||||
.project
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Project config path is not available"))?,
|
||||
ProviderScope::Custom => layers
|
||||
.paths
|
||||
.custom
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Custom config path is not available"))?,
|
||||
ProviderScope::User => layers.paths.user.clone(),
|
||||
};
|
||||
|
||||
let config = get_config_for_path(&mut layers, &target_path);
|
||||
let mut removed = false;
|
||||
let mut remove_provider_key = false;
|
||||
let mut remove_providers_key = false;
|
||||
|
||||
if let Some(provider_section) = config
|
||||
.get_mut("provider")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
if provider_section.remove(provider_id).is_some() {
|
||||
removed = true;
|
||||
if provider_section.is_empty() {
|
||||
remove_provider_key = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(provider_section) = config
|
||||
.get_mut("providers")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
if provider_section.remove(provider_id).is_some() {
|
||||
removed = true;
|
||||
if provider_section.is_empty() {
|
||||
remove_providers_key = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !removed {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if remove_provider_key {
|
||||
config.as_object_mut().map(|map| map.remove("provider"));
|
||||
}
|
||||
if remove_providers_key {
|
||||
config.as_object_mut().map(|map| map.remove("providers"));
|
||||
}
|
||||
|
||||
write_config_at(config, &target_path).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// ============== AGENT SCOPE HELPERS ==============
|
||||
|
||||
/// Get project-level agent directory path
|
||||
|
||||
@@ -51,6 +51,18 @@ interface ProviderOption {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface ProviderSourceInfo {
|
||||
exists: boolean;
|
||||
path?: string | null;
|
||||
}
|
||||
|
||||
interface ProviderSources {
|
||||
auth: ProviderSourceInfo;
|
||||
user: ProviderSourceInfo;
|
||||
project: ProviderSourceInfo;
|
||||
custom?: ProviderSourceInfo;
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
|
||||
@@ -142,6 +154,8 @@ export const ProvidersPage: React.FC = () => {
|
||||
const [candidateProviderId, setCandidateProviderId] = React.useState('');
|
||||
const [providerSearchQuery, setProviderSearchQuery] = React.useState('');
|
||||
const [providerDropdownOpen, setProviderDropdownOpen] = React.useState(false);
|
||||
const [providerSources, setProviderSources] = React.useState<Record<string, ProviderSources>>({});
|
||||
const [showAuthPanel, setShowAuthPanel] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!selectedProviderId && providers.length > 0) {
|
||||
@@ -247,7 +261,57 @@ export const ProvidersPage: React.FC = () => {
|
||||
}
|
||||
}, [selectedProviderId, candidateProviderId, unconnectedProviders]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedProviderId === ADD_PROVIDER_ID) {
|
||||
setShowAuthPanel(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowAuthPanel(false);
|
||||
}, [selectedProviderId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!selectedProviderId || selectedProviderId === ADD_PROVIDER_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadSources = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/provider/${encodeURIComponent(selectedProviderId)}/source`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error || 'Failed to load provider sources');
|
||||
}
|
||||
|
||||
const sources = (payload?.sources ?? payload?.data?.sources) as ProviderSources | undefined;
|
||||
if (!cancelled && sources) {
|
||||
setProviderSources((prev) => ({
|
||||
...prev,
|
||||
[selectedProviderId]: sources,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
console.error('Failed to load provider sources:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadSources();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedProviderId]);
|
||||
|
||||
const selectedProvider = providers.find((provider) => provider.id === selectedProviderId);
|
||||
const selectedSources = selectedProviderId ? providerSources[selectedProviderId] : undefined;
|
||||
|
||||
const handleSaveApiKey = async (providerId: string) => {
|
||||
const apiKey = apiKeyInputs[providerId]?.trim() ?? '';
|
||||
@@ -408,7 +472,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
setAuthBusyKey(busyKey);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/provider/${encodeURIComponent(providerId)}/auth`, {
|
||||
const response = await fetch(`/api/provider/${encodeURIComponent(providerId)}/auth?scope=all`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
@@ -743,9 +807,25 @@ export const ProvidersPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground">Authentication</h2>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground">Authentication</h2>
|
||||
{selectedProviderId !== ADD_PROVIDER_ID && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowAuthPanel((prev) => !prev)}
|
||||
className="h-8"
|
||||
>
|
||||
{showAuthPanel ? 'Hide' : 'Reconnect'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{authLoading ? (
|
||||
{!showAuthPanel && selectedProviderId !== ADD_PROVIDER_ID ? (
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Connected. Use Reconnect to update credentials.
|
||||
</p>
|
||||
) : authLoading ? (
|
||||
<p className="typography-meta text-muted-foreground">Loading authentication methods…</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
@@ -777,18 +857,6 @@ export const ProvidersPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-border/40">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDisconnectProvider(selectedProvider.id)}
|
||||
disabled={authBusyKey === `disconnect:${selectedProvider.id}`}
|
||||
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
{authBusyKey === `disconnect:${selectedProvider.id}` ? 'Disconnecting…' : 'Disconnect provider'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{oauthAuthMethods.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{oauthAuthMethods.map((method, index) => {
|
||||
@@ -895,12 +963,33 @@ export const ProvidersPage: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground">Connection</h2>
|
||||
{selectedSources && (selectedSources.auth.exists || selectedSources.user.exists || selectedSources.project.exists || selectedSources.custom?.exists) && (
|
||||
<div className="typography-meta text-muted-foreground">
|
||||
Configured in: {[
|
||||
selectedSources.auth.exists ? 'auth credentials' : null,
|
||||
selectedSources.user.exists ? 'user config' : null,
|
||||
selectedSources.project.exists ? 'project config' : null,
|
||||
selectedSources.custom?.exists ? 'custom config' : null,
|
||||
].filter(Boolean).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDisconnectProvider(selectedProvider.id)}
|
||||
disabled={authBusyKey === `disconnect:${selectedProvider.id}`}
|
||||
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
{authBusyKey === `disconnect:${selectedProvider.id}` ? 'Disconnecting…' : 'Disconnect provider'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground">Models</h2>
|
||||
|
||||
@@ -3,8 +3,8 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { type OpenCodeManager } from './opencode';
|
||||
import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type AgentScope, type CommandScope, AGENT_SCOPE, COMMAND_SCOPE, discoverSkills, getSkillSources, createSkill, updateSkill, deleteSkill, readSkillSupportingFile, writeSkillSupportingFile, deleteSkillSupportingFile, type SkillScope, SKILL_SCOPE } from './opencodeConfig';
|
||||
import { removeProviderAuth } from './opencodeAuth';
|
||||
import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type AgentScope, type CommandScope, AGENT_SCOPE, COMMAND_SCOPE, discoverSkills, getSkillSources, createSkill, updateSkill, deleteSkill, readSkillSupportingFile, writeSkillSupportingFile, deleteSkillSupportingFile, type SkillScope, SKILL_SCOPE, getProviderSources, removeProviderConfig } from './opencodeConfig';
|
||||
import { getProviderAuth, removeProviderAuth } from './opencodeAuth';
|
||||
import * as gitService from './gitService';
|
||||
import {
|
||||
getSkillsCatalog,
|
||||
@@ -1293,13 +1293,31 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:provider/auth:delete': {
|
||||
const { providerId } = (payload || {}) as { providerId?: string };
|
||||
case 'api:provider/auth:delete': {
|
||||
const { providerId, scope } = (payload || {}) as { providerId?: string; scope?: string };
|
||||
if (!providerId) {
|
||||
return { id, type, success: false, error: 'Provider ID is required' };
|
||||
}
|
||||
const normalizedScope = typeof scope === 'string' ? scope : 'auth';
|
||||
try {
|
||||
const removed = removeProviderAuth(providerId);
|
||||
let removed = false;
|
||||
if (normalizedScope === 'auth') {
|
||||
removed = removeProviderAuth(providerId);
|
||||
} else if (normalizedScope === 'user' || normalizedScope === 'project' || normalizedScope === 'custom') {
|
||||
removed = removeProviderConfig(providerId, ctx?.manager?.getWorkingDirectory(), normalizedScope);
|
||||
} else if (normalizedScope === 'all') {
|
||||
const workingDirectory = ctx?.manager?.getWorkingDirectory();
|
||||
const authRemoved = removeProviderAuth(providerId);
|
||||
const userRemoved = removeProviderConfig(providerId, workingDirectory, 'user');
|
||||
const projectRemoved = workingDirectory
|
||||
? removeProviderConfig(providerId, workingDirectory, 'project')
|
||||
: false;
|
||||
const customRemoved = removeProviderConfig(providerId, workingDirectory, 'custom');
|
||||
removed = authRemoved || userRemoved || projectRemoved || customRemoved;
|
||||
} else {
|
||||
return { id, type, success: false, error: 'Invalid scope' };
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
await ctx?.manager?.restart();
|
||||
}
|
||||
@@ -1323,6 +1341,23 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:provider/source:get': {
|
||||
const { providerId } = (payload || {}) as { providerId?: string };
|
||||
if (!providerId) {
|
||||
return { id, type, success: false, error: 'Provider ID is required' };
|
||||
}
|
||||
try {
|
||||
const sources = getProviderSources(providerId, ctx?.manager?.getWorkingDirectory());
|
||||
const auth = getProviderAuth(providerId);
|
||||
sources.auth.exists = Boolean(auth);
|
||||
return { id, type, success: true, data: { providerId, sources } };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return { id, type, success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case 'vscode:command': {
|
||||
const { command, args } = (payload || {}) as { command?: string; args?: unknown[] };
|
||||
if (!command) {
|
||||
|
||||
@@ -259,6 +259,13 @@ const readConfigLayers = (workingDirectory?: string) => {
|
||||
const readConfig = (workingDirectory?: string): Record<string, unknown> =>
|
||||
readConfigLayers(workingDirectory).mergedConfig;
|
||||
|
||||
const getConfigForPath = (layers: ReturnType<typeof readConfigLayers>, targetPath?: string | null) => {
|
||||
if (!targetPath) return layers.userConfig;
|
||||
if (layers.paths.customPath && targetPath === layers.paths.customPath) return layers.customConfig;
|
||||
if (layers.paths.projectPath && targetPath === layers.paths.projectPath) return layers.projectConfig;
|
||||
return layers.userConfig;
|
||||
};
|
||||
|
||||
const writeConfig = (config: Record<string, unknown>, filePath: string = CONFIG_FILE) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const backupFile = `${filePath}.openchamber.backup`;
|
||||
@@ -732,6 +739,99 @@ export const updateCommand = (commandName: string, updates: Record<string, unkno
|
||||
}
|
||||
};
|
||||
|
||||
export const getProviderSources = (providerId: string, workingDirectory?: string) => {
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
const customProviders = isPlainObject((layers.customConfig as Record<string, unknown>)?.provider)
|
||||
? (layers.customConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const customProvidersAlias = isPlainObject((layers.customConfig as Record<string, unknown>)?.providers)
|
||||
? (layers.customConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
const projectProviders = isPlainObject((layers.projectConfig as Record<string, unknown>)?.provider)
|
||||
? (layers.projectConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const projectProvidersAlias = isPlainObject((layers.projectConfig as Record<string, unknown>)?.providers)
|
||||
? (layers.projectConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
const userProviders = isPlainObject((layers.userConfig as Record<string, unknown>)?.provider)
|
||||
? (layers.userConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const userProvidersAlias = isPlainObject((layers.userConfig as Record<string, unknown>)?.providers)
|
||||
? (layers.userConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
|
||||
const customExists = Object.prototype.hasOwnProperty.call(customProviders, providerId)
|
||||
|| Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId);
|
||||
const projectExists = Object.prototype.hasOwnProperty.call(projectProviders, providerId)
|
||||
|| Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId);
|
||||
const userExists = Object.prototype.hasOwnProperty.call(userProviders, providerId)
|
||||
|| Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId);
|
||||
|
||||
return {
|
||||
auth: { exists: false },
|
||||
user: { exists: userExists, path: layers.paths.userPath },
|
||||
project: { exists: projectExists, path: layers.paths.projectPath ?? null },
|
||||
custom: { exists: customExists, path: layers.paths.customPath },
|
||||
};
|
||||
};
|
||||
|
||||
export const removeProviderConfig = (providerId: string, workingDirectory?: string, scope: 'user' | 'project' | 'custom' = 'user') => {
|
||||
if (!providerId) throw new Error('Provider ID is required');
|
||||
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
let targetPath: string | null | undefined = layers.paths.userPath;
|
||||
|
||||
if (scope === 'project') {
|
||||
if (!workingDirectory) {
|
||||
throw new Error('Working directory is required for project scope');
|
||||
}
|
||||
targetPath = layers.paths.projectPath ?? targetPath;
|
||||
}
|
||||
|
||||
if (scope === 'custom') {
|
||||
if (!layers.paths.customPath) {
|
||||
return false;
|
||||
}
|
||||
targetPath = layers.paths.customPath;
|
||||
}
|
||||
|
||||
const targetConfig = getConfigForPath(layers, targetPath);
|
||||
const providerConfig = isPlainObject((targetConfig as Record<string, unknown>).provider)
|
||||
? (targetConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const providersConfig = isPlainObject((targetConfig as Record<string, unknown>).providers)
|
||||
? (targetConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
|
||||
const removedProvider = Object.prototype.hasOwnProperty.call(providerConfig, providerId);
|
||||
const removedProviders = Object.prototype.hasOwnProperty.call(providersConfig, providerId);
|
||||
|
||||
if (!removedProvider && !removedProviders) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (removedProvider) {
|
||||
delete providerConfig[providerId];
|
||||
if (Object.keys(providerConfig).length === 0) {
|
||||
delete (targetConfig as Record<string, unknown>).provider;
|
||||
} else {
|
||||
(targetConfig as Record<string, unknown>).provider = providerConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedProviders) {
|
||||
delete providersConfig[providerId];
|
||||
if (Object.keys(providersConfig).length === 0) {
|
||||
delete (targetConfig as Record<string, unknown>).providers;
|
||||
} else {
|
||||
(targetConfig as Record<string, unknown>).providers = providersConfig;
|
||||
}
|
||||
}
|
||||
|
||||
writeConfig(targetConfig as Record<string, unknown>, targetPath || CONFIG_FILE);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deleteCommand = (commandName: string, workingDirectory?: string) => {
|
||||
let deleted = false;
|
||||
|
||||
|
||||
@@ -613,8 +613,22 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
|
||||
const providerAuthMatch = pathname.match(/^\/api\/provider\/([^/]+)\/auth$/);
|
||||
if (providerAuthMatch && (init?.method || 'GET').toUpperCase() === 'DELETE') {
|
||||
const providerId = decodeURIComponent(providerAuthMatch[1]);
|
||||
const scope = url.searchParams.get('scope') || 'auth';
|
||||
try {
|
||||
const data = await sendBridgeMessage('api:provider/auth:delete', { providerId });
|
||||
const data = await sendBridgeMessage('api:provider/auth:delete', { providerId, scope });
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return new Response(JSON.stringify({ error: message }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle provider source lookup: GET /api/provider/:providerId/source
|
||||
const providerSourceMatch = pathname.match(/^\/api\/provider\/([^/]+)\/source$/);
|
||||
if (providerSourceMatch && (init?.method || 'GET').toUpperCase() === 'GET') {
|
||||
const providerId = decodeURIComponent(providerSourceMatch[1]);
|
||||
try {
|
||||
const data = await sendBridgeMessage('api:provider/source:get', { providerId });
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -2536,7 +2536,9 @@ async function main(options = {}) {
|
||||
app.post('/api/push/subscribe', async (req, res) => {
|
||||
await ensurePushInitialized();
|
||||
|
||||
const uiToken = getUiSessionTokenFromRequest(req);
|
||||
const uiToken = uiAuthController?.ensureSessionToken
|
||||
? uiAuthController.ensureSessionToken(req, res)
|
||||
: getUiSessionTokenFromRequest(req);
|
||||
if (!uiToken) {
|
||||
return res.status(401).json({ error: 'UI session missing' });
|
||||
}
|
||||
@@ -2582,7 +2584,9 @@ async function main(options = {}) {
|
||||
app.delete('/api/push/subscribe', async (req, res) => {
|
||||
await ensurePushInitialized();
|
||||
|
||||
const uiToken = getUiSessionTokenFromRequest(req);
|
||||
const uiToken = uiAuthController?.ensureSessionToken
|
||||
? uiAuthController.ensureSessionToken(req, res)
|
||||
: getUiSessionTokenFromRequest(req);
|
||||
if (!uiToken) {
|
||||
return res.status(401).json({ error: 'UI session missing' });
|
||||
}
|
||||
@@ -2597,7 +2601,9 @@ async function main(options = {}) {
|
||||
});
|
||||
|
||||
app.post('/api/push/visibility', (req, res) => {
|
||||
const uiToken = getUiSessionTokenFromRequest(req);
|
||||
const uiToken = uiAuthController?.ensureSessionToken
|
||||
? uiAuthController.ensureSessionToken(req, res)
|
||||
: getUiSessionTokenFromRequest(req);
|
||||
if (!uiToken) {
|
||||
return res.status(401).json({ error: 'UI session missing' });
|
||||
}
|
||||
@@ -3056,6 +3062,8 @@ async function main(options = {}) {
|
||||
createCommand,
|
||||
updateCommand,
|
||||
deleteCommand,
|
||||
getProviderSources,
|
||||
removeProviderConfig,
|
||||
AGENT_SCOPE,
|
||||
COMMAND_SCOPE
|
||||
} = await import('./lib/opencode-config.js');
|
||||
@@ -3811,6 +3819,42 @@ async function main(options = {}) {
|
||||
return authLibrary;
|
||||
};
|
||||
|
||||
app.get('/api/provider/:providerId/source', async (req, res) => {
|
||||
try {
|
||||
const { providerId } = req.params;
|
||||
if (!providerId) {
|
||||
return res.status(400).json({ error: 'Provider ID is required' });
|
||||
}
|
||||
|
||||
const headerDirectory = typeof req.get === 'function' ? req.get('x-opencode-directory') : null;
|
||||
const queryDirectory = Array.isArray(req.query?.directory)
|
||||
? req.query.directory[0]
|
||||
: req.query?.directory;
|
||||
const requestedDirectory = headerDirectory || queryDirectory || null;
|
||||
|
||||
let directory = null;
|
||||
const resolved = await resolveProjectDirectory(req);
|
||||
if (resolved.directory) {
|
||||
directory = resolved.directory;
|
||||
} else if (requestedDirectory) {
|
||||
return res.status(400).json({ error: resolved.error });
|
||||
}
|
||||
|
||||
const sources = getProviderSources(providerId, directory);
|
||||
const { getProviderAuth } = await getAuthLibrary();
|
||||
const auth = getProviderAuth(providerId);
|
||||
sources.sources.auth.exists = Boolean(auth);
|
||||
|
||||
res.json({
|
||||
providerId,
|
||||
sources: sources.sources,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get provider sources:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to get provider sources' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/provider/:providerId/auth', async (req, res) => {
|
||||
try {
|
||||
const { providerId } = req.params;
|
||||
@@ -3818,17 +3862,54 @@ async function main(options = {}) {
|
||||
return res.status(400).json({ error: 'Provider ID is required' });
|
||||
}
|
||||
|
||||
const { removeProviderAuth } = await getAuthLibrary();
|
||||
const removed = removeProviderAuth(providerId);
|
||||
const scope = typeof req.query?.scope === 'string' ? req.query.scope : 'auth';
|
||||
const headerDirectory = typeof req.get === 'function' ? req.get('x-opencode-directory') : null;
|
||||
const queryDirectory = Array.isArray(req.query?.directory)
|
||||
? req.query.directory[0]
|
||||
: req.query?.directory;
|
||||
const requestedDirectory = headerDirectory || queryDirectory || null;
|
||||
let directory = null;
|
||||
|
||||
await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected`);
|
||||
if (scope === 'project' || requestedDirectory) {
|
||||
const resolved = await resolveProjectDirectory(req);
|
||||
if (!resolved.directory) {
|
||||
return res.status(400).json({ error: resolved.error });
|
||||
}
|
||||
directory = resolved.directory;
|
||||
} else {
|
||||
const resolved = await resolveProjectDirectory(req);
|
||||
if (resolved.directory) {
|
||||
directory = resolved.directory;
|
||||
}
|
||||
}
|
||||
|
||||
let removed = false;
|
||||
if (scope === 'auth') {
|
||||
const { removeProviderAuth } = await getAuthLibrary();
|
||||
removed = removeProviderAuth(providerId);
|
||||
} else if (scope === 'user' || scope === 'project' || scope === 'custom') {
|
||||
removed = removeProviderConfig(providerId, directory, scope);
|
||||
} else if (scope === 'all') {
|
||||
const { removeProviderAuth } = await getAuthLibrary();
|
||||
const authRemoved = removeProviderAuth(providerId);
|
||||
const userRemoved = removeProviderConfig(providerId, directory, 'user');
|
||||
const projectRemoved = directory ? removeProviderConfig(providerId, directory, 'project') : false;
|
||||
const customRemoved = removeProviderConfig(providerId, directory, 'custom');
|
||||
removed = authRemoved || userRemoved || projectRemoved || customRemoved;
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Invalid scope' });
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected (${scope})`);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
removed,
|
||||
requiresReload: true,
|
||||
requiresReload: removed,
|
||||
message: removed ? 'Provider disconnected successfully' : 'Provider was not connected',
|
||||
reloadDelayMs: CLIENT_RELOAD_DELAY_MS,
|
||||
reloadDelayMs: removed ? CLIENT_RELOAD_DELAY_MS : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect provider:', error);
|
||||
|
||||
@@ -653,6 +653,19 @@ function readConfig(workingDirectory) {
|
||||
return readConfigLayers(workingDirectory).mergedConfig;
|
||||
}
|
||||
|
||||
function getConfigForPath(layers, targetPath) {
|
||||
if (!targetPath) {
|
||||
return layers.userConfig;
|
||||
}
|
||||
if (layers.paths.customPath && targetPath === layers.paths.customPath) {
|
||||
return layers.customConfig;
|
||||
}
|
||||
if (layers.paths.projectPath && targetPath === layers.paths.projectPath) {
|
||||
return layers.projectConfig;
|
||||
}
|
||||
return layers.userConfig;
|
||||
}
|
||||
|
||||
function writeConfig(config, filePath = CONFIG_FILE) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
@@ -1314,6 +1327,90 @@ function updateCommand(commandName, updates, workingDirectory) {
|
||||
console.log(`Updated command: ${commandName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
|
||||
}
|
||||
|
||||
function getProviderSources(providerId, workingDirectory) {
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
const { userConfig, projectConfig, customConfig, paths } = layers;
|
||||
|
||||
const customProviders = isPlainObject(customConfig?.provider) ? customConfig.provider : {};
|
||||
const customProvidersAlias = isPlainObject(customConfig?.providers) ? customConfig.providers : {};
|
||||
const projectProviders = isPlainObject(projectConfig?.provider) ? projectConfig.provider : {};
|
||||
const projectProvidersAlias = isPlainObject(projectConfig?.providers) ? projectConfig.providers : {};
|
||||
const userProviders = isPlainObject(userConfig?.provider) ? userConfig.provider : {};
|
||||
const userProvidersAlias = isPlainObject(userConfig?.providers) ? userConfig.providers : {};
|
||||
|
||||
const customExists =
|
||||
(customProviders && Object.prototype.hasOwnProperty.call(customProviders, providerId)) ||
|
||||
(customProvidersAlias && Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId));
|
||||
const projectExists =
|
||||
(projectProviders && Object.prototype.hasOwnProperty.call(projectProviders, providerId)) ||
|
||||
(projectProvidersAlias && Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId));
|
||||
const userExists =
|
||||
(userProviders && Object.prototype.hasOwnProperty.call(userProviders, providerId)) ||
|
||||
(userProvidersAlias && Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId));
|
||||
|
||||
return {
|
||||
sources: {
|
||||
auth: { exists: false },
|
||||
user: { exists: userExists, path: paths.userPath },
|
||||
project: { exists: projectExists, path: paths.projectPath || null },
|
||||
custom: { exists: customExists, path: paths.customPath }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
|
||||
if (!providerId || typeof providerId !== 'string') {
|
||||
throw new Error('Provider ID is required');
|
||||
}
|
||||
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
let targetPath = layers.paths.userPath;
|
||||
|
||||
if (scope === 'project') {
|
||||
if (!workingDirectory) {
|
||||
throw new Error('Working directory is required for project scope');
|
||||
}
|
||||
targetPath = layers.paths.projectPath || targetPath;
|
||||
} else if (scope === 'custom') {
|
||||
if (!layers.paths.customPath) {
|
||||
return false;
|
||||
}
|
||||
targetPath = layers.paths.customPath;
|
||||
}
|
||||
|
||||
const targetConfig = getConfigForPath(layers, targetPath);
|
||||
const providerConfig = isPlainObject(targetConfig.provider) ? targetConfig.provider : {};
|
||||
const providersConfig = isPlainObject(targetConfig.providers) ? targetConfig.providers : {};
|
||||
const removedProvider = providerConfig && Object.prototype.hasOwnProperty.call(providerConfig, providerId);
|
||||
const removedProviders = providersConfig && Object.prototype.hasOwnProperty.call(providersConfig, providerId);
|
||||
|
||||
if (!removedProvider && !removedProviders) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (removedProvider) {
|
||||
delete providerConfig[providerId];
|
||||
if (Object.keys(providerConfig).length === 0) {
|
||||
delete targetConfig.provider;
|
||||
} else {
|
||||
targetConfig.provider = providerConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedProviders) {
|
||||
delete providersConfig[providerId];
|
||||
if (Object.keys(providersConfig).length === 0) {
|
||||
delete targetConfig.providers;
|
||||
} else {
|
||||
targetConfig.providers = providersConfig;
|
||||
}
|
||||
}
|
||||
|
||||
writeConfig(targetConfig, targetPath || CONFIG_FILE);
|
||||
console.log(`Removed provider ${providerId} from config: ${targetPath}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function deleteCommand(commandName, workingDirectory) {
|
||||
let deleted = false;
|
||||
|
||||
@@ -1669,6 +1766,8 @@ export {
|
||||
deleteSkillSupportingFile,
|
||||
readConfig,
|
||||
writeConfig,
|
||||
getProviderSources,
|
||||
removeProviderConfig,
|
||||
AGENT_DIR,
|
||||
COMMAND_DIR,
|
||||
SKILL_DIR,
|
||||
|
||||
@@ -81,6 +81,28 @@ export const createUiAuth = ({
|
||||
const normalizedPassword = normalizePassword(password);
|
||||
|
||||
if (!normalizedPassword) {
|
||||
const setSessionCookie = (req, res, token) => {
|
||||
const secure = isSecureRequest(req);
|
||||
const maxAgeSeconds = Math.floor(sessionTtlMs / 1000);
|
||||
const header = buildCookie({
|
||||
name: cookieName,
|
||||
value: encodeURIComponent(token),
|
||||
maxAge: maxAgeSeconds,
|
||||
secure,
|
||||
});
|
||||
res.setHeader('Set-Cookie', header);
|
||||
};
|
||||
|
||||
const ensureSessionToken = (req, res) => {
|
||||
const cookies = parseCookies(req.headers.cookie);
|
||||
if (cookies[cookieName]) {
|
||||
return cookies[cookieName];
|
||||
}
|
||||
const token = crypto.randomBytes(32).toString('base64url');
|
||||
setSessionCookie(req, res, token);
|
||||
return token;
|
||||
};
|
||||
|
||||
return {
|
||||
enabled: false,
|
||||
requireAuth: (_req, _res, next) => next(),
|
||||
@@ -90,6 +112,7 @@ export const createUiAuth = ({
|
||||
handleSessionCreate: (_req, res) => {
|
||||
res.status(400).json({ error: 'UI password not configured' });
|
||||
},
|
||||
ensureSessionToken,
|
||||
dispose: () => {
|
||||
|
||||
},
|
||||
@@ -261,6 +284,10 @@ export const createUiAuth = ({
|
||||
requireAuth,
|
||||
handleSessionStatus,
|
||||
handleSessionCreate,
|
||||
ensureSessionToken: (req, _res) => {
|
||||
const token = getTokenFromRequest(req);
|
||||
return isSessionValid(token) ? token : null;
|
||||
},
|
||||
dispose,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user