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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user