Compare commits

...

3 Commits

Author SHA1 Message Date
diegosouzapw
69e9bd81e9 chore: release v2.3.7
- Cline OAuth base64 decodeURIComponent fix
- OAuth account name normalization (name=email fallback)
- Remove sequential Account N naming
2026-03-12 12:25:17 -03:00
diegosouzapw
26f927f798 fix: replace sequential Account N with stable ID-based fallback for OAuth accounts
Remove Account cntValue+1 sequential naming (confusing when accounts deleted)
Leave name=null when no email → getAccountDisplayName returns Account ID-based label
2026-03-12 12:23:51 -03:00
diegosouzapw
2042dcf991 fix: Cline OAuth base64 parsing + name=email fallback for all OAuth accounts
- cline.ts: add decodeURIComponent before base64 decode to handle URL-encoded codes
- cline.ts: populate name = firstName+lastName || email in mapTokens
- oauth/exchange route: normalize name=email for all providers on exchange/poll/poll-callback
- Fixes: accounts showing Account #ID instead of email in providers dashboard
2026-03-12 12:22:20 -03:00
5 changed files with 80 additions and 21 deletions

View File

@@ -0,0 +1,31 @@
# Changelog
## [2.3.7] - 2026-03-12
### Fixed
- **Cline OAuth**: Add `decodeURIComponent` before base64 decode so URL-encoded auth codes from the callback URL are parsed correctly, fixing "invalid or expired authorization code" errors on remote (LAN IP) setups
- **Cline OAuth**: `mapTokens` now populates `name = firstName + lastName || email` so Cline accounts show real user names instead of "Account #ID"
- **OAuth account names**: All OAuth exchange flows (exchange, poll, poll-callback) now normalize `name = email` when name is missing, so every OAuth account shows its email as the display label in the Providers dashboard
- **OAuth account names**: Removed sequential "Account N" fallback in `db/providers.ts` — accounts with no email/name now use a stable ID-based label via `getAccountDisplayName()` instead of a sequential number that changes when accounts are deleted
## [2.3.6] - 2026-03-12
### Fixed
- **Provider test batch**: Fixed Zod schema to accept `providerId: null` (frontend sends null for non-provider modes); was incorrectly returning "Invalid request" for all batch tests
- **Provider test modal**: Fixed `[object Object]` display by normalizing API error objects to strings before rendering in `setTestResults` and `ProviderTestResultsView`
- **i18n**: Added missing keys `cliTools.toolDescriptions.opencode`, `cliTools.toolDescriptions.kiro`, `cliTools.guides.opencode`, `cliTools.guides.kiro` to `en.json`
- **i18n**: Synchronized 1111 missing keys across all 29 non-English language files using English values as fallbacks
## [2.3.5] - 2026-03-11
### Fixed
- **@swc/helpers**: Added permanent `postinstall` fix to copy `@swc/helpers` into the standalone app's `node_modules` — prevents MODULE_NOT_FOUND crash on global npm installs
## [2.3.4] - 2026-03-10
### Added
- Multiple provider integrations and dashboard improvements

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "2.3.6",
"version": "2.3.7",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {

View File

@@ -226,6 +226,12 @@ export async function POST(
exchangeTokens(provider, code, redirectUri, codeVerifier, state)
);
// Normalize: if name is missing, use email or displayName as fallback so accounts
// always show a real label (e.g. user@gmail.com) instead of "Account #abc123"
if (!tokenData.name && (tokenData.email || tokenData.displayName)) {
tokenData.name = tokenData.email || tokenData.displayName;
}
// Upsert: update existing connection if same provider+email, else create new
const expiresAt = tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
@@ -297,6 +303,11 @@ export async function POST(
}
if (result.success) {
// Normalize: if name is missing, use email as fallback display label
if (!result.tokens.name && (result.tokens.email || result.tokens.displayName)) {
result.tokens.name = result.tokens.email || result.tokens.displayName;
}
// Upsert: update existing connection if same provider+email, else create new
const expiresAt = result.tokens.expiresIn
? new Date(Date.now() + result.tokens.expiresIn * 1000).toISOString()
@@ -418,6 +429,11 @@ export async function POST(
exchangeTokens(provider, params.code, redirectUri, codeVerifier, params.state)
);
// Normalize: if name is missing, use email as fallback display label
if (!tokenData.name && (tokenData.email || tokenData.displayName)) {
tokenData.name = tokenData.email || tokenData.displayName;
}
// Upsert: update existing connection if same provider+email, else create new
const expiresAt = tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()

View File

@@ -126,18 +126,16 @@ export async function createProviderConnection(data: JsonRecord) {
return cleanNulls(merged);
}
// Generate name
// Generate name: prefer explicit name, then email, then a stable short-ID label.
// Avoid sequential "Account N" — it reassigns when accounts are deleted/reordered.
let connectionName = data.name || null;
if (!connectionName && data.authType === "oauth") {
if (data.email) {
connectionName = data.email;
} else {
const count = db
.prepare("SELECT COUNT(*) as cnt FROM provider_connections WHERE provider = ?")
.get(data.provider) as JsonRecord | undefined;
const cntValue = toNumberOrZero(toRecord(count).cnt);
connectionName = `Account ${cntValue + 1}`;
connectionName = data.email as string;
} else if (data.displayName) {
connectionName = data.displayName as string;
}
// Otherwise leave null — UI will fall back to getAccountDisplayName() → "Account #<id>"
}
// Auto-increment priority

View File

@@ -13,7 +13,14 @@ export const cline = {
},
exchangeToken: async (config, code, redirectUri) => {
try {
// Cline embeds tokens as base64-encoded JSON in the auth code.
// The code may be URL-encoded when pasted from the callback URL.
let base64 = code;
try {
base64 = decodeURIComponent(base64);
} catch {
/* already decoded */
}
const padding = 4 - (base64.length % 4);
if (padding !== 4) {
base64 += "=".repeat(padding);
@@ -62,16 +69,23 @@ export const cline = {
};
}
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
email: tokens.email,
providerSpecificData: {
firstName: tokens.firstName,
lastName: tokens.lastName,
},
}),
mapTokens: (tokens) => {
const firstName = tokens.firstName || "";
const lastName = tokens.lastName || "";
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
// Use full name if available, fallback to email so UI shows a real label
name: fullName || tokens.email || null,
email: tokens.email,
providerSpecificData: {
firstName: tokens.firstName,
lastName: tokens.lastName,
},
};
},
};