@h-ai/serv 0.1.0-alpha.67 → 0.1.0-alpha.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,61 +1,70 @@
1
- import { wrapItemsResult } from '../chunk-27PCPE37.js';
2
- import { implement } from '../chunk-R354OMWL.js';
1
+ import { wrapItemsResult } from '../chunk-XKCLD42I.js';
2
+ import { implement, servM } from '../chunk-TWHCU6HT.js';
3
3
  import { apiContract } from '@h-ai/api-contract';
4
- import { ok } from '@h-ai/core';
4
+ import { ok, err, HaiCommonError } from '@h-ai/core';
5
5
 
6
6
  var aiContract = apiContract.ai;
7
7
  function createAiProcedures(deps) {
8
- return implement(aiContract).context().route("chats.createCompletion").auth().handle(async ({ input }) => {
9
- return mapChatCompletionResult(await deps.ai.llm.chat(toChatCompletionRequest(input)));
10
- }).route("chats.sendMessage").auth().handle(({ input }) => sendMessage(deps, input)).route("chats.listHistory").auth().handle(async ({ input }) => {
11
- const { objectId, sessionId, limit, order } = input;
8
+ async function resolveObjectId(operation, context, input) {
9
+ const objectId = input.objectId ?? context.session.userId;
10
+ if (objectId !== context.session.userId && await deps.authorizeObject?.({ context, objectId, sessionId: input.sessionId, operation }) !== true) {
11
+ return err(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale: context.locale }));
12
+ }
13
+ return ok(objectId);
14
+ }
15
+ return implement(aiContract).context().route("chats.createCompletion").auth().handle(async ({ input, context }) => {
16
+ const objectId = await resolveObjectId("completion", context, input);
17
+ if (!objectId.success)
18
+ return objectId;
19
+ return mapChatCompletionResult(await deps.ai.llm.chat({
20
+ ...toChatCompletionRequest(input),
21
+ objectId: objectId.data,
22
+ sessionId: input.sessionId ?? "default",
23
+ signal: context.request.signal
24
+ }));
25
+ }).route("chats.sendMessage").auth().handle(async ({ input, context }) => {
26
+ const objectId = await resolveObjectId("message", context, input);
27
+ if (!objectId.success)
28
+ return objectId;
29
+ return sendMessage(deps, input, { objectId: objectId.data, sessionId: input.sessionId ?? "default", signal: context.request.signal });
30
+ }).route("chats.listHistory").auth().handle(async ({ input, context }) => {
31
+ const objectId = await resolveObjectId("history", context, input);
32
+ if (!objectId.success)
33
+ return objectId;
34
+ const { sessionId, limit, order } = input;
12
35
  return mapChatRecordsResult(
13
- await deps.ai.llm.getHistory({ objectId, sessionId }, { limit, order })
36
+ await deps.ai.llm.getHistory({ objectId: objectId.data, sessionId }, { limit, order })
14
37
  );
15
- }).route("memories.recall").auth().handle(async ({ input }) => {
38
+ }).route("memories.recall").auth().handle(async ({ input, context }) => {
39
+ const objectId = await resolveObjectId("memoryRecall", context, input);
40
+ if (!objectId.success)
41
+ return objectId;
16
42
  const { query, ...options } = input;
17
- return wrapItemsResult(await deps.ai.memory.recall(query, options));
18
- }).route("memories.list").auth().handle(({ input }) => deps.ai.memory.listPage(input)).route("sessions.list").auth().handle(async ({ input }) => {
19
- return wrapItemsResult(await deps.ai.llm.listSessions(input.objectId));
43
+ return wrapItemsResult(await deps.ai.memory.recall(query, { ...options, objectId: objectId.data }));
44
+ }).route("memories.list").auth().handle(async ({ input, context }) => {
45
+ const objectId = await resolveObjectId("memoryList", context, input);
46
+ if (!objectId.success)
47
+ return objectId;
48
+ return deps.ai.memory.listPage({ ...input, objectId: objectId.data });
49
+ }).route("sessions.list").auth().handle(async ({ input, context }) => {
50
+ const objectId = await resolveObjectId("sessions", context, input);
51
+ if (!objectId.success)
52
+ return objectId;
53
+ return wrapItemsResult(await deps.ai.llm.listSessions(objectId.data));
20
54
  }).build();
21
55
  }
22
56
  function toChatCompletionRequest(input) {
23
57
  return {
24
- messages: input.messages.map(toChatMessage),
58
+ // Schema 已按角色校验完整消息,直接传递以保留 name、图片、tool_calls 和工具结果。
59
+ messages: input.messages,
25
60
  model: input.model,
26
61
  temperature: input.temperature,
27
62
  top_p: input.top_p,
28
- max_tokens: input.max_tokens
29
- };
30
- }
31
- function toChatMessage(message) {
32
- if (message.role === "system") {
33
- return { role: "system", content: toMessageText(message.content) };
34
- }
35
- if (message.role === "user") {
36
- return { role: "user", content: message.content ?? "" };
37
- }
38
- if (message.role === "tool") {
39
- return { role: "tool", content: toMessageText(message.content), tool_call_id: message.tool_call_id ?? "" };
40
- }
41
- return {
42
- role: "assistant",
43
- content: toAssistantContent(message.content),
44
- tool_calls: message.tool_calls
63
+ max_tokens: input.max_tokens,
64
+ tools: input.tools,
65
+ tool_choice: input.tool_choice
45
66
  };
46
67
  }
47
- function toMessageText(content) {
48
- if (typeof content === "string") {
49
- return content;
50
- }
51
- return content?.map((part) => part.type === "text" ? part.text : part.image_url.url).join("\n") ?? "";
52
- }
53
- function toAssistantContent(content) {
54
- if (typeof content === "string") {
55
- return content;
56
- }
57
- return content ? toMessageText(content) : null;
58
- }
59
68
  function mapChatCompletionResult(result) {
60
69
  if (!result.success) {
61
70
  return result;
@@ -124,13 +133,14 @@ function toApiChatMessage(message) {
124
133
  return {
125
134
  role: "assistant",
126
135
  content: toContentText(message.content),
136
+ name: message.name,
127
137
  tool_calls: mapFunctionToolCalls(message.tool_calls)
128
138
  };
129
139
  }
130
140
  if (message.role === "user") {
131
- return { role: "user", content: toContentText(message.content) };
141
+ return { role: "user", content: toContentText(message.content), name: message.name };
132
142
  }
133
- return { role: "system", content: toContentText(message.content) };
143
+ return { role: "system", content: toContentText(message.content), name: message.role === "system" || message.role === "developer" ? message.name : void 0 };
134
144
  }
135
145
  function toContentText(content) {
136
146
  if (typeof content === "string") {
@@ -169,11 +179,12 @@ function mapFunctionToolCalls(toolCalls) {
169
179
  }];
170
180
  });
171
181
  }
