@h-ai/cli 0.1.0-alpha.57 → 0.1.0-alpha.58

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.
Files changed (175) hide show
  1. package/README.en.md +37 -3
  2. package/README.ja.md +37 -3
  3. package/README.md +37 -3
  4. package/dist/chunk-2H7UOFLK.js +0 -0
  5. package/dist/{chunk-MJGUBOAV.js → chunk-F4IBPVZ5.js} +6 -42
  6. package/dist/chunk-F4IBPVZ5.js.map +1 -0
  7. package/dist/{chunk-O3ANVICQ.js → chunk-WUSFJFW5.js} +13 -7
  8. package/dist/chunk-WUSFJFW5.js.map +1 -0
  9. package/dist/cli-config-templates-6XA7V6CC.js +5 -0
  10. package/dist/{cli-config-templates-OWBEGBZQ.js.map → cli-config-templates-6XA7V6CC.js.map} +1 -1
  11. package/dist/{cli-skill-templates-ZYXES3NT.js → cli-skill-templates-45CK4RQ2.js} +3 -3
  12. package/dist/{cli-skill-templates-ZYXES3NT.js.map → cli-skill-templates-45CK4RQ2.js.map} +1 -1
  13. package/dist/index.js +107 -61
  14. package/dist/index.js.map +1 -1
  15. package/dist/{src-TBIET7YU.js → src-DUGLX4YB.js} +7270 -2119
  16. package/dist/src-DUGLX4YB.js.map +1 -0
  17. package/package.json +17 -17
  18. package/templates/README.md +4 -4
  19. package/templates/apps/admin/src/routes/+layout.svelte.hbs +7 -0
  20. package/templates/apps/admin/src/routes/+page.svelte.hbs +5 -4
  21. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +2 -2
  22. package/templates/apps/api/.npmrc.hbs +1 -2
  23. package/templates/apps/api/Dockerfile.hbs +6 -5
  24. package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +1 -1
  25. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +2 -0
  26. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
  27. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +8 -0
  28. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -1
  29. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +7 -8
  30. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +4 -3
  31. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +0 -1
  32. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/app-procedures.ts.hbs +4 -0
  33. package/templates/apps/api/apps/{{projectName}}-service/tests/service.test.ts.hbs +3 -1
  34. package/templates/apps/api/e2e/api-service.spec.ts.hbs +3 -1
  35. package/templates/apps/api/package.json.hbs +2 -2
  36. package/templates/apps/api/pnpm-workspace.yaml.hbs +26 -0
  37. package/templates/apps/api/tsconfig.base.json +0 -1
  38. package/templates/apps/fullstack/.dockerignore.hbs +3 -0
  39. package/templates/apps/fullstack/.npmrc.hbs +1 -2
  40. package/templates/apps/fullstack/Dockerfile.hbs +6 -5
  41. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +2 -2
  42. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +1 -1
  43. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +7 -2
  44. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/en-US.json.hbs +1 -34
  45. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/zh-CN.json.hbs +1 -34
  46. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +2 -2
  47. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +30 -170
  48. package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.d.ts.hbs +1 -1
  49. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +6 -5
  50. package/templates/apps/fullstack/apps/{{projectName}}-app/src/main.ts.hbs +1 -1
  51. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +48 -22
  52. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -1
  53. package/templates/apps/fullstack/apps/{{projectName}}-desktop/index.html.hbs +6 -1
  54. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/en-US.json.hbs +1 -28
  55. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/zh-CN.json.hbs +1 -28
  56. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +5 -4
  57. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +10 -141
  58. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/Cargo.toml.hbs +5 -5
  59. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +3 -3
  60. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +21 -2
  61. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -2
  62. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +6 -1
  63. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/en-US.json.hbs +1 -28
  64. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/zh-CN.json.hbs +1 -28
  65. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +2 -2
  66. package/templates/apps/fullstack/apps/{{projectName}}-web/src/App.svelte.hbs +5 -143
  67. package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.d.ts.hbs +1 -1
  68. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +1 -1
  69. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +48 -22
  70. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +206 -16
  71. package/templates/apps/fullstack/package.json.hbs +2 -2
  72. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -0
  73. package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +1 -1
  74. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +66 -0
  75. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +96 -0
  76. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-schemas.ts.hbs +3 -0
  77. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +3 -1
  78. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +5 -0
  79. package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +3 -0
  80. package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
  81. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +13 -3
  82. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_audit.yml.hbs +3 -0
  83. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_cache.yml.hbs +2 -0
  84. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +10 -3
  85. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_db.yml.hbs +3 -0
  86. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +21 -0
  87. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +10 -3
  88. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_storage.yml.hbs +3 -0
  89. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +10 -3
  90. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +63 -28
  91. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +26 -11
  92. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +276 -0
  93. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +29 -0
  94. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +94 -0
  95. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -0
  96. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +5 -1
  97. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +98 -0
  98. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +446 -33
  99. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +48 -0
  100. package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +1 -1
  101. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +19 -26
  102. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +78 -15
  103. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +78 -15
  104. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +4 -1
  105. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/index.ts.hbs +3 -4
  106. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +53 -27
  107. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +73 -0
  108. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +55 -45
  109. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AuditManagement.svelte.hbs +61 -0
  110. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +57 -0
  111. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/RoleManagement.svelte.hbs +49 -0
  112. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/UserManagement.svelte.hbs +174 -0
  113. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +399 -0
  114. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +7 -39
  115. package/templates/apps/fullstack/packages/{{projectName}}-shared/svelte.config.js.hbs +1 -0
  116. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +151 -0
  117. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +101 -0
  118. package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -0
  119. package/templates/apps/fullstack/playwright.config.ts.hbs +6 -0
  120. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +24 -0
  121. package/templates/apps/fullstack/tsconfig.base.json +0 -1
  122. package/templates/apps/h5/src/routes/+layout.svelte.hbs +4 -0
  123. package/templates/apps/h5/src/routes/+page.svelte.hbs +3 -2
  124. package/templates/apps/mobile-app/index.html.hbs +2 -2
  125. package/templates/apps/mobile-app/src/App.svelte.hbs +6 -2
  126. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +13 -0
  127. package/templates/apps/website/src/routes/+layout.svelte.hbs +8 -2
  128. package/templates/apps/website/src/routes/+page.svelte.hbs +4 -3
  129. package/templates/base/.npmrc.hbs +1 -15
  130. package/templates/base/Dockerfile.hbs +9 -8
  131. package/templates/base/e2e/app.spec.ts.hbs +1 -0
  132. package/templates/base/package.json.hbs +39 -42
  133. package/templates/base/pnpm-workspace.yaml.hbs +26 -0
  134. package/templates/base/src/app.d.ts.hbs +4 -0
  135. package/templates/base/src/hooks.server.ts.hbs +0 -8
  136. package/templates/base/src/lib/server/init.ts.hbs +26 -5
  137. package/templates/base/src/routes/+layout.server.ts.hbs +18 -0
  138. package/templates/base/tests/scaffold.test.ts.hbs +21 -1
  139. package/templates/base/vite.config.ts.hbs +21 -0
  140. package/templates/features/ai/routes/api/chat/+server.ts +6 -7
  141. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +1 -1
  142. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +1 -1
  143. package/templates/features/iam/routes-shared/api/auth/login/+server.ts +14 -10
  144. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +6 -4
  145. package/templates/features/storage/routes/api/upload/+server.ts +10 -8
  146. package/templates/skills/CLAUDE.md +0 -14
  147. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +13 -2
  148. package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +3 -0
  149. package/templates/skills/hai-api-client/SKILL.md +1 -1
  150. package/templates/skills/hai-app-create/development.md +6 -0
  151. package/templates/skills/hai-build/reference.md +4 -2
  152. package/templates/skills/hai-core/SKILL.md +2 -0
  153. package/templates/skills/hai-core/reference.md +6 -4
  154. package/templates/skills/hai-framework-sync/SKILL.md +1 -2
  155. package/templates/skills/hai-serv/SKILL.md +1 -0
  156. package/templates/skills/hai-serv/reference.md +3 -1
  157. package/templates/skills/hai-ui/SKILL.md +6 -0
  158. package/templates/skills/hai-ui/reference.md +7 -4
  159. package/templates/skills/hai-vecdb/reference.md +53 -49
  160. package/dist/chunk-MJGUBOAV.js.map +0 -1
  161. package/dist/chunk-O3ANVICQ.js.map +0 -1
  162. package/dist/cli-config-templates-OWBEGBZQ.js +0 -5
  163. package/dist/src-TBIET7YU.js.map +0 -1
  164. package/templates/apps/fullstack/apps/{{projectName}}-app/tests/api.test.ts.hbs +0 -28
  165. package/templates/apps/fullstack/apps/{{projectName}}-desktop/tests/api.test.ts.hbs +0 -28
  166. package/templates/apps/fullstack/apps/{{projectName}}-web/tests/api.test.ts.hbs +0 -55
  167. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/LanguageSwitcher.svelte.hbs +0 -27
  168. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ThemeSwitcher.svelte.hbs +0 -18
  169. package/templates/skills/bridges/admin/CLAUDE.md +0 -15
  170. package/templates/skills/bridges/api/CLAUDE.md +0 -15
  171. package/templates/skills/bridges/fullstack/CLAUDE.md.hbs +0 -9
  172. package/templates/skills/bridges/h5/CLAUDE.md +0 -15
  173. package/templates/skills/bridges/mobile-app/CLAUDE.md +0 -15
  174. package/templates/skills/bridges/website/CLAUDE.md +0 -15
  175. package/templates/skills/claude-skill.md.hbs +0 -7
