dms-token.mjs 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /**
  2. * DMS token 的**唯一**读取口径(vite 代理与 harness 脚本共用这一份)。
  3. *
  4. * 为什么要有这个模块:token 是外部 OAuth 签发的 JWT,**约 24 小时就过期**,
  5. * 而它以前散在三处(`.env.development.local` 的 `DMS_TOKEN`、脚本的参数、
  6. * `process.env.DMS_TOKEN`),每处都要人肉填一遍 —— 用户的原话是「每次都要重新填 token 太麻烦了」。
  7. *
  8. * 现在的顺序:**显式参数 → 环境变量 → 本机文件 `.dms-token`**。
  9. * `.dms-token` 是被 gitignore 的单行文件,由 `npm run dms:token` 写入(可从剪贴板读)。
  10. *
  11. * vite 代理**每个请求**调用一次 `readDmsToken()`(见 vite.config.ts),
  12. * 所以换 token 之后 **dev server 不用重启**。
  13. *
  14. * 📌 **2026-09-29 用户确认:`.dms-token` 保留、机制不动**(问过"能不能删")——
  15. * 因为**前端已改成自己经 `/oauth` 换 token**(`src/network/api/dms/service-token.ts`),
  16. * 这个文件现在只为**命令行工具**服务:它们默认打 `localhost:8083/dms` 且不带 token 头,
  17. * 靠 vite 代理从这里注入(`npm run dev` 的 predev 也会自动续期)。
  18. * 真要彻底不落盘,需要三件事(都还没做):
  19. * ① 代理改成"没带 token 时用服务账号现换 + 内存缓存"(不再读文件);
  20. * ② 6 个走代理的工具(verify-dms-* / verify-company-classify-* / audit-company-info-coverage /
  21. * gen-company-info-mapping-doc / upload-policy-library)改成自己换 token 并带 `token` 头;
  22. * ③ 去掉 predev 的自动续期与 `dms-login.mjs` 的写文件逻辑。
  23. */
  24. import { readFileSync } from 'node:fs';
  25. import { fileURLToPath } from 'node:url';
  26. import { dirname, join } from 'node:path';
  27. /** 本机单行 token 文件(gitignore;仓库根) */
  28. export const DMS_TOKEN_FILE = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '.dms-token');
  29. /**
  30. * 取 token。`explicit` 通常来自命令行参数;都拿不到时返回空串(调用方自己决定是报错还是告警)。
  31. * 每次调用都**现读文件**,所以运行中换 token 立即生效。
  32. */
  33. export const readDmsToken = (explicit) => {
  34. const fromArg = String(explicit || '').trim();
  35. if (fromArg) return fromArg;
  36. const fromEnv = String(process.env.DMS_TOKEN || '').trim();
  37. if (fromEnv) return fromEnv;
  38. try {
  39. return readFileSync(DMS_TOKEN_FILE, 'utf8').trim();
  40. } catch {
  41. return '';
  42. }
  43. };
  44. /** 只解 JWT payload(不校验签名;拿不到返回 null)—— 只为给出人能看懂的过期提示 */
  45. export const parseJwtPayload = (token) => {
  46. const parts = String(token || '').split('.');
  47. if (parts.length !== 3 || !parts[1]) return null;
  48. try {
  49. return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
  50. } catch {
  51. return null;
  52. }
  53. };
  54. /** 人类可读的 token 状态:用户名 + 有效期 + 剩余时长(脚本与 vite 告警共用同一套措辞) */
  55. export const describeDmsToken = (token) => {
  56. const payload = parseJwtPayload(token);
  57. if (!payload) return '⚠️ 不像 JWT(应为 a.b.c 三段),请确认复制的是 access_token';
  58. const who = payload.username || payload.userId || '未知用户';
  59. const exp = Number(payload.exp);
  60. if (!Number.isFinite(exp)) return `用户 ${who};payload 里没有 exp,无法判断有效期`;
  61. const leftMs = exp * 1000 - Date.now();
  62. const at = new Date(exp * 1000).toLocaleString('zh-CN');
  63. return leftMs > 0
  64. ? `用户 ${who};有效期至 ${at}(还剩 ${(leftMs / 3600000).toFixed(1)} 小时)`
  65. : `用户 ${who};**已于 ${at} 过期**(${(Math.abs(leftMs) / 3600000).toFixed(1)} 小时前)`;
  66. };