fix(plugins): match remote HMAC tenants with the same host rules as get_user

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 10:50:40 +03:30
co-authored by Cursor
parent 2d43ce6c29
commit 94d79c175b
3 changed files with 124 additions and 149 deletions
+101
View File
@@ -0,0 +1,101 @@
export type TenantHostFields = {
domain?: string | null;
domains?: unknown;
adminUrl?: string | null;
appUrl?: string | null;
nodeServer?: string | null;
};
export function normalizeTenantHost(value?: string | null): string {
if (!value) return '';
return String(value)
.trim()
.toLowerCase()
.replace(/^https?:\/\//, '')
.split('/')[0]
.split(':')[0]
.replace(/\.$/, '');
}
/** aliases JSON array, object map, CSV, or a single host/URL */
export function readHostList(raw: unknown): string[] {
if (!raw) return [];
if (Array.isArray(raw)) return raw.flatMap((item) => readHostList(item));
if (typeof raw === 'object') {
return Object.values(raw as Record<string, unknown>).flatMap(readHostList);
}
const text = String(raw).trim();
if (!text) return [];
if (
(text.startsWith('[') && text.endsWith(']')) ||
(text.startsWith('{') && text.endsWith('}'))
) {
try {
return readHostList(JSON.parse(text));
} catch {
/* fall through */
}
}
return text
.split(/[\s,;]+/)
.map((item) => item.trim())
.filter(Boolean);
}
function looksLikeHostname(value: string): boolean {
if (!value.includes('.')) return false;
if (/^\d+(\.\d+)*$/.test(value)) return false;
if (/^09\d{9}$/.test(value)) return false;
return true;
}
function hostsEqual(left: string, right: string): boolean {
if (!left || !right) return false;
if (left === right) return true;
return left.replace(/^www\./, '') === right.replace(/^www\./, '');
}
/**
* Same identity as GET /get_user?domain= — primary domain, then aliases,
* then app/admin/node URLs so HMAC and get_user cannot disagree.
*/
export function scoreTenantHost(
tenant: TenantHostFields,
hostRaw: string,
): number {
const host = normalizeTenantHost(hostRaw);
if (!host) return 0;
const eq = (raw?: string | null) =>
hostsEqual(normalizeTenantHost(raw), host);
if (eq(tenant.domain)) return 100;
for (const alias of readHostList(tenant.domains)) {
if (eq(alias)) return 90;
}
for (const value of readHostList(tenant.appUrl)) {
if (eq(value)) return 80;
}
for (const value of readHostList(tenant.adminUrl)) {
const normalized = normalizeTenantHost(value);
if (looksLikeHostname(normalized) && hostsEqual(normalized, host)) {
return 50;
}
}
for (const value of readHostList(tenant.nodeServer)) {
if (eq(value)) return 10;
}
return 0;
}
export function findTenantForHost<T extends TenantHostFields>(
tenants: T[],
host: string,
): T | undefined {
let best: { row: T; score: number } | undefined;
for (const row of tenants) {
const score = scoreTenantHost(row, host);
if (score <= 0) continue;
if (!best || score > best.score) best = { row, score };
}
return best?.row;
}