Files
OpenClaw Agent 2329955e32 baseline: 薪酬逻辑收口到 shared/payroll.js 后的当前全量状态
这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。

此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。

当前状态(提交前已实测):
- index.html 内联薪酬算法已删除,改调 WHTPayroll
- shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用
- 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装

未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、
运行时 sqlite 数据与日志、时间戳备份、.trash。
2026-09-05 14:35:00 +08:00

66 lines
2.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 认证:只用 Web Crypto(globalThis.crypto.subtle)
// 这样同一份代码在 Node 22 与 Cloudflare Workers 上都能跑,不引入任何 npm 依赖。
const enc = new TextEncoder();
export const DEFAULT_PW_ITER = 100000;
export const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 天
function toHex(buf) {
const b = new Uint8Array(buf);
let s = '';
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, '0');
return s;
}
function fromHex(hex) {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
return out;
}
/** PBKDF2-SHA256 派生 32 字节,返回 hex */
export async function derive(password, saltHex, iterations) {
const key = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits(
{ name: 'PBKDF2', salt: fromHex(saltHex), iterations, hash: 'SHA-256' },
key,
256
);
return toHex(bits);
}
export async function hashPassword(password, iterations = DEFAULT_PW_ITER) {
const salt = toHex(crypto.getRandomValues(new Uint8Array(16)));
const hash = await derive(password, salt, iterations);
return { pwHash: hash, pwSalt: salt, pwIter: iterations };
}
/** 定长比较,避免按字符提前返回造成的时序泄漏 */
export function safeEqualHex(a, b) {
if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
export async function verifyPassword(password, user) {
const hash = await derive(password, user.pw_salt, user.pw_iter);
return safeEqualHex(hash, user.pw_hash);
}
/** 会话令牌:明文只回给客户端一次,库里只存 SHA-256,泄库也换不出可用 token */
export function newToken() {
return toHex(crypto.getRandomValues(new Uint8Array(32)));
}
export async function hashToken(token) {
return toHex(await crypto.subtle.digest('SHA-256', enc.encode(token)));
}
export function bearerFrom(request) {
const h = request.headers.get('authorization') || '';
const m = /^Bearer\s+([A-Za-z0-9._-]+)$/.exec(h.trim());
return m ? m[1] : null;
}