@h-ai/serv 0.1.0-alpha.46 → 0.1.0-alpha.47
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.md +61 -64
- package/dist/chunk-KIWHKWLD.js +317 -0
- package/dist/chunk-KIWHKWLD.js.map +1 -0
- package/dist/features/ai.d.ts +20 -21
- package/dist/features/ai.js +14 -31
- package/dist/features/ai.js.map +1 -1
- package/dist/features/iam.d.ts +58 -58
- package/dist/features/iam.js +38 -97
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +18 -19
- package/dist/features/storage.js +39 -44
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +102 -122
- package/dist/index.js +3 -19
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-BUrtWoFC.d.ts → serv-context-jc3rZxbj.d.ts} +2 -2
- package/package.json +10 -10
- package/dist/chunk-4QBF4PUG.js +0 -128
- package/dist/chunk-4QBF4PUG.js.map +0 -1
- package/dist/chunk-YQV2OMQK.js +0 -16
- package/dist/chunk-YQV2OMQK.js.map +0 -1
package/dist/features/iam.js
CHANGED
|
@@ -1,108 +1,49 @@
|
|
|
1
|
-
import { requirePermission } from '../chunk-YQV2OMQK.js';
|
|
2
1
|
import { mapHaiResult } from '../chunk-27PCPE37.js';
|
|
3
|
-
import {
|
|
2
|
+
import { implement, servM } from '../chunk-KIWHKWLD.js';
|
|
4
3
|
import { apiContract } from '@h-ai/api-contract';
|
|
5
4
|
import { core, err, HaiCommonError, ok } from '@h-ai/core';
|
|
6
|
-
import { implement } from '@orpc/server';
|
|
7
5
|
|
|
8
6
|
var iamContract = apiContract.iam;
|
|
9
7
|
var iamFeatureLogger = core.logger.child({ module: "serv", scope: "feature-iam" });
|
|
10
8
|
function createIamProcedures(deps) {
|
|
11
|
-
const p = implement(iamContract).$context();
|
|
12
9
|
const { iam } = deps;
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
users: {
|
|
52
|
-
list: p.users.list.handler(requirePermission("iam.users.read", ({ input }) => {
|
|
53
|
-
return iam.user.listUsers(input);
|
|
54
|
-
})),
|
|
55
|
-
get: p.users.get.handler(requirePermission("iam.users.read", ({ input }) => {
|
|
56
|
-
return iam.user.getUser(input.id, { include: ["roles"] });
|
|
57
|
-
})),
|
|
58
|
-
create: p.users.create.handler(requirePermission("iam.users.write", ({ input }) => {
|
|
59
|
-
return createUser(iam, input);
|
|
60
|
-
})),
|
|
61
|
-
update: p.users.update.handler(requirePermission("iam.users.write", ({ input }) => {
|
|
62
|
-
return updateUser(iam, input);
|
|
63
|
-
})),
|
|
64
|
-
delete: p.users.delete.handler(requirePermission("iam.users.write", ({ input, context }) => {
|
|
65
|
-
if (context.session?.userId === input.id)
|
|
66
|
-
return Promise.resolve(err(HaiCommonError.FORBIDDEN, servM("serv_iamCannotDeleteCurrentUser", { locale: context.locale })));
|
|
67
|
-
return iam.user.deleteUser(input.id);
|
|
68
|
-
})),
|
|
69
|
-
resetPassword: p.users.resetPassword.handler(requirePermission("iam.users.write", ({ input }) => {
|
|
70
|
-
return iam.user.adminResetPassword(input.id, input.newPassword);
|
|
71
|
-
}))
|
|
72
|
-
},
|
|
73
|
-
roles: {
|
|
74
|
-
list: p.roles.list.handler(requirePermission("iam.roles.read", () => {
|
|
75
|
-
return iam.authz.getAllRoles();
|
|
76
|
-
})),
|
|
77
|
-
get: p.roles.get.handler(requirePermission("iam.roles.read", ({ input }) => {
|
|
78
|
-
return iam.authz.getRole(input.id);
|
|
79
|
-
})),
|
|
80
|
-
create: p.roles.create.handler(requirePermission("iam.roles.write", ({ input }) => {
|
|
81
|
-
return iam.authz.createRole(input);
|
|
82
|
-
})),
|
|
83
|
-
update: p.roles.update.handler(requirePermission("iam.roles.write", ({ input }) => {
|
|
84
|
-
const { id, ...data } = input;
|
|
85
|
-
return iam.authz.updateRole(id, data);
|
|
86
|
-
})),
|
|
87
|
-
delete: p.roles.delete.handler(requirePermission("iam.roles.write", ({ input }) => {
|
|
88
|
-
return iam.authz.deleteRole(input.id);
|
|
89
|
-
}))
|
|
90
|
-
},
|
|
91
|
-
permissions: {
|
|
92
|
-
list: p.permissions.list.handler(requirePermission("iam.permissions.read", ({ input }) => {
|
|
93
|
-
return iam.authz.getAllPermissions(input);
|
|
94
|
-
})),
|
|
95
|
-
get: p.permissions.get.handler(requirePermission("iam.permissions.read", ({ input }) => {
|
|
96
|
-
return iam.authz.getPermission(input.id);
|
|
97
|
-
})),
|
|
98
|
-
create: p.permissions.create.handler(requirePermission("iam.permissions.write", ({ input }) => {
|
|
99
|
-
return iam.authz.createPermission(input);
|
|
100
|
-
})),
|
|
101
|
-
delete: p.permissions.delete.handler(requirePermission("iam.permissions.write", ({ input }) => {
|
|
102
|
-
return iam.authz.deletePermission(input.id);
|
|
103
|
-
}))
|
|
104
|
-
}
|
|
105
|
-
});
|
|
10
|
+
return implement(iamContract).context().route("auth.login", ({ input }) => iam.auth.login(input)).route("auth.loginWithOtp", ({ input }) => iam.auth.loginWithOtp(input)).route("auth.logout").auth().handle(({ input, context }) => {
|
|
11
|
+
const token = input.accessToken ?? context.accessToken;
|
|
12
|
+
if (!token)
|
|
13
|
+
return err(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: context.locale }));
|
|
14
|
+
return iam.auth.logout(token);
|
|
15
|
+
}).route("auth.currentUser").auth().handle(async ({ context }) => {
|
|
16
|
+
const token = context.accessToken;
|
|
17
|
+
if (!token)
|
|
18
|
+
return err(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: context.locale }));
|
|
19
|
+
return mapHaiResult(
|
|
20
|
+
await iam.user.getCurrentUser(token),
|
|
21
|
+
(user) => ({
|
|
22
|
+
...user,
|
|
23
|
+
roles: context.session.roles,
|
|
24
|
+
permissions: context.session.permissions
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
}).route("auth.refresh", async ({ input }) => {
|
|
28
|
+
return mapHaiResult(await iam.session.refresh(input.refreshToken), (tokens) => ({ tokens }));
|
|
29
|
+
}).route("auth.sendOtp", ({ input }) => iam.auth.sendOtp(input.identifier)).route("auth.register", ({ input }) => iam.auth.registerAndLogin(input)).route("auth.changePassword").auth().handle(({ input, context }) => {
|
|
30
|
+
const token = context.accessToken;
|
|
31
|
+
if (!token)
|
|
32
|
+
return err(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: context.locale }));
|
|
33
|
+
return iam.user.changeCurrentUserPassword(token, input.oldPassword, input.newPassword);
|
|
34
|
+
}).route("auth.updateCurrentUser").auth().handle(({ input, context }) => {
|
|
35
|
+
const token = context.accessToken;
|
|
36
|
+
if (!token)
|
|
37
|
+
return err(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: context.locale }));
|
|
38
|
+
return iam.user.updateCurrentUser(token, input);
|
|
39
|
+
}).route("users.list").permission("iam.users.read").handle(({ input }) => iam.user.listUsers(input)).route("users.get").permission("iam.users.read").handle(({ input }) => iam.user.getUser(input.id, { include: ["roles"] })).route("users.create").permission("iam.users.write").handle(({ input }) => createUser(iam, input)).route("users.update").permission("iam.users.write").handle(({ input }) => updateUser(iam, input)).route("users.delete").permission("iam.users.write").handle(({ input, context }) => {
|
|
40
|
+
if (context.session.userId === input.id)
|
|
41
|
+
return err(HaiCommonError.FORBIDDEN, servM("serv_iamCannotDeleteCurrentUser", { locale: context.locale }));
|
|
42
|
+
return iam.user.deleteUser(input.id);
|
|
43
|
+
}).route("users.resetPassword").permission("iam.users.write").handle(({ input }) => iam.user.adminResetPassword(input.id, input.newPassword)).route("roles.list").permission("iam.roles.read").handle(() => iam.authz.getAllRoles()).route("roles.get").permission("iam.roles.read").handle(({ input }) => iam.authz.getRole(input.id)).route("roles.create").permission("iam.roles.write").handle(({ input }) => iam.authz.createRole(input)).route("roles.update").permission("iam.roles.write").handle(({ input }) => {
|
|
44
|
+
const { id, ...data } = input;
|
|
45
|
+
return iam.authz.updateRole(id, data);
|
|
46
|
+
}).route("roles.delete").permission("iam.roles.write").handle(({ input }) => iam.authz.deleteRole(input.id)).route("permissions.list").permission("iam.permissions.read").handle(({ input }) => iam.authz.getAllPermissions(input)).route("permissions.get").permission("iam.permissions.read").handle(({ input }) => iam.authz.getPermission(input.id)).route("permissions.create").permission("iam.permissions.write").handle(({ input }) => iam.authz.createPermission(input)).route("permissions.delete").permission("iam.permissions.write").handle(({ input }) => iam.authz.deletePermission(input.id)).build();
|
|
106
47
|
}
|
|
107
48
|
async function createUser(iam, input) {
|
|
108
49
|
const registerResult = await iam.user.register(input);
|
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 * 通过 `createIamProcedures(deps)` 组装后直接挂载到 oRPC router。\n * @module features/serv-feature-iam\n */\n\nimport type {\n IamAdminCreateUserInput,\n IamAdminResetPasswordInput,\n IamAdminUpdateUserInput,\n IamChangePasswordInput,\n IamCreatePermissionInput,\n IamCreateRoleInput,\n IamCurrentUser,\n IamListPermissionsInput,\n IamListUsersInput,\n IamLoginInput,\n IamLogoutInput,\n IamOtpLoginInput,\n IamRegisterInput,\n IamSendOtpInput,\n IamUpdateCurrentUserInput,\n IamUpdateRoleInput,\n IamUserIdInput,\n} from '@h-ai/api-contract'\nimport type { HaiResult, PaginatedResult } from '@h-ai/core'\nimport type { IamFunctions, Permission, Role, 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 { implement } from '@orpc/server'\nimport { requireAuth } from '../pipelines/serv-pipeline-require-auth.js'\nimport { requirePermission } from '../pipelines/serv-pipeline-require-permission.js'\nimport { servM } from '../serv-i18n.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 p = implement(iamContract).$context<ServContext>()\n const { iam } = deps\n\n return p.router({\n auth: {\n login: p.auth.login.handler(({ input }: { input: IamLoginInput }) => iam.auth.login(input)),\n loginWithOtp: p.auth.loginWithOtp.handler(({ input }: { input: IamOtpLoginInput }) => iam.auth.loginWithOtp(input)),\n logout: p.auth.logout.handler(requireAuth<IamLogoutInput, void>(({ input, context }) => {\n // 不再使用空字符串兜底:requireAuth 已确保 session 存在,但 accessToken 字段可能仍缺失\n // (例如自定义 createContext 未填充);此时显式返回 UNAUTHORIZED 而非把空 token 透传给 IAM。\n const token = input.accessToken ?? context.accessToken\n if (!token)\n return Promise.resolve(err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale })))\n return iam.auth.logout(token)\n })),\n currentUser: p.auth.currentUser.handler(requireAuth<unknown, IamCurrentUser>(async ({ context }) => {\n const token = context.accessToken\n if (!token)\n return Promise.resolve(err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale })))\n return mapHaiResult<User, IamCurrentUser>(await iam.user.getCurrentUser(token), user => ({\n ...user,\n roles: context.session?.roles ?? [],\n permissions: context.session?.permissions ?? [],\n }))\n })),\n refresh: p.auth.refresh.handler(async ({ input }: { input: { refreshToken: string } }) => {\n return mapHaiResult(await iam.session.refresh(input.refreshToken), tokens => ({ tokens }))\n }),\n sendOtp: p.auth.sendOtp.handler(({ input }: { input: IamSendOtpInput }) => iam.auth.sendOtp(input.identifier)),\n register: p.auth.register.handler(({ input }: { input: IamRegisterInput }) => iam.auth.registerAndLogin(input)),\n changePassword: p.auth.changePassword.handler(requireAuth<IamChangePasswordInput, void>(({ input, context }) => {\n const token = context.accessToken\n if (!token)\n return Promise.resolve(err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale })))\n return iam.user.changeCurrentUserPassword(token, input.oldPassword, input.newPassword)\n })),\n updateCurrentUser: p.auth.updateCurrentUser.handler(requireAuth<IamUpdateCurrentUserInput, User>(({ input, context }) => {\n const token = context.accessToken\n if (!token)\n return Promise.resolve(err(HaiCommonError.UNAUTHORIZED, servM('serv_errorUnauthorized', { locale: context.locale })))\n return iam.user.updateCurrentUser(token, input)\n })),\n },\n users: {\n list: p.users.list.handler(requirePermission<IamListUsersInput, PaginatedResult<User>>('iam.users.read', ({ input }) => {\n return iam.user.listUsers(input)\n })),\n get: p.users.get.handler(requirePermission<IamUserIdInput, User | null>('iam.users.read', ({ input }) => {\n return iam.user.getUser(input.id, { include: ['roles'] })\n })),\n create: p.users.create.handler(requirePermission<IamAdminCreateUserInput, User>('iam.users.write', ({ input }) => {\n return createUser(iam, input)\n })),\n update: p.users.update.handler(requirePermission<IamAdminUpdateUserInput, User>('iam.users.write', ({ input }) => {\n return updateUser(iam, input)\n })),\n delete: p.users.delete.handler(requirePermission<IamUserIdInput, void>('iam.users.write', ({ input, context }) => {\n // 防止自删:避免管理员误删自己的账号导致授权锁死。\n if (context.session?.userId === input.id)\n return Promise.resolve(err(HaiCommonError.FORBIDDEN, servM('serv_iamCannotDeleteCurrentUser', { locale: context.locale })))\n return iam.user.deleteUser(input.id)\n })),\n resetPassword: p.users.resetPassword.handler(requirePermission<IamAdminResetPasswordInput, void>('iam.users.write', ({ input }) => {\n return iam.user.adminResetPassword(input.id, input.newPassword)\n })),\n },\n roles: {\n list: p.roles.list.handler(requirePermission<unknown, PaginatedResult<Role>>('iam.roles.read', () => {\n return iam.authz.getAllRoles()\n })),\n get: p.roles.get.handler(requirePermission<IamUserIdInput, Role | null>('iam.roles.read', ({ input }) => {\n return iam.authz.getRole(input.id)\n })),\n create: p.roles.create.handler(requirePermission<IamCreateRoleInput, Role>('iam.roles.write', ({ input }) => {\n return iam.authz.createRole(input)\n })),\n update: p.roles.update.handler(requirePermission<IamUpdateRoleInput, Role>('iam.roles.write', ({ input }) => {\n const { id, ...data } = input\n return iam.authz.updateRole(id, data)\n })),\n delete: p.roles.delete.handler(requirePermission<IamUserIdInput, void>('iam.roles.write', ({ input }) => {\n return iam.authz.deleteRole(input.id)\n })),\n },\n permissions: {\n list: p.permissions.list.handler(requirePermission<IamListPermissionsInput, PaginatedResult<Permission>>('iam.permissions.read', ({ input }) => {\n return iam.authz.getAllPermissions(input)\n })),\n get: p.permissions.get.handler(requirePermission<IamUserIdInput, Permission | null>('iam.permissions.read', ({ input }) => {\n return iam.authz.getPermission(input.id)\n })),\n create: p.permissions.create.handler(requirePermission<IamCreatePermissionInput, Permission>('iam.permissions.write', ({ input }) => {\n return iam.authz.createPermission(input)\n })),\n delete: p.permissions.delete.handler(requirePermission<IamUserIdInput, void>('iam.permissions.write', ({ input }) => {\n return iam.authz.deletePermission(input.id)\n })),\n },\n })\n}\n\nasync function createUser(iam: IamFunctions, input: IamAdminCreateUserInput): Promise<HaiResult<User>> {\n const registerResult = await iam.user.register(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?.length) {\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/** 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\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.register(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?.length) {\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,9 +1,8 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
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
|
-
import * as _orpc_server from '@orpc/server';
|
|
5
4
|
import { StorageFunctions } from '@h-ai/storage';
|
|
6
|
-
import { a as ServContext } from '../serv-context-
|
|
5
|
+
import { a as ServContext } from '../serv-context-jc3rZxbj.js';
|
|
7
6
|
import '@h-ai/core';
|
|
8
7
|
|
|
9
8
|
/** Storage 默认 procedures 依赖。 */
|
|
@@ -12,8 +11,8 @@ interface StorageProcedureDeps {
|
|
|
12
11
|
}
|
|
13
12
|
/** 创建 Storage 默认 procedures。 */
|
|
14
13
|
declare function createStorageProcedures(deps: StorageProcedureDeps): {
|
|
15
|
-
presignedUrls: {
|
|
16
|
-
createDownload: _orpc_server.
|
|
14
|
+
presignedUrls: _orpc_server.Lazyable<{
|
|
15
|
+
createDownload: _orpc_server.Lazyable<_orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
|
|
17
16
|
key: zod.ZodString;
|
|
18
17
|
expiresIn: zod.ZodOptional<zod.ZodNumber>;
|
|
19
18
|
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
@@ -35,8 +34,8 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
|
|
|
35
34
|
suggestion: zod.ZodOptional<zod.ZodString>;
|
|
36
35
|
ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
|
|
37
36
|
}, zod_v4_core.$strip>;
|
|
38
|
-
}, zod_v4_core.$strip>], "success">,
|
|
39
|
-
createUpload: _orpc_server.
|
|
37
|
+
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>>;
|
|
38
|
+
createUpload: _orpc_server.Lazyable<_orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
|
|
40
39
|
key: zod.ZodString;
|
|
41
40
|
contentType: zod.ZodOptional<zod.ZodString>;
|
|
42
41
|
contentLength: zod.ZodOptional<zod.ZodNumber>;
|
|
@@ -60,10 +59,10 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
|
|
|
60
59
|
suggestion: zod.ZodOptional<zod.ZodString>;
|
|
61
60
|
ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
|
|
62
61
|
}, zod_v4_core.$strip>;
|
|
63
|
-
}, zod_v4_core.$strip>], "success">,
|
|
64
|
-
}
|
|
65
|
-
files: {
|
|
66
|
-
list: _orpc_server.
|
|
62
|
+
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>>;
|
|
63
|
+
}>;
|
|
64
|
+
files: _orpc_server.Lazyable<{
|
|
65
|
+
list: _orpc_server.Lazyable<_orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
|
|
67
66
|
prefix: zod.ZodOptional<zod.ZodString>;
|
|
68
67
|
maxKeys: zod.ZodOptional<zod.ZodCoercedNumber<unknown>>;
|
|
69
68
|
continuationToken: zod.ZodOptional<zod.ZodString>;
|
|
@@ -95,8 +94,8 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
|
|
|
95
94
|
suggestion: zod.ZodOptional<zod.ZodString>;
|
|
96
95
|
ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
|
|
97
96
|
}, zod_v4_core.$strip>;
|
|
98
|
-
}, zod_v4_core.$strip>], "success">,
|
|
99
|
-
getMetadata: _orpc_server.
|
|
97
|
+
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>>;
|
|
98
|
+
getMetadata: _orpc_server.Lazyable<_orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
|
|
100
99
|
key: zod.ZodString;
|
|
101
100
|
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
102
101
|
success: zod.ZodLiteral<true>;
|
|
@@ -120,8 +119,8 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
|
|
|
120
119
|
suggestion: zod.ZodOptional<zod.ZodString>;
|
|
121
120
|
ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
|
|
122
121
|
}, zod_v4_core.$strip>;
|
|
123
|
-
}, zod_v4_core.$strip>], "success">,
|
|
124
|
-
delete: _orpc_server.
|
|
122
|
+
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>>;
|
|
123
|
+
delete: _orpc_server.Lazyable<_orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
|
|
125
124
|
key: zod.ZodString;
|
|
126
125
|
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
127
126
|
success: zod.ZodLiteral<true>;
|
|
@@ -138,8 +137,8 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
|
|
|
138
137
|
suggestion: zod.ZodOptional<zod.ZodString>;
|
|
139
138
|
ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
|
|
140
139
|
}, zod_v4_core.$strip>;
|
|
141
|
-
}, zod_v4_core.$strip>], "success">,
|
|
142
|
-
deleteMany: _orpc_server.
|
|
140
|
+
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>>;
|
|
141
|
+
deleteMany: _orpc_server.Lazyable<_orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
|
|
143
142
|
keys: zod.ZodArray<zod.ZodString>;
|
|
144
143
|
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
145
144
|
success: zod.ZodLiteral<true>;
|
|
@@ -156,8 +155,8 @@ declare function createStorageProcedures(deps: StorageProcedureDeps): {
|
|
|
156
155
|
suggestion: zod.ZodOptional<zod.ZodString>;
|
|
157
156
|
ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
|
|
158
157
|
}, zod_v4_core.$strip>;
|
|
159
|
-
}, zod_v4_core.$strip>], "success">,
|
|
160
|
-
}
|
|
158
|
+
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>>;
|
|
159
|
+
}>;
|
|
161
160
|
};
|
|
162
161
|
|
|
163
162
|
export { type StorageProcedureDeps, createStorageProcedures };
|
package/dist/features/storage.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { mapHaiResult } from '../chunk-27PCPE37.js';
|
|
2
|
-
import {
|
|
2
|
+
import { implement, servM } from '../chunk-KIWHKWLD.js';
|
|
3
3
|
import { apiContract } from '@h-ai/api-contract';
|
|
4
4
|
import { ok, err, HaiCommonError } from '@h-ai/core';
|
|
5
|
-
import { implement } from '@orpc/server';
|
|
6
5
|
|
|
7
6
|
var storageContract = apiContract.storage;
|
|
8
7
|
var STORAGE_KEY_MAX_LENGTH = 1024;
|
|
@@ -37,49 +36,45 @@ function validateStorageKeysOrFail(keys, locale) {
|
|
|
37
36
|
return ok(keys);
|
|
38
37
|
}
|
|
39
38
|
function createStorageProcedures(deps) {
|
|
40
|
-
const p = implement(storageContract).$context();
|
|
41
39
|
const { storage } = deps;
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}))
|
|
81
|
-
}
|
|
82
|
-
});
|
|
40
|
+
return implement(storageContract).context().route("presignedUrls.createDownload").auth().handle(async ({ input, context }) => {
|
|
41
|
+
const keyResult = validateStorageKeyOrFail(input.key, context.locale);
|
|
42
|
+
if (!keyResult.success)
|
|
43
|
+
return keyResult;
|
|
44
|
+
const expiresIn = input.expiresIn ?? 900;
|
|
45
|
+
return toPresignedResult(
|
|
46
|
+
keyResult.data,
|
|
47
|
+
expiresIn,
|
|
48
|
+
await storage.presign.getUrl(keyResult.data, { expiresIn })
|
|
49
|
+
);
|
|
50
|
+
}).route("presignedUrls.createUpload").auth().handle(async ({ input, context }) => {
|
|
51
|
+
const keyResult = validateStorageKeyOrFail(input.key, context.locale);
|
|
52
|
+
if (!keyResult.success)
|
|
53
|
+
return keyResult;
|
|
54
|
+
const expiresIn = input.expiresIn ?? 900;
|
|
55
|
+
const contentType = input.contentType ?? "application/octet-stream";
|
|
56
|
+
const { key: _, ...options } = input;
|
|
57
|
+
return toPresignedResult(
|
|
58
|
+
keyResult.data,
|
|
59
|
+
expiresIn,
|
|
60
|
+
await storage.presign.putUrl(keyResult.data, { ...options, expiresIn, contentType })
|
|
61
|
+
);
|
|
62
|
+
}).route("files.list").auth().handle(({ input }) => storage.dir.list(input)).route("files.getMetadata").auth().handle(({ input, context }) => {
|
|
63
|
+
const keyResult = validateStorageKeyOrFail(input.key, context.locale);
|
|
64
|
+
if (!keyResult.success)
|
|
65
|
+
return keyResult;
|
|
66
|
+
return storage.file.head(keyResult.data);
|
|
67
|
+
}).route("files.delete").auth().handle(({ input, context }) => {
|
|
68
|
+
const keyResult = validateStorageKeyOrFail(input.key, context.locale);
|
|
69
|
+
if (!keyResult.success)
|
|
70
|
+
return keyResult;
|
|
71
|
+
return storage.file.delete(keyResult.data);
|
|
72
|
+
}).route("files.deleteMany").auth().handle(({ input, context }) => {
|
|
73
|
+
const keysResult = validateStorageKeysOrFail(input.keys, context.locale);
|
|
74
|
+
if (!keysResult.success)
|
|
75
|
+
return keysResult;
|
|
76
|
+
return storage.file.deleteMany(keysResult.data);
|
|
77
|
+
}).build();
|
|
83
78
|
}
|
|
84
79
|
function toPresignedResult(key, expiresIn, result) {
|
|
85
80
|
return mapHaiResult(result, (url) => ({
|
|
@@ -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 * 通过 `createStorageProcedures(deps)` 组装后直接挂载到 oRPC router。\n *\n * **⚠️ 安全说明(必读):**\n *\n * 1. **路径穿越防护**:内置 `validateStorageKey` 拒绝 `..`、绝对路径、反斜杠、NUL 字节等危险输入。\n * 2. **多租户 / 多用户隔离(需应用层实现)**:默认 procedure 仅做 `requireAuth`,**任何登录用户均可访问任意 key**。\n * 生产环境必须在外层重新装配这些 procedure,以强制用 `context.session.userId` 作为 key 前缀,例如:\n * `users/${context.session.userId}/...`,并拒绝跳出该前缀的访问。\n * 3. **权限收紧**:推荐用 `serv.requirePermission('storage.files.read'|'storage.files.write')` 替换 `requireAuth`。\n * @module features/serv-feature-storage\n */\n\nimport type {\n StorageDeleteFilesInput,\n StorageFileKeyInput,\n StorageListFilesInput,\n StoragePresignDownloadInput,\n StoragePresignUploadInput,\n} from '@h-ai/api-contract'\nimport type { HaiResult } from '@h-ai/core'\nimport type { FileMetadata, ListResult, 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 { implement } from '@orpc/server'\nimport { requireAuth } from '../pipelines/serv-pipeline-require-auth.js'\nimport { servM } from '../serv-i18n.js'\nimport { mapHaiResult } from './serv-feature-helpers.js'\n\nconst storageContract = apiContract.storage\n\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 p = implement(storageContract).$context<ServContext>()\n const { storage } = deps\n\n return p.router({\n presignedUrls: {\n createDownload: p.presignedUrls.createDownload.handler(requireAuth<StoragePresignDownloadInput, PresignedResponse>(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(keyResult.data, expiresIn, await storage.presign.getUrl(keyResult.data, { expiresIn }))\n })),\n createUpload: p.presignedUrls.createUpload.handler(requireAuth<StoragePresignUploadInput, PresignedResponse>(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(keyResult.data, expiresIn, await storage.presign.putUrl(keyResult.data, { ...options, expiresIn, contentType }))\n })),\n },\n files: {\n list: p.files.list.handler(requireAuth<StorageListFilesInput, ListResult>(({ input }) => storage.dir.list(input))),\n getMetadata: p.files.getMetadata.handler(requireAuth<StorageFileKeyInput, FileMetadata>(({ 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 delete: p.files.delete.handler(requireAuth<StorageFileKeyInput, void>(({ 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 deleteMany: p.files.deleteMany.handler(requireAuth<StorageDeleteFilesInput, void>(({ 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 })\n}\n\nfunction toPresignedResult(key: string, expiresIn: number | undefined, result: HaiResult<string>): 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":"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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"]}
|