@@ -1,5 +1,5 @@
1
1
  {
2
- "engines": { "node": ">=22.12.0" },
2
+ "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
3
3
  "name": "{{fullstack.servPackageName}}",
4
4
  "type": "module",
5
5
  "version": "0.1.0",
@@ -25,12 +25,19 @@
25
25
  "test": "vitest run",
26
26
  "test:watch": "vitest",
27
27
  "clean": "rimraf dist .turbo",
28
- "dev": "pnpm build && node dist/index.js",
29
- "start": "node dist/index.js"
28
+ "dev": "pnpm build && node --env-file-if-exists=../../.env dist/index.js",
29
+ "start": "node --env-file-if-exists=../../.env dist/index.js"
30
30
  },
31
31
  "dependencies": {
32
+ "@h-ai/audit": "{{haiPackageSpecifier}}",
33
+ "@h-ai/cache": "{{haiPackageSpecifier}}",
32
34
  "@h-ai/core": "{{haiPackageSpecifier}}",
35
+ "@h-ai/crypto": "{{haiPackageSpecifier}}",
36
+ "@h-ai/iam": "{{haiPackageSpecifier}}",
37
+ "@h-ai/reldb": "{{haiPackageSpecifier}}",
33
38
  "@h-ai/serv": "{{haiPackageSpecifier}}",
39
+ "@h-ai/storage": "{{haiPackageSpecifier}}",
40
+ "zod": "{{fullstack.packageSpecifiers.zod}}",
34
41
  "{{fullstack.contractPackageName}}": "workspace:*"
35
42
  }
