Archived
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
/**
|
|
* Hono 应用 —— 自建应用,Mastra 以库形态嵌入(ADR-001)。
|
|
* 鉴权、岗位权限、SSE 全归 app 层;不暴露 Mastra 默认端点。
|
|
*/
|
|
import { Hono } from 'hono';
|
|
import type { ApiError } from '@contracts';
|
|
import { healthRoute, ingestMailRoute, loginRoute } from '@contracts';
|
|
import type { Container } from './container.ts';
|
|
import type { AppEnv } from './http-env.ts';
|
|
import { readSessionToken } from './session-cookie.ts';
|
|
import { registerDeskRoutes } from './routes/desk.ts';
|
|
import { registerDeskElementRoutes } from './routes/desk-elements.ts';
|
|
import { registerDeskApprovalRoutes } from './routes/desk-approval.ts';
|
|
import { registerRegistrationRoutes } from './routes/registration.ts';
|
|
import { registerGovernanceOpsRoutes } from './routes/governance-ops.ts';
|
|
import { registerGovernanceRoutes } from './routes/governance.ts';
|
|
import { registerIngestRoutes } from './routes/ingest.ts';
|
|
import { registerStreamRoute } from './routes/stream.ts';
|
|
import { registerWorkbenchRoutes } from './routes/workbench.ts';
|
|
|
|
export type { AppEnv };
|
|
|
|
/**
|
|
* 免鉴权白名单(ADR-010)—— 只三条,其余一律 401。
|
|
* 入站邮件是**外界边界**:它代替真实 IMAP 拉件,不是人的请求,没有会话可言。
|
|
*/
|
|
const INGEST_PREFIX = ingestMailRoute.path.slice(0, ingestMailRoute.path.indexOf(':'));
|
|
|
|
function isPublic(method: string, path: string): boolean {
|
|
if (method === 'GET' && path === healthRoute.path) return true;
|
|
if (method === 'POST' && path === loginRoute.path) return true;
|
|
if (method === 'POST' && path.startsWith(INGEST_PREFIX)) return true;
|
|
return false;
|
|
}
|
|
|
|
export function createServer(container: Container): Hono<AppEnv> {
|
|
const app = new Hono<AppEnv>();
|
|
|
|
/**
|
|
* 鉴权中间件(编码规范 §4:中间件在 app 层,身份判定读 `org` 出口;ADR-010)。
|
|
*
|
|
* 身份**只能**来自服务端会话表:cookie 里是一枚不透明 token,改它只会让会话解不出来,
|
|
* 冒充不了任何人 —— 这正是明文 `jcai_account` cookie 被废止的理由。
|
|
*/
|
|
app.use('*', async (c, next) => {
|
|
const token = readSessionToken(c.req.header('cookie'));
|
|
const session = token === undefined ? undefined : await container.org.resolveSession(token);
|
|
c.set('session', session);
|
|
c.set('account', session?.staff.account ?? '');
|
|
|
|
if (session === undefined && !isPublic(c.req.method, c.req.path)) {
|
|
const body: ApiError = { code: 'unauthenticated', message: '尚未登录' };
|
|
return c.json(body, 401);
|
|
}
|
|
await next();
|
|
return undefined;
|
|
});
|
|
|
|
app.onError((error, c) => {
|
|
const body: ApiError = {
|
|
code: 'internal',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
};
|
|
process.stderr.write(`[api] ${body.message}\n`);
|
|
return c.json(body, 500);
|
|
});
|
|
|
|
app.notFound((c) => {
|
|
const body: ApiError = { code: 'not-found', message: `无此路由:${c.req.path}` };
|
|
return c.json(body, 404);
|
|
});
|
|
|
|
registerWorkbenchRoutes(app, container);
|
|
registerGovernanceRoutes(app, container);
|
|
registerDeskRoutes(app, container);
|
|
registerDeskElementRoutes(app, container);
|
|
registerDeskApprovalRoutes(app, container);
|
|
registerRegistrationRoutes(app, container);
|
|
registerGovernanceOpsRoutes(app, container);
|
|
registerIngestRoutes(app, container);
|
|
registerStreamRoute(app, container.broadcaster);
|
|
|
|
return app;
|
|
}
|