@h-ai/cli 1.0.0-beta.17 → 1.0.0-beta.19

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 (195) hide show
  1. package/README.en.md +6 -0
  2. package/README.ja.md +6 -0
  3. package/README.md +6 -0
  4. package/dist/{chunk-EWTIVFHN.js → chunk-2ZL5ORQ2.js} +94 -28
  5. package/dist/chunk-2ZL5ORQ2.js.map +1 -0
  6. package/dist/{chunk-S2ZP7BHG.js → chunk-L53P43B2.js} +138 -32
  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-MKVYSK3E.js.map → cli-config-templates-LWMD2I6V.js.map} +1 -1
  10. package/dist/{cli-skill-templates-M64VPXZ4.js → cli-skill-templates-D6S2OQ6X.js} +3 -3
  11. package/dist/{cli-skill-templates-M64VPXZ4.js.map → cli-skill-templates-D6S2OQ6X.js.map} +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +2797 -666
  14. package/dist/index.js.map +1 -1
  15. package/dist/metafile-esm.json +1 -1
  16. package/package.json +22 -22
  17. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -10
  18. package/templates/apps/admin/src/routes/+page.svelte.hbs +4 -4
  19. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +17 -7
  20. package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +26 -6
  21. package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +2 -2
  22. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +6 -1
  23. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +13 -5
  24. package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +13 -5
  25. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  26. package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +71 -32
  27. package/templates/apps/api/apps/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  28. package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +131 -54
  29. package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/index.ts.hbs +39 -13
  30. package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +78 -31
  31. package/templates/apps/api/apps/{{projectName}}-serv/tsup.config.ts.hbs +3 -1
  32. package/templates/apps/api/apps/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  33. package/templates/apps/api/e2e/api-serv.spec.ts.hbs +20 -3
  34. package/templates/apps/api/e2e/capture-evidence.ts.hbs +4 -1
  35. package/templates/apps/api/eslint.config.js.hbs +3 -1
  36. package/templates/apps/api/images/serv.Dockerfile.hbs +3 -4
  37. package/templates/apps/api/playwright.config.ts.hbs +23 -19
  38. package/templates/apps/api/pnpm-workspace.yaml.hbs +27 -16
  39. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +1 -3
  40. package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +10 -2
  41. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +10 -2
  42. package/templates/apps/fullstack/apps/{{projectName}}-app/src/build-metadata.ts.hbs +0 -1
  43. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +0 -1
  44. package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +4 -1
  45. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +16 -4
  46. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +8 -2
  47. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/build-metadata.ts.hbs +0 -1
  48. package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +4 -1
  49. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +16 -4
  50. package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +10 -2
  51. package/templates/apps/fullstack/apps/{{projectName}}-web/src/build-metadata.ts.hbs +0 -1
  52. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +0 -2
  53. package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +4 -1
  54. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +19 -5
  55. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +111 -26
  56. package/templates/apps/fullstack/e2e/session-close.spec.ts.hbs +19 -5
  57. package/templates/apps/fullstack/eslint.config.js.hbs +3 -1
  58. package/templates/apps/fullstack/images/serv.Dockerfile.hbs +3 -4
  59. package/templates/apps/fullstack/images/web.Dockerfile.hbs +0 -2
  60. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +114 -23
  61. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +57 -11
  62. package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  63. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +65 -30
  64. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  65. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +69 -21
  66. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs +21 -5
  67. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +63 -11
  68. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -1
  69. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -1
  70. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +139 -49
  71. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/security-roles.ts.hbs +43 -12
  72. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +139 -28
  73. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +33 -10
  74. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +141 -25
  75. package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +6 -2
  76. package/templates/apps/fullstack/packages/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  77. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +9 -2
  78. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/feedback-client.ts.hbs +0 -3
  79. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/sensitive-actions.svelte.ts.hbs +15 -3
  80. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +33 -7
  81. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/FeedbackEntry.svelte.hbs +16 -2
  82. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +104 -13
  83. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/feedback.svelte.hbs +37 -7
  84. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/roles.svelte.hbs +9 -3
  85. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +25 -3
  86. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +133 -19
  87. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/security/role-label.ts.hbs +14 -2
  88. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +66 -34
  89. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +132 -30
  90. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +8 -2
  91. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +23 -5
  92. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +143 -21
  93. package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -2
  94. package/templates/apps/fullstack/playwright.config.ts.hbs +9 -3
  95. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +30 -15
  96. package/templates/apps/h5/src/routes/+layout.svelte.hbs +57 -21
  97. package/templates/apps/h5/src/routes/+page.svelte.hbs +2 -2
  98. package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +2 -2
  99. package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +2 -2
  100. package/templates/apps/mobile-app/src/App.svelte.hbs +17 -5
  101. package/templates/apps/mobile-app/src/lib/api.ts.hbs +14 -3
  102. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +8 -1
  103. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +6 -2
  104. package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +5 -1
  105. package/templates/apps/website/scripts/static-website.mjs.hbs +16 -3
  106. package/templates/apps/website/src/routes/+layout.svelte.hbs +44 -23
  107. package/templates/apps/website/src/routes/+page.svelte.hbs +4 -4
  108. package/templates/apps/website/src/routes/about/+page.svelte.hbs +2 -2
  109. package/templates/apps/website/src/routes/contact/+page.svelte.hbs +2 -2
  110. package/templates/apps/website/src/routes/services/+page.svelte.hbs +20 -5
  111. package/templates/base/e2e/app.spec.ts.hbs +49 -16
  112. package/templates/base/eslint-rules.js +44 -25
  113. package/templates/base/eslint.config.js.hbs +3 -1
  114. package/templates/base/images/web.Dockerfile.hbs +3 -4
  115. package/templates/base/package.json.hbs +5 -4
  116. package/templates/base/patches/daisyui-5.7.50.patch +8 -0
  117. package/templates/base/patches/deepmerge-4.3.1.patch +51 -0
  118. package/templates/base/playwright.config.ts +3 -1
  119. package/templates/base/pnpm-workspace.yaml.hbs +33 -16
  120. package/templates/base/src/app.d.ts.hbs +1 -1
  121. package/templates/base/src/hooks.server.ts.hbs +36 -27
  122. package/templates/base/src/lib/build-metadata.ts.hbs +0 -1
  123. package/templates/base/src/lib/server/init.ts.hbs +124 -51
  124. package/templates/base/src/lib/server/kit-config.ts.hbs +7 -4
  125. package/templates/base/src/routes/+layout.server.ts.hbs +2 -2
  126. package/templates/base/svelte.config.js.hbs +10 -100
  127. package/templates/base/tests/scaffold.test.ts.hbs +12 -3
  128. package/templates/base/tsconfig.json.hbs +7 -4
  129. package/templates/base/vite.config.ts.hbs +108 -14
  130. package/templates/base/vitest.config.ts.hbs +6 -2
  131. package/templates/features/ai/routes/api/chat/+server.ts +3 -1
  132. package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +3 -3
  133. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +3 -3
  134. package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +3 -3
  135. package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +3 -3
  136. package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +3 -3
  137. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +3 -3
  138. package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +3 -3
  139. package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +3 -3
  140. package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +3 -3
  141. package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +3 -3
  142. package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +3 -3
  143. package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +3 -3
  144. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +11 -2
  145. package/templates/shared/browser-check/startup.js +32 -6
  146. package/templates/shared/browser-startup.spec.ts +3 -1
  147. package/templates/shared/build-metadata.ts +18 -4
  148. package/templates/shared/e2e-env.ts +22 -6
  149. package/templates/shared/native-name.mjs +9 -2
  150. package/templates/shared/public-base-path.mjs +17 -4
  151. package/templates/shared/run-e2e.mjs +40 -9
  152. package/templates/shared/run-node.mjs +15 -4
  153. package/templates/shared/run-tauri.mjs +37 -8
  154. package/templates/shared/vite-runtime-config.ts +9 -3
  155. package/templates/skills/AGENTS.md +10 -73
  156. package/templates/skills/bridges/admin/AGENTS.md +9 -65
  157. package/templates/skills/bridges/api/AGENTS.md +8 -59
  158. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +9 -54
  159. package/templates/skills/bridges/h5/AGENTS.md +8 -63
  160. package/templates/skills/bridges/mobile-app/AGENTS.md +8 -62
  161. package/templates/skills/bridges/website/AGENTS.md +8 -63
  162. package/templates/skills/hai-ai/acp.md +9 -1
  163. package/templates/skills/hai-ai/guide.md +17 -6
  164. package/templates/skills/hai-ai/reference.md +21 -12
  165. package/templates/skills/hai-api-client/reference.md +1 -1
  166. package/templates/skills/hai-app-create/development.md +3 -1
  167. package/templates/skills/hai-app-create/project.md +33 -0
  168. package/templates/skills/hai-app-create/reference.md +8 -8
  169. package/templates/skills/hai-app-tests/reference.md +1 -1
  170. package/templates/skills/hai-build/SKILL.md +3 -2
  171. package/templates/skills/hai-build/paths.md +1 -1
  172. package/templates/skills/hai-build/reference.md +11 -7
  173. package/templates/skills/hai-capacitor/reference.md +2 -0
  174. package/templates/skills/hai-ci/SKILL.md +1 -0
  175. package/templates/skills/hai-core/reference.md +3 -0
  176. package/templates/skills/hai-crypto/reference.md +2 -0
  177. package/templates/skills/hai-deploy/SKILL.md +7 -1
  178. package/templates/skills/hai-deploy/middleware.md +3 -1
  179. package/templates/skills/hai-framework-sync/SKILL.md +13 -9
  180. package/templates/skills/hai-framework-sync/sync.md +2 -0
  181. package/templates/skills/hai-kit/reference.md +16 -4
  182. package/templates/skills/hai-payment/reference.md +2 -0
  183. package/templates/skills/hai-reach/SKILL.md +2 -1
  184. package/templates/skills/hai-reach/reference.md +1 -1
  185. package/templates/skills/hai-reldb/SKILL.md +1 -1
  186. package/templates/skills/hai-reldb/reference.md +4 -0
  187. package/templates/skills/hai-serv/reference.md +12 -2
  188. package/templates/skills/hai-storage/reference.md +12 -3
  189. package/templates/skills/hai-ui/SKILL.md +3 -2
  190. package/templates/skills/hai-ui/guide.md +1 -1
  191. package/templates/skills/hai-ui/reference.md +23 -35
  192. package/templates/skills/hai-vecdb/reference.md +1 -1
  193. package/dist/chunk-EWTIVFHN.js.map +0 -1
  194. package/dist/chunk-S2ZP7BHG.js.map +0 -1
  195. package/dist/cli-config-templates-MKVYSK3E.js +0 -5
