export const AUDIT_BIZ_SIGN_HEADER = 'X-Audit-Biz-Sign';
|
|
const EVENT_TIMEOUT_MS = 5 * 60 * 1000;
|
const SIGN_PATTERN = /^[\w-]{1,40}$/;
|
|
interface AuditBizSignEvent {
|
bizSign: string;
|
expiresAt: number;
|
timeout: ReturnType<typeof setTimeout>;
|
token: symbol;
|
}
|
|
let activeEvent: AuditBizSignEvent | undefined;
|
let eventQueue: Promise<void> = Promise.resolve();
|
|
export async function runWithAuditBizSignEvent<T>(bizSign: unknown, submit: () => Promise<T> | T): Promise<T> {
|
const normalizedBizSign = normalizeBizSign(bizSign);
|
if (!normalizedBizSign) throw new Error('电子签名认证结果无效');
|
|
const previousEvent = eventQueue;
|
let releaseEvent!: () => void;
|
eventQueue = new Promise<void>((resolve) => {
|
releaseEvent = resolve;
|
});
|
await previousEvent;
|
|
const token = Symbol('audit-biz-sign-event');
|
const timeout = setTimeout(() => clearEvent(token), EVENT_TIMEOUT_MS);
|
activeEvent = {
|
bizSign: normalizedBizSign,
|
expiresAt: Date.now() + EVENT_TIMEOUT_MS,
|
timeout,
|
token,
|
};
|
|
try {
|
return await submit();
|
} finally {
|
clearEvent(token);
|
releaseEvent();
|
}
|
}
|
|
export function getActiveAuditBizSign(): string {
|
if (!activeEvent) return '';
|
if (activeEvent.expiresAt <= Date.now()) {
|
clearEvent(activeEvent.token);
|
return '';
|
}
|
return activeEvent.bizSign;
|
}
|
|
export function applyActiveAuditBizSign(headers: Record<string, any>, url: string | undefined, method: string | undefined, apiBaseUrl = ''): void {
|
const bizSign = getActiveAuditBizSign();
|
if (!bizSign || hasHeader(headers, AUDIT_BIZ_SIGN_HEADER) || !shouldAttachAuditBizSign(url, method, apiBaseUrl)) return;
|
headers[AUDIT_BIZ_SIGN_HEADER] = bizSign;
|
}
|
|
export function shouldAttachAuditBizSign(url: string | undefined, method: string | undefined, apiBaseUrl = ''): boolean {
|
if (!['delete', 'patch', 'post', 'put'].includes(String(method || '').toLowerCase())) return false;
|
if (!url || url.startsWith('//')) return false;
|
|
try {
|
const browserOrigin = globalThis.location?.origin || 'http://audit.local';
|
const resolved = new URL(url, browserOrigin);
|
const configuredApi = new URL(apiBaseUrl || '/', browserOrigin);
|
if (resolved.origin !== configuredApi.origin) return false;
|
const pathname = stripApiBasePath(resolved.pathname, configuredApi.pathname);
|
if (!pathname.startsWith('/api/')) return false;
|
return !isExcludedPath(pathname);
|
} catch {
|
return false;
|
}
|
}
|
|
function hasHeader(headers: Record<string, any>, name: string): boolean {
|
if (typeof headers?.has === 'function' && headers.has(name)) return true;
|
return Object.keys(headers || {}).some((key) => key.toLowerCase() === name.toLowerCase());
|
}
|
|
function normalizeBizSign(value: unknown): string {
|
const normalized = value == null ? '' : String(value).trim();
|
return SIGN_PATTERN.test(normalized) ? normalized : '';
|
}
|
|
function stripApiBasePath(pathname: string, apiBasePath: string): string {
|
const normalizedBasePath = apiBasePath.replace(/\/$/, '');
|
if (!normalizedBasePath || normalizedBasePath === '/') return pathname;
|
if (pathname === normalizedBasePath) return '/';
|
return pathname.startsWith(`${normalizedBasePath}/`) ? pathname.slice(normalizedBasePath.length) : pathname;
|
}
|
|
function clearEvent(token: symbol) {
|
if (activeEvent?.token !== token) return;
|
clearTimeout(activeEvent.timeout);
|
activeEvent = undefined;
|
}
|
|
function isExcludedPath(pathname: string): boolean {
|
return (
|
/\/sign\/(?:init-all|modify-password|verify|verify_user)\/?$/i.test(pathname) ||
|
/^\/api\/oauth(?:\/|$)/i.test(pathname) ||
|
/^\/api\/audit(?:\/|$)/i.test(pathname)
|
);
|
}
|