36
43
  }
@@ -1,48 +1,73 @@
1
- import type { ServConfigInput } from '@h-ai/serv'
2
- import { core } from '@h-ai/core'
1
+ import type { CoreConfig } from '@h-ai/core'
2
+ import type { ServHttpApp } from '@h-ai/serv'
3
+ import { core, err, HaiCommonError, ok } from '@h-ai/core'
4
+ import { createReldbTransportKeyStore, crypto } from '@h-ai/crypto'
5
+ import { iam } from '@h-ai/iam'
6
+ import { reldb } from '@h-ai/reldb'
3
7
  import { serv, ServConfigSchema } from '@h-ai/serv'
4
- import { ENABLED_FRONTENDS, {{fullstack.contractExportName}} } from '{{fullstack.contractPackageName}}'
8
+ import { storage } from '@h-ai/storage'
9
+ import { ENABLED_FRONTENDS, {{fullstack.contractExportName}}, SUPPORTED_LOCALES } from '{{fullstack.contractPackageName}}'
5
10
  import { createServerProcedures } from './server/procedures/index.js'
6
11
 
7
12
  const STARTED_AT = new Date().toISOString()
8
13
 
9
- /** `_core.yml` 中的应用元信息(由 core.config 加载,未初始化时回退到默认值)。 */
10
- interface AppCoreConfig {
11
- name?: string
12
- version?: string
13
- env?: string
14
- }
15
-
16
- /** 应用侧最小 HTTP App 抽象;不把底层 Hono 类型暴露给项目代码。 */
17
- export interface ServerApp {
18
- readonly fetch: (request: Request) => Response | Promise<Response>
19
- readonly request: (input: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>
20
- }
21
-
22
14
  /** 创建后端 HTTP App;与 @h-ai/serv 的 createApp 返回语义保持一致。 */
23
- export function createServerApp(): ServerApp {
24
- // 配置来自 config/_core.yml 与 config/_serv.yml。
25
- // 单元测试可不初始化 core,此时回退到内置默认值。
26
- const coreConfig = core.config.get<AppCoreConfig>('core')
27
- const servConfig = ServConfigSchema.parse(core.config.get<ServConfigInput>('serv') ?? {})
28
- const appName = coreConfig?.name ?? '{{fullstack.servPackageName}}'
29
- const appVersion = coreConfig?.version ?? '0.1.0'
15
+ export function createServerApp(): ServHttpApp {
16
+ const coreConfig = core.config.getOrThrow<CoreConfig>('core')
17
+ const servConfig = ServConfigSchema.parse(core.config.getOrThrow('serv'))
30
18
  const security = serv.createRuntimeSecurityPolicy({
31
- environment: coreConfig?.env,
19
+ environment: coreConfig.env,
32
20
  corsOrigin: servConfig.cors.origin,
33
21
  nativeOrigins: servConfig.cors.nativeOrigins,
34
22
  })
23
+ const publicPaths = [
24
+ ...(servConfig.http.health ? [servConfig.http.health.path, servConfig.http.health.readyPath].filter((path): path is `/${string}` => Boolean(path)) : []),
25
+ ...(servConfig.http.openapi && security.exposeApiDocs ? [servConfig.http.openapi.path] : []),
26
+ ...(servConfig.http.docs && security.exposeApiDocs ? [servConfig.http.docs.path] : []),
27
+ ]
35
28
 
36
29
  return serv.createApp({
37
30
  contract: {{fullstack.contractExportName}},
38
31
  procedures: createServerProcedures({
39
32
  app: {
40
- name: appName,
41
- version: appVersion,
33
+ name: coreConfig.name,
34
+ version: coreConfig.version,
35
+ env: coreConfig.env,
36
+ defaultLocale: coreConfig.defaultLocale,
37
+ supportedLocales: SUPPORTED_LOCALES,
42
38
  startedAt: STARTED_AT,
43
39
  frontends: [...ENABLED_FRONTENDS],
44
40
  },
45
41
  }),
42
+ iam,
43
+ verifyToken: async (token) => {
44
+ const session = await iam.session.verifyToken(token)
45
+ if (!session.success)
46
+ return session
47
+ const user = await iam.user.getUser(session.data.userId)
48
+ if (!user.success)
49
+ return user
50
+ if (!user.data?.enabled)
51
+ return err(HaiCommonError.UNAUTHORIZED, core.i18n.coreM('core_errorUnauthorized'))
52
+ // 会话中的授权同步属于最大努力,业务守卫始终读取最新持久化角色与权限。
53
+ const [roles, permissions] = await Promise.all([
54
+ iam.authz.getUserRoles(user.data.id),
55
+ iam.authz.getUserPermissions(user.data.id),
56
+ ])
57
+ if (!roles.success)
58
+ return roles
59
+ if (!permissions.success)
60
+ return permissions
61
+ return ok({ ...session.data, roles: roles.data.map(role => role.code), permissions: permissions.data.map(permission => permission.code) })
62
+ },
63
+ refreshCookie: {
64
+ secure: security.secureRefreshCookie,
65
+ nativeTokenTransport: { isRequest: request => request.headers.get('x-hai-token-transport') === 'body'
66
+ && ['{{projectName}}-app', '{{projectName}}-desktop'].includes(request.headers.get('x-request-client') ?? '')
67
+ && security.isNativeOrigin(request.headers.get('origin') ?? '') },
68
+ },
69
+ // 管理业务默认强制双向加密;客户端公钥复用 reldb 保存,服务端密钥仅驻留进程。
70
+ transport: { ...(servConfig.transport || {}), excludePaths: publicPaths, crypto, keyStore: createReldbTransportKeyStore({ reldb }) },
46
71
  http: {
47
72
  ...servConfig.http,
48
73
  openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
@@ -52,8 +77,18 @@ export function createServerApp(): ServerApp {
52
77
  middleware: serv.cors({
53
78
  origin: security.allowOrigin,
54
79
  credentials: servConfig.cors.credentials,
55
- allowedHeaders: [...servConfig.cors.allowedHeaders],
56
- exposedHeaders: [...servConfig.cors.exposedHeaders],
80
+ allowedHeaders: [...new Set([...servConfig.cors.allowedHeaders, 'X-Client-Id', 'X-Encrypted', 'X-Request-Client', 'X-HAI-Token-Transport'])],
81
+ exposedHeaders: [...new Set([...servConfig.cors.exposedHeaders, 'X-Encrypted'])],
82
+ }),
83
+ }, {
84
+ path: `${servConfig.http.apiPrefix}/assets/*`,
85
+ middleware: serv.storageAssets({
86
+ storage,
87
+ pathPrefix: `${servConfig.http.apiPrefix}/assets/`,
88
+ keyPattern: /^avatars\/[\w-]+\/[\w-]+\.(png|jpg|webp)$/,
89
+ allowedContentTypes: ['image/png', 'image/jpeg', 'image/webp'],
90
+ cacheControl: 'public, max-age=31536000, immutable',
91
+ crossOriginResourcePolicy: 'cross-origin',
57
92
  }),
58
93
  }],
59
94
  })
@@ -1,18 +1,33 @@
1
+ import type { CoreConfig } from '@h-ai/core'
1
2
  import type { ServConfigInput } from '@h-ai/serv'
3
+ import process from 'node:process'
2
4
  import { core } from '@h-ai/core'
3
5
  import { serv, ServConfigSchema } from '@h-ai/serv'
4
6
  import { createServerApp } from './app-server.js'
7
+ import { closeModules, initializeModules } from './server/runtime.js'
5
8
 
6
- // 加载 config/ 下的 _core.yml / _serv.yml 等配置文件(约定优于配置)
7
- core.init({ configDir: './config', logging: { level: 'info' } })
9
+ async function startServer(): Promise<void> {
10
+ // 加载配置后按依赖顺序初始化;日志设置沿用 _core.yml。
11
+ core.init({ configDir: './config' })
12
+ await initializeModules()
13
+ try {
14
+ // HOST/PORT 环境变量优先于 config/_serv.yml。
15
+ const { server } = ServConfigSchema.parse(core.config.getOrThrow<ServConfigInput>('serv'))
16
+ const app = createServerApp()
17
+ serv.listen(app, {
18
+ host: server.host,
19
+ port: server.port,
20
+ onListening: info => core.logger.info('Application listening', { name: core.config.getOrThrow<CoreConfig>('core').name, host: server.host, port: info.port }),
21
+ onClose: closeModules,
22
+ })
23
+ }
24
+ catch (error) {
25
+ await closeModules()
26
+ throw error
27
+ }
28
+ }
8
29
 
9
- // host/port 来自 config/_serv.yml 的 server(HOST/PORT 环境变量高于配置文件)。
10
- const { server } = ServConfigSchema.parse(core.config.get<ServConfigInput>('serv') ?? {})
11
- const app = createServerApp()
12
- const listenApp = app as Parameters<typeof serv.listen>[0]
13
-
14
- serv.listen(listenApp, {
15
- host: server.host,
16
- port: server.port,
17
- onListening: info => core.logger.info('{{fullstack.servPackageName}} listening', { host: server.host, port: info.port }),
30
+ void startServer().catch((error: unknown) => {
31
+ core.logger.fatal('Application startup failed', { error: error instanceof Error ? error.message : 'Unknown startup failure' })
32
+ process.exitCode = 1
18
33
  })
@@ -0,0 +1,276 @@
1
+ import type { HaiResult } from '@h-ai/core'
2
+ import type { Role, User } from '@h-ai/iam'
3
+ import type { ServConfig, ServContext } from '@h-ai/serv'
4
+ import type { AdminCreateUser, AdminRole, AdminRoleCode, AdminUpdateUser, AdminUser, AvatarUpload, ProfileUpdate } from '{{fullstack.contractPackageName}}'
5
+ import { Buffer } from 'node:buffer'
6
+ import { audit } from '@h-ai/audit'
7
+ import { cache } from '@h-ai/cache'
8
+ import { core, err, HaiCommonError, ok } from '@h-ai/core'
9
+ import { iam } from '@h-ai/iam'
10
+ import { storage } from '@h-ai/storage'
11
+ import { SUPPORTED_LOCALES } from '{{fullstack.contractPackageName}}'
12
+ import { serverM } from './messages.js'
13
+
14
+ const ROLE_CODES = new Set<AdminRoleCode>(['admin', 'user'])
15
+ const SUPPORTED_THEMES = [{{#each fullstack.theme.themes}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}] as readonly string[]
16
+ let adminMutationActive = false
17
+
18
+ /** 管理写操作串行化,锁内重新鉴权,防止两个管理员同时互相删除或降级。 */
19
+ export async function withAdminMutation<T>(context: ServContext, work: () => Promise<HaiResult<T>>): Promise<HaiResult<T>> {
20
+ // mini 的管理写入仅支持单写实例;进程内保护无租期,慢请求不会在 TTL 到期后被并发穿透。
21
+ if (adminMutationActive)
22
+ return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
23
+ adminMutationActive = true
24
+ const owner = core.id.uuid()
25
+ const key = 'hai:app:admin-mutation'
26
+ let acquired = false
27
+ let leaseLost = false
28
+ let renewalPending = false
29
+ let renewalTimer: ReturnType<typeof setInterval> | undefined
30
+ try {
31
+ const locked = await cache.lock.acquire(key, { owner, ttl: 300 })
32
+ if (!locked.success)
33
+ return locked
34
+ if (!locked.data)
35
+ return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
36
+ acquired = true
37
+ renewalTimer = setInterval(() => {
38
+ if (renewalPending || leaseLost)
39
+ return
40
+ renewalPending = true
41
+ void cache.lock.extend(key, 300, owner).then((renewed) => {
42
+ if (!renewed.success || !renewed.data) {
43
+ leaseLost = true
44
+ core.logger.error('Admin mutation lease renewal failed', { owner, error: renewed.success ? 'lease_not_owned' : renewed.error.code })
45
+ }
46
+ }).catch(() => {
47
+ leaseLost = true
48
+ core.logger.error('Admin mutation lease renewal failed', { owner, error: 'unexpected_renewal_failure' })
49
+ }).finally(() => {
50
+ renewalPending = false
51
+ })
52
+ }, 100_000)
53
+ renewalTimer.unref()
54
+ const actor = await getAdminUser(context.session?.userId ?? '')
55
+ if (!actor.success)
56
+ return actor
57
+ if (!actor.data.enabled || !actor.data.roles.includes('admin'))
58
+ return err(HaiCommonError.FORBIDDEN, serverM('selfProtection', { locale: context.locale }))
59
+ if (leaseLost)
60
+ return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
61
+ return await work()
62
+ }
63
+ finally {
64
+ if (renewalTimer)
65
+ clearInterval(renewalTimer)
66
+ try {
67
+ if (acquired) {
68
+ const released = await cache.lock.release(key, owner)
69
+ if (!released.success || !released.data)
70
+ core.logger.error('Failed to release admin mutation lock', { owner, error: released.success ? 'lease_not_owned' : released.error.code })
71
+ }
72
+ }
73
+ finally {
74
+ adminMutationActive = false
75
+ }
76
+ }
77
+ }
78
+
79
+ /** 用户与角色读取失败直接透传,不能把角色查询失败当作普通用户。 */
80
+ export async function getAdminUser(id: string): Promise<HaiResult<AdminUser>> {
81
+ const user = await iam.user.getUser(id)
82
+ if (!user.success)
83
+ return user
84
+ if (!user.data)
85
+ return err(HaiCommonError.NOT_FOUND, serverM('notFound'))
86
+ const roles = await iam.authz.getUserRoles(id)
87
+ if (!roles.success)
88
+ return roles
89
+ return ok(toAdminUser(user.data, roles.data))
90
+ }
91
+
92
+ export function toAdminUser(user: User, roles: Role[]): AdminUser {
93
+ return {
94
+ id: user.id,
95
+ username: user.username,
96
+ email: user.email,
97
+ displayName: user.displayName,
98
+ avatarUrl: user.avatarUrl,
99
+ enabled: user.enabled,
100
+ roles: roles.map(role => role.code).filter(isRoleCode),
101
+ createdAt: user.createdAt.toISOString(),
102
+ updatedAt: user.updatedAt.toISOString(),
103
+ }
104
+ }
105
+
106
+ export function isRoleCode(code: string): code is AdminRoleCode {
107
+ return ROLE_CODES.has(code as AdminRoleCode)
108
+ }
109
+
110
+ export async function createAdminUser(input: AdminCreateUser, context: ServContext): Promise<HaiResult<AdminUser>> {
111
+ const role = await iam.authz.getRoleByCode(input.role)
112
+ if (!role.success)
113
+ return role
114
+ if (!role.data)
115
+ return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
116
+ const registered = await iam.user.register(input)
117
+ if (!registered.success)
118
+ return registered
119
+ const userId = registered.data.user.id
120
+ const assigned = await iam.authz.syncRoles(userId, [role.data.id])
121
+ if (!assigned.success) {
122
+ await rollbackCreatedUser(userId)
123
+ return assigned
124
+ }
125
+ if (!input.enabled) {
126
+ const disabled = await iam.user.updateUser(userId, { enabled: false })
127
+ if (!disabled.success) {
128
+ await rollbackCreatedUser(userId)
129
+ return disabled
130
+ }
131
+ }
132
+ await recordAudit(context, 'create', 'users', userId, { role: input.role, enabled: input.enabled })
133
+ return getAdminUser(userId)
134
+ }
135
+
136
+ async function rollbackCreatedUser(id: string): Promise<void> {
137
+ const removed = await iam.user.deleteUser(id)
138
+ if (!removed.success)
139
+ core.logger.error('Failed to roll back user creation', { userId: id, error: removed.error.code })
140
+ }
141
+
142
+ export async function updateAdminUser(input: AdminUpdateUser, context: ServContext): Promise<HaiResult<AdminUser>> {
143
+ if (input.id === context.session?.userId && (input.enabled === false || input.role === 'user'))
144
+ return err(HaiCommonError.FORBIDDEN, serverM('selfProtection', { locale: context.locale }))
145
+ const existing = await iam.user.getUser(input.id)
146
+ if (!existing.success)
147
+ return existing
148
+ if (!existing.data)
149
+ return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
150
+ const role = input.role ? await iam.authz.getRoleByCode(input.role) : undefined
151
+ if (role && !role.success)
152
+ return role
153
+ if (role?.success && !role.data)
154
+ return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
155
+ const { id, role: _role, ...profile } = input
156
+ const previousRoles = await iam.authz.getUserRoles(id)
157
+ if (!previousRoles.success)
158
+ return previousRoles
159
+ if (role?.success && role.data) {
160
+ const assigned = await iam.authz.syncRoles(id, [role.data.id])
161
+ if (!assigned.success)
162
+ return assigned
163
+ }
164
+ const updated = await iam.user.updateUser(id, profile)
165
+ if (!updated.success) {
166
+ // 角色 ID 可精确还原,避免 undefined 资料字段被 updateUser 忽略后无法补偿。
167
+ if (role?.success && role.data) {
168
+ const restored = await iam.authz.syncRoles(id, previousRoles.data.map(item => item.id))
169
+ if (!restored.success)
170
+ core.logger.error('Failed to restore roles after user update failure', { userId: id, error: restored.error.code })
171
+ }
172
+ return updated
173
+ }
174
+ await recordAudit(context, 'update', 'users', id, { fields: Object.keys(profile), role: input.role })
175
+ return getAdminUser(id)
176
+ }
177
+
178
+ export async function getAdminRoles(): Promise<HaiResult<{ items: AdminRole[] }>> {
179
+ const roles = await iam.authz.getAllRoles({ page: 1, pageSize: 100 })
180
+ if (!roles.success)
181
+ return roles
182
+ const items = roles.data.items.filter(role => isRoleCode(role.code))
183
+ const counts = await iam.authz.getRoleUserCounts(items.map(role => role.id))
184
+ if (!counts.success)
185
+ return counts
186
+ return ok({ items: items.map(role => ({
187
+ id: role.id,
188
+ code: role.code as AdminRoleCode,
189
+ name: role.name,
190
+ description: role.description,
191
+ userCount: counts.data.get(role.id) ?? 0,
192
+ })) })
193
+ }
194
+
195
+ export async function updateProfile(input: ProfileUpdate, context: ServContext): Promise<HaiResult<AdminUser>> {
196
+ const token = context.accessToken ?? ''
197
+ const current = await iam.user.getCurrentUser(token)
198
+ if (!current.success)
199
+ return current
200
+ if ((input.locale && !(SUPPORTED_LOCALES as readonly string[]).includes(input.locale)) || (input.theme && !SUPPORTED_THEMES.includes(input.theme)))
201
+ return err(HaiCommonError.VALIDATION_ERROR, serverM('unsupportedPreference', { locale: context.locale }))
202
+ const metadata = { ...current.data.metadata }
203
+ if (input.locale)
204
+ metadata.locale = input.locale
205
+ if (input.theme)
206
+ metadata.theme = input.theme
207
+ const updated = await iam.user.updateCurrentUser(token, { displayName: input.displayName, email: input.email, metadata })
208
+ if (!updated.success)
209
+ return updated
210
+ await recordAudit(context, 'update', 'profile', current.data.id, { fields: Object.keys(input) })
211
+ return getAdminUser(current.data.id)
212
+ }
213
+
214
+ /** 上传路径由服务端生成,禁止客户端写入任意 storage key。 */
215
+ export async function uploadAvatar(userId: string, input: AvatarUpload, context: ServContext): Promise<HaiResult<{ avatarUrl: string }>> {
216
+ const current = await iam.user.getUser(userId)
217
+ if (!current.success)
218
+ return current
219
+ if (!current.data)
220
+ return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
221
+ const bytes = Buffer.from(input.base64, 'base64')
222
+ if (bytes.length === 0 || bytes.length > 512 * 1024 || bytes.toString('base64') !== input.base64 || !matchesImage(bytes, input.contentType))
223
+ return err(HaiCommonError.VALIDATION_ERROR, serverM('invalidAvatar', { locale: context.locale }))
224
+ const extension = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' }[input.contentType]
225
+ const key = `avatars/${userId}/${core.id.uuid()}.${extension}`
226
+ const stored = await storage.file.put(key, bytes, { contentType: input.contentType })
227
+ if (!stored.success)
228
+ return stored
229
+ const apiPrefix = core.config.getOrThrow<ServConfig>('serv').http.apiPrefix
230
+ // 只保存生成资源路径,公开 Origin 由客户端已配置的 API 地址决定,禁止信任 Host。
231
+ const avatarUrl = `${apiPrefix}/assets/${key}`
232
+ const updated = await iam.user.updateUser(userId, { avatarUrl })
233
+ if (!updated.success) {
234
+ const cleaned = await storage.file.delete(key)
235
+ if (!cleaned.success)
236
+ core.logger.warn('Failed to clean up rejected avatar', { error: cleaned.error.code })
237
+ return updated
238
+ }
239
+ // 更新成功后删除旧头像;只清理此用户且符合服务端生成规则的资源。
240
+ const oldKey = current.data.avatarUrl?.split('/assets/')[1]
241
+ if (oldKey && oldKey.startsWith(`avatars/${userId}/`) && /^avatars\/[\w-]+\/[\w-]+\.(?:png|jpg|webp)$/.test(oldKey)) {
242
+ const cleaned = await storage.file.delete(oldKey)
243
+ if (!cleaned.success)
244
+ core.logger.warn('Failed to clean up previous avatar', { error: cleaned.error.code })
245
+ }
246
+ await recordAudit(context, 'update', 'avatars', userId)
247
+ return ok({ avatarUrl })
248
+ }
249
+
250
+ function matchesImage(bytes: Buffer, contentType: AvatarUpload['contentType']): boolean {
251
+ if (contentType === 'image/png') {
252
+ if (bytes.length < 33 || bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a' || bytes.toString('ascii', 12, 16) !== 'IHDR')
253
+ return false
254
+ const width = bytes.readUInt32BE(16)
255
+ const height = bytes.readUInt32BE(20)
256
+ return width > 0 && height > 0 && width <= 4096 && height <= 4096
257
+ }
258
+ if (contentType === 'image/jpeg')
259
+ return bytes.length >= 4 && bytes.subarray(0, 3).toString('hex') === 'ffd8ff' && bytes.subarray(-2).toString('hex') === 'ffd9'
260
+ return bytes.length >= 20 && bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP' && bytes.readUInt32LE(4) + 8 === bytes.length
261
+ }
262
+
263
+ /** 审计记录仅含操作字段与资源标识,不记录密码、token、头像内容或联系信息。 */
264
+ export async function recordAudit(context: ServContext, action: string, resource: string, resourceId?: string, details?: Record<string, unknown>): Promise<void> {
265
+ const result = await audit.log({
266
+ userId: context.session?.userId,
267
+ action,
268
+ resource,
269
+ resourceId,
270
+ details,
271
+ ipAddress: context.ip,
272
+ userAgent: context.userAgent,
273
+ })
274
+ if (!result.success)
275
+ core.logger.error('Failed to record audit event', { action, resource, error: result.error.code })
276
+ }
@@ -0,0 +1,29 @@
1
+ import { core } from '@h-ai/core'
2
+
3
+ /** 服务端业务提示与角色种子名称同步维护中英文。 */
4
+ export const serverM = core.i18n.createMessageGetter({
5
+ 'zh-CN': {
6
+ admin: '管理员',
7
+ user: '普通用户',
8
+ adminDescription: '管理用户、角色和审计日志',
9
+ userDescription: '仅可维护个人设置',
10
+ notFound: '用户或角色不存在',
11
+ selfProtection: '不能删除、禁用或降级当前管理员账号',
12
+ invalidAvatar: '请选择不超过 512 KB 的有效 PNG、JPEG 或 WebP 图片',
13
+ busy: '系统繁忙,请稍后重试',
14
+ unsupportedPreference: '不支持此语言或主题',
15
+ invalidDateRange: '结束时间不能早于开始时间',
16
+ },
17
+ 'en-US': {
18
+ admin: 'Administrator',
19
+ user: 'User',
20
+ adminDescription: 'Manage users, roles and audit logs',
21
+ userDescription: 'Manage personal settings only',
22
+ notFound: 'User or role does not exist',
23
+ selfProtection: 'You cannot delete, disable or demote your own administrator account',
24
+ invalidAvatar: 'Choose a valid PNG, JPEG or WebP image up to 512 KB',
25
+ busy: 'The system is busy. Please try again',
26
+ unsupportedPreference: 'This language or theme is not supported',
27
+ invalidDateRange: 'End date must not precede start date',
28
+ },
29
+ })
@@ -0,0 +1,94 @@
1
+ import type { ServContext } from '@h-ai/serv'
2
+ import { audit } from '@h-ai/audit'
3
+ import { err, HaiCommonError, ok } from '@h-ai/core'
4
+ import { iam } from '@h-ai/iam'
5
+ import { serv } from '@h-ai/serv'
6
+ import { createIamProcedures } from '@h-ai/serv/features/iam'
7
+ import { adminContract, authContract, profileContract } from '{{fullstack.contractPackageName}}'
8
+ import {
9
+ createAdminUser,
10
+ getAdminRoles,
11
+ isRoleCode,
12
+ recordAudit,
13
+ toAdminUser,
14
+ updateAdminUser,
15
+ updateProfile,
16
+ uploadAvatar,
17
+ withAdminMutation,
18
+ } from '../admin-service.js'
19
+ import { serverM } from '../messages.js'
20
+
21
+ /** 身份认证沿用框架公共契约与 refresh-cookie 传输。 */
22
+ export function createAuthProcedures() {
23
+ const { currentUser, refresh } = createIamProcedures({ iam }).auth
24
+ const { login, logout, changePassword } = authContract
25
+ // 仅覆写需要业务审计的认证操作,其余直接复用框架;未选认证入口不挂载。
26
+ const audited = serv.implement({ login, logout, changePassword }).context<ServContext>().route('login', async ({ input, context }) => {
27
+ const result = await iam.auth.login(input)
28
+ if (!result.success)
29
+ return result
30
+ await recordAudit({ ...context, session: { userId: result.data.user.id, roles: result.data.roles, permissions: result.data.permissions } }, 'login', 'auth')
31
+ return result
32
+ }).route('logout').auth().handle(async ({ context }) => {
33
+ const result = await iam.auth.logout(context.accessToken ?? '')
34
+ if (!result.success)
35
+ return result
36
+ await recordAudit(context, 'logout', 'auth')
37
+ return result
38
+ }).route('changePassword').auth().handle(async ({ input, context }) => {
39
+ const result = await iam.user.changeCurrentUserPassword(context.accessToken ?? '', input.oldPassword, input.newPassword)
40
+ if (!result.success)
41
+ return result
42
+ await recordAudit(context, 'password-change', 'profile', context.session.userId)
43
+ return result
44
+ }).build()
45
+ return { ...audited, currentUser, refresh }
46
+ }
47
+
48
+ export function createAdminProcedures() {
49
+ return serv.implement(adminContract).context<ServContext>().route('users.list').role('admin').handle(async ({ input }) => {
50
+ const users = await iam.user.listUsers(input)
51
+ if (!users.success)
52
+ return users
53
+ const roles = await iam.authz.getUserRolesForMany(users.data.items.map(user => user.id))
54
+ if (!roles.success)
55
+ return roles
56
+ return ok({ ...users.data, items: users.data.items.map(user => toAdminUser(user, roles.data.get(user.id) ?? [])) })
57
+ }).route('users.create').role('admin').handle(({ input, context }) => withAdminMutation(context, () => createAdminUser(input, context))).route('users.update').role('admin').handle(({ input, context }) => withAdminMutation(context, () => updateAdminUser(input, context))).route('users.delete').role('admin').handle(({ input, context }) => withAdminMutation(context, async () => {
58
+ if (context.session.userId === input.id)
59
+ return err(HaiCommonError.FORBIDDEN, serverM('selfProtection', { locale: context.locale }))
60
+ const result = await iam.user.deleteUser(input.id)
61
+ if (!result.success)
62
+ return result
63
+ await recordAudit(context, 'delete', 'users', input.id)
64
+ return ok({ deleted: true as const })
65
+ })).route('users.avatar').role('admin').handle(({ input, context }) => withAdminMutation(context, () => uploadAvatar(input.id, input, context))).route('roles.list').role('admin').handle(() => getAdminRoles()).route('roles.update').role('admin').handle(({ input, context }) => withAdminMutation(context, async () => {
66
+ const role = await iam.authz.getRole(input.id)
67
+ if (!role.success)
68
+ return role
69
+ if (!role.data || !isRoleCode(role.data.code))
70
+ return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
71
+ const updated = await iam.authz.updateRole(input.id, { description: input.description })
72
+ if (!updated.success)
73
+ return updated
74
+ const roles = await getAdminRoles()
75
+ if (!roles.success)
76
+ return roles
77
+ const result = roles.data.items.find(item => item.id === input.id)
78
+ if (!result)
79
+ return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
80
+ await recordAudit(context, 'update', 'roles', input.id, { fields: ['description'] })
81
+ return ok(result)
82
+ })).route('audit.list').role('admin').handle(async ({ input, context }) => {
83
+ if (input.startDate && input.endDate && input.startDate > input.endDate)
84
+ return err(HaiCommonError.VALIDATION_ERROR, serverM('invalidDateRange', { locale: context.locale }))
85
+ const result = await audit.list({ ...input, startDate: input.startDate ? new Date(input.startDate) : undefined, endDate: input.endDate ? new Date(input.endDate) : undefined })
86
+ if (!result.success)
87
+ return result
88
+ return ok({ ...result.data, page: input.page, pageSize: input.pageSize, items: result.data.items.map(item => ({ ...item, createdAt: new Date(item.createdAt).toISOString() })) })
89
+ }).build()
90
+ }
91
+
92
+ export function createProfileProcedures() {
93
+ return serv.implement(profileContract).context<ServContext>().route('update').auth().handle(({ input, context }) => updateProfile(input, context)).route('avatar').auth().handle(({ input, context }) => uploadAvatar(context.session.userId, input, context)).build()
94
+ }
@@ -7,6 +7,9 @@ import { appContract } from '{{fullstack.contractPackageName}}'
7
7
  export interface CreateAppProceduresOptions {
8
8
  readonly name: string
9
9
  readonly version: string
10
+ readonly env: string
11
+ readonly defaultLocale: string
12
+ readonly supportedLocales: readonly string[]
10
13
  readonly startedAt: string
11
14
  readonly frontends: readonly AppFrontendTarget[]
12
15
  }
@@ -32,6 +35,9 @@ function createAppInfo(options: CreateAppProceduresOptions): AppInfoOutputData {
32
35
  return {
33
36
  name: options.name,
34
37
  version: options.version,
38
+ env: options.env,
39
+ defaultLocale: options.defaultLocale,
40
+ supportedLocales: [...options.supportedLocales],
35
41
  startedAt: options.startedAt,
36
42
  frontends: [...options.frontends],
37
43
  }
@@ -1,4 +1,5 @@
1
1
  import type { CreateAppProceduresOptions } from './app-procedures.js'
2
+ import { createAdminProcedures, createAuthProcedures, createProfileProcedures } from './admin-procedures.js'
2
3
  import { createAppProcedures } from './app-procedures.js'
3
4
 
4
5
  export interface CreateServerProceduresOptions {
@@ -9,5 +10,8 @@ export interface CreateServerProceduresOptions {
9
10
  export function createServerProcedures(options: CreateServerProceduresOptions) {
10
11
  return {
11
12
  app: createAppProcedures(options.app),
13
+ iam: { auth: createAuthProcedures() },
14
+ admin: createAdminProcedures(),
15
+ profile: createProfileProcedures(),
12
16
  }
13
- }
17
+ }