@h-ai/cli 1.0.0-beta.2 → 1.0.0-beta.21

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 (254) hide show
  1. package/README.en.md +74 -154
  2. package/README.ja.md +74 -154
  3. package/README.md +74 -154
  4. package/dist/{chunk-AKV2CQZV.js → chunk-2ZL5ORQ2.js} +316 -97
  5. package/dist/chunk-2ZL5ORQ2.js.map +1 -0
  6. package/dist/{chunk-EF7WOFWZ.js → chunk-L53P43B2.js} +1312 -163
  7. package/dist/chunk-L53P43B2.js.map +1 -0
  8. package/dist/cli-config-templates-LWMD2I6V.js +5 -0
  9. package/dist/{cli-config-templates-HHFHNXBN.js.map → cli-config-templates-LWMD2I6V.js.map} +1 -1
  10. package/dist/{cli-skill-templates-PN4FLGBC.js → cli-skill-templates-D6S2OQ6X.js} +3 -3
  11. package/dist/{cli-skill-templates-PN4FLGBC.js.map → cli-skill-templates-D6S2OQ6X.js.map} +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +30118 -3253
  14. package/dist/index.js.map +1 -1
  15. package/dist/metafile-esm.json +1 -1
  16. package/package.json +22 -18
  17. package/templates/README.md +45 -30
  18. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -10
  19. package/templates/apps/admin/src/routes/+page.svelte.hbs +4 -4
  20. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +17 -7
  21. package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +26 -6
  22. package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +2 -2
  23. package/templates/apps/api/.dockerignore.hbs +10 -0
  24. package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +8 -0
  25. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +6 -1
  26. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +13 -5
  27. package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +13 -5
  28. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  29. package/templates/apps/api/apps/{{projectName}}-serv/README.md.hbs +6 -2
  30. package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +24 -2
  31. package/templates/apps/api/apps/{{projectName}}-serv/package.json.hbs +4 -4
  32. package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +74 -24
  33. package/templates/apps/api/apps/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  34. package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +153 -37
  35. package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/index.ts.hbs +39 -13
  36. package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +106 -33
  37. package/templates/apps/api/apps/{{projectName}}-serv/tsup.config.ts.hbs +3 -1
  38. package/templates/apps/api/apps/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  39. package/templates/apps/api/e2e/api-serv.spec.ts.hbs +20 -3
  40. package/templates/apps/api/e2e/capture-evidence.ts.hbs +4 -1
  41. package/templates/apps/api/eslint.config.js.hbs +3 -1
  42. package/templates/apps/api/images/serv.Dockerfile.hbs +15 -10
  43. package/templates/apps/api/playwright.config.ts.hbs +23 -19
  44. package/templates/apps/api/pnpm-workspace.yaml.hbs +27 -16
  45. package/templates/apps/fullstack/.dockerignore.hbs +10 -0
  46. package/templates/apps/fullstack/.gitignore.hbs +7 -2
  47. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +14 -12
  48. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +5 -3
  49. package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +10 -2
  50. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +1 -1
  51. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +10 -2
  52. package/templates/apps/fullstack/apps/{{projectName}}-app/src/build-metadata.ts.hbs +0 -1
  53. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +0 -1
  54. package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +4 -1
  55. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +16 -4
  56. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +10 -8
  57. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +3 -3
  58. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +8 -2
  59. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/build-metadata.ts.hbs +0 -1
  60. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +0 -2
  61. package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +4 -1
  62. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +16 -4
  63. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +2 -0
  64. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +9 -9
  65. package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +10 -2
  66. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +1 -0
  67. package/templates/apps/fullstack/apps/{{projectName}}-web/src/build-metadata.ts.hbs +0 -1
  68. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +0 -2
  69. package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +4 -1
  70. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +19 -5
  71. package/templates/apps/fullstack/docs/security-configuration.md.hbs +4 -1
  72. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +140 -28
  73. package/templates/apps/fullstack/e2e/session-close.spec.ts.hbs +19 -5
  74. package/templates/apps/fullstack/eslint.config.js.hbs +3 -1
  75. package/templates/apps/fullstack/images/serv.Dockerfile.hbs +19 -11
  76. package/templates/apps/fullstack/images/web.Dockerfile.hbs +17 -8
  77. package/templates/apps/fullstack/package.json.hbs +2 -1
  78. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +7 -5
  79. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +114 -23
  80. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +57 -11
  81. package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  82. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +17 -7
  83. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +13 -7
  84. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +17 -7
  85. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +10 -4
  86. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +65 -30
  87. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  88. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +1 -2
  89. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +69 -21
  90. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs +21 -5
  91. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +63 -11
  92. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -1
  93. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -1
  94. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +147 -33
  95. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/security-roles.ts.hbs +43 -12
  96. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +146 -30
  97. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +33 -10
  98. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +142 -26
  99. package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +6 -2
  100. package/templates/apps/fullstack/packages/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  101. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +48 -66
  102. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +1 -0
  103. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +1 -0
  104. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +1 -0
  105. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +9 -2
  106. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/feedback-client.ts.hbs +0 -3
  107. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/sensitive-actions.svelte.ts.hbs +15 -3
  108. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +33 -7
  109. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/FeedbackEntry.svelte.hbs +16 -2
  110. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +104 -13
  111. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/feedback.svelte.hbs +37 -7
  112. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/roles.svelte.hbs +9 -3
  113. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +25 -3
  114. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +133 -19
  115. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/security/role-label.ts.hbs +14 -2
  116. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +66 -34
  117. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +132 -30
  118. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +8 -2
  119. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +23 -5
  120. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +143 -21
  121. package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -2
  122. package/templates/apps/fullstack/playwright.config.ts.hbs +25 -3
  123. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +30 -14
  124. package/templates/apps/h5/src/routes/+layout.svelte.hbs +74 -30
  125. package/templates/apps/h5/src/routes/+page.svelte.hbs +2 -2
  126. package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +2 -2
  127. package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +2 -2
  128. package/templates/apps/mobile-app/capacitor.config.ts.hbs +5 -2
  129. package/templates/apps/mobile-app/messages/en-US.json +2 -2
  130. package/templates/apps/mobile-app/messages/ja-JP.json +2 -2
  131. package/templates/apps/mobile-app/messages/zh-CN.json +2 -2
  132. package/templates/apps/mobile-app/src/App.svelte.hbs +17 -5
  133. package/templates/apps/mobile-app/src/lib/api.ts.hbs +20 -6
  134. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +8 -1
  135. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +6 -2
  136. package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -1
  137. package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +5 -1
  138. package/templates/apps/website/scripts/static-website.mjs.hbs +19 -6
  139. package/templates/apps/website/src/routes/+layout.svelte.hbs +56 -29
  140. package/templates/apps/website/src/routes/+page.svelte.hbs +4 -4
  141. package/templates/apps/website/src/routes/about/+page.svelte.hbs +2 -2
  142. package/templates/apps/website/src/routes/contact/+page.svelte.hbs +2 -2
  143. package/templates/apps/website/src/routes/services/+page.svelte.hbs +20 -5
  144. package/templates/base/.dockerignore.hbs +10 -0
  145. package/templates/base/.gitignore.hbs +7 -2
  146. package/templates/base/e2e/app.spec.ts.hbs +130 -36
  147. package/templates/base/eslint-rules.js +44 -25
  148. package/templates/base/eslint.config.js.hbs +3 -1
  149. package/templates/base/images/web.Dockerfile.hbs +25 -14
  150. package/templates/base/package.json.hbs +13 -7
  151. package/templates/base/patches/daisyui-5.7.50.patch +8 -0
  152. package/templates/base/patches/deepmerge-4.3.1.patch +51 -0
  153. package/templates/base/playwright.config.ts +3 -1
  154. package/templates/base/pnpm-workspace.yaml.hbs +33 -16
  155. package/templates/base/src/app.d.ts.hbs +1 -1
  156. package/templates/base/src/app.html.hbs +1 -0
  157. package/templates/base/src/hooks.server.ts.hbs +44 -33
  158. package/templates/base/src/lib/build-metadata.ts.hbs +0 -1
  159. package/templates/base/src/lib/server/init.ts.hbs +146 -35
  160. package/templates/base/src/lib/server/kit-config.ts.hbs +7 -4
  161. package/templates/base/src/routes/+layout.server.ts.hbs +2 -2
  162. package/templates/base/svelte.config.js.hbs +10 -100
  163. package/templates/base/tests/scaffold.test.ts.hbs +12 -3
  164. package/templates/base/tsconfig.json.hbs +7 -4
  165. package/templates/base/vite.config.ts.hbs +108 -14
  166. package/templates/base/vitest.config.ts.hbs +6 -2
  167. package/templates/features/ai/routes/api/chat/+server.ts +3 -1
  168. package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +3 -3
  169. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +3 -3
  170. package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +3 -3
  171. package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +3 -3
  172. package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +3 -3
  173. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +3 -3
  174. package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +3 -3
  175. package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +3 -3
  176. package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +3 -3
  177. package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +3 -3
  178. package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +3 -3
  179. package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +3 -3
  180. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +14 -3
  181. package/templates/shared/browser-check/startup.js +115 -0
  182. package/templates/shared/browser-check/update.css +37 -0
  183. package/templates/shared/browser-check/update.html +19 -0
  184. package/templates/shared/browser-startup.spec.ts +23 -0
  185. package/templates/shared/build-metadata.ts +18 -4
  186. package/templates/shared/e2e-env.ts +22 -6
  187. package/templates/shared/native-name.d.mts +5 -0
  188. package/templates/shared/native-name.mjs +94 -0
  189. package/templates/shared/public-base-path.mjs +17 -4
  190. package/templates/shared/run-e2e.mjs +46 -12
  191. package/templates/shared/run-node.mjs +56 -0
  192. package/templates/shared/run-tauri.mjs +74 -0
  193. package/templates/shared/vite-runtime-config.ts +9 -3
  194. package/templates/skills/AGENTS.md +10 -73
  195. package/templates/skills/bridges/admin/AGENTS.md +9 -65
  196. package/templates/skills/bridges/api/AGENTS.md +8 -59
  197. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +9 -54
  198. package/templates/skills/bridges/h5/AGENTS.md +8 -63
  199. package/templates/skills/bridges/mobile-app/AGENTS.md +8 -62
  200. package/templates/skills/bridges/website/AGENTS.md +8 -63
  201. package/templates/skills/hai-ai/SKILL.md +6 -2
  202. package/templates/skills/hai-ai/acp.md +153 -0
  203. package/templates/skills/hai-ai/guide.md +27 -12
  204. package/templates/skills/hai-ai/reference.md +24 -15
  205. package/templates/skills/hai-ai/resources.md +69 -0
  206. package/templates/skills/hai-api-client/SKILL.md +1 -1
  207. package/templates/skills/hai-api-client/reference.md +3 -1
  208. package/templates/skills/hai-app-create/collaboration.md +7 -0
  209. package/templates/skills/hai-app-create/development.md +19 -10
  210. package/templates/skills/hai-app-create/project.md +33 -0
  211. package/templates/skills/hai-app-create/reference.md +10 -10
  212. package/templates/skills/hai-app-tests/reference.md +1 -1
  213. package/templates/skills/hai-app-tests/validation.md +1 -0
  214. package/templates/skills/hai-build/SKILL.md +19 -9
  215. package/templates/skills/hai-build/paths.md +4 -2
  216. package/templates/skills/hai-build/reference.md +26 -8
  217. package/templates/skills/hai-cache/reference.md +6 -2
  218. package/templates/skills/hai-capacitor/SKILL.md +1 -0
  219. package/templates/skills/hai-capacitor/reference.md +24 -0
  220. package/templates/skills/hai-ci/SKILL.md +3 -0
  221. package/templates/skills/hai-core/SKILL.md +9 -4
  222. package/templates/skills/hai-core/reference.md +40 -10
  223. package/templates/skills/hai-core/request-access.md +2 -2
  224. package/templates/skills/hai-crypto/SKILL.md +2 -2
  225. package/templates/skills/hai-crypto/reference.md +7 -3
  226. package/templates/skills/hai-deploy/SKILL.md +41 -5
  227. package/templates/skills/hai-deploy/middleware.md +26 -14
  228. package/templates/skills/hai-framework-sync/SKILL.md +13 -9
  229. package/templates/skills/hai-framework-sync/sync.md +2 -0
  230. package/templates/skills/hai-iam/reference.md +2 -2
  231. package/templates/skills/hai-kit/SKILL.md +1 -1
  232. package/templates/skills/hai-kit/reference.md +19 -5
  233. package/templates/skills/hai-payment/reference.md +41 -17
  234. package/templates/skills/hai-reach/SKILL.md +3 -1
  235. package/templates/skills/hai-reach/reference.md +29 -9
  236. package/templates/skills/hai-reldb/SKILL.md +1 -1
  237. package/templates/skills/hai-reldb/reference.md +9 -2
  238. package/templates/skills/hai-scheduler/reference.md +1 -1
  239. package/templates/skills/hai-serv/SKILL.md +2 -1
  240. package/templates/skills/hai-serv/reference.md +26 -15
  241. package/templates/skills/hai-storage/reference.md +23 -6
  242. package/templates/skills/hai-ui/SKILL.md +11 -6
  243. package/templates/skills/hai-ui/charts.md +51 -0
  244. package/templates/skills/hai-ui/guide.md +1 -1
  245. package/templates/skills/hai-ui/reference.md +58 -38
  246. package/templates/skills/hai-vecdb/reference.md +8 -3
  247. package/dist/chunk-AKV2CQZV.js.map +0 -1
  248. package/dist/chunk-EF7WOFWZ.js.map +0 -1
  249. package/dist/chunk-MUHRBJSW.js +0 -29272
  250. package/dist/chunk-MUHRBJSW.js.map +0 -1
  251. package/dist/cli-config-templates-HHFHNXBN.js +0 -5
  252. package/dist/src-TEBHR5I5.js +0 -5
  253. package/dist/src-TEBHR5I5.js.map +0 -1
  254. package/templates/apps/fullstack/playwright.session-close.config.ts.hbs +0 -18
