mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
chore(release): finalize v3.8.0 stabilization and fix typescript regressions
- Fix stream readiness loop and upstream error code propagation in chatCore.ts - Resolve Headers iterator TypeScript errors - Fix type mismatches and missing props in BuilderIntelligentStep, Card, and providers page - Fix providerLimits typecasts and resolve implicit any errors - Ensure green build and strict type compliance for production
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(pricing):** make `getPricingForModel` fully case-insensitive to ensure custom prices correctly reflect in new incoming requests cost calculations
|
||||
- **fix(gemini):** prevent `functionDeclarations` from being dropped by the sanitizer when `googleSearch` tool is present (#2077)
|
||||
- **fix(pollinations):** add `jsonMode: true` flag in the request transformation to enforce correct JSON structure from Pollinations API (#2109)
|
||||
- **fix(docker):** update Dockerfile to copy `/docs` directory during build ensuring API catalog availability at runtime (#2083)
|
||||
- **fix(providers):** strip OpenAI-specific fields in Kiro translator to prevent 400 errors (#2037)
|
||||
- **fix(ui):** resolve text contrast issues for zero-config warning banner in light mode (#2050)
|
||||
- **fix(core):** inject global system prompt correctly into downstream chat completions pipeline (#2080)
|
||||
@@ -25,6 +29,7 @@
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **fix(security):** remediate CodeQL vulnerabilities (ReDoS, cryptographic bias, stack trace exposure, and weak password hashing) (#216, #215, #211, #208, #206, #210)
|
||||
- **fix(security):** remediate regex validation backtracking path in core compression cleanup (#1990)
|
||||
- **fix(core):** harden input handling and stabilization for prompt compression edge cases
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ COPY --from=builder /app/src/lib/db/migrations ./migrations
|
||||
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
|
||||
# MITM server.cjs is spawned at runtime via child_process — not traced by nft
|
||||
COPY --from=builder /app/src/mitm/server.cjs ./src/mitm/server.cjs
|
||||
# OpenAPI spec is read from disk by /api/openapi/spec at runtime for the
|
||||
# Endpoints dashboard. Next.js standalone tracing does not include it.
|
||||
COPY --from=builder /app/docs/openapi.yaml ./docs/openapi.yaml
|
||||
# Documentation files and OpenAPI spec are read from disk at runtime.
|
||||
# Next.js standalone tracing does not include them.
|
||||
COPY --from=builder /app/docs ./docs
|
||||
|
||||
COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs
|
||||
COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs
|
||||
|
||||
@@ -42,7 +42,9 @@ export class PollinationsExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
transformRequest(model: string, body: any, _stream: boolean, _credentials: any): any {
|
||||
// Pollinations uses provider aliases directly: "openai", "claude", "gemini", etc.
|
||||
if (typeof body === "object" && body !== null) {
|
||||
body.jsonMode = true;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2994,7 +2994,11 @@ export async function handleChatCore({
|
||||
_dedupSnapshot: {
|
||||
status,
|
||||
statusText,
|
||||
headers: Array.from(headers.entries()),
|
||||
headers: (() => {
|
||||
const arr: [string, string][] = [];
|
||||
headers.forEach((v, k) => arr.push([k, v]));
|
||||
return arr;
|
||||
})(),
|
||||
payload,
|
||||
},
|
||||
};
|
||||
@@ -3249,6 +3253,8 @@ export async function handleChatCore({
|
||||
let statusCode = providerResponse.status;
|
||||
let message = "";
|
||||
let retryAfterMs: number | null = null;
|
||||
let upstreamErrorCode: string | undefined;
|
||||
let upstreamErrorType: string | undefined;
|
||||
|
||||
if (upstreamErrorParsed) {
|
||||
statusCode = parsedStatusCode;
|
||||
@@ -3260,6 +3266,8 @@ export async function handleChatCore({
|
||||
message = details.message;
|
||||
retryAfterMs = details.retryAfterMs;
|
||||
upstreamErrorBody = details.responseBody;
|
||||
upstreamErrorCode = details.errorCode;
|
||||
upstreamErrorType = details.errorType;
|
||||
}
|
||||
|
||||
// T06/T10/T36: classify provider errors and persist terminal account states.
|
||||
@@ -3428,7 +3436,13 @@ export async function handleChatCore({
|
||||
cacheSource: "upstream",
|
||||
});
|
||||
persistFailureUsage(statusCode, "model_unavailable");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
return createErrorResult(
|
||||
statusCode,
|
||||
errMsg,
|
||||
retryAfterMs,
|
||||
upstreamErrorCode,
|
||||
upstreamErrorType
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
persistAttemptLogs({
|
||||
@@ -3440,7 +3454,13 @@ export async function handleChatCore({
|
||||
cacheSource: "upstream",
|
||||
});
|
||||
persistFailureUsage(statusCode, "model_unavailable");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
return createErrorResult(
|
||||
statusCode,
|
||||
errMsg,
|
||||
retryAfterMs,
|
||||
upstreamErrorCode,
|
||||
upstreamErrorType
|
||||
);
|
||||
}
|
||||
} else {
|
||||
persistAttemptLogs({
|
||||
@@ -3452,7 +3472,13 @@ export async function handleChatCore({
|
||||
cacheSource: "upstream",
|
||||
});
|
||||
persistFailureUsage(statusCode, "model_unavailable");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
return createErrorResult(
|
||||
statusCode,
|
||||
errMsg,
|
||||
retryAfterMs,
|
||||
upstreamErrorCode,
|
||||
upstreamErrorType
|
||||
);
|
||||
}
|
||||
} else if (isContextOverflowError(statusCode, message)) {
|
||||
const familyCandidates = getModelFamily(currentModel).filter(
|
||||
@@ -3488,7 +3514,13 @@ export async function handleChatCore({
|
||||
cacheSource: "upstream",
|
||||
});
|
||||
persistFailureUsage(statusCode, "context_overflow");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
return createErrorResult(
|
||||
statusCode,
|
||||
errMsg,
|
||||
retryAfterMs,
|
||||
upstreamErrorCode,
|
||||
upstreamErrorType
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
persistAttemptLogs({
|
||||
@@ -3500,7 +3532,13 @@ export async function handleChatCore({
|
||||
cacheSource: "upstream",
|
||||
});
|
||||
persistFailureUsage(statusCode, "context_overflow");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
return createErrorResult(
|
||||
statusCode,
|
||||
errMsg,
|
||||
retryAfterMs,
|
||||
upstreamErrorCode,
|
||||
upstreamErrorType
|
||||
);
|
||||
}
|
||||
} else {
|
||||
persistAttemptLogs({
|
||||
@@ -3512,7 +3550,13 @@ export async function handleChatCore({
|
||||
cacheSource: "upstream",
|
||||
});
|
||||
persistFailureUsage(statusCode, "context_overflow");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
return createErrorResult(
|
||||
statusCode,
|
||||
errMsg,
|
||||
retryAfterMs,
|
||||
upstreamErrorCode,
|
||||
upstreamErrorType
|
||||
);
|
||||
}
|
||||
} else {
|
||||
persistAttemptLogs({
|
||||
@@ -3595,7 +3639,13 @@ export async function handleChatCore({
|
||||
}
|
||||
|
||||
if (!emergencyFallbackServed) {
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
return createErrorResult(
|
||||
statusCode,
|
||||
errMsg,
|
||||
retryAfterMs,
|
||||
upstreamErrorCode,
|
||||
upstreamErrorType
|
||||
);
|
||||
}
|
||||
}
|
||||
// ── End T5 ───────────────────────────────────────────────────────────────
|
||||
@@ -4067,9 +4117,11 @@ export async function handleChatCore({
|
||||
|
||||
const responseHeaders: Record<string, string> = {
|
||||
...Object.fromEntries(
|
||||
Array.from(providerResponse.headers.entries()).filter(
|
||||
([k]) => k.toLowerCase() !== "content-type"
|
||||
)
|
||||
(() => {
|
||||
const arr: [string, string][] = [];
|
||||
providerResponse.headers.forEach((v, k) => arr.push([k, v]));
|
||||
return arr;
|
||||
})().filter(([k]) => k.toLowerCase() !== "content-type")
|
||||
),
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
|
||||
@@ -209,19 +209,15 @@ export function buildGeminiTools(
|
||||
}
|
||||
}
|
||||
|
||||
if (googleSearchTool && functionDeclarations.length > 0) {
|
||||
console.warn(
|
||||
`[GeminiTools] Removing ${functionDeclarations.length} functionDeclarations because googleSearch cannot be mixed with Gemini function tools`
|
||||
);
|
||||
const result: GeminiTool[] = [];
|
||||
|
||||
if (functionDeclarations.length > 0) {
|
||||
result.push({ functionDeclarations });
|
||||
}
|
||||
|
||||
if (googleSearchTool) {
|
||||
return [googleSearchTool];
|
||||
result.push(googleSearchTool);
|
||||
}
|
||||
|
||||
if (functionDeclarations.length > 0) {
|
||||
return [{ functionDeclarations }];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return result.length > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ export async function parseUpstreamError(response, provider = null) {
|
||||
let message = "";
|
||||
let retryAfterMs = null;
|
||||
let responseBody = null;
|
||||
let errorCode = undefined;
|
||||
let errorType = undefined;
|
||||
|
||||
try {
|
||||
const text = await response.text();
|
||||
@@ -125,6 +127,8 @@ export async function parseUpstreamError(response, provider = null) {
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
message = json.error?.message || json.message || json.error || text;
|
||||
errorCode = json.error?.code || json.code;
|
||||
errorType = json.error?.type || json.type;
|
||||
} catch {
|
||||
message = text;
|
||||
}
|
||||
@@ -179,6 +183,8 @@ export async function parseUpstreamError(response, provider = null) {
|
||||
return {
|
||||
statusCode: response.status,
|
||||
message: messageStr,
|
||||
errorCode,
|
||||
errorType,
|
||||
retryAfterMs,
|
||||
responseBody,
|
||||
responseHeaders,
|
||||
@@ -195,19 +201,34 @@ export async function parseUpstreamError(response, provider = null) {
|
||||
export function createErrorResult(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
retryAfterMs: number | null = null
|
||||
retryAfterMs: number | null = null,
|
||||
errorCode?: string,
|
||||
errorType?: string
|
||||
) {
|
||||
const body = buildErrorBody(statusCode, message);
|
||||
if (errorCode) {
|
||||
(body.error as any).code = errorCode;
|
||||
}
|
||||
if (errorType) {
|
||||
(body.error as any).type = errorType;
|
||||
}
|
||||
|
||||
const result: {
|
||||
success: false;
|
||||
status: number;
|
||||
error: string;
|
||||
errorType?: string;
|
||||
response: Response;
|
||||
retryAfterMs?: number;
|
||||
} = {
|
||||
success: false,
|
||||
status: statusCode,
|
||||
error: message,
|
||||
response: errorResponse(statusCode, message),
|
||||
errorType,
|
||||
response: new Response(JSON.stringify(body), {
|
||||
status: statusCode,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
};
|
||||
|
||||
// Add retryAfterMs if available (for Antigravity quota errors)
|
||||
|
||||
@@ -15,7 +15,7 @@ function getI18nOrFallback(t: any, key: string, fallback: string) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toProviderOptions(activeProviders: any[] = []) {
|
||||
function toProviderOptions(activeProviders: any[] = [], candidatePool: string[] = []) {
|
||||
const uniqueProviders = new Map<string, { id: string; label: string; connectionCount: number }>();
|
||||
|
||||
activeProviders.forEach((provider) => {
|
||||
@@ -41,6 +41,16 @@ function toProviderOptions(activeProviders: any[] = []) {
|
||||
});
|
||||
});
|
||||
|
||||
candidatePool.forEach((poolId) => {
|
||||
if (!uniqueProviders.has(poolId)) {
|
||||
uniqueProviders.set(poolId, {
|
||||
id: poolId,
|
||||
label: `${poolId} (Offline/Deleted)`,
|
||||
connectionCount: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...uniqueProviders.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||
}
|
||||
|
||||
@@ -56,7 +66,10 @@ export default function BuilderIntelligentStep({
|
||||
activeProviders: any[];
|
||||
}) {
|
||||
const normalizedConfig = normalizeIntelligentRoutingConfig(config);
|
||||
const providerOptions = useMemo(() => toProviderOptions(activeProviders), [activeProviders]);
|
||||
const providerOptions = useMemo(
|
||||
() => toProviderOptions(activeProviders, normalizedConfig.candidatePool),
|
||||
[activeProviders, normalizedConfig.candidatePool]
|
||||
);
|
||||
|
||||
const updateConfig = (patch: Record<string, unknown>) => {
|
||||
onChange({
|
||||
@@ -64,7 +77,7 @@ export default function BuilderIntelligentStep({
|
||||
...patch,
|
||||
weights: {
|
||||
...normalizedConfig.weights,
|
||||
...(patch.weights || {}),
|
||||
...((patch.weights as Record<string, number>) || {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -509,6 +509,7 @@ interface ConnectionRowConnection {
|
||||
expiresAt?: string;
|
||||
tokenExpiresAt?: string;
|
||||
maxConcurrent?: number | null;
|
||||
authType?: string;
|
||||
}
|
||||
|
||||
interface ConnectionRowProps {
|
||||
@@ -3024,7 +3025,7 @@ export default function ProviderDetailPage() {
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
icon="delete"
|
||||
loading={batchDeleting}
|
||||
@@ -3154,7 +3155,7 @@ export default function ProviderDetailPage() {
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
icon="delete"
|
||||
loading={batchDeleting}
|
||||
|
||||
@@ -854,8 +854,10 @@ export async function GET(
|
||||
return localCatalog.map((model) => ({
|
||||
id: model.id,
|
||||
name: model.name || model.id,
|
||||
...(model.apiFormat ? { apiFormat: model.apiFormat } : {}),
|
||||
...(model.supportedEndpoints ? { supportedEndpoints: model.supportedEndpoints } : {}),
|
||||
...((model as any).apiFormat ? { apiFormat: (model as any).apiFormat } : {}),
|
||||
...((model as any).supportedEndpoints
|
||||
? { supportedEndpoints: (model as any).supportedEndpoints }
|
||||
: {}),
|
||||
...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}),
|
||||
}));
|
||||
};
|
||||
@@ -902,7 +904,7 @@ export async function GET(
|
||||
}
|
||||
) => {
|
||||
const status = getSafeOutboundFetchErrorStatus(error);
|
||||
if (status === 400) return null;
|
||||
if (status === 400 || status === 503 || status === 504) return null;
|
||||
return buildDiscoveryFallbackResponse(warnings);
|
||||
};
|
||||
|
||||
@@ -1852,8 +1854,10 @@ export async function GET(
|
||||
models: localCatalog.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name || m.id,
|
||||
...(m.apiFormat ? { apiFormat: m.apiFormat } : {}),
|
||||
...(m.supportedEndpoints ? { supportedEndpoints: m.supportedEndpoints } : {}),
|
||||
...((m as any).apiFormat ? { apiFormat: (m as any).apiFormat } : {}),
|
||||
...((m as any).supportedEndpoints
|
||||
? { supportedEndpoints: (m as any).supportedEndpoints }
|
||||
: {}),
|
||||
...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}),
|
||||
})),
|
||||
source: "local_catalog",
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isClaudeExtraUsageBlockEnabled,
|
||||
} from "@/lib/providers/claudeExtraUsage";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { isApiKeyRevealEnabled, maskStoredApiKey } from "@/lib/apiKeyExposure";
|
||||
|
||||
function normalizeCodexLimitPolicy(
|
||||
incoming: unknown,
|
||||
@@ -61,9 +62,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Hide sensitive fields
|
||||
const revealKeys = isApiKeyRevealEnabled();
|
||||
|
||||
// Hide or mask sensitive fields
|
||||
const result: Record<string, any> = { ...connection };
|
||||
delete result.apiKey;
|
||||
if (!revealKeys) {
|
||||
result.apiKey = result.apiKey ? maskStoredApiKey(result.apiKey) : undefined;
|
||||
}
|
||||
delete result.accessToken;
|
||||
delete result.refreshToken;
|
||||
delete result.idToken;
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "@/lib/providers/requestDefaults";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { isManagedProviderConnectionId } from "@/lib/providers/catalog";
|
||||
import { isApiKeyRevealEnabled, maskStoredApiKey } from "@/lib/apiKeyExposure";
|
||||
|
||||
// GET /api/providers - List all connections
|
||||
export async function GET(request: Request) {
|
||||
@@ -35,11 +36,12 @@ export async function GET(request: Request) {
|
||||
|
||||
try {
|
||||
const connections = await getProviderConnections();
|
||||
const revealKeys = isApiKeyRevealEnabled();
|
||||
|
||||
// Hide sensitive fields
|
||||
// Hide or mask sensitive fields
|
||||
const safeConnections = connections.map((c) => ({
|
||||
...c,
|
||||
apiKey: undefined,
|
||||
apiKey: revealKeys ? c.apiKey : c.apiKey ? maskStoredApiKey(c.apiKey) : undefined,
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
idToken: undefined,
|
||||
|
||||
@@ -271,24 +271,51 @@ export async function getPricingWithSources(): Promise<{
|
||||
|
||||
export async function getPricingForModel(provider: string, model: string) {
|
||||
const pricing = await getPricing();
|
||||
if (pricing[provider]?.[model]) return pricing[provider][model];
|
||||
|
||||
const { PROVIDER_ID_TO_ALIAS } = await import("@omniroute/open-sse/config/providerModels");
|
||||
// Check if provider is an ID -> map to ALIAS
|
||||
const alias = PROVIDER_ID_TO_ALIAS[provider];
|
||||
if (alias && pricing[alias]) return pricing[alias][model] || null;
|
||||
const findKeyInsensitive = (obj: Record<string, any> | undefined | null, key: string) => {
|
||||
if (!obj || !key) return undefined;
|
||||
const lowerKey = key.toLowerCase();
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (k.toLowerCase() === lowerKey) return v;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Check if provider is an ALIAS -> map to ID (search values)
|
||||
for (const [id, mappedAlias] of Object.entries(PROVIDER_ID_TO_ALIAS)) {
|
||||
if (mappedAlias === provider && pricing[id]?.[model]) {
|
||||
return pricing[id][model];
|
||||
const pLower = (provider || "").toLowerCase();
|
||||
let providerPricing = findKeyInsensitive(pricing, pLower);
|
||||
|
||||
if (!providerPricing) {
|
||||
const alias = findKeyInsensitive(PROVIDER_ID_TO_ALIAS, pLower);
|
||||
if (alias) providerPricing = findKeyInsensitive(pricing, alias);
|
||||
}
|
||||
|
||||
if (!providerPricing) {
|
||||
for (const [id, mappedAlias] of Object.entries(PROVIDER_ID_TO_ALIAS)) {
|
||||
if (typeof mappedAlias === "string" && mappedAlias.toLowerCase() === pLower) {
|
||||
providerPricing = findKeyInsensitive(pricing, id);
|
||||
if (providerPricing) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const np = provider?.replace(/-cn$/, "");
|
||||
if (np && np !== provider && pricing[np]) return pricing[np][model] || null;
|
||||
if (!providerPricing) {
|
||||
const np = pLower.replace(/-cn$/, "");
|
||||
if (np && np !== pLower) {
|
||||
providerPricing = findKeyInsensitive(pricing, np);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
if (!providerPricing) return null;
|
||||
|
||||
const mLower = (model || "").toLowerCase();
|
||||
let modelPricing = findKeyInsensitive(providerPricing, mLower);
|
||||
|
||||
if (!modelPricing) {
|
||||
const hyphenModel = mLower.replace(/\./g, "-");
|
||||
modelPricing = findKeyInsensitive(providerPricing, hyphenModel);
|
||||
}
|
||||
|
||||
return modelPricing || null;
|
||||
}
|
||||
|
||||
export async function updatePricing(pricingData: PricingByProvider) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getProviderNodes } from "@/lib/localDb";
|
||||
|
||||
type ValidatedEmbeddingBody = Record<string, unknown> & { model: string };
|
||||
|
||||
interface EmbeddingHandlerOptions {
|
||||
export interface EmbeddingHandlerOptions {
|
||||
clientRawRequest?: {
|
||||
endpoint: string;
|
||||
body: Record<string, unknown>;
|
||||
|
||||
@@ -298,7 +298,9 @@ async function fetchLiveProviderLimitsWithOptions(
|
||||
connection: ProviderConnectionLike;
|
||||
usage: JsonRecord;
|
||||
}> {
|
||||
let connection = (await getProviderConnectionById(connectionId)) as ProviderConnectionLike | null;
|
||||
let connection = (await getProviderConnectionById(
|
||||
connectionId
|
||||
)) as unknown as ProviderConnectionLike | null;
|
||||
if (!connection) {
|
||||
throw withStatus(new Error("Connection not found"), 404);
|
||||
}
|
||||
@@ -436,7 +438,7 @@ export async function syncAllProviderLimits(
|
||||
}> {
|
||||
const { source = "manual", concurrency = 5 } = options;
|
||||
const connections = (
|
||||
(await getProviderConnections({ isActive: true })) as ProviderConnectionLike[]
|
||||
(await getProviderConnections({ isActive: true })) as unknown as ProviderConnectionLike[]
|
||||
).filter(isSupportedUsageConnection);
|
||||
const cacheEntries: Array<{ connectionId: string; entry: ProviderLimitsCacheEntry }> = [];
|
||||
const caches: Record<string, ProviderLimitsCacheEntry> = {};
|
||||
|
||||
@@ -122,7 +122,16 @@ async function installCertLinux(sudoPassword: string, certPath: string): Promise
|
||||
try {
|
||||
await execFileWithPassword("sudo", ["-S", "mkdir", "-p", LINUX_CA_DIR], sudoPassword);
|
||||
await execFileWithPassword("sudo", ["-S", "cp", certPath, LINUX_CERT_DEST], sudoPassword);
|
||||
await execFileWithPassword("sudo", ["-S", "update-ca-certificates"], sudoPassword);
|
||||
await execFileWithPassword(
|
||||
"sudo",
|
||||
[
|
||||
"-S",
|
||||
"sh",
|
||||
"-c",
|
||||
"update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true",
|
||||
],
|
||||
sudoPassword
|
||||
);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
const msg = message.includes("canceled")
|
||||
@@ -186,7 +195,16 @@ async function uninstallCertLinux(sudoPassword: string, certPath: string): Promi
|
||||
if (fs.existsSync(LINUX_CERT_DEST)) {
|
||||
await execFileWithPassword("sudo", ["-S", "rm", "-f", LINUX_CERT_DEST], sudoPassword);
|
||||
}
|
||||
await execFileWithPassword("sudo", ["-S", "update-ca-certificates", "--fresh"], sudoPassword);
|
||||
await execFileWithPassword(
|
||||
"sudo",
|
||||
[
|
||||
"-S",
|
||||
"sh",
|
||||
"-c",
|
||||
"update-ca-certificates --fresh 2>/dev/null || update-ca-trust 2>/dev/null || true",
|
||||
],
|
||||
sudoPassword
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error("Failed to uninstall certificate");
|
||||
}
|
||||
|
||||
@@ -65,8 +65,12 @@ export default function Card({
|
||||
);
|
||||
}
|
||||
|
||||
interface CardSectionProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Sub-component: Bordered section inside Card
|
||||
Card.Section = function CardSection({ children, className, ...props }) {
|
||||
Card.Section = function CardSection({ children, className, ...props }: CardSectionProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -82,8 +86,12 @@ Card.Section = function CardSection({ children, className, ...props }) {
|
||||
);
|
||||
};
|
||||
|
||||
interface CardRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Sub-component: Hoverable row inside Card
|
||||
Card.Row = function CardRow({ children, className, ...props }) {
|
||||
Card.Row = function CardRow({ children, className, ...props }: CardRowProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -99,8 +107,18 @@ Card.Row = function CardRow({ children, className, ...props }) {
|
||||
);
|
||||
};
|
||||
|
||||
interface CardListItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Sub-component: List item with hover actions (macOS style)
|
||||
Card.ListItem = function CardListItem({ children, actions, className, ...props }) {
|
||||
Card.ListItem = function CardListItem({
|
||||
children,
|
||||
actions,
|
||||
className,
|
||||
...props
|
||||
}: CardListItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
Reference in New Issue
Block a user