Archived
40 lines
1.8 KiB
TypeScript
40 lines
1.8 KiB
TypeScript
/**
|
|
* token claims → 一个人(纯函数,零 IO;ADR-012)。
|
|
*
|
|
* 只认手册 §17 列出的那几个 claim;`name` 缺了回退用户名(服务账号常常没有 name)。
|
|
* Keycloak 给每个用户自动挂的三个「家具角色」不是业务角色,这里滤掉:
|
|
* `default-roles-<realm>` / `offline_access` / `uma_authorization`。
|
|
*/
|
|
import type { Identity } from '../../contract.ts';
|
|
import type { VerifiedClaims } from '../../port.ts';
|
|
|
|
const NOISE_ROLES: ReadonlySet<string> = new Set(['offline_access', 'uma_authorization']);
|
|
|
|
export function isNoiseRole(role: string): boolean {
|
|
return NOISE_ROLES.has(role) || role.startsWith('default-roles-');
|
|
}
|
|
|
|
/** 去重、滤噪声、排序 —— 角色快照要能逐字比对(「变了没有」靠它) */
|
|
export function normalizeRoles(roles: readonly string[]): string[] {
|
|
return [...new Set(roles.filter((r) => r.trim() !== '' && !isNoiseRole(r)))].sort();
|
|
}
|
|
|
|
export function identityOf(claims: VerifiedClaims): Identity {
|
|
const account = claims.preferred_username;
|
|
/* 姓名展示姓在前(2026-08-26 用户定):有姓/名散件就按「姓 名」拼;
|
|
没有散件才用 Keycloak 的全名 `name`(它按「名 姓」拼,中文场景顺序是反的) */
|
|
const cjk = [claims.family_name, claims.given_name]
|
|
.filter((s): s is string => s !== undefined && s.trim() !== '')
|
|
.join(' ');
|
|
const fallback = claims.name === undefined || claims.name.trim() === '' ? account : claims.name.trim();
|
|
const name = cjk !== '' ? cjk : fallback;
|
|
const email = claims.email === undefined || claims.email.trim() === '' ? undefined : claims.email.trim();
|
|
return {
|
|
sub: claims.sub,
|
|
account,
|
|
name,
|
|
roles: normalizeRoles(claims.realm_access?.roles ?? []),
|
|
...(email === undefined ? {} : { email }),
|
|
};
|
|
}
|