@@ -29,11 +29,23 @@ beforeAll(async () => {
29
29
  expect((await reldb.init({ type: 'sqlite', database: ':memory:' })).success).toBe(true)
30
30
  expect((await cache.init({ type: 'memory' })).success).toBe(true)
31
31
  expect((await storage.init({ type: 'local', root: directory })).success).toBe(true)
32
- expect((await audit.init({ storage, backup: { enabled: true } })).success).toBe(true)
32
+ expect((await audit.init({
33
+ storage,
34
+ backup: { enabled: true },
35
+ })).success).toBe(true)
33
36
  expect((await iam.init({
34
37
  initialCredentials: { enabled: true },
35
38
  passwordReset: { enabled: true, mode: 'email' },
36
- reauthentication: { enabled: true, operations: ['iam.users.create', 'iam.users.invite', 'iam.users.resetPassword', 'audit.backup', 'business.settings.update'] },
39
+ reauthentication: {
40
+ enabled: true,
41
+ operations: [
42
+ 'iam.users.create',
43
+ 'iam.users.invite',
44
+ 'iam.users.resetPassword',
45
+ 'audit.backup',
46
+ 'business.settings.update',
47
+ ],
48
+ },
37
49
  onPasswordResetRequest: async (user, token) => {
38
50
  invitations.set(user.username, token)
39
51
  if (deliveryUnknown)
@@ -56,9 +68,19 @@ beforeAll(async () => {
56
68
  }
57
69
  const app = serv.createApp({
58
70
  contract: {{fullstack.contractExportName}},
59
- procedures: createServerProcedures({ app: { name: 'Security fixture', version: 'test', env: 'test', defaultLocale: 'en-US', supportedLocales: ['en-US'], startedAt: new Date().toISOString(), frontends: ['web'] } }),
71
+ procedures: createServerProcedures({
72
+ app: {
73
+ name: 'Security fixture',
74
+ version: 'test',
75
+ env: 'test',
76
+ defaultLocale: 'en-US',
77
+ supportedLocales: ['en-US'],
78
+ startedAt: new Date().toISOString(),
79
+ frontends: ['web'],
80
+ },
81
+ }),
60
82
  iam,
61
- http: { openapi: false, docs: false, rpc: false },
83
+ http: { openapi: false, docs: false },
62
84
  })
63
85
  server = serv.listen(app, { host: '127.0.0.1', port: 0 })
64
86
  if (!server.server.listening)
@@ -100,23 +122,57 @@ async function proof(code: Duty, operation: string) {
100
122
  }
101
123
 
102
124
  function post(code: Duty, route: string, input: object = {}, ticket?: string) {
103
- return fetch(`${base}${route}`, { method: 'POST', headers: { 'content-type': 'application/json', 'authorization': `Bearer ${actor(code).token}`, ...(ticket ? { 'x-hai-reauthentication': ticket } : {}) }, body: JSON.stringify(input) })
125
+ return fetch(`${base}${route}`, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'content-type': 'application/json',
129
+ 'authorization': `Bearer ${actor(code).token}`,
130
+ ...(ticket
131
+ ? { 'x-hai-reauthentication': ticket }
132
+ : {}),
133
+ },
134
+ body: JSON.stringify(input),
135
+ })
104
136
  }
105
137
 
106
138
  it('系统、审计、配置与业务职责的直接 HTTP 请求各有边界', async () => {
107
- expect(await (await post('audit_admin', '/admin/users/list')).json()).toMatchObject({ success: false, error: { httpStatus: 403 } })
108
- expect(await (await post('admin', '/admin/audit/list')).json()).toMatchObject({ success: false, error: { httpStatus: 403 } })
109
- expect(await (await post('admin', '/admin/configuration/update', { notice: 'Rejected' })).json()).toMatchObject({ success: false, error: { httpStatus: 403 } })
110
- expect(await (await post('business_operator', '/admin/users/list')).json()).toMatchObject({ success: false, error: { httpStatus: 403 } })
139
+ expect(await (await post('audit_admin', '/admin/users/list')).json()).toMatchObject({
140
+ success: false,
141
+ error: { httpStatus: 403 },
142
+ })
143
+ expect(await (await post('admin', '/admin/audit/list')).json()).toMatchObject({
144
+ success: false,
145
+ error: { httpStatus: 403 },
146
+ })
147
+ expect(await (await post('admin', '/admin/configuration/update', { notice: 'Rejected' })).json()).toMatchObject({
148
+ success: false,
149
+ error: { httpStatus: 403 },
150
+ })
151
+ expect(await (await post('business_operator', '/admin/users/list')).json()).toMatchObject({
152
+ success: false,
153
+ error: { httpStatus: 403 },
154
+ })
111
155
  const ticket = await proof('config_admin', 'business.settings.update')
112
156
  expect(await (await post('config_admin', '/admin/configuration/update', { notice: 'Authorized public notice' }, ticket)).json()).toMatchObject({ success: true })
113
157
  const info = await fetch(`${base}/app/info`, { method: 'POST' })
114
- expect(await info.json()).toMatchObject({ success: true, data: { notice: 'Authorized public notice' } })
158
+ expect(await info.json()).toMatchObject({
159
+ success: true,
160
+ data: { notice: 'Authorized public notice' },
161
+ })
115
162
  })
116
163
 
117
164
  it('再鉴别按用途单次使用,临时账号状态变化立即阻止旧会话', async () => {
118
- const input = { username: 'temporary-member', password: 'Temporary-example-123!', role: 'user', accountType: 'temporary', accountExpiresAt: new Date(Date.now() + 3600000).toISOString() }
119
- expect(await (await post('admin', '/admin/users/create', input)).json()).toMatchObject({ success: false, error: { code: 'hai:iam:027' } })
165
+ const input = {
166
+ username: 'temporary-member',
167
+ password: 'Temporary-example-123!',
168
+ role: 'user',
169
+ accountType: 'temporary',
170
+ accountExpiresAt: new Date(Date.now() + 3600000).toISOString(),
171
+ }
172
+ expect(await (await post('admin', '/admin/users/create', input)).json()).toMatchObject({
173
+ success: false,
174
+ error: { code: 'hai:iam:027' },
175
+ })
120
176
  const ticket = await proof('admin', 'iam.users.create')
121
177
  const response = await post('admin', '/admin/users/create', input, ticket)
122
178
  const created = apiContract.haiResultSchema(AdminUserSchema).parse(await response.json())
@@ -135,7 +191,12 @@ it('再鉴别按用途单次使用,临时账号状态变化立即阻止旧会
135
191
 
136
192
  it('设密邀请可完成,投递未知时保留账号;系统管理员可创建其他职责账号', async () => {
137
193
  const createdDuty = await post('admin', '/admin/users/create', { username: 'managed-auditor', password: 'Example-123!', role: 'audit_admin' }, await proof('admin', 'iam.users.create'))
138
- expect(await createdDuty.json()).toMatchObject({ success: true, data: { roles: ['audit_admin'] } })
194
+ expect(await createdDuty.json()).toMatchObject({
195
+ success: true,
196
+ data: {
197
+ roles: ['audit_admin'],
198
+ },
199
+ })
139
200
  const invite = await post('admin', '/admin/users/invite', { username: 'invited-member', email: 'member@example.test' }, await proof('admin', 'iam.users.invite'))
140
201
  expect(await invite.json()).toMatchObject({ success: true })
141
202
  const token = invitations.get('invited-member')
@@ -145,39 +206,94 @@ it('设密邀请可完成,投递未知时保留账号;系统管理员可创
145
206
  deliveryUnknown = true
146
207
  try {
147
208
  const unknown = await post('admin', '/admin/users/invite', { username: 'delivery-unknown-member', email: 'unknown@example.test' }, await proof('admin', 'iam.users.invite'))
148
- expect(await unknown.json()).toMatchObject({ success: false, error: { code: 'hai:iam:028' } })
209
+ expect(await unknown.json()).toMatchObject({
210
+ success: false,
211
+ error: { code: 'hai:iam:028' },
212
+ })
149
213
  const found = await iam.user.listUsers({ search: 'delivery-unknown-member' })
150
- expect(found).toMatchObject({ success: true, data: { total: 1 } })
214
+ expect(found).toMatchObject({
215
+ success: true,
216
+ data: { total: 1 },
217
+ })
151
218
  }
152
219
  finally { deliveryUnknown = false }
153
220
  })
154
221
 
155
222
  it('只有审计职责可手动备份且私有归档可验证', async () => {
156
223
  const id = crypto.randomUUID()
157
- expect(await (await post('admin', '/admin/audit/backup', { id })).json()).toMatchObject({ success: false, error: { httpStatus: 403 } })
224
+ expect(await (await post('admin', '/admin/audit/backup', { id })).json()).toMatchObject({
225
+ success: false,
226
+ error: { httpStatus: 403 },
227
+ })
158
228
  const result = await post('audit_admin', '/admin/audit/backup', { id }, await proof('audit_admin', 'audit.backup'))
159
229
  const body: unknown = await result.json()
160
- expect(body).toMatchObject({ success: true, data: { id, key: expect.stringContaining('hai-private/audit-backups/'), sha256: expect.stringMatching(/^[a-f\d]{64}$/) } })
230
+ expect(body).toMatchObject({
231
+ success: true,
232
+ data: {
233
+ id,
234
+ key: expect.stringContaining('hai-private/audit-backups/'),
235
+ sha256: expect.stringMatching(/^[a-f\d]{64}$/),
236
+ },
237
+ })
161
238
  const verified = await audit.backup.verify(`hai-private/audit-backups/${id}.jsonl`)
162
239
  expect(verified.success).toBe(true)
163
240
  })
164
241
 
165
- it.each(['admin', 'audit_admin', 'config_admin', 'business_operator', 'user'] as const)('系统管理员统一管理 %s 的资料、生命周期、角色和密码', async (role) => {
242
+ it.each([
243
+ 'admin',
244
+ 'audit_admin',
245
+ 'config_admin',
246
+ 'business_operator',
247
+ 'user',
248
+ ] as const)('系统管理员统一管理 %s 的资料、生命周期、角色和密码', async (role) => {
166
249
  const username = `managed-${role}`
167
250
  const created = apiContract.haiResultSchema(AdminUserSchema).parse(await (await post('admin', '/admin/users/create', { username, password: 'Managed-example-123!', role }, await proof('admin', 'iam.users.create'))).json())
168
- expect(created).toMatchObject({ success: true, data: { roles: [role], accountStatus: 'active' } })
251
+ expect(created).toMatchObject({
252
+ success: true,
253
+ data: {
254
+ roles: [role],
255
+ accountStatus: 'active',
256
+ },
257
+ })
169
258
  if (!created.success)
170
259
  throw new Error('Managed account fixture failed')
171
- const input = { id: created.data.id, displayName: 'Managed profile', accountStatus: 'dormant', role: 'user' }
172
- expect(await (await post('admin', '/admin/users/update', input)).json()).toMatchObject({ success: true, data: { displayName: 'Managed profile', roles: ['user'], accountStatus: 'dormant' } })
260
+ const input = {
261
+ id: created.data.id,
262
+ displayName: 'Managed profile',
263
+ accountStatus: 'dormant',
264
+ role: 'user',
265
+ }
266
+ expect(await (await post('admin', '/admin/users/update', input)).json()).toMatchObject({
267
+ success: true,
268
+ data: {
269
+ displayName: 'Managed profile',
270
+ roles: ['user'],
271
+ accountStatus: 'dormant',
272
+ },
273
+ })
173
274
  const reset = { id: created.data.id, newPassword: 'Reset-example-456!' }
174
- expect(await (await post('admin', '/admin/users/reset-password', reset)).json()).toMatchObject({ success: false, error: { code: 'hai:iam:027' } })
275
+ expect(await (await post('admin', '/admin/users/reset-password', reset)).json()).toMatchObject({
276
+ success: false,
277
+ error: { code: 'hai:iam:027' },
278
+ })
175
279
  const ticket = await proof('admin', 'iam.users.resetPassword')
176
- expect(await (await post('admin', '/admin/users/reset-password', reset, ticket)).json()).toMatchObject({ success: true, data: { reset: true } })
280
+ expect(await (await post('admin', '/admin/users/reset-password', reset, ticket)).json()).toMatchObject({
281
+ success: true,
282
+ data: { reset: true },
283
+ })
177
284
  expect(await (await post('admin', '/admin/users/reset-password', reset, ticket)).json()).toMatchObject({ success: false })
178
- expect(await iam.user.getUser(created.data.id)).toMatchObject({ success: true, data: { accountStatus: 'dormant' } })
179
- expect(await iam.auth.login({ identifier: username, password: reset.newPassword })).toMatchObject({ success: false, error: { code: 'hai:iam:002' } })
285
+ expect(await iam.user.getUser(created.data.id)).toMatchObject({
286
+ success: true,
287
+ data: { accountStatus: 'dormant' },
288
+ })
289
+ expect(await iam.auth.login({ identifier: username, password: reset.newPassword })).toMatchObject({
290
+ success: false,
291
+ error: { code: 'hai:iam:002' },
292
+ })
180
293
  expect(await (await post('admin', '/admin/users/update', { id: created.data.id, accountStatus: 'active' })).json()).toMatchObject({ success: true })
181
294
  expect((await iam.auth.login({ identifier: username, password: reset.newPassword })).success).toBe(true)
182
- expect(await (await post('audit_admin', '/admin/users/reset-password', reset)).json()).toMatchObject({ success: false, error: { httpStatus: 403 } })
295
+ expect(await (await post('audit_admin', '/admin/users/reset-password', reset)).json()).toMatchObject({
296
+ success: false,
297
+ error: { httpStatus: 403 },
298
+ })
183
299
  })
@@ -6,13 +6,17 @@ import { resolveBuildMetadata } from '../../scripts/build-metadata.js'
6
6
  const buildMetadata = resolveBuildMetadata(fileURLToPath(new URL('.', import.meta.url)))
7
7
 
8
8
  export default defineConfig({
9
- define: { __HAI_BUILD_METADATA__: JSON.stringify(buildMetadata) },
9
+ define: {
10
+ __HAI_BUILD_METADATA__: JSON.stringify(buildMetadata),
11
+ },
10
12
  async onSuccess() {
11
13
  await writeFile(new URL('./dist/build-metadata.json', import.meta.url), JSON.stringify(buildMetadata))
12
14
  },
13
15
  entry: ['src/index.ts'],
14
16
  format: ['esm'],
15
- dts: { compilerOptions: { ignoreDeprecations: '6.0' } },
17
+ dts: {
18
+ compilerOptions: { ignoreDeprecations: '6.0' },
19
+ },
16
20
  sourcemap: true,
17
21
  clean: true,
18
22
  })
@@ -3,7 +3,9 @@ import { defineConfig } from 'vitest/config'
3
3
  import { resolveBuildMetadata } from '../../scripts/build-metadata.js'
4
4
 
5
5
  export default defineConfig({
6
- define: { __HAI_BUILD_METADATA__: JSON.stringify(resolveBuildMetadata(fileURLToPath(new URL('.', import.meta.url)))) },
6
+ define: {
7
+ __HAI_BUILD_METADATA__: JSON.stringify(resolveBuildMetadata(fileURLToPath(new URL('.', import.meta.url)))),
8
+ },
7
9
  test: {
8
10
  globals: true,
9
11
  environment: 'node',
@@ -1,97 +1,79 @@
1
1
  # {{fullstack.sharedPackageName}}
2
2
 
3
- ## Introduction
3
+ 供{{projectName}}的Web、Capacitor App和Tauri Desktop复用的前端工作区,提供AdminWorkspace、AppShell、typed API、管理状态、语言主题、引导和帮助。它不单独部署,也不在浏览器中初始化服务端数据库/IAM。
4
4
 
5
- Shared mini administration workspace for **{{projectName}}**. Web, Capacitor App and Tauri desktop use the same `AdminWorkspace`, responsive `AppShell`, typed API client and management store.
5
+ ## 运行与接入
6
6
 
7
- ## Core features
7
+ 从工程根安装依赖,启动Serv,再启动所选前端。各端已经接入此共享包;增加页面优先复用现有状态和组件。公开入口包含AdminWorkspace/AppShell、initializeManagementApi/managementApi、AVAILABLE_THEMES/themeStore及语言消息与runtime。
8
8
 
9
- - Shared management pages, forms, navigation and application state.
10
- - Typed encrypted API client and platform-specific token storage.
11
- - Themes, language preferences, onboarding and help.
9
+ 以下示例用于已有前端宿主,地址替换为实际API;原有宿主已初始化时直接复用,不重复执行初始化:
12
10
 
13
- ## Getting started
11
+ ```ts
12
+ import { initializeManagementApi, managementApi } from '{{fullstack.sharedPackageName}}'
14
13
 
15
- ### Behavior and configuration
14
+ const ready = await initializeManagementApi({ baseUrl: 'https://api.example.invalid/api/v1' })
15
+ if (!ready.success) throw new Error(ready.error.message)
16
+ const info = await managementApi.app.info()
17
+ if (!info.success) throw new Error(info.error.message)
18
+ // info.data包含后端Core名称/版本、环境、语言、公告及前端清单。
19
+ ```
16
20
 
17
- The behavior below describes the initial example. Roles, permissions, workflows, limits, preferences and audit policies can change with the user's requirements. Update conflicting implementation, tests and guidance together; preserve the applicable public API contracts and server security checks.
21
+ 输入为可信API基址,输出为统一HaiResult。初始化装配认证刷新和传输加密,不能把请求结束当成功;应用入口统一处理安全错误与生命周期,退出时先停止当前身份的后台操作,再清凭据和关闭客户端。
18
22
 
19
- - Administrators manage users, upload user avatars, assign the built-in administrator/user roles, edit role descriptions and query audit logs.
20
- - By default, ordinary users update their own profile, avatar, language, appearance and password. The server enforces the actual business permissions on every request.
21
- - The workspace uses the available window width. Tables scroll within their own area; settings panels form two columns on large screens and stack on phones.
22
- - `ManagementStore` owns API calls and business state; components render the state and forward user interactions.
23
- - Application name, version, environment and default locale come from the backend `app.info` contract, which reads `core` configuration. Do not duplicate these values in translations or frontend constants.
24
- - Requests, token refresh and JSON responses use the same `@h-ai/api-client` + `@h-ai/crypto` transport. Avatar uploads use encrypted JSON and validate PNG/JPEG/WebP content, with a 512 KiB limit.
25
- - Read metadata through the same initialized `managementApi.app.info()` client; inspect `success` before using `data`.
23
+ ## 功能与输入输出
26
24
 
27
- Vite development and preview servers read the workspace root environment files and proxy /api to the backend. Builds do not load deployment environment files. Web containers receive their public API address at startup; native apps follow the platform configuration below.
25
+ | 功能 | 用户输入/调用场景 | 可观察输出与限制 |
26
+ | --- | --- | --- |
27
+ | 元信息 | managementApi.app.info() | 后端Core名称、版本、环境及语言;不在前端重复硬编码 |
28
+ | 用户/角色管理 | 当前职责允许的列表、筛选、排序与修改 | 服务端授权后的数据与明确结果;隐藏按钮不代替服务端检查 |
29
+ | 个人资料/头像 | 资料值、允许的PNG/JPEG/WebP(不超过512KiB) | 成功保存后更新;失败保留草稿。头像key由服务端生成 |
30
+ | 审计 | 分类、结果、等级、排序或备份条件 | 当前权限范围内记录、近7天统计、私有备份key/数量/SHA256回执;不公开备份对象 |
31
+ | 公告 | 配置人员的受限正文输入 | 公告与审计同事务提交,失败回滚,不把客户端编辑当已发布 |
32
+ | 主题/语言 | 个人设置选择已编译主题/语言 | 保存到IAM用户metadata,登录后恢复,并同步shared与宿主runtime |
33
+ | 引导/帮助 | 首次使用邀请、稍后、关闭、完成或重播 | 按用户及引导版本保存偏好,帮助入口可再次打开,不作为业务完成证明 |
34
+ | 表单/提示 | 输入、失焦、提交与失败重试 | 统一Toast显示安全说明,控件保留错误边框和读屏关联;成功信号决定退出编辑 |
28
35
 
29
- An origin-only API base uses `/api/v1`. For a custom service `http.apiPrefix`, include that path in `PUBLIC_API_BASE` or the runtime API base; the client preserves the configured path and restricts avatar URLs to its matching resource prefix.
36
+ 默认示例职责为系统管理、审计、配置、业务操作员和普通成员,实际规则由应用维护。临时账号、生命周期、邀请、敏感操作再认证及密码/会话限制通过服务端策略实现;取消再认证不发送对应敏感业务请求。
30
37
 
31
- ### Configurable security and duties
38
+ 工作区按窗口扩展,表格内部滚动,宽屏表单可两列,窄屏收拢。FormField.colSpan可明确占列。页面和数据逻辑分别维护在组件与ManagementStore,错误恢复保留用户输入。
32
39
 
33
- System administrators manage accounts and roles, auditors query/export private audit archives, configuration staff maintain the public notice, and business operators process feedback. Basic members retain self-serv access. Navigation and server permissions follow the same application-owned duties. Management forms support temporary account expiry and active/dormant/closed states; optional invitations use one-time password setup notifications. `ReauthenticationDialog` collects confirmation for configured operations through the service layer, and proofs are passed only to the corresponding API call. Audit pages expose category/outcome/severity filters, sorting, seven-day statistics and private backup receipts. Page-exit revocation is optional and best effort; combine it with server idle/absolute deadlines.
40
+ ## 配置与扩展
34
41
 
35
- ### Authentication and preferences
42
+ ### API地址、认证与平台存储
36
43
 
37
- Web and browser previews keep access tokens in memory and refresh tokens in the server's httpOnly Cookie. Requests include credentials; reload restores the session through the encrypted refresh flow. Never store authentication tokens in localStorage.
44
+ initializeManagementApi接受baseUrl、fetchFn、clientName、terminalType、tokenStorage、headers、requireApiBase和onSessionExpired。基址顺序为显式baseUrl→公开runtime-config→PUBLIC_API_BASE→当前origin;只给origin时补/api/v1,自定义http.apiPrefix必须明确包含。
38
45
 
39
- Capacitor native uses `createCapacitorTokenStorage()` and the trusted native token channel. Set its public API address in the App's `public/runtime-config.js` before compiling and syncing native assets. Tauri uses memory token storage and requires a new login after restart; set process `PUBLIC_API_BASE` before launching the installed application, and its Rust host injects the address. Both send `clientName` and `X-HAI-Token-Transport: body`; the service verifies the trusted native origin before returning a refresh token in JSON. Capacitor requires explicit API configuration. The Tauri host defaults to http://localhost:3000/api/v1 when PUBLIC_API_BASE is absent; configure the actual service address for installed applications. A configured address does not prove connectivity.
46
+ - Web/浏览器预览使用内存access token和服务端HttpOnly刷新Cookie,刷新恢复会话;认证token不放localStorage。
47
+ - Capacitor原生使用createCapacitorTokenStorage及受信原生通道。编译/同步前在App的public/runtime-config.js设置公开API地址,不放服务端秘密。
48
+ - Tauri原生凭据只在内存,重启重新登录。启动安装程序前设置进程PUBLIC_API_BASE,Rust宿主注入地址;缺省http://localhost:3000/api/v1只适合本机服务。
49
+ - 原生请求声明clientName及body通道头,服务端还核验可信Origin;仅声明头不获得原生权限。
40
50
 
41
- Language and theme preferences are saved in IAM user metadata, restored after login and synchronized with the shared/host Paraglide runtimes and `core.i18n`. The small reactive theme store delegates DOM updates and safe appearance caching to `@h-ai/ui/theme` (`applyTheme` / `getSavedTheme`); the framework uses the `theme` localStorage key. Appearance selectors live in personal settings and save through the authenticated profile contract.
51
+ 仅配置地址不证明设备可达。代理、HTTPS、凭据传递与认证状态恢复应在对应平台验证。
42
52
 
43
- ### Onboarding and help
53
+ ### IAM与通知
44
54
 
45
- AdminWorkspace mounts the shared `@h-ai/ui/guide` controller and GuideHost after authentication. AppShell includes a help button; first-use invitations offer start, later and dismiss. Web stores preferences in Cookies, while App/Desktop use localStorage, scoped by the current user and tour version. Help articles link to the overview and profile tours.
55
+ Serv的config/_iam.yml控制登录/注册/重置、协议与ui品牌,AuthPanel只使用安全公开配置。密码、验证码、LDAP及发送回调的完整配置见[生成应用IAM技能](../../.agents/skills/hai-iam/reference.md)。协议正文Markdown优先于URL并安全渲染;邮件/短信投递未知需查回执,不能自动重复发送。
46
56
 
47
- Extend `src/lib/stores/guide-store.ts` and the shared Chinese/English/Japanese messages, then add stable data-guide attributes to existing components. Navigation uses the same ManagementStore. Keep business text as i18n callbacks and preserve the host's locale/scope binding. Browser tests cover completion, reload, replay, settings navigation and mobile help.
57
+ 页面离开撤销为可选且尽力而为;生产仍用共享数据库/Redis、闲置与绝对期限。管理写入路由和最后管理员边界按根README的实际部署方案执行。
48
58
 
49
- ### i18n and themes
59
+ ### 消息、主题、组件与引导
50
60
 
51
- Shared page copy belongs in `messages/{zh-CN,en-US,ja-JP}.json`. Frontend-specific copy belongs in each app's own messages directory. Keep `baseLocale` and `locales` aligned across all enabled projects, then run root `pnpm i18n:compile`; do not edit generated Paraglide files.
61
+ 共享文案在messages,前端专属文案放各端messages;启用语言及baseLocale一致后执行根 `pnpm i18n:compile`,不手改生成文件。插件来自已安装workspace依赖,不在编译时从CDN获取。
52
62
 
53
- All frontend and shared projects load the installed message-format plugin from the workspace root `node_modules`, matching the scaffold's hoisted dependency layout. Compilation uses the workspace dependency version and does not fetch a plugin from a CDN.
63
+ AVAILABLE_THEMES只暴露已编译CSS主题,eye-care为可选护眼主题;默认主题和显式白名单由宿主决定。外观缓存可保存theme,认证token不能复用该存储。修改AppShell、相邻管理组件或ManagementStore时同步三语与键盘/窄屏恢复流程。
54
64
 
55
- The scaffold theme configuration determines the themes compiled into frontend CSS. Personal settings offer only those available themes. Customize the common layout in `src/lib/components/AppShell.svelte`, the management pages in adjacent components, and business behavior in `src/lib/stores/management-store.svelte.ts`.
65
+ 引导定义在src/lib/stores/guide-store.ts,组件提供稳定data-guide属性;Web引导偏好用Cookie,App/Desktop用按当前用户与版本划分的localStorage,不存认证信息。设计、实现和复核分别使用根skills中的hai-ui-design、hai-ui、hai-ui-review。
56
66
 
57
- ### Validation
67
+ {{#if features.feedback}}
68
+ ### 用户反馈
58
69
 
59
- Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Shared API tests exercise real transport key exchange, encrypted request/response handling, configured core metadata and credential forwarding. Root browser tests cover administrator and ordinary-user workflows, avatar rendering, persisted preferences, password/session invalidation and narrow-screen layout. All selected Web/App/Desktop previews also exercise login, tours, help and profile navigation. Clean screenshots, reports, temporary projects, containers and helper processes in finally/teardown, including failure paths. These browser checks do not replace native-shell or miniapp device acceptance.
70
+ 反馈页面复用shared SDK预览、标注、保存草稿与导出轮询;提交受理编号不表示材料和通知全部完成。服务端 `_feedback.yml` 配置采集预算/业务采集超时/Webhook,密钥仅在服务端私有环境。退出身份清理草稿与观察器,导出文件通过受授权私有通道交付。App/Desktop按平台安全保存文件并清理临时资源。
71
+ {{/if}}
60
72
 
61
- ### IAM configuration
73
+ ## 验证
62
74
 
63
- The service's `config/_iam.yml` controls login methods, registration, password reset, agreements and `ui` branding. Email and SMS use providers/templates in `_reach.yml` or custom callbacks. `AuthPanel` uses the public configuration and framework auth actions to manage tokens, resend countdowns and errors. Memory cache is for local development and explicit tests. Production requires supported shared database/cache backends even with one instance, write routing and a shared managed transport key ring and registry as described in the project README.
75
+ 从根目录运行 `pnpm typecheck`、`pnpm lint`、`pnpm build`、`pnpm test`、`pnpm test:e2e`。核对管理员与普通用户、跨资源拒绝、资料失败保留、头像、偏好恢复、密码/会话失效及窄屏布局。所有已选择前端共用流程,但浏览器测试不替代原生壳、设备权限或miniapp验收。
64
76
 
65
- Authentication defaults to a split layout. Set `ui.variant`, `title`, `subtitle`, `brandTitle`, `brandText`, `logoUrl` and `illustrationUrl` to customize login and registration. Without a `brandTitle`, the displayed application name comes from `app.info` and follows `core` configuration.
66
-
67
- LDAP and Reach are optional. Install ldapts in the IAM service workspace only when using its default LDAP client, then enable login.ldap and set connection settings. Custom onOtpSendEmail/onOtpSendSms/onPasswordResetRequest callbacks work without Reach and must reject on delivery failure. Their third context argument supplies expiresAt/idempotencyKey; senders durably deduplicate that key, and errors/timeouts mean unknown delivery with no automatic resend. Agreements open in a Modal. LoginForm/RegisterForm support header/extraFields/footer snippets; AuthPanel forwards loginHeader/loginFields/loginFooter and registerHeader/registerFields/registerFooter. See the generated hai-iam/hai-ui skills for configuration and examples.
68
-
69
- Agreements accept Markdown text or a URL. Markdown takes precedence and opens in a safely rendered dialog.
70
-
71
- See `.agents/skills/hai-iam/reference.md` in the generated application for the complete contract.
72
-
73
- ### Operation feedback
74
-
75
- Operation results use the shared `toast` queue from the lightweight `@h-ai/ui/toast` entry. `AdminWorkspace` mounts one `ToastContainer` for Web, App and Desktop: notifications float at the bottom right by default, use UI-owned semantic colors for information, success, warning and error, with a soft tinted background, a narrow color border and a distinct icon, close automatically or manually, and appear one at a time in FIFO order. Waiting notifications do not expire before being shown. Set `position` in `toast.add` for any of the six supported positions. Field validation and persistent business state stay inline.
76
-
77
- ### Form layouts
78
-
79
- The user editor uses `FormLayout columns={2} labelPosition="left"`. Personal settings fill the available width and use two columns on wide screens, with stacked labels and a single avatar editor. Narrow containers automatically stack cards, fields and labels. UI controls and CRUD forms show client validation on blur, update errors while correcting input and validate untouched fields on submit. Field errors follow the current UI language. The management store syncs locale through the lightweight `@h-ai/ui/i18n` entry, without loading components or tour CSS. Configure `colSpan` on FormField to override automatic spans for textareas, uploads and multiple choices.
80
-
81
- ## 界面设计与验收
82
-
83
- 共享管理界面的 Web/App/Desktop 使用同一套 UI 组件和状态。设计、实现、审查分别参考根目录 `.agents/skills/hai-ui-design/`、`hai-ui/`、`hai-ui-review/`;正常、加载、空态、错误恢复、键盘、语言/主题和窄容器均按实际场景验收。组件的筛选、排序和分页仍由业务数据回调接入。
84
-
85
- ## 反馈现场与交付检查
86
-
87
- feedback.init(config, { collectEvidence }) 支持等待业务 text/data 与文本/ZIP/GZIP 附件;_feedback.yml 提供业务采集超时和 Webhook URL/签名密钥/请求超时。密钥仅放服务端私有环境。Webhook 持久重试,详情展示状态并允许管理员重试。
88
-
89
- 反馈的 console/disabled/reach 独立选择;SDK getView/ratio/saveDraft 与 createFeedbackReview 统一预览、标注、保存及轮询。退出身份清理草稿和观察器。宿主生命周期由 core.module.createRuntime 处理,框架表升级前备份停旧写入并运行各模块 dryRun 预检。
90
-
91
- ## Development and testing
92
-
93
- Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Browser checks cover shared workflows; test native shells on their target platforms separately.
94
-
95
- ## License
77
+ ## 许可证
96
78
 
97
79
  Apache-2.0
@@ -6,6 +6,7 @@
6
6
  "settings_theme": "Theme",
7
7
  "theme_high_contrast": "High contrast",
8
8
  "theme_formal": "Formal",
9
+ "theme_eye_care": "Eye care",
9
10
  "home_refresh": "Refresh",
10
11
  "home_loading": "Loading…",
11
12
  "home_error": "Unable to reach the API service",
@@ -6,6 +6,7 @@
6
6
  "settings_theme": "テーマ",
7
7
  "theme_high_contrast": "ハイコントラスト",
8
8
  "theme_formal": "ビジネス(角丸なし)",
9
+ "theme_eye_care": "目にやさしい",
9
10
  "home_refresh": "更新",
10
11
  "home_loading": "読み込み中…",
11
12
  "home_error": "APIサービスに接続できません",
@@ -6,6 +6,7 @@
6
6
  "settings_theme": "主题",
7
7
  "theme_high_contrast": "高对比",
8
8
  "theme_formal": "简洁商务",
9
+ "theme_eye_care": "护眼",
9
10
  "home_refresh": "刷新",
10
11
  "home_loading": "加载中…",
11
12
  "home_error": "无法连接到 API 服务",
@@ -40,7 +40,12 @@ export async function initializeManagementApi(options: ManagementApiOptions = {}
40
40
  clientName: options.clientName ?? '{{fullstack.sharedPackageName}}',
41
41
  fetch: createCredentialedFetch(options.fetchFn),
42
42
  headers: options.headers,
43
- auth: { recoverSession: sessionRecovery.recover, terminalType: options.terminalType ?? 'web', storage: options.tokenStorage, onRefreshFailed: options.onSessionExpired },
43
+ auth: {
44
+ recoverSession: sessionRecovery.recover,
45
+ terminalType: options.terminalType ?? 'web',
46
+ storage: options.tokenStorage,
47
+ onRefreshFailed: options.onSessionExpired,
48
+ },
44
49
  transport: { crypto },
45
50
  })
46
51
  }
@@ -65,7 +70,9 @@ export function resolveAvatarUrl(value: string | undefined): string | undefined
65
70
  function resolveDefaultApiOrigin(): string {
66
71
  return resolveRuntimeApiOrigin()
67
72
  || import.meta.env.PUBLIC_API_BASE
68
- || (typeof globalThis.location === 'undefined' ? DEFAULT_API_ORIGIN : globalThis.location.origin)
73
+ || (typeof globalThis.location === 'undefined'
74
+ ? DEFAULT_API_ORIGIN
75
+ : globalThis.location.origin)
69
76
  }
70
77
 
71
78
  /** 跨端浏览器请求携带服务端管理的 httpOnly refresh Cookie。 */
@@ -2,12 +2,10 @@
2
2
  {{#if features.feedback}}
3
3
  import { createFeedbackClient, createFeedbackReview, saveFeedbackFile } from '@h-ai/feedback/client'
4
4
  import { managementApi } from './api-client.js'
5
-
6
5
  /** 仅装配共享采集器,不复制采集、脱敏、上传或离线打包逻辑。 */
7
6
  export const browserFeedback = createFeedbackClient()
8
7
  export const feedbackReview = createFeedbackReview(managementApi.feedback.reports)
9
8
  export { feedbackDataBytes } from '@h-ai/feedback/client'
10
-
11
9
  export async function initializeBrowserFeedback(metadata: { appVersion: string, buildId: string, commit: string }): Promise<void> {
12
10
  const result = await browserFeedback.init({
13
11
  appId: '{{projectName}}',
@@ -26,7 +24,6 @@ export async function initializeBrowserFeedback(metadata: { appVersion: string,
26
24
  if (!result.success)
27
25
  throw new Error(result.error.message, { cause: result.error })
28
26
  }
29
-
30
27
  /** 下载的内容来自受权 API 或本地采集器;对象 URL 在发起后释放。 */
31
28
  export async function downloadFeedback(bytes: Uint8Array, name: string, contentType = 'application/zip'): Promise<void> {
32
29
  const result = await saveFeedbackFile(bytes, name, contentType)
@@ -10,7 +10,19 @@ export const sensitiveActions = createSensitiveActions(authActions)
10
10
  /** 用途凭据只传给本次 typed client 调用,不使用共享 header。 */
11
11
  export async function requestSensitive<T>(operation: string, run: (options: { context: { reauthentication?: string, reauthorize: () => Promise<string | null | undefined> } }) => Promise<HaiResult<T>>): Promise<HaiResult<T>> {
12
12
  const ticket = await sensitiveActions.authorize(operation)
13
- if (ticket === null)
14
- return err({ code: 'app:cancelled', httpStatus: 400, message: m.security_action_cancelled() })
15
- return run({ context: { ...(ticket ? { reauthentication: ticket } : {}), reauthorize: () => sensitiveActions.authorize(operation) } })
13
+ if (ticket === null) {
14
+ return err({
15
+ code: 'app:cancelled',
16
+ httpStatus: 400,
17
+ message: m.security_action_cancelled(),
18
+ })
19
+ }
20
+ return run({
21
+ context: {
22
+ ...(ticket
23
+ ? { reauthentication: ticket }
24
+ : {}),
25
+ reauthorize: () => sensitiveActions.authorize(operation),
26
+ },
27
+ })
16
28
  }
@@ -30,14 +30,38 @@
30
30
  })
31
31
  let menuOpen = $state(false)
32
32
  const items = $derived([
33
- { id: 'users', label: m.nav_users(), icon: 'icon-[tabler--users]' },
34
- { id: 'roles', label: m.nav_roles(), icon: 'icon-[tabler--shield-lock]' },
35
- { id: 'configuration', label: m.business_settings_title(), icon: 'icon-[tabler--adjustments]' },
36
- { id: 'audit', label: m.nav_audit(), icon: 'icon-[tabler--file-text]' },
33
+ {
34
+ id: 'users',
35
+ label: m.nav_users(),
36
+ icon: 'icon-[tabler--users]',
37
+ },
38
+ {
39
+ id: 'roles',
40
+ label: m.nav_roles(),
41
+ icon: 'icon-[tabler--shield-lock]',
42
+ },
43
+ {
44
+ id: 'configuration',
45
+ label: m.business_settings_title(),
46
+ icon: 'icon-[tabler--adjustments]',
47
+ },
48
+ {
49
+ id: 'audit',
50
+ label: m.nav_audit(),
51
+ icon: 'icon-[tabler--file-text]',
52
+ },
37
53
  {{#if features.feedback}}
38
- { id: 'feedback', label: m.nav_feedback(), icon: 'icon-[tabler--message-report]' },
54
+ {
55
+ id: 'feedback',
56
+ label: m.nav_feedback(),
57
+ icon: 'icon-[tabler--message-report]',
58
+ },
39
59
  {{/if}}
40
- { id: 'settings', label: m.settings_title(), icon: 'icon-[tabler--settings]' },
60
+ {
61
+ id: 'settings',
62
+ label: m.settings_title(),
63
+ icon: 'icon-[tabler--settings]',
64
+ },
41
65
  ].filter(item => store.canOpenPage(item.id)))
42
66
  const title = $derived(items.find(item => item.id === store.page)?.label ?? store.info?.name ?? '')
43
67
 
@@ -61,7 +85,9 @@
61
85
  </div>
62
86
  <nav class="flex-1 space-y-1 p-3" aria-label={m.navigation()}>
63
87
  {#each items as item (item.id)}
64
- <BareButton class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors {store.page === item.id ? 'bg-primary/10 font-medium text-primary' : 'text-base-content/60 hover:bg-base-content/4'}" onclick={() => navigate(item.id)} data-testid="nav-{item.id}">
88
+ <BareButton class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors {store.page === item.id
89
+ ? 'bg-primary/10 font-medium text-primary'
90
+ : 'text-base-content/60 hover:bg-base-content/4'}" onclick={() => navigate(item.id)} data-testid="nav-{item.id}">
65
91
  <span class="{item.icon} size-4.5"></span>{item.label}
66
92
  </BareButton>
67
93
  {/each}
@@ -41,7 +41,13 @@
41
41
  }
42
42
  }
43
43
  function input(data: FeedbackFormData) {
44
- return { description: data.description, category: data.type, contact: data.contact, files: data.files, removedAttachmentIds: data.removedAttachmentIds }
44
+ return {
45
+ description: data.description,
46
+ category: data.type,
47
+ contact: data.contact,
48
+ files: data.files,
49
+ removedAttachmentIds: data.removedAttachmentIds,
50
+ }
45
51
  }
46
52
  async function submit(data: FeedbackFormData) {
47
53
  const result = await browserFeedback.submit(input(data))
@@ -82,5 +88,13 @@
82
88
  <Button variant='ghost' size='sm' loading={preparing} onclick={() => void prepare()} data-testid='feedback-open'>{m.nav_feedback()}</Button>
83
89
  <FeedbackModal bind:open {attachments} budget={browserFeedback.budget} resetKey={draft?.submissionId} onsubmit={submit} oncapture={capturePage} onannotate={annotate} onexport={exportLocal} onfileschange={addFiles}
84
90
  evidence={draft?.evidence ?? []}
85
- evidenceSummary={draft?.missing.length ? [{ label: m.feedback_logs(), status: 'partial', detail: draft.missing.join(', ') }] : []} />
91
+ evidenceSummary={draft?.missing.length
92
+ ? [
93
+ {
94
+ label: m.feedback_logs(),
95
+ status: 'partial',
96
+ detail: draft.missing.join(', '),
97
+ },
98
+ ]
99
+ : []} />
86
100
  {{/if}}