Archived
统一鉴权中心接入(ADR-012):org 重写为 Keycloak 身份 + 角色→端;中间件按路由归属统一判;口令/会话/岗位授权退役;迁移 0032
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
This commit is contained in:
@@ -16,18 +16,20 @@ import { createEventService, createEventStore } from '@modules/event';
|
||||
import type { LedgerService } from '@modules/ledger';
|
||||
import type { LoopEngineService } from '@modules/loop-engine';
|
||||
import { createDraftStore, createLedgerService, createLedgerStore, DRAFT_DDL } from '@modules/ledger';
|
||||
import type { OrgService, InFlightWorkProbe } from '@modules/org';
|
||||
import type { InFlightWorkProbe, OrgService, RosterSource, TokenVerifier } from '@modules/org';
|
||||
import {
|
||||
createGrantRepository,
|
||||
createKeycloakRoster,
|
||||
createKeycloakTokenVerifier,
|
||||
createOrgService,
|
||||
createPositionRepository,
|
||||
createSessionRepository,
|
||||
createRoleAccessRepository,
|
||||
createStaffRepository,
|
||||
createWorkGroupRepository,
|
||||
hashPassword,
|
||||
newSessionToken,
|
||||
verifyPassword,
|
||||
TokenRejectedError,
|
||||
} from '@modules/org';
|
||||
import { createRemoteJWKSet } from 'jose';
|
||||
import { createTicketIssuer } from './ticket.ts';
|
||||
import type { TicketIssuer } from './ticket.ts';
|
||||
import type { LlmService, ModelGateway } from '@modules/llm';
|
||||
import {
|
||||
createLlmInstanceRepository,
|
||||
@@ -94,6 +96,8 @@ export type Container = {
|
||||
readonly pool: Pool;
|
||||
readonly db: Db;
|
||||
readonly org: OrgService;
|
||||
/** 60 秒票据(ADR-012 §9.3)—— 给 EventSource 与 <a href> 下载这两条带不了 Authorization 头的路 */
|
||||
readonly tickets: TicketIssuer;
|
||||
readonly ledger: LedgerService;
|
||||
readonly event: EventService;
|
||||
readonly llm: LlmService;
|
||||
@@ -171,7 +175,13 @@ const sfeelEvaluator = (expression: string, scope: Readonly<Record<string, unkno
|
||||
/** 第一方工具实现 —— 由 app 注册进 ToolRuntime;活动只见调用契约 */
|
||||
export const firstPartyToolHandlers = new Map<string, ToolHandler>(FIRST_PARTY_TOOLS);
|
||||
|
||||
export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
/** 集成测试的注入缝:验签与名册来源都是外界,测试给替身,不联网 */
|
||||
export type ContainerOverrides = {
|
||||
readonly tokenVerifier?: TokenVerifier;
|
||||
readonly roster?: RosterSource;
|
||||
};
|
||||
|
||||
export async function createContainer(config: AppConfig, overrides: ContainerOverrides = {}): Promise<Container> {
|
||||
/*
|
||||
* 广播器**先建** —— 装配期就要往它上面接线。
|
||||
*
|
||||
@@ -197,25 +207,60 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
*/
|
||||
const bridge = bridgeBroadcaster(pool, broadcaster);
|
||||
const ids = createIdGenerator();
|
||||
const secrets = createSecretBox(loadSecretKey());
|
||||
const masterKey = loadSecretKey();
|
||||
const secrets = createSecretBox(masterKey);
|
||||
const tickets = createTicketIssuer(masterKey);
|
||||
|
||||
// 草稿层的建表:ledger schema 的唯一写者是 ledger adapter,DDL 在其出口给出
|
||||
await pool.query(DRAFT_DDL);
|
||||
|
||||
const inFlightProbe = createDeferredInFlightProbe();
|
||||
|
||||
/*
|
||||
* 身份归统一鉴权中心(ADR-012):验签走 jose 远程 JWKS(首次用到才拉、缓存 10 分钟、
|
||||
* 30 秒冷却、5 秒超时 —— 手册 4.1);没配 KEYCLOAK_* 就装一个恒拒的 verifier:
|
||||
* 集成测试走用例不走 HTTP,不受影响;真跑起来缺配置的话 main.ts 早就拒绝启动了。
|
||||
*/
|
||||
const keycloak = config.keycloak;
|
||||
const verifier: TokenVerifier =
|
||||
overrides.tokenVerifier ??
|
||||
(keycloak === undefined
|
||||
? {
|
||||
verify: async () => {
|
||||
throw new TokenRejectedError('未配置 KEYCLOAK_URL / KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID');
|
||||
},
|
||||
}
|
||||
: createKeycloakTokenVerifier({
|
||||
issuer: keycloak.issuer,
|
||||
audience: keycloak.clientId,
|
||||
getKey: createRemoteJWKSet(new URL(keycloak.jwksUrl), {
|
||||
cooldownDuration: 30_000,
|
||||
cacheMaxAge: 10 * 60_000,
|
||||
timeoutDuration: 5_000,
|
||||
}),
|
||||
}));
|
||||
const roster: RosterSource | undefined =
|
||||
overrides.roster ??
|
||||
(keycloak?.provisioning === undefined
|
||||
? undefined
|
||||
: createKeycloakRoster({
|
||||
url: keycloak.url,
|
||||
realm: keycloak.realm,
|
||||
clientId: keycloak.provisioning.clientId,
|
||||
secret: keycloak.provisioning.secret,
|
||||
}));
|
||||
|
||||
const org = createOrgService({
|
||||
verifier,
|
||||
...(roster === undefined ? {} : { roster }),
|
||||
positions: createPositionRepository(db),
|
||||
staff: createStaffRepository(db),
|
||||
grants: createGrantRepository(db),
|
||||
sessions: createSessionRepository(db),
|
||||
roleAccess: createRoleAccessRepository(db),
|
||||
workGroups: createWorkGroupRepository(db),
|
||||
// 散列算法在 adapter,usecase 只认这三个动作(ADR-010)
|
||||
passwords: { hash: hashPassword, verify: verifyPassword, newToken: newSessionToken },
|
||||
inFlight: inFlightProbe,
|
||||
clock: systemClock,
|
||||
seedPassword: config.seedPassword,
|
||||
ids,
|
||||
testAccounts: config.testAccounts,
|
||||
});
|
||||
|
||||
// message 是 event 的 `MessageFallback` 实现面 —— event(L1)不反向依赖它,由此处注入
|
||||
@@ -226,16 +271,8 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
const { staff } = await org.listStaff();
|
||||
return staff.filter((s) => s.positionId === positionId).map((s) => ({ id: s.id }));
|
||||
},
|
||||
governanceStaff: async () => {
|
||||
const { staff } = await org.listStaff();
|
||||
const governing = await Promise.all(
|
||||
staff.map(async (s) => ({
|
||||
s,
|
||||
ok: await org.authorize(s.id, { kind: 'governance', function: 'task' }, 'govern'),
|
||||
})),
|
||||
);
|
||||
return governing.filter((g) => g.ok).map((g) => ({ id: g.s.id }));
|
||||
},
|
||||
// 治理台收件人 = 能进运营中心的活跃员工(任务在那儿管;ADR-012 权限只到端)
|
||||
governanceStaff: async () => (await org.staffWithEnd('ops')).map((s) => ({ id: s.id })),
|
||||
},
|
||||
clock: systemClock,
|
||||
ids,
|
||||
@@ -715,6 +752,7 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
pool,
|
||||
db,
|
||||
org,
|
||||
tickets,
|
||||
ledger,
|
||||
event,
|
||||
llm,
|
||||
|
||||
Reference in New Issue
Block a user