DevOS v1 + Phase 2: unified resource model, infra ops, website deployment, AI chat ops
- Monorepo: NestJS API (Fastify) + React web (antd/ProComponents) + shared packages - PostgreSQL/Prisma (unified resources, relations graph, audit, events, jobs) - Auth/RBAC/workspace isolation, credential vault (AES-256-GCM) - Servers/SSH, Docker discovery & lifecycle, Job Center - Websites & deployments (git provider credential clone, env, health verification) - Domains/DNS (Cloudflare adapter), certificates (TLS probe), monitoring - Automation (cron/event/webhook HMAC + notifications), AI Gateway, MCP (29 tools) - AI chat window (LLM function-calling + rules fallback, SSE streaming, approvals) - Notes: Vditor WYSIWYG markdown editor, non-blocking autosave - Tests: 35 unit/security tests; UI audit clean (desktop+mobile, deep links)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@devos/auth",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@devos/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* # @devos/auth
|
||||
* 认证相关的纯逻辑实现:HS256 JWT 签名/校验、密码哈希/校验。
|
||||
* 设计原则:零第三方依赖、算法固定(防止 alg 混淆)、常量时间比较。
|
||||
*/
|
||||
|
||||
import { createHmac, randomBytes, randomUUID, timingSafeEqual, scrypt as scryptCb } from "node:crypto";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
/** # scrypt Promise 封装 **/
|
||||
const scrypt = promisify(scryptCb) as (
|
||||
password: string,
|
||||
salt: Buffer,
|
||||
keylen: number,
|
||||
) => Promise<Buffer>;
|
||||
|
||||
/** # base64url 编码
|
||||
* @param input 待编码字符串
|
||||
* @returns base64url 字符串(无填充)
|
||||
*/
|
||||
function b64url(input: Buffer | string): string {
|
||||
return Buffer.from(input).toString("base64url");
|
||||
}
|
||||
|
||||
/** # base64url 解码 */
|
||||
function b64urlDecode(input: string): Buffer {
|
||||
return Buffer.from(input, "base64url");
|
||||
}
|
||||
|
||||
/** # JWT 载荷结构 **/
|
||||
export interface JwtPayload {
|
||||
/** # 用户 ID **/
|
||||
sub: string;
|
||||
/** # 登录邮箱 **/
|
||||
email: string;
|
||||
/** # 登录时激活的工作区 ID **/
|
||||
wid: string | null;
|
||||
/** # 令牌唯一 ID **/
|
||||
jti: string;
|
||||
/** # 签发时间(秒) **/
|
||||
iat: number;
|
||||
/** # 过期时间(秒) **/
|
||||
exp: number;
|
||||
}
|
||||
|
||||
/** # 签发选项 **/
|
||||
export interface SignOptions {
|
||||
/** # 有效期(秒),默认 12 小时 **/
|
||||
expiresInSec?: number;
|
||||
/** # 当前时间(秒),用于测试注入 **/
|
||||
nowSec?: number;
|
||||
}
|
||||
|
||||
/** # 签发 HS256 JWT
|
||||
* @param payload 业务载荷(不含 iat/exp/jti)
|
||||
* @param secret 应用密钥
|
||||
* @param options 签发选项
|
||||
* @returns JWT 字符串
|
||||
*/
|
||||
export function signJwt(
|
||||
payload: Omit<JwtPayload, "iat" | "exp" | "jti">,
|
||||
secret: string,
|
||||
options: SignOptions = {},
|
||||
): string {
|
||||
const now = options.nowSec ?? Math.floor(Date.now() / 1000);
|
||||
const expiresInSec = options.expiresInSec ?? 12 * 3600;
|
||||
const header = { alg: "HS256", typ: "JWT" };
|
||||
const body: JwtPayload = { ...payload, jti: randomUUID(), iat: now, exp: now + expiresInSec };
|
||||
const head = b64url(JSON.stringify(header));
|
||||
const data = b64url(JSON.stringify(body));
|
||||
const sig = createHmac("sha256", secret).update(`${head}.${data}`).digest("base64url");
|
||||
return `${head}.${data}.${sig}`;
|
||||
}
|
||||
|
||||
/** # 校验结果 **/
|
||||
export type VerifyResult = { ok: true; payload: JwtPayload } | { ok: false; reason: string };
|
||||
|
||||
/** # 校验 HS256 JWT
|
||||
* 安全约束:仅接受 alg=HS256 的令牌;签名使用常量时间比较;校验 exp 过期。
|
||||
* @param token JWT 字符串
|
||||
* @param secret 应用密钥
|
||||
* @returns 校验结果
|
||||
*/
|
||||
export function verifyJwt(token: string, secret: string, nowSec?: number): VerifyResult {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return { ok: false, reason: "MALFORMED" };
|
||||
const [head, data, sig] = parts;
|
||||
let header: { alg?: string };
|
||||
try {
|
||||
header = JSON.parse(b64urlDecode(head).toString("utf8"));
|
||||
} catch {
|
||||
return { ok: false, reason: "MALFORMED" };
|
||||
}
|
||||
if (header.alg !== "HS256") return { ok: false, reason: "BAD_ALG" };
|
||||
const expected = createHmac("sha256", secret).update(`${head}.${data}`).digest();
|
||||
let given: Buffer;
|
||||
try {
|
||||
given = b64urlDecode(sig);
|
||||
} catch {
|
||||
return { ok: false, reason: "MALFORMED" };
|
||||
}
|
||||
if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
|
||||
return { ok: false, reason: "BAD_SIGNATURE" };
|
||||
}
|
||||
let payload: JwtPayload;
|
||||
try {
|
||||
payload = JSON.parse(b64urlDecode(data).toString("utf8"));
|
||||
} catch {
|
||||
return { ok: false, reason: "MALFORMED" };
|
||||
}
|
||||
const now = nowSec ?? Math.floor(Date.now() / 1000);
|
||||
if (typeof payload.exp !== "number" || payload.exp <= now) return { ok: false, reason: "EXPIRED" };
|
||||
return { ok: true, payload };
|
||||
}
|
||||
|
||||
/** # 密码哈希
|
||||
* Bun 运行时下使用 argon2id;否则回退 scrypt。
|
||||
* @param password 明文密码
|
||||
* @returns 哈希字符串
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const bun = (globalThis as Record<string, unknown>).Bun as typeof Bun | undefined;
|
||||
if (bun?.password) {
|
||||
return bun.password.hash(password, { algorithm: "argon2id" });
|
||||
}
|
||||
const salt = randomBytes(16);
|
||||
const key = await scrypt(password, salt, 32);
|
||||
return `scrypt$${salt.toString("hex")}$${key.toString("hex")}`;
|
||||
}
|
||||
|
||||
/** # 校验密码
|
||||
* @param password 明文密码
|
||||
* @param hash 存储的哈希
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
if (hash.startsWith("scrypt$")) {
|
||||
const [, saltHex, keyHex] = hash.split("$");
|
||||
const key = await scrypt(password, Buffer.from(saltHex, "hex"), 32);
|
||||
const expected = Buffer.from(keyHex, "hex");
|
||||
return key.length === expected.length && timingSafeEqual(key, expected);
|
||||
}
|
||||
const bun = (globalThis as Record<string, unknown>).Bun as typeof Bun | undefined;
|
||||
if (bun?.password) {
|
||||
return bun.password.verify(password, hash);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** # 生成随机令牌(如 API 临时凭证) */
|
||||
export function randomToken(bytes = 32): string {
|
||||
return randomBytes(bytes).toString("hex");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"types": [
|
||||
"node",
|
||||
"bun-types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user