@@ -15,7 +15,6 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest'
15
15
  import { closeModules, initializeModules } from '../src/server/runtime.js'
16
16
 
17
17
  let storageRoot: string
18
-
19
18
  beforeEach(async () => {
20
19
  storageRoot = await mkdtemp(path.join(os.tmpdir(), '{{projectName}}-bootstrap-'))
21
20
  vi.stubEnv('HAI_DB_DATABASE', '":memory:"')
@@ -23,22 +22,33 @@ beforeEach(async () => {
23
22
  vi.stubEnv('HAI_IAM_PASSWORD_REQUIREUPPERCASE', 'true')
24
23
  core.init({ configDir: './config' })
25
24
  })
26
-
27
25
  afterEach(async () => {
28
26
  await closeModules()
29
27
  vi.unstubAllEnvs()
30
28
  await rm(storageRoot, { recursive: true, force: true })
31
29
  })
32
-
33
30
  it.each([
34
31
  { password: 'abc' },
35
32
  { password: 'lowercase123!' },
36
33
  ])('startup explains the failed password policy and permits a clean retry: $password', async ({ password }) => {
37
34
  vi.stubEnv('HAI_ADMIN_DEFAULT_PASSWORD', password)
38
- await expect(initializeModules()).rejects.toMatchObject({ cause: { ext: { module: 'bootstrap', fields: ['HAI_ADMIN_DEFAULT_PASSWORD'] } } })
39
- for (const module of [audit, iam, storage, crypto, cache, reldb])
35
+ await expect(initializeModules()).rejects.toMatchObject({
36
+ cause: {
37
+ ext: {
38
+ module: 'bootstrap',
39
+ fields: ['HAI_ADMIN_DEFAULT_PASSWORD'],
40
+ },
41
+ },
42
+ })
43
+ for (const module of [
44
+ audit,
45
+ iam,
46
+ storage,
47
+ crypto,
48
+ cache,
49
+ reldb,
50
+ ])
40
51
  expect(module.isInitialized).toBe(false)
41
-
42
52
  // 失败后再次初始化,确认未保留半成品管理员或未释放的模块。
43
53
  vi.stubEnv('HAI_ADMIN_DEFAULT_PASSWORD', 'Bootstrap-retry-Admin123!')
44
54
  await initializeModules()
@@ -49,14 +59,27 @@ it.each([
49
59
  expect(users.data.items[0]?.username).toBe('admin')
50
60
  }
51
61
  })
52
-
53
62
  it.each(['', ' '])('startup identifies an unset administrator password without exposing its value', async (password) => {
54
63
  vi.stubEnv('HAI_ADMIN_DEFAULT_PASSWORD', password)
55
- await expect(initializeModules()).rejects.toMatchObject({ cause: { ext: { module: 'bootstrap', code: HaiConfigError.CONFIG_ENV_VAR_MISSING.code, fields: ['HAI_ADMIN_DEFAULT_PASSWORD'] } } })
56
- for (const module of [audit, iam, storage, crypto, cache, reldb])
64
+ await expect(initializeModules()).rejects.toMatchObject({
65
+ cause: {
66
+ ext: {
67
+ module: 'bootstrap',
68
+ code: HaiConfigError.CONFIG_ENV_VAR_MISSING.code,
69
+ fields: ['HAI_ADMIN_DEFAULT_PASSWORD'],
70
+ },
71
+ },
72
+ })
73
+ for (const module of [
74
+ audit,
75
+ iam,
76
+ storage,
77
+ crypto,
78
+ cache,
79
+ reldb,
80
+ ])
57
81
  expect(module.isInitialized).toBe(false)
58
82
  })
59
-
60
83
  {{#if features.feedback}}
61
84
  it('optional feedback failure does not roll back authentication and business modules', async () => {
62
85
  vi.stubEnv('HAI_ADMIN_DEFAULT_PASSWORD', 'Bootstrap-retry-Admin123!')
@@ -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,7 +68,17 @@ 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
83
  http: { openapi: false, docs: false },
62
84
  })
@@ -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',
@@ -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}}
@@ -9,14 +9,44 @@
9
9
  const { store }: Props = $props()
10
10
  let selected = $state<AdminAuditLog | null>(null)
11
11
  const columns = $derived([
12
- { key: 'createdAt', sortable: true, label: m.created_at() },
13
- { key: 'username', label: m.audit_actor() },
14
- { key: 'action', sortable: true, label: m.audit_action() },
15
- { key: 'category', sortable: true, label: m.audit_category() },
16
- { key: 'outcome', sortable: true, label: m.audit_outcome() },
17
- { key: 'severity', sortable: true, label: m.audit_severity() },
18
- { key: 'resource', sortable: true, label: m.audit_resource() },
19
- { key: 'ipAddress', label: m.audit_ip() },
12
+ {
13
+ key: 'createdAt',
14
+ sortable: true,
15
+ label: m.created_at(),
16
+ },
17
+ {
18
+ key: 'username',
19
+ label: m.audit_actor(),
20
+ },
21
+ {
22
+ key: 'action',
23
+ sortable: true,
24
+ label: m.audit_action(),
25
+ },
26
+ {
27
+ key: 'category',
28
+ sortable: true,
29
+ label: m.audit_category(),
30
+ },
31
+ {
32
+ key: 'outcome',
33
+ sortable: true,
34
+ label: m.audit_outcome(),
35
+ },
36
+ {
37
+ key: 'severity',
38
+ sortable: true,
39
+ label: m.audit_severity(),
40
+ },
41
+ {
42
+ key: 'resource',
43
+ sortable: true,
44
+ label: m.audit_resource(),
45
+ },
46
+ {
47
+ key: 'ipAddress',
48
+ label: m.audit_ip(),
49
+ },
20
50
  ])
21
51
  </script>
22
52
 
@@ -30,7 +60,15 @@
30
60
  event.preventDefault()
31
61
  void store.queryAudit()
32
62
  }}>
33
- <FilterBar data-testid='audit-filters' activeCount={[store.auditAction, store.auditResource, store.auditStartDate, store.auditEndDate, store.auditCategory, store.auditOutcome, store.auditSeverity].filter(Boolean).length} onreset={() => {
63
+ <FilterBar data-testid='audit-filters' activeCount={[
64
+ store.auditAction,
65
+ store.auditResource,
66
+ store.auditStartDate,
67
+ store.auditEndDate,
68
+ store.auditCategory,
69
+ store.auditOutcome,
70
+ store.auditSeverity,
71
+ ].filter(Boolean).length} onreset={() => {
34
72
  store.auditAction = ''
35
73
  store.auditResource = ''
36
74
  store.auditStartDate = ''
@@ -48,9 +86,60 @@
48
86
  <FormField label={m.audit_resource()}><Input size='sm' id="audit-resource" bind:value={store.auditResource} placeholder={m.audit_resource_placeholder()} data-testid="audit-resource" /></FormField>
49
87
  <FormField label={m.audit_start()} for="audit-start"><input id="audit-start" type="date" class="input block h-9 min-w-0 w-full" bind:value={store.auditStartDate} /></FormField>
50
88
  <FormField label={m.audit_end()} for="audit-end"><input id="audit-end" type="date" class="input block h-9 min-w-0 w-full" bind:value={store.auditEndDate} /></FormField>
51
- <FormField label={m.audit_category()}><Select size='sm' bind:value={store.auditCategory} options={[{ value: '', label: m.audit_all() }, { value: 'system', label: m.audit_category_system() }, { value: 'business', label: m.audit_category_business() }]} data-testid='audit-category' /></FormField>
52
- <FormField label={m.audit_outcome()}><Select size='sm' bind:value={store.auditOutcome} options={[{ value: '', label: m.audit_all() }, { value: 'success', label: m.audit_success() }, { value: 'failure', label: m.audit_failure() }, { value: 'denied', label: m.audit_denied() }]} data-testid='audit-outcome' /></FormField>
53
- <FormField label={m.audit_severity()}><Select size='sm' bind:value={store.auditSeverity} options={[{ value: '', label: m.audit_all() }, { value: 'info', label: m.audit_info() }, { value: 'warning', label: m.audit_warning() }, { value: 'error', label: m.audit_error() }, { value: 'critical', label: m.audit_critical() }]} data-testid='audit-severity' /></FormField>
89
+ <FormField label={m.audit_category()}><Select size='sm' bind:value={store.auditCategory} options={[
90
+ {
91
+ value: '',
92
+ label: m.audit_all(),
93
+ },
94
+ {
95
+ value: 'system',
96
+ label: m.audit_category_system(),
97
+ },
98
+ {
99
+ value: 'business',
100
+ label: m.audit_category_business(),
101
+ },
102
+ ]} data-testid='audit-category' /></FormField>
103
+ <FormField label={m.audit_outcome()}><Select size='sm' bind:value={store.auditOutcome} options={[
104
+ {
105
+ value: '',
106
+ label: m.audit_all(),
107
+ },
108
+ {
109
+ value: 'success',
110
+ label: m.audit_success(),
111
+ },
112
+ {
113
+ value: 'failure',
114
+ label: m.audit_failure(),
115
+ },
116
+ {
117
+ value: 'denied',
118
+ label: m.audit_denied(),
119
+ },
120
+ ]} data-testid='audit-outcome' /></FormField>
121
+ <FormField label={m.audit_severity()}><Select size='sm' bind:value={store.auditSeverity} options={[
122
+ {
123
+ value: '',
124
+ label: m.audit_all(),
125
+ },
126
+ {
127
+ value: 'info',
128
+ label: m.audit_info(),
129
+ },
130
+ {
131
+ value: 'warning',
132
+ label: m.audit_warning(),
133
+ },
134
+ {
135
+ value: 'error',
136
+ label: m.audit_error(),
137
+ },
138
+ {
139
+ value: 'critical',
140
+ label: m.audit_critical(),
141
+ },
142
+ ]} data-testid='audit-severity' /></FormField>
54
143
  </FormLayout>
55
144
  <div class='w-full space-y-2'>
56
145
  <p class='text-xs text-base-content/60'>{m.audit_stats_hint()}</p>
@@ -81,7 +170,9 @@
81
170
  <dt>{m.audit_request_id()}</dt><dd class='break-all'>{selected.requestId ?? '—'}</dd>
82
171
  <dt>{m.audit_trace_id()}</dt><dd class='break-all'>{selected.traceId ?? '—'}</dd>
83
172
  <dt>{m.audit_scope()}</dt><dd class='break-all'>{selected.scopeId ?? '—'}</dd>
84
- <dt>{m.audit_actor_snapshot()}</dt><dd class='break-all'>{selected.actorSnapshot ? JSON.stringify(selected.actorSnapshot) : '—'}</dd>
173
+ <dt>{m.audit_actor_snapshot()}</dt><dd class='break-all'>{selected.actorSnapshot
174
+ ? JSON.stringify(selected.actorSnapshot)
175
+ : '—'}</dd>
85
176
  <dt class="text-base-content/50">{m.created_at()}</dt><dd>{store.formatDate(selected.createdAt)}</dd>
86
177
  <dt class="text-base-content/50">{m.audit_actor()}</dt><dd class="break-all">{selected.username || m.audit_system()}</dd>
87
178
  <dt class="text-base-content/50">{m.audit_action()}</dt><dd>{selected.action}</dd>