@h-ai/serv 0.1.0-alpha.66 → 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.
- package/README.en.md +75 -37
- package/README.ja.md +76 -38
- package/README.md +77 -39
- package/dist/chunk-BGWE4S6Z.js +37 -0
- package/dist/chunk-BGWE4S6Z.js.map +1 -0
- package/dist/{chunk-R354OMWL.js → chunk-TWHCU6HT.js} +48 -6
- package/dist/chunk-TWHCU6HT.js.map +1 -0
- package/dist/{chunk-27PCPE37.js → chunk-XKCLD42I.js} +2 -2
- package/dist/chunk-XKCLD42I.js.map +1 -0
- package/dist/features/ai.d.ts +139 -57
- package/dist/features/ai.js +57 -46
- package/dist/features/ai.js.map +1 -1
- package/dist/features/iam.d.ts +29 -4
- package/dist/features/iam.js +3 -3
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +94 -9
- package/dist/features/storage.js +48 -12
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +587 -187
- package/dist/index.js +89 -111
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-CWzyrsdq.d.ts → serv-context-C48WdBcd.d.ts} +127 -7
- package/dist/serv-router-BX8hgYxo.d.ts +322 -0
- package/package.json +14 -10
- package/dist/chunk-27PCPE37.js.map +0 -1
- package/dist/chunk-R354OMWL.js.map +0 -1
package/dist/features/iam.js.map
CHANGED
|
@@ -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-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.$
|
|
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.$
|
|
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.
|
|
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.
|
|
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 };
|
package/dist/features/storage.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
18
|
+
const issue = getStorageKeyIssue(key);
|
|
19
|
+
if (issue === "empty")
|
|
18
20
|
return buildStorageKeyValidationFailure(field, locale, "serv_storageKeyRequired");
|
|
19
|
-
if (
|
|
21
|
+
if (issue === "tooLong") {
|
|
20
22
|
return buildStorageKeyValidationFailure(field, locale, "serv_storageKeyTooLong", {
|
|
21
|
-
max:
|
|
23
|
+
max: STORAGE_KEY_MAX_BYTES
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
|
-
if (
|
|
26
|
+
if (issue === "characters")
|
|
25
27
|
return buildStorageKeyValidationFailure(field, locale, "serv_storageKeyIllegalChars");
|
|
26
|
-
if (
|
|
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(
|
|
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
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/features/serv-feature-storage.ts"],"names":[],"mappings":"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","file":"storage.js","sourcesContent":["/**\n * @h-ai/serv — Storage 默认 procedures\n *\n * 基于 `@h-ai/storage` 提供开箱即用的 Storage procedures 实现:预签名 URL、列表、元数据、删除。\n *\n * **⚠️ 安全说明(必读):**\n *\n * 1. **路径穿越防护**:内置 `validateStorageKeyOrFail` 拒绝 `..`、绝对路径、反斜杠、NUL 字节等危险输入。\n * 2. **多租户 / 多用户隔离(需应用层实现)**:默认 procedure 仅使用 `.auth()`,任何登录用户均可访问任意 key。\n * 生产环境必须在应用层重新装配这些 procedure,以强制用 `context.session.userId` 作为 key 前缀。\n * 3. **权限收紧**:推荐把 `.auth()` 改成 `.permission('storage.files.read'|'storage.files.write')`。\n * @module features/serv-feature-storage\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type { StorageFunctions } from '@h-ai/storage'\nimport type { ServContext } from '../serv-context.js'\nimport type { ServMessageKey } from '../serv-i18n.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 { mapHaiResult } from './serv-feature-helpers.js'\n\nconst storageContract = apiContract.storage\nconst STORAGE_KEY_MAX_LENGTH = 1024\n\nfunction buildStorageKeyValidationFailure(\n field: string,\n locale: string,\n messageKey: ServMessageKey,\n params?: Record<string, string | number>,\n): HaiResult<never> {\n const message = servM(messageKey, { locale, params })\n return err(\n HaiCommonError.VALIDATION_ERROR,\n servM('serv_validationFailed', { locale }),\n [{ field, message }],\n )\n}\n\n/**\n * 对业务层再次校验存储 key 是否安全。\n *\n * 这里不额外构造 Zod schema,而是直接按业务规则返回统一的\n * `HaiResult + ValidationFormError[]` 形态,避免把简单路径安全规则包装得过重。\n */\nfunction validateStorageKeyOrFail(key: string, locale: string, field: string = 'key'): HaiResult<string> {\n if (key.trim() === '')\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyRequired')\n if (key.length > STORAGE_KEY_MAX_LENGTH) {\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyTooLong', {\n max: STORAGE_KEY_MAX_LENGTH,\n })\n }\n if (key.startsWith('/') || key.includes('\\\\') || key.includes('\\0'))\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyIllegalChars')\n if (key.split('/').some(seg => seg === '.' || seg === '..'))\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyDotSegments')\n return ok(key)\n}\n\nfunction validateStorageKeysOrFail(keys: string[], locale: string): HaiResult<string[]> {\n for (const [index, key] of keys.entries()) {\n const keyResult = validateStorageKeyOrFail(key, locale, `keys.${index}`)\n if (!keyResult.success)\n return keyResult\n }\n return ok(keys)\n}\n\n/** Storage 默认 procedures 依赖。 */\nexport interface StorageProcedureDeps {\n readonly storage: StorageFunctions\n}\n\ninterface PresignedResponse {\n readonly url: string\n readonly key: string\n readonly expiresAt?: Date\n}\n\n/** 创建 Storage 默认 procedures。 */\nexport function createStorageProcedures(deps: StorageProcedureDeps) {\n const { storage } = deps\n\n return implement(storageContract)\n .context<ServContext>()\n\n .route('presignedUrls.createDownload')\n .auth()\n .handle(async ({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n const expiresIn = input.expiresIn ?? 900\n return toPresignedResult(\n keyResult.data,\n expiresIn,\n await storage.presign.getUrl(keyResult.data, { expiresIn }),\n )\n })\n\n .route('presignedUrls.createUpload')\n .auth()\n .handle(async ({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n const expiresIn = input.expiresIn ?? 900\n const contentType = input.contentType ?? 'application/octet-stream'\n const { key: _, ...options } = input\n return toPresignedResult(\n keyResult.data,\n expiresIn,\n await storage.presign.putUrl(keyResult.data, { ...options, expiresIn, contentType }),\n )\n })\n\n .route('files.list')\n .auth()\n .handle(({ input }) => storage.dir.list(input))\n\n .route('files.getMetadata')\n .auth()\n .handle(({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n return storage.file.head(keyResult.data)\n })\n\n .route('files.delete')\n .auth()\n .handle(({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n return storage.file.delete(keyResult.data)\n })\n\n .route('files.deleteMany')\n .auth()\n .handle(({ input, context }) => {\n const keysResult = validateStorageKeysOrFail(input.keys, context.locale)\n if (!keysResult.success)\n return keysResult\n return storage.file.deleteMany(keysResult.data)\n })\n\n .build()\n}\n\nfunction toPresignedResult(\n key: string,\n expiresIn: number | undefined,\n result: HaiResult<string>,\n): HaiResult<PresignedResponse> {\n return mapHaiResult(result, url => ({\n url,\n key,\n expiresAt: expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined,\n }))\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/features/serv-feature-storage.ts"],"names":[],"mappings":";;;;;;;AAuBA,IAAM,kBAAkB,WAAA,CAAY,OAAA;AAEpC,SAAS,gCAAA,CACP,KAAA,EACA,MAAA,EACA,UAAA,EACA,MAAA,EACkB;AAClB,EAAA,MAAM,UAAU,KAAA,CAAM,UAAA,EAAY,EAAE,MAAA,EAAQ,QAAQ,CAAA;AACpD,EAAA,OAAO,GAAA;AAAA,IACL,cAAA,CAAe,gBAAA;AAAA,IACf,KAAA,CAAM,uBAAA,EAAyB,EAAE,MAAA,EAAQ,CAAA;AAAA,IACzC,CAAC,EAAE,KAAA,EAAO,OAAA,EAAS;AAAA,GACrB;AACF;AAOA,SAAS,wBAAA,CAAyB,GAAA,EAAa,MAAA,EAAgB,KAAA,GAAgB,KAAA,EAA0B;AACvG,EAAA,MAAM,KAAA,GAAQ,mBAAmB,GAAG,CAAA;AACpC,EAAA,IAAI,KAAA,KAAU,OAAA;AACZ,IAAA,OAAO,gCAAA,CAAiC,KAAA,EAAO,MAAA,EAAQ,yBAAyB,CAAA;AAClF,EAAA,IAAI,UAAU,SAAA,EAAW;AACvB,IAAA,OAAO,gCAAA,CAAiC,KAAA,EAAO,MAAA,EAAQ,wBAAA,EAA0B;AAAA,MAC/E,GAAA,EAAK;AAAA,KACN,CAAA;AAAA,EACH;AACA,EAAA,IAAI,KAAA,KAAU,YAAA;AACZ,IAAA,OAAO,gCAAA,CAAiC,KAAA,EAAO,MAAA,EAAQ,6BAA6B,CAAA;AACtF,EAAA,IAAI,KAAA,KAAU,UAAA;AACZ,IAAA,OAAO,gCAAA,CAAiC,KAAA,EAAO,MAAA,EAAQ,4BAA4B,CAAA;AACrF,EAAA,OAAO,GAAG,GAAG,CAAA;AACf;AAEA,SAAS,yBAAA,CAA0B,MAAgB,MAAA,EAAqC;AACtF,EAAA,MAAM,MAAA,GAAS,mBAAA,CAAoB,uBAAA,EAAyB,IAAA,EAAM,MAAM,CAAA;AACxE,EAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,IAAA,OAAO,MAAA;AACT,EAAA,KAAA,MAAW,CAAC,KAAA,EAAO,GAAG,CAAA,IAAK,IAAA,CAAK,SAAQ,EAAG;AACzC,IAAA,MAAM,YAAY,wBAAA,CAAyB,GAAA,EAAK,MAAA,EAAQ,CAAA,KAAA,EAAQ,KAAK,CAAA,CAAE,CAAA;AACvE,IAAA,IAAI,CAAC,SAAA,CAAU,OAAA;AACb,MAAA,OAAO,SAAA;AAAA,EACX;AACA,EAAA,OAAO,GAAG,IAAI,CAAA;AAChB;AAsFO,SAAS,wBAAwB,IAAA,EAA4B;AAClE,EAAA,MAAM,EAAE,SAAQ,GAAI,IAAA;AAEpB,EAAA,eAAe,UAAU,OAAA,EAAyD;AAChF,IAAA,IAAI,OAAO,KAAK,SAAA,KAAc,UAAA,IAAc,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,CAAA,KAAM,IAAA;AAC5E,MAAA,OAAO,GAAA,CAAI,cAAA,CAAe,SAAA,EAAW,KAAA,CAAM,qBAAA,EAAuB,EAAE,MAAA,EAAQ,OAAA,CAAQ,OAAA,CAAQ,MAAA,EAAQ,CAAC,CAAA;AACvG,IAAA,OAAO,GAAG,MAAS,CAAA;AAAA,EACrB;AAEA,EAAA,OAAO,SAAA,CAAU,eAAe,CAAA,CAC7B,OAAA,GAEA,KAAA,CAAM,8BAA8B,CAAA,CACpC,IAAA,GACA,MAAA,CAAO,OAAO,EAAE,KAAA,EAAO,SAAQ,KAAM;AACpC,IAAA,MAAM,SAAA,GAAY,wBAAA,CAAyB,KAAA,CAAM,GAAA,EAAK,QAAQ,MAAM,CAAA;AACpE,IAAA,IAAI,CAAC,SAAA,CAAU,OAAA;AACb,MAAA,OAAO,SAAA;AACT,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,EAAE,OAAA,EAAS,SAAA,EAAW,UAAA,EAAY,IAAA,EAAM,CAAC,SAAA,CAAU,IAAI,CAAA,EAAG,CAAA;AACzF,IAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,MAAA,OAAO,MAAA;AACT,IAAA,MAAM,SAAA,GAAY,MAAM,SAAA,IAAa,GAAA;AACrC,IAAA,OAAO,iBAAA;AAAA,MACL,SAAA,CAAU,IAAA;AAAA,MACV,SAAA;AAAA,MACA,MAAM,QAAQ,OAAA,CAAQ,MAAA,CAAO,UAAU,IAAA,EAAM,EAAE,WAAW;AAAA,KAC5D;AAAA,EACF,CAAC,CAAA,CAEA,KAAA,CAAM,4BAA4B,CAAA,CAClC,IAAA,EAAK,CACL,MAAA,CAAO,OAAO,EAAE,KAAA,EAAO,OAAA,EAAQ,KAAM;AACpC,IAAA,MAAM,SAAA,GAAY,wBAAA,CAAyB,KAAA,CAAM,GAAA,EAAK,QAAQ,MAAM,CAAA;AACpE,IAAA,IAAI,CAAC,SAAA,CAAU,OAAA;AACb,MAAA,OAAO,SAAA;AACT,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,EAAE,OAAA,EAAS,SAAA,EAAW,QAAA,EAAU,IAAA,EAAM,CAAC,SAAA,CAAU,IAAI,CAAA,EAAG,CAAA;AACvF,IAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,MAAA,OAAO,MAAA;AACT,IAAA,MAAM,SAAA,GAAY,MAAM,SAAA,IAAa,GAAA;AACrC,IAAA,MAAM,WAAA,GAAc,MAAM,WAAA,IAAe,0BAAA;AACzC,IAAA,MAAM,EAAE,GAAA,EAAK,CAAA,EAAG,GAAG,SAAQ,GAAI,KAAA;AAC/B,IAAA,OAAO,iBAAA;AAAA,MACL,SAAA,CAAU,IAAA;AAAA,MACV,SAAA;AAAA,MACA,MAAM,OAAA,CAAQ,OAAA,CAAQ,MAAA,CAAO,SAAA,CAAU,IAAA,EAAM,EAAE,GAAG,OAAA,EAAS,SAAA,EAAW,WAAA,EAAa;AAAA,KACrF;AAAA,EACF,CAAC,CAAA,CAEA,KAAA,CAAM,YAAY,CAAA,CAClB,IAAA,EAAK,CACL,MAAA,CAAO,OAAO,EAAE,KAAA,EAAO,OAAA,EAAQ,KAAM;AACpC,IAAA,MAAM,MAAA,GAAS,MAAM,MAAA,IAAU,EAAA;AAC/B,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,MAAM,YAAA,GAAe,wBAAA,CAAyB,MAAA,EAAQ,OAAA,CAAQ,QAAQ,QAAQ,CAAA;AAC9E,MAAA,IAAI,CAAC,YAAA,CAAa,OAAA;AAChB,QAAA,OAAO,YAAA;AAAA,IACX;AACA,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,EAAE,OAAA,EAAS,SAAA,EAAW,MAAA,EAAQ,IAAA,EAAM,CAAC,MAAM,CAAA,EAAG,CAAA;AAC7E,IAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,MAAA,OAAO,MAAA;AACT,IAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,KAAK,CAAA;AAAA,EAC/B,CAAC,CAAA,CAEA,KAAA,CAAM,mBAAmB,CAAA,CACzB,IAAA,EAAK,CACL,MAAA,CAAO,OAAO,EAAE,KAAA,EAAO,OAAA,EAAQ,KAAM;AACpC,IAAA,MAAM,SAAA,GAAY,wBAAA,CAAyB,KAAA,CAAM,GAAA,EAAK,QAAQ,MAAM,CAAA;AACpE,IAAA,IAAI,CAAC,SAAA,CAAU,OAAA;AACb,MAAA,OAAO,SAAA;AACT,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,EAAE,OAAA,EAAS,SAAA,EAAW,UAAA,EAAY,IAAA,EAAM,CAAC,SAAA,CAAU,IAAI,CAAA,EAAG,CAAA;AACzF,IAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,MAAA,OAAO,MAAA;AACT,IAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAAA,EACzC,CAAC,CAAA,CAEA,KAAA,CAAM,cAAc,CAAA,CACpB,IAAA,EAAK,CACL,MAAA,CAAO,OAAO,EAAE,KAAA,EAAO,OAAA,EAAQ,KAAM;AACpC,IAAA,MAAM,SAAA,GAAY,wBAAA,CAAyB,KAAA,CAAM,GAAA,EAAK,QAAQ,MAAM,CAAA;AACpE,IAAA,IAAI,CAAC,SAAA,CAAU,OAAA;AACb,MAAA,OAAO,SAAA;AACT,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,EAAE,OAAA,EAAS,SAAA,EAAW,QAAA,EAAU,IAAA,EAAM,CAAC,SAAA,CAAU,IAAI,CAAA,EAAG,CAAA;AACvF,IAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,MAAA,OAAO,MAAA;AACT,IAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,SAAA,CAAU,IAAI,CAAA;AAAA,EAC3C,CAAC,CAAA,CAEA,KAAA,CAAM,kBAAkB,CAAA,CACxB,IAAA,EAAK,CACL,MAAA,CAAO,OAAO,EAAE,KAAA,EAAO,OAAA,EAAQ,KAAM;AACpC,IAAA,MAAM,UAAA,GAAa,yBAAA,CAA0B,KAAA,CAAM,IAAA,EAAM,QAAQ,MAAM,CAAA;AACvE,IAAA,IAAI,CAAC,UAAA,CAAW,OAAA;AACd,MAAA,OAAO,UAAA;AACT,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,EAAE,OAAA,EAAS,WAAW,YAAA,EAAc,IAAA,EAAM,UAAA,CAAW,IAAA,EAAM,CAAA;AAC1F,IAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,MAAA,OAAO,MAAA;AACT,IAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,UAAA,CAAW,UAAA,CAAW,IAAI,CAAA;AAAA,EAChD,CAAC,EAEA,KAAA,EAAM;AACX;AAEA,SAAS,iBAAA,CACP,GAAA,EACA,SAAA,EACA,MAAA,EAC8B;AAC9B,EAAA,OAAO,YAAA,CAAa,QAAQ,CAAA,GAAA,MAAQ;AAAA,IAClC,GAAA;AAAA,IACA,GAAA;AAAA,IACA,SAAA,EAAW,YAAY,IAAI,IAAA,CAAK,KAAK,GAAA,EAAI,GAAI,SAAA,GAAY,GAAI,CAAA,GAAI;AAAA,GACnE,CAAE,CAAA;AACJ","file":"storage.js","sourcesContent":["/**\n * @h-ai/serv — Storage 默认 procedures\n *\n * 基于 `@h-ai/storage` 提供开箱即用的 Storage procedures 实现:预签名 URL、列表、元数据、删除。\n *\n * 所有操作先验证路径,再调用应用提供的资源授权策略;未提供策略时拒绝访问。\n * 策略必须从已认证 session 确定用户/租户范围,不能信任客户端提供的前缀。\n * @module features/serv-feature-storage\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type { StorageFunctions } from '@h-ai/storage'\nimport type { ServContext } from '../serv-context.js'\nimport type { ServMessageKey } from '../serv-i18n.js'\nimport type { AuthenticatedServContext } from '../serv-router.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { err, HaiCommonError, ok } from '@h-ai/core'\nimport { getStorageKeyIssue, STORAGE_KEY_MAX_BYTES, StorageDeleteKeysSchema } from '@h-ai/storage/schemas'\nimport { servM } from '../serv-i18n.js'\nimport { implement } from '../serv-router.js'\nimport { validateInputOrFail } from '../serv-validation.js'\nimport { mapHaiResult } from './serv-feature-helpers.js'\n\nconst storageContract = apiContract.storage\n\nfunction buildStorageKeyValidationFailure(\n field: string,\n locale: string,\n messageKey: ServMessageKey,\n params?: Record<string, string | number>,\n): HaiResult<never> {\n const message = servM(messageKey, { locale, params })\n return err(\n HaiCommonError.VALIDATION_ERROR,\n servM('serv_validationFailed', { locale }),\n [{ field, message }],\n )\n}\n\n/**\n * 对业务层再次校验存储 key 是否安全。\n *\n * 与公开 Schema 共用规则,保留业务入口防线;后续仍必须执行授权和 Provider 防逃逸。\n */\nfunction validateStorageKeyOrFail(key: string, locale: string, field: string = 'key'): HaiResult<string> {\n const issue = getStorageKeyIssue(key)\n if (issue === 'empty')\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyRequired')\n if (issue === 'tooLong') {\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyTooLong', {\n max: STORAGE_KEY_MAX_BYTES,\n })\n }\n if (issue === 'characters')\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyIllegalChars')\n if (issue === 'segments')\n return buildStorageKeyValidationFailure(field, locale, 'serv_storageKeyDotSegments')\n return ok(key)\n}\n\nfunction validateStorageKeysOrFail(keys: string[], locale: string): HaiResult<string[]> {\n const parsed = validateInputOrFail(StorageDeleteKeysSchema, keys, locale)\n if (!parsed.success)\n return parsed\n for (const [index, key] of keys.entries()) {\n const keyResult = validateStorageKeyOrFail(key, locale, `keys.${index}`)\n if (!keyResult.success)\n return keyResult\n }\n return ok(keys)\n}\n\n/**\n * 存储资源授权请求;批量操作只有全部资源获准后才会调用存储。\n *\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n * import { createStorageProcedures } from '@h-ai/serv/features/storage'\n *\n * // 宿主已初始化 Storage;用户根目录来自可信 session,含尾斜杠形成边界。\n * const procedures = createStorageProcedures({ storage, authorize: ({ context, keys }) => {\n * const root = `users/${encodeURIComponent(context.session.userId)}/`\n * return keys.length > 0 && keys.every(key => key.startsWith(root))\n * } })\n * // 空列表前缀与相邻用户名前缀均不能匹配此根目录。\n * ```\n */\nexport interface StorageAccessRequest {\n /** 操作类别;策略可分别授予读取、写入或删除权限。 */\n readonly operation: 'download' | 'upload' | 'list' | 'metadata' | 'delete' | 'deleteMany'\n /** 已通过认证的可信请求上下文。 */\n readonly context: AuthenticatedServContext\n /** 单项/批量 key;list 操作仅包含请求的 prefix,未提供 prefix 时为空字符串。 */\n readonly keys: readonly string[]\n}\n\n/**\n * Storage 默认 procedures 依赖。\n *\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n * import { createStorageProcedures } from '@h-ai/serv/features/storage'\n *\n * // 宿主已初始化 Storage;用户根目录来自可信 session,含尾斜杠形成边界。\n * const procedures = createStorageProcedures({ storage, authorize: ({ context, keys }) => {\n * const root = `users/${encodeURIComponent(context.session.userId)}/`\n * return keys.length > 0 && keys.every(key => key.startsWith(root))\n * } })\n * // 空列表前缀与相邻用户名前缀均不能匹配此根目录。\n * ```\n */\nexport interface StorageProcedureDeps {\n readonly storage: StorageFunctions\n /** 必需的资源授权策略;返回 true 才允许执行,缺失策略时默认拒绝。 */\n readonly authorize: (request: StorageAccessRequest) => boolean | Promise<boolean>\n}\n\n/**\n * 内部预签名结果投影,包含短期访问 URL 和可选失效时间。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * function safeUrl(value: PresignedResponse) {\n * return { url: value.url, key: value.key, expiresAt: value.expiresAt }\n * }\n * // URL 是授权后的短期凭据;只返回给已授权调用方,不记录到日志。\n * ```\n */\ninterface PresignedResponse {\n readonly url: string\n readonly key: string\n readonly expiresAt?: Date\n}\n\n/**\n * 创建具有认证、路径校验和资源授权的 Storage procedures。\n *\n * @param deps - 存储服务及必需的资源授权策略。\n * @returns 可挂载至 serv.createApp 的存储 router;拒绝访问返回 FORBIDDEN。\n *\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n * import { createStorageProcedures } from '@h-ai/serv/features/storage'\n *\n * // 宿主已初始化 Storage;用户根目录来自可信 session,含尾斜杠形成边界。\n * const procedures = createStorageProcedures({ storage, authorize: ({ context, keys }) => {\n * const root = `users/${encodeURIComponent(context.session.userId)}/`\n * return keys.length > 0 && keys.every(key => key.startsWith(root))\n * } })\n * // 空列表前缀与相邻用户名前缀均不能匹配此根目录。\n * ```\n */\nexport function createStorageProcedures(deps: StorageProcedureDeps) {\n const { storage } = deps\n\n async function authorize(request: StorageAccessRequest): Promise<HaiResult<void>> {\n if (typeof deps.authorize !== 'function' || await deps.authorize(request) !== true)\n return err(HaiCommonError.FORBIDDEN, servM('serv_errorForbidden', { locale: request.context.locale }))\n return ok(undefined)\n }\n\n return implement(storageContract)\n .context<ServContext>()\n\n .route('presignedUrls.createDownload')\n .auth()\n .handle(async ({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n const access = await authorize({ context, operation: 'download', keys: [keyResult.data] })\n if (!access.success)\n return access\n const expiresIn = input.expiresIn ?? 900\n return toPresignedResult(\n keyResult.data,\n expiresIn,\n await storage.presign.getUrl(keyResult.data, { expiresIn }),\n )\n })\n\n .route('presignedUrls.createUpload')\n .auth()\n .handle(async ({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n const access = await authorize({ context, operation: 'upload', keys: [keyResult.data] })\n if (!access.success)\n return access\n const expiresIn = input.expiresIn ?? 900\n const contentType = input.contentType ?? 'application/octet-stream'\n const { key: _, ...options } = input\n return toPresignedResult(\n keyResult.data,\n expiresIn,\n await storage.presign.putUrl(keyResult.data, { ...options, expiresIn, contentType }),\n )\n })\n\n .route('files.list')\n .auth()\n .handle(async ({ input, context }) => {\n const prefix = input.prefix ?? ''\n if (prefix) {\n const prefixResult = validateStorageKeyOrFail(prefix, context.locale, 'prefix')\n if (!prefixResult.success)\n return prefixResult\n }\n const access = await authorize({ context, operation: 'list', keys: [prefix] })\n if (!access.success)\n return access\n return storage.dir.list(input)\n })\n\n .route('files.getMetadata')\n .auth()\n .handle(async ({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n const access = await authorize({ context, operation: 'metadata', keys: [keyResult.data] })\n if (!access.success)\n return access\n return storage.file.head(keyResult.data)\n })\n\n .route('files.delete')\n .auth()\n .handle(async ({ input, context }) => {\n const keyResult = validateStorageKeyOrFail(input.key, context.locale)\n if (!keyResult.success)\n return keyResult\n const access = await authorize({ context, operation: 'delete', keys: [keyResult.data] })\n if (!access.success)\n return access\n return storage.file.delete(keyResult.data)\n })\n\n .route('files.deleteMany')\n .auth()\n .handle(async ({ input, context }) => {\n const keysResult = validateStorageKeysOrFail(input.keys, context.locale)\n if (!keysResult.success)\n return keysResult\n const access = await authorize({ context, operation: 'deleteMany', keys: keysResult.data })\n if (!access.success)\n return access\n return storage.file.deleteMany(keysResult.data)\n })\n\n .build()\n}\n\nfunction toPresignedResult(\n key: string,\n expiresIn: number | undefined,\n result: HaiResult<string>,\n): HaiResult<PresignedResponse> {\n return mapHaiResult(result, url => ({\n url,\n key,\n expiresAt: expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined,\n }))\n}\n"]}
|