172
- async function sendMessage(deps, input) {
182
+ async function sendMessage(deps, input, scope) {
173
183
  const result = await deps.ai.llm.ask(input.message, {
174
184
  systemPrompt: input.systemPrompt,
175
185
  model: input.model,
176
- temperature: input.temperature
186
+ temperature: input.temperature,
187
+ ...scope
177
188
  });
178
189
  if (!result.success) {
179
190
  return result;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/features/serv-feature-ai.ts"],"names":[],"mappings":"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","file":"ai.js","sourcesContent":["/**\n * @h-ai/serv — AI 默认 procedures\n *\n * 基于 `@h-ai/ai` 提供开箱即用的 AI procedures 实现:对话补全、消息发送、聊天历史、记忆管理、会话列表。\n * @module features/serv-feature-ai\n */\n\nimport type {\n AIFunctions,\n ChatCompletionRequest,\n ChatCompletionResponse,\n ChatMessage,\n ToolCall,\n} from '@h-ai/ai'\nimport type {\n AiChatCompletionData,\n AiChatCompletionInput,\n AiChatRecord,\n AiSendMessageInput,\n} from '@h-ai/api-contract'\nimport type { HaiResult } from '@h-ai/core'\nimport type { ServContext } from '../serv-context.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { ok } from '@h-ai/core'\nimport { implement } from '../serv-router.js'\nimport { wrapItemsResult } from './serv-feature-helpers.js'\n\nconst aiContract = apiContract.ai\n\n/** AI 默认 procedures 依赖。 */\nexport interface AiProcedureDeps {\n readonly ai: AIFunctions\n}\n\n/** 创建 AI 默认 procedures。 */\nexport function createAiProcedures(deps: AiProcedureDeps) {\n return implement(aiContract)\n .context<ServContext>()\n\n .route('chats.createCompletion')\n .auth()\n .handle(async ({ input }) => {\n return mapChatCompletionResult(await deps.ai.llm.chat(toChatCompletionRequest(input)))\n })\n\n .route('chats.sendMessage')\n .auth()\n .handle(({ input }) => sendMessage(deps, input))\n\n .route('chats.listHistory')\n .auth()\n .handle(async ({ input }) => {\n const { objectId, sessionId, limit, order } = input\n return mapChatRecordsResult(\n await deps.ai.llm.getHistory({ objectId, sessionId }, { limit, order }),\n )\n })\n\n .route('memories.recall')\n .auth()\n .handle(async ({ input }) => {\n const { query, ...options } = input\n return wrapItemsResult(await deps.ai.memory.recall(query, options))\n })\n\n .route('memories.list')\n .auth()\n .handle(({ input }) => deps.ai.memory.listPage(input))\n\n .route('sessions.list')\n .auth()\n .handle(async ({ input }) => {\n return wrapItemsResult(await deps.ai.llm.listSessions(input.objectId))\n })\n\n .build()\n}\n\nfunction toChatCompletionRequest(input: AiChatCompletionInput): ChatCompletionRequest {\n return {\n messages: input.messages.map(toChatMessage),\n model: input.model,\n temperature: input.temperature,\n top_p: input.top_p,\n max_tokens: input.max_tokens,\n }\n}\n\nfunction toChatMessage(message: AiChatCompletionInput['messages'][number]): ChatMessage {\n if (message.role === 'system') {\n return { role: 'system', content: toMessageText(message.content) }\n }\n\n if (message.role === 'user') {\n return { role: 'user', content: message.content ?? '' }\n }\n\n if (message.role === 'tool') {\n return { role: 'tool', content: toMessageText(message.content), tool_call_id: message.tool_call_id ?? '' }\n }\n\n return {\n role: 'assistant',\n content: toAssistantContent(message.content),\n tool_calls: message.tool_calls,\n }\n}\n\nfunction toMessageText(content: AiChatCompletionInput['messages'][number]['content']): string {\n if (typeof content === 'string') {\n return content\n }\n\n return content?.map(part => part.type === 'text' ? part.text : part.image_url.url).join('\\n') ?? ''\n}\n\nfunction toAssistantContent(content: AiChatCompletionInput['messages'][number]['content']): string | null {\n if (typeof content === 'string') {\n return content\n }\n\n return content ? toMessageText(content) : null\n}\n\nfunction mapChatCompletionResult(result: HaiResult<ChatCompletionResponse>): HaiResult<AiChatCompletionData> {\n if (!result.success) {\n return result\n }\n\n return ok({\n id: result.data.id,\n object: 'chat.completion',\n created: result.data.created,\n model: result.data.model,\n choices: result.data.choices.map(choice => ({\n index: choice.index,\n message: {\n role: 'assistant',\n content: choice.message.content ?? null,\n tool_calls: mapFunctionToolCalls(choice.message.tool_calls),\n },\n finish_reason: toApiFinishReason(choice.finish_reason),\n })),\n usage: {\n prompt_tokens: result.data.usage?.prompt_tokens ?? 0,\n completion_tokens: result.data.usage?.completion_tokens ?? 0,\n total_tokens: result.data.usage?.total_tokens ?? 0,\n },\n })\n}\n\nfunction toApiFinishReason(reason: string | null): AiChatCompletionData['choices'][number]['finish_reason'] {\n if (reason === 'length' || reason === 'tool_calls' || reason === 'content_filter') {\n return reason\n }\n\n return 'stop'\n}\n\nfunction mapChatRecordsResult(result: HaiResult<Array<import('@h-ai/ai').ChatRecord>>): HaiResult<{ items: AiChatRecord[] }> {\n if (!result.success) {\n return result\n }\n\n return ok({ items: result.data.map(toApiChatRecord) })\n}\n\nfunction toApiChatRecord(record: import('@h-ai/ai').ChatRecord): AiChatRecord {\n return {\n id: record.id,\n objectId: record.objectId,\n sessionId: record.sessionId,\n request: {\n model: record.request.model,\n messages: record.request.messages.map(toApiChatMessage),\n },\n response: {\n content: record.response.content,\n toolCalls: mapFunctionToolCalls(record.response.toolCalls),\n finishReason: record.response.finishReason,\n usage: {\n prompt_tokens: record.response.usage.prompt_tokens,\n completion_tokens: record.response.usage.completion_tokens,\n total_tokens: record.response.usage.total_tokens,\n },\n },\n createdAt: record.createdAt,\n duration: record.duration,\n }\n}\n\nfunction toApiChatMessage(message: ChatMessage): AiChatCompletionInput['messages'][number] {\n if (message.role === 'tool') {\n return { role: 'tool', content: toContentText(message.content), tool_call_id: message.tool_call_id }\n }\n\n if (message.role === 'assistant') {\n return {\n role: 'assistant',\n content: toContentText(message.content),\n tool_calls: mapFunctionToolCalls(message.tool_calls),\n }\n }\n\n if (message.role === 'user') {\n return { role: 'user', content: toContentText(message.content) }\n }\n\n return { role: 'system', content: toContentText(message.content) }\n}\n\nfunction toContentText(content: unknown): string {\n if (typeof content === 'string') {\n return content\n }\n\n if (!Array.isArray(content)) {\n return ''\n }\n\n return content.map((part) => {\n if (typeof part !== 'object' || part === null) {\n return ''\n }\n\n const record = part as Record<string, unknown>\n if (typeof record.text === 'string') {\n return record.text\n }\n\n if (typeof record.image_url === 'object' && record.image_url !== null) {\n const image = record.image_url as Record<string, unknown>\n return typeof image.url === 'string' ? image.url : ''\n }\n\n return ''\n }).filter(Boolean).join('\\n')\n}\n\nfunction mapFunctionToolCalls(toolCalls: ToolCall[] | undefined) {\n return toolCalls?.flatMap((toolCall) => {\n if (toolCall.type !== 'function' || !('function' in toolCall)) {\n return []\n }\n\n return [{\n id: toolCall.id,\n type: 'function' as const,\n function: {\n name: toolCall.function.name,\n arguments: toolCall.function.arguments,\n },\n }]\n })\n}\n\nasync function sendMessage(\n deps: AiProcedureDeps,\n input: AiSendMessageInput,\n): Promise<HaiResult<{ content: string, model: string }>> {\n const result = await deps.ai.llm.ask(input.message, {\n systemPrompt: input.systemPrompt,\n model: input.model,\n temperature: input.temperature,\n })\n\n if (!result.success) {\n return result\n }\n\n return ok({\n content: result.data,\n model: input.model ?? deps.ai.config?.llm.model ?? 'default',\n })\n}\n"]}
1
+ {"version":3,"sources":["../../src/features/serv-feature-ai.ts"],"names":[],"mappings":"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","file":"ai.js","sourcesContent":["/**\n * @h-ai/serv — AI 默认 procedures\n *\n * 基于 `@h-ai/ai` 提供开箱即用的 AI procedures 实现:对话补全、消息发送、聊天历史、记忆管理、会话列表。\n * 默认将所有读取和写入绑定到认证 session.userId;跨主体访问必须通过显式授权。\n * @module features/serv-feature-ai\n */\n\nimport type {\n AIFunctions,\n ChatCompletionRequest,\n ChatCompletionResponse,\n ChatMessage,\n InteractionScope,\n ToolCall,\n} from '@h-ai/ai'\nimport type {\n AiChatCompletionData,\n AiChatCompletionInput,\n AiChatRecord,\n AiSendMessageInput,\n} from '@h-ai/api-contract'\nimport type { HaiResult } from '@h-ai/core'\nimport type { ServContext } from '../serv-context.js'\nimport type { AuthenticatedServContext } from '../serv-router.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { err, HaiCommonError, ok } from '@h-ai/core'\nimport { servM } from '../serv-i18n.js'\nimport { implement } from '../serv-router.js'\nimport { wrapItemsResult } from './serv-feature-helpers.js'\n\nconst aiContract = apiContract.ai\n\n/**\n * 对不同于当前认证用户的 AI 主体进行授权的请求。\n *\n * @example\n * ```ts\n * import type { AiObjectAuthorizationRequest } from '@h-ai/serv/features/ai'\n *\n * const authorize = ({ context, operation }: AiObjectAuthorizationRequest) =>\n * operation === 'history' && context.session.permissions.includes('ai.history.manage')\n * // 租户型应用还应在此核验目标 objectId 与当前租户的真实关系。\n * ```\n */\nexport interface AiObjectAuthorizationRequest {\n /** 本次操作;授权可按读写能力分别限制。 */\n readonly operation: 'completion' | 'message' | 'history' | 'memoryRecall' | 'memoryList' | 'sessions'\n /** 可信认证上下文;不从客户端输入解析身份。 */\n readonly context: AuthenticatedServContext\n /** 请求访问的主体 ID;必须由策略核验所有者/租户关系。 */\n readonly objectId: string\n /** 请求指定的会话 ID;始终与 objectId 一起构成作用域。 */\n readonly sessionId?: string\n}\n\n/**\n * AI 默认 procedures 依赖;只要求实际使用的 AI 能力。\n *\n * @example\n * ```ts\n * import { ai } from '@h-ai/ai'\n * import { createAiProcedures } from '@h-ai/serv/features/ai'\n *\n * // 宿主已初始化 AI 及其共享存储;省略策略时只允许当前 session.userId。\n * const procedures = createAiProcedures({ ai })\n * // 默认 completion 为非流式 JSON;object/session 作用域来自可信认证上下文。\n * ```\n */\nexport interface AiProcedureDeps {\n readonly ai: Pick<AIFunctions, 'config'> & {\n readonly llm: Pick<AIFunctions['llm'], 'chat' | 'ask' | 'getHistory' | 'listSessions'>\n readonly memory: Pick<AIFunctions['memory'], 'recall' | 'listPage'>\n }\n /** 跨主体资源授权;省略时仅允许当前 session.userId,必须严格返回 true 才放行。 */\n readonly authorizeObject?: (request: AiObjectAuthorizationRequest) => boolean | Promise<boolean>\n}\n\n/**\n * 创建以认证用户为默认主体的 AI procedures。\n * 补全仅返回非流式 JSON,完整转发 function 工具和角色消息;工具执行由业务方授权处理。\n *\n * @param deps - 已初始化 AI 能力及可选的跨主体授权策略。\n * @returns AI router;跨主体访问未获授权时返回 FORBIDDEN。\n *\n * @example\n * ```ts\n * import { ai } from '@h-ai/ai'\n * import { createAiProcedures } from '@h-ai/serv/features/ai'\n *\n * // 宿主已初始化 AI 及其共享存储;省略策略时只允许当前 session.userId。\n * const procedures = createAiProcedures({ ai })\n * // 默认 completion 为非流式 JSON;object/session 作用域来自可信认证上下文。\n * ```\n */\nexport function createAiProcedures(deps: AiProcedureDeps) {\n async function resolveObjectId(\n operation: AiObjectAuthorizationRequest['operation'],\n context: AuthenticatedServContext,\n input: { objectId?: string, sessionId?: string },\n ): Promise<HaiResult<string>> {\n const objectId = input.objectId ?? context.session.userId\n if (objectId !== context.session.userId\n && await deps.authorizeObject?.({ context, objectId, sessionId: input.sessionId, operation }) !== true) {\n return err(HaiCommonError.FORBIDDEN, servM('serv_errorForbidden', { locale: context.locale }))\n }\n return ok(objectId)\n }\n\n return implement(aiContract)\n .context<ServContext>()\n\n .route('chats.createCompletion')\n .auth()\n .handle(async ({ input, context }) => {\n const objectId = await resolveObjectId('completion', context, input)\n if (!objectId.success)\n return objectId\n return mapChatCompletionResult(await deps.ai.llm.chat({\n ...toChatCompletionRequest(input),\n objectId: objectId.data,\n sessionId: input.sessionId ?? 'default',\n signal: context.request.signal,\n }))\n })\n\n .route('chats.sendMessage')\n .auth()\n .handle(async ({ input, context }) => {\n const objectId = await resolveObjectId('message', context, input)\n if (!objectId.success)\n return objectId\n return sendMessage(deps, input, { objectId: objectId.data, sessionId: input.sessionId ?? 'default', signal: context.request.signal })\n })\n\n .route('chats.listHistory')\n .auth()\n .handle(async ({ input, context }) => {\n const objectId = await resolveObjectId('history', context, input)\n if (!objectId.success)\n return objectId\n const { sessionId, limit, order } = input\n return mapChatRecordsResult(\n await deps.ai.llm.getHistory({ objectId: objectId.data, sessionId }, { limit, order }),\n )\n })\n\n .route('memories.recall')\n .auth()\n .handle(async ({ input, context }) => {\n const objectId = await resolveObjectId('memoryRecall', context, input)\n if (!objectId.success)\n return objectId\n const { query, ...options } = input\n return wrapItemsResult(await deps.ai.memory.recall(query, { ...options, objectId: objectId.data }))\n })\n\n .route('memories.list')\n .auth()\n .handle(async ({ input, context }) => {\n const objectId = await resolveObjectId('memoryList', context, input)\n if (!objectId.success)\n return objectId\n return deps.ai.memory.listPage({ ...input, objectId: objectId.data })\n })\n\n .route('sessions.list')\n .auth()\n .handle(async ({ input, context }) => {\n const objectId = await resolveObjectId('sessions', context, input)\n if (!objectId.success)\n return objectId\n return wrapItemsResult(await deps.ai.llm.listSessions(objectId.data))\n })\n\n .build()\n}\n\nfunction toChatCompletionRequest(input: AiChatCompletionInput): ChatCompletionRequest {\n return {\n // Schema 已按角色校验完整消息,直接传递以保留 name、图片、tool_calls 和工具结果。\n messages: input.messages,\n model: input.model,\n temperature: input.temperature,\n top_p: input.top_p,\n max_tokens: input.max_tokens,\n tools: input.tools,\n tool_choice: input.tool_choice,\n }\n}\n\nfunction mapChatCompletionResult(result: HaiResult<ChatCompletionResponse>): HaiResult<AiChatCompletionData> {\n if (!result.success) {\n return result\n }\n\n return ok({\n id: result.data.id,\n object: 'chat.completion',\n created: result.data.created,\n model: result.data.model,\n choices: result.data.choices.map(choice => ({\n index: choice.index,\n message: {\n role: 'assistant',\n content: choice.message.content ?? null,\n tool_calls: mapFunctionToolCalls(choice.message.tool_calls),\n },\n finish_reason: toApiFinishReason(choice.finish_reason),\n })),\n usage: {\n prompt_tokens: result.data.usage?.prompt_tokens ?? 0,\n completion_tokens: result.data.usage?.completion_tokens ?? 0,\n total_tokens: result.data.usage?.total_tokens ?? 0,\n },\n })\n}\n\nfunction toApiFinishReason(reason: string | null): AiChatCompletionData['choices'][number]['finish_reason'] {\n if (reason === 'length' || reason === 'tool_calls' || reason === 'content_filter') {\n return reason\n }\n\n return 'stop'\n}\n\nfunction mapChatRecordsResult(result: HaiResult<Array<import('@h-ai/ai').ChatRecord>>): HaiResult<{ items: AiChatRecord[] }> {\n if (!result.success) {\n return result\n }\n\n return ok({ items: result.data.map(toApiChatRecord) })\n}\n\nfunction toApiChatRecord(record: import('@h-ai/ai').ChatRecord): AiChatRecord {\n return {\n id: record.id,\n objectId: record.objectId,\n sessionId: record.sessionId,\n request: {\n model: record.request.model,\n messages: record.request.messages.map(toApiChatMessage),\n },\n response: {\n content: record.response.content,\n toolCalls: mapFunctionToolCalls(record.response.toolCalls),\n finishReason: record.response.finishReason,\n usage: {\n prompt_tokens: record.response.usage.prompt_tokens,\n completion_tokens: record.response.usage.completion_tokens,\n total_tokens: record.response.usage.total_tokens,\n },\n },\n createdAt: record.createdAt,\n duration: record.duration,\n }\n}\n\nfunction toApiChatMessage(message: ChatMessage): AiChatCompletionInput['messages'][number] {\n if (message.role === 'tool') {\n return { role: 'tool', content: toContentText(message.content), tool_call_id: message.tool_call_id }\n }\n\n if (message.role === 'assistant') {\n return {\n role: 'assistant',\n content: toContentText(message.content),\n name: message.name,\n tool_calls: mapFunctionToolCalls(message.tool_calls),\n }\n }\n\n if (message.role === 'user') {\n return { role: 'user', content: toContentText(message.content), name: message.name }\n }\n\n return { role: 'system', content: toContentText(message.content), name: message.role === 'system' || message.role === 'developer' ? message.name : undefined }\n}\n\nfunction toContentText(content: unknown): string {\n if (typeof content === 'string') {\n return content\n }\n\n if (!Array.isArray(content)) {\n return ''\n }\n\n return content.map((part) => {\n if (typeof part !== 'object' || part === null) {\n return ''\n }\n\n const record = part as Record<string, unknown>\n if (typeof record.text === 'string') {\n return record.text\n }\n\n if (typeof record.image_url === 'object' && record.image_url !== null) {\n const image = record.image_url as Record<string, unknown>\n return typeof image.url === 'string' ? image.url : ''\n }\n\n return ''\n }).filter(Boolean).join('\\n')\n}\n\nfunction mapFunctionToolCalls(toolCalls: ToolCall[] | undefined) {\n return toolCalls?.flatMap((toolCall) => {\n if (toolCall.type !== 'function' || !('function' in toolCall)) {\n return []\n }\n\n return [{\n id: toolCall.id,\n type: 'function' as const,\n function: {\n name: toolCall.function.name,\n arguments: toolCall.function.arguments,\n },\n }]\n })\n}\n\nasync function sendMessage(\n deps: AiProcedureDeps,\n input: AiSendMessageInput,\n scope: InteractionScope & { signal: AbortSignal },\n): Promise<HaiResult<{ content: string, model: string }>> {\n const result = await deps.ai.llm.ask(input.message, {\n systemPrompt: input.systemPrompt,\n model: input.model,\n temperature: input.temperature,\n ...scope,\n })\n\n if (!result.success) {\n return result\n }\n\n return ok({\n content: result.data,\n model: input.model ?? deps.ai.config?.llm.model ?? 'default',\n })\n}\n"]}
@@ -4,14 +4,40 @@ import * as zod from 'zod';
4
4
  import * as _orpc_contract from '@orpc/contract';
5
5
  import { IamFunctions } from '@h-ai/iam';
6
6
  export { TokenPair } from '@h-ai/iam';
7
- import { S as ServContext } from '../serv-context-CWzyrsdq.js';
7
+ import { S as ServContext } from '../serv-context-C48WdBcd.js';
8
8
  import '@h-ai/core';
9
9
 
10
- /** IAM 默认 procedures 依赖。 */
10
+ /**
11
+ * IAM 默认 procedures 依赖。
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { iam } from '@h-ai/iam'
16
+ * import { createIamProcedures } from '@h-ai/serv/features/iam'
17
+ *
18
+ * // 宿主已初始化 IAM 及共享依赖。
19
+ * const procedures = createIamProcedures({ iam })
20
+ * // logout 撤销请求持有的 opaque access token;过期 access 也可撤销 refresh,
21
+ * // 不接收 userId,不要求有效 access 的 auth guard,失败仍检查 HaiResult。
22
+ * ```
23
+ */
11
24
  interface IamProcedureDeps {
12
25
  readonly iam: IamFunctions;
13
26
  }
14
- /** 创建 IAM 默认 procedures。 */
27
+ /**
28
+ * 创建默认 IAM procedures;按操作声明认证/权限。logout 只撤销请求持有的完整 opaque token,不要求该 access 尚有效,也不能按 userId 撤销。
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { iam } from '@h-ai/iam'
33
+ * import { createIamProcedures } from '@h-ai/serv/features/iam'
34
+ *
35
+ * // 宿主已初始化 IAM 及共享依赖。
36
+ * const procedures = createIamProcedures({ iam })
37
+ * // logout 撤销请求持有的 opaque access token;过期 access 也可撤销 refresh,
38
+ * // 不接收 userId,不要求有效 access 的 auth guard,失败仍检查 HaiResult。
39
+ * ```
40
+ */
15
41
  declare function createIamProcedures(deps: IamProcedureDeps): {
16
42
  auth: {
17
43
  publicConfig: _orpc_server.Procedure<ServContext & {}, any, _orpc_contract.Schema<unknown, unknown>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
@@ -174,7 +200,6 @@ declare function createIamProcedures(deps: IamProcedureDeps): {
174
200
  login: _orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
175
201
  identifier: zod.ZodString;
176
202
  password: zod.ZodString;
177
- rememberMe: zod.ZodOptional<zod.ZodBoolean>;
178
203
  }, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
179
204
  success: zod.ZodLiteral<true>;
180
205
  data: zod.ZodObject<{
@@ -1,5 +1,5 @@
1
- import { mapHaiResult } from '../chunk-27PCPE37.js';
2
- import { implement, servM } from '../chunk-R354OMWL.js';
1
+ import { mapHaiResult } from '../chunk-XKCLD42I.js';
2
+ import { implement, servM } from '../chunk-TWHCU6HT.js';
3
3
  import { apiContract } from '@h-ai/api-contract';
4
4
  import { core, ok, err, HaiCommonError } from '@h-ai/core';
5
5
 
@@ -7,7 +7,7 @@ var iamContract = apiContract.iam;
7
7
  var iamFeatureLogger = core.logger.child({ module: "serv", scope: "feature-iam" });
8
8
  function createIamProcedures(deps) {
9
9
  const { iam } = deps;
10
- return implement(iamContract).context().route("auth.login", ({ input }) => iam.auth.login(input)).route("auth.loginWithOtp", ({ input }) => iam.auth.loginWithOtp(input)).route("auth.loginWithLdap", ({ input }) => iam.auth.loginWithLdap(input)).route("auth.publicConfig", () => ok(iam.publicConfig)).route("auth.requestPasswordReset", ({ input }) => iam.user.requestPasswordReset(input.identifier)).route("auth.confirmPasswordReset", ({ input }) => iam.user.confirmPasswordReset(input.token, input.newPassword)).route("auth.logout").auth().handle(({ input, context }) => {
10
+ return implement(iamContract).context().route("auth.login", ({ input }) => iam.auth.login(input)).route("auth.loginWithOtp", ({ input }) => iam.auth.loginWithOtp(input)).route("auth.loginWithLdap", ({ input }) => iam.auth.loginWithLdap(input)).route("auth.publicConfig", () => ok(iam.publicConfig)).route("auth.requestPasswordReset", ({ input }) => iam.user.requestPasswordReset(input.identifier)).route("auth.confirmPasswordReset", ({ input }) => iam.user.confirmPasswordReset(input.token, input.newPassword)).route("auth.logout", ({ input, context }) => {
11
11
  const token = input.accessToken ?? context.accessToken;
12
12
  if (!token)
13
13
  return err(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: context.locale }));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/features/serv-feature-iam.ts"],"names":[],"mappings":"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","file":"iam.js","sourcesContent":["/**\n * @h-ai/serv — IAM 默认 procedures\n *\n * 基于 `@h-ai/iam` 提供开箱即用的 IAM procedures 实现:认证、用户管理、角色权限 CRUD。\n * @module features/serv-feature-iam\n */\n\nimport type {\n IamAdminCreateUserInput,\n IamAdminUpdateUserInput,\n IamCurrentUser,\n} from '@h-ai/api-contract'\nimport type { HaiResult } from '@h-ai/core'\nimport type { IamFunctions, TokenPair, User } from '@h-ai/iam'\nimport type { ServContext } from '../serv-context.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { core, err, HaiCommonError, ok } from '@h-ai/core'\nimport { servM } from '../serv-i18n.js'\nimport { implement } from '../serv-router.js'\nimport { mapHaiResult } from './serv-feature-helpers.js'\n\nconst iamContract = apiContract.iam\n\nconst iamFeatureLogger = core.logger.child({ module: 'serv', scope: 'feature-iam' })\n\n/** IAM 默认 procedures 依赖。 */\nexport interface IamProcedureDeps {\n readonly iam: IamFunctions\n}\n\n/** 创建 IAM 默认 procedures。 */\nexport function createIamProcedures(deps: IamProcedureDeps) {\n const { iam } = deps\n\n return implement(iamContract)\n .context<ServContext>()\n\n .route('auth.login', ({ input }) => iam.auth.login(input))\n .route('auth.loginWithOtp', ({ input }) => iam.auth.loginWithOtp(input))\n .route('auth.loginWithLdap', ({ input }) => iam.auth.loginWithLdap(input))\n .route('auth.publicConfig', () => ok(iam.publicConfig))\n .route('auth.requestPasswordReset', ({ input }) => iam.user.requestPasswordReset(input.identifier))\n .route('auth.confirmPasswordReset', ({ input }) => iam.user.confirmPasswordReset(input.token, input.newPassword))\n\n .route('auth.logout')\n .auth()\n .handle(({ input, context }) => {\n const token = input.accessToken ?? context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n return iam.auth.logout(token)\n })\n\n .route('auth.currentUser')\n .auth()\n .handle(async ({ context }) => {\n const token = context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n\n return mapHaiResult<User, IamCurrentUser>(\n await iam.user.getCurrentUser(token),\n user => ({\n ...user,\n roles: context.session.roles,\n permissions: context.session.permissions,\n }),\n )\n })\n\n .route('auth.refresh', async ({ input }) => {\n return mapHaiResult(await iam.session.refresh(input.refreshToken), tokens => ({ tokens }))\n })\n .route('auth.sendOtp', ({ input }) => iam.auth.sendOtp(input.identifier))\n .route('auth.register', ({ input }) => iam.auth.registerAndLogin(input))\n\n .route('auth.changePassword')\n .auth()\n .handle(({ input, context }) => {\n const token = context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n return iam.user.changeCurrentUserPassword(token, input.oldPassword, input.newPassword)\n })\n\n .route('auth.updateCurrentUser')\n .auth()\n .handle(({ input, context }) => {\n const token = context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n return iam.user.updateCurrentUser(token, input)\n })\n\n .route('users.list')\n .permission('iam.users.read')\n .handle(({ input }) => iam.user.listUsers(input))\n\n .route('users.get')\n .permission('iam.users.read')\n .handle(({ input }) => iam.user.getUser(input.id, { include: ['roles'] }))\n\n .route('users.create')\n .permission('iam.users.write')\n .handle(({ input }) => createUser(iam, input))\n\n .route('users.update')\n .permission('iam.users.write')\n .handle(({ input }) => updateUser(iam, input))\n\n .route('users.delete')\n .permission('iam.users.write')\n .handle(({ input, context }) => {\n // 防止自删:避免管理员误删自己的账号导致授权锁死。\n if (context.session.userId === input.id)\n return err(HaiCommonError.FORBIDDEN, servM('serv_iamCannotDeleteCurrentUser', { locale: context.locale }))\n return iam.user.deleteUser(input.id)\n })\n\n .route('users.resetPassword')\n .permission('iam.users.write')\n .handle(({ input }) => iam.user.adminResetPassword(input.id, input.newPassword))\n\n .route('roles.list')\n .permission('iam.roles.read')\n .handle(() => iam.authz.getAllRoles())\n\n .route('roles.get')\n .permission('iam.roles.read')\n .handle(({ input }) => iam.authz.getRole(input.id))\n\n .route('roles.create')\n .permission('iam.roles.write')\n .handle(({ input }) => iam.authz.createRole(input))\n\n .route('roles.update')\n .permission('iam.roles.write')\n .handle(({ input }) => {\n const { id, ...data } = input\n return iam.authz.updateRole(id, data)\n })\n\n .route('roles.delete')\n .permission('iam.roles.write')\n .handle(({ input }) => iam.authz.deleteRole(input.id))\n\n .route('permissions.list')\n .permission('iam.permissions.read')\n .handle(({ input }) => iam.authz.getAllPermissions(input))\n\n .route('permissions.get')\n .permission('iam.permissions.read')\n .handle(({ input }) => iam.authz.getPermission(input.id))\n\n .route('permissions.create')\n .permission('iam.permissions.write')\n .handle(({ input }) => iam.authz.createPermission(input))\n\n .route('permissions.delete')\n .permission('iam.permissions.write')\n .handle(({ input }) => iam.authz.deletePermission(input.id))\n\n .build()\n}\n\nasync function createUser(iam: IamFunctions, input: IamAdminCreateUserInput): Promise<HaiResult<User>> {\n const registerResult = await iam.user.createUser(input)\n if (!registerResult.success)\n return registerResult\n\n const userId = registerResult.data.user.id\n\n // 任何后续步骤失败都需要回滚刚创建的用户,以避免重复用户名/邮箱错误阅存。\n async function rollback(reason: string): Promise<void> {\n const deleteResult = await iam.user.deleteUser(userId)\n if (!deleteResult.success)\n iamFeatureLogger.error('createUser rollback failed, orphan user may exist', { userId, reason, error: deleteResult.error })\n }\n\n if (input.roleIds !== undefined) {\n const rolesResult = await iam.authz.syncRoles(userId, input.roleIds)\n if (!rolesResult.success) {\n await rollback('syncRoles failed')\n return rolesResult\n }\n }\n\n if (input.enabled === false) {\n const updateResult = await iam.user.updateUser(userId, { enabled: false })\n if (!updateResult.success) {\n await rollback('initial disable failed')\n return updateResult\n }\n return updateResult\n }\n\n return ok(registerResult.data.user)\n}\n\nasync function updateUser(iam: IamFunctions, input: IamAdminUpdateUserInput): Promise<HaiResult<User>> {\n const { id, roleIds, ...data } = input\n const updateResult = await iam.user.updateUser(id, data)\n if (!updateResult.success)\n return updateResult\n\n if (roleIds) {\n const rolesResult = await iam.authz.syncRoles(id, roleIds)\n if (!rolesResult.success)\n return rolesResult\n }\n\n return updateResult\n}\n\n// re-export 类型以便 IDE 跳转\nexport type { TokenPair }\n"]}
1
+ {"version":3,"sources":["../../src/features/serv-feature-iam.ts"],"names":[],"mappings":"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","file":"iam.js","sourcesContent":["/**\n * @h-ai/serv — IAM 默认 procedures\n *\n * 基于 `@h-ai/iam` 提供开箱即用的 IAM procedures 实现:认证、用户管理、角色权限 CRUD。\n * @module features/serv-feature-iam\n */\n\nimport type {\n IamAdminCreateUserInput,\n IamAdminUpdateUserInput,\n IamCurrentUser,\n} from '@h-ai/api-contract'\nimport type { HaiResult } from '@h-ai/core'\nimport type { IamFunctions, TokenPair, User } from '@h-ai/iam'\nimport type { ServContext } from '../serv-context.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { core, err, HaiCommonError, ok } from '@h-ai/core'\nimport { servM } from '../serv-i18n.js'\nimport { implement } from '../serv-router.js'\nimport { mapHaiResult } from './serv-feature-helpers.js'\n\nconst iamContract = apiContract.iam\n\nconst iamFeatureLogger = core.logger.child({ module: 'serv', scope: 'feature-iam' })\n\n/**\n * IAM 默认 procedures 依赖。\n *\n * @example\n * ```ts\n * import { iam } from '@h-ai/iam'\n * import { createIamProcedures } from '@h-ai/serv/features/iam'\n *\n * // 宿主已初始化 IAM 及共享依赖。\n * const procedures = createIamProcedures({ iam })\n * // logout 撤销请求持有的 opaque access token;过期 access 也可撤销 refresh,\n * // 不接收 userId,不要求有效 access 的 auth guard,失败仍检查 HaiResult。\n * ```\n */\nexport interface IamProcedureDeps {\n readonly iam: IamFunctions\n}\n\n/**\n * 创建默认 IAM procedures;按操作声明认证/权限。logout 只撤销请求持有的完整 opaque token,不要求该 access 尚有效,也不能按 userId 撤销。\n *\n * @example\n * ```ts\n * import { iam } from '@h-ai/iam'\n * import { createIamProcedures } from '@h-ai/serv/features/iam'\n *\n * // 宿主已初始化 IAM 及共享依赖。\n * const procedures = createIamProcedures({ iam })\n * // logout 撤销请求持有的 opaque access token;过期 access 也可撤销 refresh,\n * // 不接收 userId,不要求有效 access 的 auth guard,失败仍检查 HaiResult。\n * ```\n */\nexport function createIamProcedures(deps: IamProcedureDeps) {\n const { iam } = deps\n\n return implement(iamContract)\n .context<ServContext>()\n\n .route('auth.login', ({ input }) => iam.auth.login(input))\n .route('auth.loginWithOtp', ({ input }) => iam.auth.loginWithOtp(input))\n .route('auth.loginWithLdap', ({ input }) => iam.auth.loginWithLdap(input))\n .route('auth.publicConfig', () => ok(iam.publicConfig))\n .route('auth.requestPasswordReset', ({ input }) => iam.user.requestPasswordReset(input.identifier))\n .route('auth.confirmPasswordReset', ({ input }) => iam.user.confirmPasswordReset(input.token, input.newPassword))\n\n .route('auth.logout', ({ input, context }) => {\n // 仅撤销调用方持有的完整随机令牌;不要求 access 仍有效,也不接受 userId 撤销。\n const token = input.accessToken ?? context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n return iam.auth.logout(token)\n })\n\n .route('auth.currentUser')\n .auth()\n .handle(async ({ context }) => {\n const token = context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n\n return mapHaiResult<User, IamCurrentUser>(\n await iam.user.getCurrentUser(token),\n user => ({\n ...user,\n roles: context.session.roles,\n permissions: context.session.permissions,\n }),\n )\n })\n\n .route('auth.refresh', async ({ input }) => {\n return mapHaiResult(await iam.session.refresh(input.refreshToken), tokens => ({ tokens }))\n })\n .route('auth.sendOtp', ({ input }) => iam.auth.sendOtp(input.identifier))\n .route('auth.register', ({ input }) => iam.auth.registerAndLogin(input))\n\n .route('auth.changePassword')\n .auth()\n .handle(({ input, context }) => {\n const token = context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n return iam.user.changeCurrentUserPassword(token, input.oldPassword, input.newPassword)\n })\n\n .route('auth.updateCurrentUser')\n .auth()\n .handle(({ input, context }) => {\n const token = context.accessToken\n if (!token)\n return err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale }))\n return iam.user.updateCurrentUser(token, input)\n })\n\n .route('users.list')\n .permission('iam.users.read')\n .handle(({ input }) => iam.user.listUsers(input))\n\n .route('users.get')\n .permission('iam.users.read')\n .handle(({ input }) => iam.user.getUser(input.id, { include: ['roles'] }))\n\n .route('users.create')\n .permission('iam.users.write')\n .handle(({ input }) => createUser(iam, input))\n\n .route('users.update')\n .permission('iam.users.write')\n .handle(({ input }) => updateUser(iam, input))\n\n .route('users.delete')\n .permission('iam.users.write')\n .handle(({ input, context }) => {\n // 防止自删:避免管理员误删自己的账号导致授权锁死。\n if (context.session.userId === input.id)\n return err(HaiCommonError.FORBIDDEN, servM('serv_iamCannotDeleteCurrentUser', { locale: context.locale }))\n return iam.user.deleteUser(input.id)\n })\n\n .route('users.resetPassword')\n .permission('iam.users.write')\n .handle(({ input }) => iam.user.adminResetPassword(input.id, input.newPassword))\n\n .route('roles.list')\n .permission('iam.roles.read')\n .handle(() => iam.authz.getAllRoles())\n\n .route('roles.get')\n .permission('iam.roles.read')\n .handle(({ input }) => iam.authz.getRole(input.id))\n\n .route('roles.create')\n .permission('iam.roles.write')\n .handle(({ input }) => iam.authz.createRole(input))\n\n .route('roles.update')\n .permission('iam.roles.write')\n .handle(({ input }) => {\n const { id, ...data } = input\n return iam.authz.updateRole(id, data)\n })\n\n .route('roles.delete')\n .permission('iam.roles.write')\n .handle(({ input }) => iam.authz.deleteRole(input.id))\n\n .route('permissions.list')\n .permission('iam.permissions.read')\n .handle(({ input }) => iam.authz.getAllPermissions(input))\n\n .route('permissions.get')\n .permission('iam.permissions.read')\n .handle(({ input }) => iam.authz.getPermission(input.id))\n\n .route('permissions.create')\n .permission('iam.permissions.write')\n .handle(({ input }) => iam.authz.createPermission(input))\n\n .route('permissions.delete')\n .permission('iam.permissions.write')\n .handle(({ input }) => iam.authz.deletePermission(input.id))\n\n .build()\n}\n\nasync function createUser(iam: IamFunctions, input: IamAdminCreateUserInput): Promise<HaiResult<User>> {\n const registerResult = await iam.user.createUser(input)\n if (!registerResult.success)\n return registerResult\n\n const userId = registerResult.data.user.id\n\n // 任何后续步骤失败都需要回滚刚创建的用户,以避免重复用户名/邮箱错误阅存。\n async function rollback(reason: string): Promise<void> {\n const deleteResult = await iam.user.deleteUser(userId)\n if (!deleteResult.success)\n iamFeatureLogger.error('createUser rollback failed, orphan user may exist', { userId, reason, error: deleteResult.error })\n }\n\n if (input.roleIds !== undefined) {\n const rolesResult = await iam.authz.syncRoles(userId, input.roleIds)\n if (!rolesResult.success) {\n await rollback('syncRoles failed')\n return rolesResult\n }\n }\n\n if (input.enabled === false) {\n const updateResult = await iam.user.updateUser(userId, { enabled: false })\n if (!updateResult.success) {\n await rollback('initial disable failed')\n return updateResult\n }\n return updateResult\n }\n\n return ok(registerResult.data.user)\n}\n\nasync function updateUser(iam: IamFunctions, input: IamAdminUpdateUserInput): Promise<HaiResult<User>> {\n const { id, roleIds, ...data } = input\n const updateResult = await iam.user.updateUser(id, data)\n if (!updateResult.success)\n return updateResult\n\n if (roleIds) {\n const rolesResult = await iam.authz.syncRoles(id, roleIds)\n if (!rolesResult.success)\n return rolesResult\n }\n\n return updateResult\n}\n\n// re-export 类型以便 IDE 跳转\nexport type { TokenPair }\n"]}
@@ -2,20 +2,80 @@ import * as _orpc_server from '@orpc/server';
2
2
  import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import { StorageFunctions } from '@h-ai/storage';
5
- import { S as ServContext } from '../serv-context-CWzyrsdq.js';
5
+ import { S as ServContext } from '../serv-context-C48WdBcd.js';
6
+ import { A as AuthenticatedServContext } from '../serv-router-BX8hgYxo.js';
6
7
  import '@h-ai/core';
7
8
 
8
- /** Storage 默认 procedures 依赖。 */
9
+ /**
10
+ * 存储资源授权请求;批量操作只有全部资源获准后才会调用存储。
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { storage } from '@h-ai/storage'
15
+ * import { createStorageProcedures } from '@h-ai/serv/features/storage'
16
+ *
17
+ * // 宿主已初始化 Storage;用户根目录来自可信 session,含尾斜杠形成边界。
18
+ * const procedures = createStorageProcedures({ storage, authorize: ({ context, keys }) => {
19
+ * const root = `users/${encodeURIComponent(context.session.userId)}/`
20
+ * return keys.length > 0 && keys.every(key => key.startsWith(root))
21
+ * } })
22
+ * // 空列表前缀与相邻用户名前缀均不能匹配此根目录。
23
+ * ```
24
+ */
25
+ interface StorageAccessRequest {
26
+ /** 操作类别;策略可分别授予读取、写入或删除权限。 */
27
+ readonly operation: 'download' | 'upload' | 'list' | 'metadata' | 'delete' | 'deleteMany';
28
+ /** 已通过认证的可信请求上下文。 */
29
+ readonly context: AuthenticatedServContext;
30
+ /** 单项/批量 key;list 操作仅包含请求的 prefix,未提供 prefix 时为空字符串。 */
31
+ readonly keys: readonly string[];
32
+ }
33
+ /**
34
+ * Storage 默认 procedures 依赖。
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { storage } from '@h-ai/storage'
39
+ * import { createStorageProcedures } from '@h-ai/serv/features/storage'
40
+ *
41
+ * // 宿主已初始化 Storage;用户根目录来自可信 session,含尾斜杠形成边界。
42
+ * const procedures = createStorageProcedures({ storage, authorize: ({ context, keys }) => {
43
+ * const root = `users/${encodeURIComponent(context.session.userId)}/`
44
+ * return keys.length > 0 && keys.every(key => key.startsWith(root))
45
+ * } })
46
+ * // 空列表前缀与相邻用户名前缀均不能匹配此根目录。
47
+ * ```
48
+ */
9
49
  interface StorageProcedureDeps {
10
50
  readonly storage: StorageFunctions;
51
+ /** 必需的资源授权策略;返回 true 才允许执行,缺失策略时默认拒绝。 */
52
+ readonly authorize: (request: StorageAccessRequest) => boolean | Promise<boolean>;
11
53
  }
12
- /** 创建 Storage 默认 procedures。 */
54
+ /**
55
+ * 创建具有认证、路径校验和资源授权的 Storage procedures。
56
+ *
57
+ * @param deps - 存储服务及必需的资源授权策略。
58
+ * @returns 可挂载至 serv.createApp 的存储 router;拒绝访问返回 FORBIDDEN。
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { storage } from '@h-ai/storage'
63
+ * import { createStorageProcedures } from '@h-ai/serv/features/storage'
64
+ *
65
+ * // 宿主已初始化 Storage;用户根目录来自可信 session,含尾斜杠形成边界。
66
+ * const procedures = createStorageProcedures({ storage, authorize: ({ context, keys }) => {
67
+ * const root = `users/${encodeURIComponent(context.session.userId)}/`
68
+ * return keys.length > 0 && keys.every(key => key.startsWith(root))
69
+ * } })
70
+ * // 空列表前缀与相邻用户名前缀均不能匹配此根目录。
71
+ * ```
72
+ */
13
73
  declare function createStorageProcedures(deps: StorageProcedureDeps): {
14
74
  presignedUrls: {
15
75
  createDownload: _orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
16
76
  key: zod.ZodString;
17
77
  expiresIn: zod.ZodOptional<zod.ZodNumber>;
18
- }, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
78
+ }, zod_v4_core.$strict>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
19
79
  success: zod.ZodLiteral<true>;
20
80
  data: zod.ZodObject<{
21
81
  url: zod.ZodString;
@@ -38,9 +98,8 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
38
98
  createUpload: _orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
39
99
  key: zod.ZodString;
40
100
  contentType: zod.ZodOptional<zod.ZodString>;
41
- contentLength: zod.ZodOptional<zod.ZodNumber>;
42
101
  expiresIn: zod.ZodOptional<zod.ZodNumber>;
43
- }, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
102
+ }, zod_v4_core.$strict>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
44
103
  success: zod.ZodLiteral<true>;
45
104
  data: zod.ZodObject<{
46
105
  url: zod.ZodString;
@@ -142,7 +201,19 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
142
201
  keys: zod.ZodArray<zod.ZodString>;
143
202
  }, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
144
203
  success: zod.ZodLiteral<true>;
145
- data: zod.ZodVoid;
204
+ data: zod.ZodObject<{
205
+ deletedKeys: zod.ZodArray<zod.ZodString>;
206
+ failures: zod.ZodArray<zod.ZodObject<{
207
+ key: zod.ZodString;
208
+ code: zod.ZodString;
209
+ retryable: zod.ZodBoolean;
210
+ outcome: zod.ZodEnum<{
211
+ unknown: "unknown";
212
+ failed: "failed";
213
+ notAttempted: "notAttempted";
214
+ }>;
215
+ }, zod_v4_core.$strip>>;
216
+ }, zod_v4_core.$strip>;
146
217
  }, zod_v4_core.$strip>, zod.ZodObject<{
147
218
  success: zod.ZodLiteral<false>;
148
219
  error: zod.ZodObject<{
@@ -153,10 +224,24 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
153
224
  module: zod.ZodOptional<zod.ZodString>;
154
225
  cause: zod.ZodOptional<zod.ZodUnknown>;
155
226
  suggestion: zod.ZodOptional<zod.ZodString>;
156
- ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
227
+ ext: zod.ZodOptional<zod.ZodObject<{
228
+ deleteMany: zod.ZodOptional<zod.ZodObject<{
229
+ deletedKeys: zod.ZodArray<zod.ZodString>;
230
+ failures: zod.ZodArray<zod.ZodObject<{
231
+ key: zod.ZodString;
232
+ code: zod.ZodString;
233
+ retryable: zod.ZodBoolean;
234
+ outcome: zod.ZodEnum<{
235
+ unknown: "unknown";
236
+ failed: "failed";
237
+ notAttempted: "notAttempted";
238
+ }>;
239
+ }, zod_v4_core.$strip>>;
240
+ }, zod_v4_core.$strip>>;
241
+ }, zod_v4_core.$catchall<zod.ZodUnknown>>>;
157
242
  }, zod_v4_core.$strip>;
158
243
  }, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>;
159
244
  };
160
245
  };
161
246
 
162
- export { type StorageProcedureDeps, createStorageProcedures };
247
+ export { type StorageAccessRequest, type StorageProcedureDeps, createStorageProcedures };
@@ -1,10 +1,11 @@
1
- import { mapHaiResult } from '../chunk-27PCPE37.js';
2
- import { implement, servM } from '../chunk-R354OMWL.js';
1
+ import { validateInputOrFail } from '../chunk-BGWE4S6Z.js';
2
+ import { mapHaiResult } from '../chunk-XKCLD42I.js';
3
+ import { implement, servM } from '../chunk-TWHCU6HT.js';
3
4
  import { apiContract } from '@h-ai/api-contract';
4
- import { ok, err, HaiCommonError } from '@h-ai/core';
5
+ import { err, HaiCommonError, ok } from '@h-ai/core';
6
+ import { getStorageKeyIssue, StorageDeleteKeysSchema, STORAGE_KEY_MAX_BYTES } from '@h-ai/storage/schemas';
5
7
 
6
8
  var storageContract = apiContract.storage;
7
- var STORAGE_KEY_MAX_LENGTH = 1024;
8
9
  function buildStorageKeyValidationFailure(field, locale, messageKey, params) {
9
10
  const message = servM(messageKey, { locale, params });
10
11
  return err(
@@ -14,20 +15,24 @@ function buildStorageKeyValidationFailure(field, locale, messageKey, params) {
14
15
  );
15
16
  }
16
17
  function validateStorageKeyOrFail(key, locale, field = "key") {
17
- if (key.trim() === "")
18
+ const issue = getStorageKeyIssue(key);
19
+ if (issue === "empty")
18
20
  return buildStorageKeyValidationFailure(field, locale, "serv_storageKeyRequired");
19
- if (key.length > STORAGE_KEY_MAX_LENGTH) {
21
+ if (issue === "tooLong") {
20
22
  return buildStorageKeyValidationFailure(field, locale, "serv_storageKeyTooLong", {
21
- max: STORAGE_KEY_MAX_LENGTH
23
+ max: STORAGE_KEY_MAX_BYTES
22
24
  });
23
25
  }
24
- if (key.startsWith("/") || key.includes("\\") || key.includes("\0"))
26
+ if (issue === "characters")
25
27
  return buildStorageKeyValidationFailure(field, locale, "serv_storageKeyIllegalChars");
26
- if (key.split("/").some((seg) => seg === "." || seg === ".."))
28
+ if (issue === "segments")
27
29
  return buildStorageKeyValidationFailure(field, locale, "serv_storageKeyDotSegments");
28
30
  return ok(key);
29
31
  }
30
32
  function validateStorageKeysOrFail(keys, locale) {
33
+ const parsed = validateInputOrFail(StorageDeleteKeysSchema, keys, locale);
34
+ if (!parsed.success)
35
+ return parsed;
31
36
  for (const [index, key] of keys.entries()) {
32
37
  const keyResult = validateStorageKeyOrFail(key, locale, `keys.${index}`);
33
38
  if (!keyResult.success)
@@ -37,10 +42,18 @@ function validateStorageKeysOrFail(keys, locale) {
37
42
  }
38
43
  function createStorageProcedures(deps) {
39
44
  const { storage } = deps;
45
+ async function authorize(request) {
46
+ if (typeof deps.authorize !== "function" || await deps.authorize(request) !== true)
47
+ return err(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale: request.context.locale }));
48
+ return ok(void 0);
49
+ }
40
50
  return implement(storageContract).context().route("presignedUrls.createDownload").auth().handle(async ({ input, context }) => {
41
51
  const keyResult = validateStorageKeyOrFail(input.key, context.locale);
42
52
  if (!keyResult.success)
43
53
  return keyResult;
54
+ const access = await authorize({ context, operation: "download", keys: [keyResult.data] });
55
+ if (!access.success)
56
+ return access;
44
57
  const expiresIn = input.expiresIn ?? 900;
45
58
  return toPresignedResult(
46
59
  keyResult.data,
@@ -51,6 +64,9 @@ function createStorageProcedures(deps) {
51
64
  const keyResult = validateStorageKeyOrFail(input.key, context.locale);
52
65
  if (!keyResult.success)
53
66
  return keyResult;
67
+ const access = await authorize({ context, operation: "upload", keys: [keyResult.data] });
68
+ if (!access.success)
69
+ return access;
54
70
  const expiresIn = input.expiresIn ?? 900;
55
71
  const contentType = input.contentType ?? "application/octet-stream";
56
72
  const { key: _, ...options } = input;
@@ -59,20 +75,40 @@ function createStorageProcedures(deps) {
59
75
  expiresIn,
60
76
  await storage.presign.putUrl(keyResult.data, { ...options, expiresIn, contentType })
61
77
  );
62
- }).route("files.list").auth().handle(({ input }) => storage.dir.list(input)).route("files.getMetadata").auth().handle(({ input, context }) => {
78
+ }).route("files.list").auth().handle(async ({ input, context }) => {
79
+ const prefix = input.prefix ?? "";
80
+ if (prefix) {
81
+ const prefixResult = validateStorageKeyOrFail(prefix, context.locale, "prefix");
82
+ if (!prefixResult.success)
83
+ return prefixResult;
84
+ }
85
+ const access = await authorize({ context, operation: "list", keys: [prefix] });
86
+ if (!access.success)
87
+ return access;
88
+ return storage.dir.list(input);
89
+ }).route("files.getMetadata").auth().handle(async ({ input, context }) => {
63
90
  const keyResult = validateStorageKeyOrFail(input.key, context.locale);
64
91
  if (!keyResult.success)
65
92
  return keyResult;
93
+ const access = await authorize({ context, operation: "metadata", keys: [keyResult.data] });
94
+ if (!access.success)
95
+ return access;
66
96
  return storage.file.head(keyResult.data);
67
- }).route("files.delete").auth().handle(({ input, context }) => {
97
+ }).route("files.delete").auth().handle(async ({ input, context }) => {
68
98
  const keyResult = validateStorageKeyOrFail(input.key, context.locale);
69
99
  if (!keyResult.success)
70
100
  return keyResult;
101
+ const access = await authorize({ context, operation: "delete", keys: [keyResult.data] });
102
+ if (!access.success)
103
+ return access;
71
104
  return storage.file.delete(keyResult.data);
72
- }).route("files.deleteMany").auth().handle(({ input, context }) => {
105
+ }).route("files.deleteMany").auth().handle(async ({ input, context }) => {
73
106
  const keysResult = validateStorageKeysOrFail(input.keys, context.locale);
74
107
  if (!keysResult.success)
75
108
  return keysResult;
109
+ const access = await authorize({ context, operation: "deleteMany", keys: keysResult.data });
110
+ if (!access.success)
111
+ return access;
76
112
  return storage.file.deleteMany(keysResult.data);
77
113
  }).build();
78
114
  }