@h-ai/cli 1.0.0-beta.18 → 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 (194) hide show
  1. package/README.en.md +4 -0
  2. package/README.ja.md +4 -0
  3. package/README.md +4 -0
  4. package/dist/{chunk-6YZBC5MO.js → chunk-2ZL5ORQ2.js} +85 -25
  5. package/dist/chunk-2ZL5ORQ2.js.map +1 -0
  6. package/dist/{chunk-7NVRYYHN.js → chunk-L53P43B2.js} +129 -31
  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-QG5HOYR5.js.map → cli-config-templates-LWMD2I6V.js.map} +1 -1
  10. package/dist/{cli-skill-templates-W4I6ZKYW.js → cli-skill-templates-D6S2OQ6X.js} +3 -3
  11. package/dist/{cli-skill-templates-W4I6ZKYW.js.map → cli-skill-templates-D6S2OQ6X.js.map} +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +2747 -671
  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 +2 -0
  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 +2 -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/sync.md +2 -0
  180. package/templates/skills/hai-kit/reference.md +16 -4
  181. package/templates/skills/hai-payment/reference.md +2 -0
  182. package/templates/skills/hai-reach/SKILL.md +2 -1
  183. package/templates/skills/hai-reach/reference.md +1 -1
  184. package/templates/skills/hai-reldb/SKILL.md +1 -1
  185. package/templates/skills/hai-reldb/reference.md +4 -0
  186. package/templates/skills/hai-serv/reference.md +12 -2
  187. package/templates/skills/hai-storage/reference.md +12 -3
  188. package/templates/skills/hai-ui/SKILL.md +3 -2
  189. package/templates/skills/hai-ui/guide.md +1 -1
  190. package/templates/skills/hai-ui/reference.md +23 -35
  191. package/templates/skills/hai-vecdb/reference.md +1 -1
  192. package/dist/chunk-6YZBC5MO.js.map +0 -1
  193. package/dist/chunk-7NVRYYHN.js.map +0 -1
  194. package/dist/cli-config-templates-QG5HOYR5.js +0 -5
@@ -42,7 +42,10 @@ function startFeedback(): Promise<void> {
42
42
  feedbackClosing = false
43
43
  feedbackStarting = (async () => {
44
44
  try {
45
- const result = await feedback.init({ ...core.config.getOrThrow<import('@h-ai/feedback').FeedbackConfigInput>('feedback'), ...buildMetadata })
45
+ const result = await feedback.init({
46
+ ...core.config.getOrThrow<import('@h-ai/feedback').FeedbackConfigInput>('feedback'),
47
+ ...buildMetadata,
48
+ })
46
49
  if (!result.success) {
47
50
  if (feedbackFailure !== result.error.code)
48
51
  core.logger.warn('Optional feedback unavailable; business runtime remains ready', { module: 'feedback', code: result.error.code })
@@ -76,62 +79,127 @@ async function closeFeedback() {
76
79
  await feedbackStarting
77
80
  return feedback.close()
78
81
  }
79
-
80
82
  {{/if}}
81
83
  const runtime = core.module.createRuntime([
82
- { name: 'configuration', start: () => {
83
- core.init({
84
- configDir: './config',
85
- configSchemas: {
84
+ {
85
+ name: 'configuration',
86
+ start: () => {
87
+ core.init({
88
+ configDir: './config',
89
+ configSchemas: {
86
90
  {{#if features.ai}}
87
- ai: AIConfigSchema,
91
+ ai: AIConfigSchema,
88
92
  {{/if}}
89
- audit: AuditInitConfigSchema,
90
- cache: CacheConfigSchema,
91
- db: ReldbConfigSchema,
93
+ audit: AuditInitConfigSchema,
94
+ cache: CacheConfigSchema,
95
+ db: ReldbConfigSchema,
92
96
  {{#if features.feedback}}
93
- feedback: FeedbackConfigSchema,
97
+ feedback: FeedbackConfigSchema,
94
98
  {{/if}}
95
- iam: IamConfigSchema,
96
- reach: ReachConfigSchema,
97
- scheduler: SchedulerConfigSchema,
98
- serv: ServConfigSchema,
99
- storage: StorageConfigSchema,
99
+ iam: IamConfigSchema,
100
+ reach: ReachConfigSchema,
101
+ scheduler: SchedulerConfigSchema,
102
+ serv: ServConfigSchema,
103
+ storage: StorageConfigSchema,
100
104
  {{#if features.vecdb}}
101
- vecdb: VecdbConfigSchema,
105
+ vecdb: VecdbConfigSchema,
102
106
  {{/if}}
103
- },
104
- })
105
- }, close: () => {} },
106
- { name: 'serv-config', start: () => core.config.validate('serv', ServConfigSchema), close: () => {} },
107
- { name: 'reldb', start: async () => {
108
- const config = ReldbConfigSchema.parse(core.config.getOrThrow('db'))
109
- if (config.type === 'sqlite' && config.database !== ':memory:')
110
- await mkdir(path.dirname(config.database), { recursive: true })
111
- return reldb.init(config)
112
- }, close: () => reldb.close() },
113
- { name: 'cache', start: () => cache.init(CacheConfigSchema.parse(core.config.getOrThrow('cache'))), close: () => cache.close() },
107
+ },
108
+ })
109
+ },
110
+ close: () => { },
111
+ },
112
+ {
113
+ name: 'serv-config',
114
+ start: () => core.config.validate('serv', ServConfigSchema),
115
+ close: () => { },
116
+ },
117
+ {
118
+ name: 'reldb',
119
+ start: async () => {
120
+ const config = ReldbConfigSchema.parse(core.config.getOrThrow('db'))
121
+ if (config.type === 'sqlite' && config.database !== ':memory:')
122
+ await mkdir(path.dirname(config.database), { recursive: true })
123
+ return reldb.init(config)
124
+ },
125
+ close: () => reldb.close(),
126
+ },
127
+ {
128
+ name: 'cache',
129
+ start: () => cache.init(CacheConfigSchema.parse(core.config.getOrThrow('cache'))),
130
+ close: () => cache.close(),
131
+ },
114
132
  {{#if features.vecdb}}
115
- { name: 'vecdb', start: () => vecdb.init(VecdbConfigSchema.parse(core.config.getOrThrow('vecdb'))), close: () => vecdb.close() },
133
+ {
134
+ name: 'vecdb',
135
+ start: () => vecdb.init(VecdbConfigSchema.parse(core.config.getOrThrow('vecdb'))),
136
+ close: () => vecdb.close(),
137
+ },
116
138
  {{/if}}
117
- { name: 'crypto', start: () => crypto.init(), close: () => crypto.close() },
118
- { name: 'storage', start: () => storage.init(StorageConfigSchema.parse(core.config.getOrThrow('storage'))), close: () => storage.close() },
119
- { name: 'reach', enabled: () => Boolean(core.config.get('reach')), start: () => reach.init(ReachConfigSchema.parse(core.config.getOrThrow('reach'))), close: () => reach.close() },
120
- { name: 'audit', start: () => audit.init({ ...AuditInitConfigSchema.parse(core.config.get('audit') ?? {}), storage, reach }), close: () => audit.close() },
121
- { name: 'iam', start: () => iam.init({ ...IamConfigSchema.parse(core.config.getOrThrow('iam')), reach }), close: () => iam.close() },
122
- { name: 'audit-jobs', enabled: () => Boolean(audit.schedulerPlan), start: startAuditJobs, close: () => scheduler.close() },
139
+ {
140
+ name: 'crypto',
141
+ start: () => crypto.init(),
142
+ close: () => crypto.close(),
143
+ },
144
+ {
145
+ name: 'storage',
146
+ start: () => storage.init(StorageConfigSchema.parse(core.config.getOrThrow('storage'))),
147
+ close: () => storage.close(),
148
+ },
149
+ {
150
+ name: 'reach',
151
+ enabled: () => Boolean(core.config.get('reach')),
152
+ start: () => reach.init(ReachConfigSchema.parse(core.config.getOrThrow('reach'))),
153
+ close: () => reach.close(),
154
+ },
155
+ {
156
+ name: 'audit',
157
+ start: () => audit.init({
158
+ ...AuditInitConfigSchema.parse(core.config.get('audit') ?? {}),
159
+ storage,
160
+ reach,
161
+ }),
162
+ close: () => audit.close(),
163
+ },
164
+ {
165
+ name: 'iam',
166
+ start: () => iam.init({
167
+ ...IamConfigSchema.parse(core.config.getOrThrow('iam')),
168
+ reach,
169
+ }),
170
+ close: () => iam.close(),
171
+ },
172
+ {
173
+ name: 'audit-jobs',
174
+ enabled: () => Boolean(audit.schedulerPlan),
175
+ start: startAuditJobs,
176
+ close: () => scheduler.close(),
177
+ },
123
178
  {{#if features.feedback}}
124
- { name: 'feedback', enabled: () => Boolean(core.config.get('feedback')), start: startFeedback, close: closeFeedback },
179
+ {
180
+ name: 'feedback',
181
+ enabled: () => Boolean(core.config.get('feedback')),
182
+ start: startFeedback,
183
+ close: closeFeedback,
184
+ },
125
185
  {{/if}}
126
186
  {{#if features.ai}}
127
- { name: 'ai', start: () => ai.init(AIConfigSchema.parse(core.config.getOrThrow('ai'))), close: () => ai.close() },
187
+ {
188
+ name: 'ai',
189
+ start: () => ai.init(AIConfigSchema.parse(core.config.getOrThrow('ai'))),
190
+ close: () => ai.close(),
191
+ },
128
192
  {{/if}}
129
- { name: 'bootstrap', start: async () => {
130
- requireInitialized(await initBusinessSettings(), 'business-settings')
131
- await ensureRolesAndAdmin()
132
- const app = core.config.getOrThrow<CoreConfig>('core')
133
- core.logger.info('Application initialized', { name: app.name, version: app.version, environment: app.env })
134
- }, close: () => {} },
193
+ {
194
+ name: 'bootstrap',
195
+ start: async () => {
196
+ requireInitialized(await initBusinessSettings(), 'business-settings')
197
+ await ensureRolesAndAdmin()
198
+ const app = core.config.getOrThrow<CoreConfig>('core')
199
+ core.logger.info('Application initialized', { name: app.name, version: app.version, environment: app.env })
200
+ },
201
+ close: () => { },
202
+ },
135
203
  ])
136
204
 
137
205
  /** 仅声明模块和业务钩子,失败回滚、并发及关闭重试由 core 负责。 */
@@ -149,15 +217,27 @@ export async function closeModules(): Promise<void> {
149
217
 
150
218
  /** 此函数仅用于进程启动边界,业务请求继续返回 HaiResult。 */
151
219
  function requireInitialized<T>(result: HaiResult<T>, moduleName: string): T {
152
- if (!result.success)
153
- throw Object.assign(new Error(moduleName), { code: result.error.code, ext: { fields: moduleName === 'iam-bootstrap' ? ['HAI_ADMIN_DEFAULT_PASSWORD'] : [] } })
220
+ if (!result.success) {
221
+ throw Object.assign(new Error(moduleName), {
222
+ code: result.error.code,
223
+ ext: {
224
+ fields: moduleName === 'iam-bootstrap'
225
+ ? ['HAI_ADMIN_DEFAULT_PASSWORD']
226
+ : [],
227
+ },
228
+ })
229
+ }
154
230
  return result.data
155
231
  }
156
232
 
157
233
  async function ensureRolesAndAdmin(): Promise<void> {
158
234
  requireInitialized(await seedAppDuties(), 'app-duties')
159
235
  await ensureInitialSystemAdmin()
160
- for (const [username, password] of [['audit_admin', process.env.HAI_AUDIT_ADMIN_PASSWORD], ['config_admin', process.env.HAI_CONFIG_ADMIN_PASSWORD], ['business_operator', process.env.HAI_BUSINESS_OPERATOR_PASSWORD]] as const)
236
+ for (const [username, password] of [
237
+ ['audit_admin', process.env.HAI_AUDIT_ADMIN_PASSWORD],
238
+ ['config_admin', process.env.HAI_CONFIG_ADMIN_PASSWORD],
239
+ ['business_operator', process.env.HAI_BUSINESS_OPERATOR_PASSWORD],
240
+ ] as const)
161
241
  requireInitialized(await bootstrapDutyAccount(username, password), 'app-duty-bootstrap')
162
242
  }
163
243
 
@@ -166,9 +246,19 @@ async function ensureInitialSystemAdmin(): Promise<void> {
166
246
  if (users.total > 0)
167
247
  return
168
248
  const password = process.env.HAI_ADMIN_DEFAULT_PASSWORD?.trim()
169
- if (!password)
170
- throw Object.assign(new Error('Administrator bootstrap configuration required'), { code: HaiConfigError.CONFIG_ENV_VAR_MISSING.code, ext: { fields: ['HAI_ADMIN_DEFAULT_PASSWORD'] } })
171
- const registered = requireInitialized(await iam.user.createUser({ username: 'admin', password, displayName: serverM('admin') }), 'iam-bootstrap')
249
+ if (!password) {
250
+ throw Object.assign(new Error('Administrator bootstrap configuration required'), {
251
+ code: HaiConfigError.CONFIG_ENV_VAR_MISSING.code,
252
+ ext: {
253
+ fields: ['HAI_ADMIN_DEFAULT_PASSWORD'],
254
+ },
255
+ })
256
+ }
257
+ const registered = requireInitialized(await iam.user.createUser({
258
+ username: 'admin',
259
+ password,
260
+ displayName: serverM('admin'),
261
+ }), 'iam-bootstrap')
172
262
  const adminRole = requireInitialized(await iam.authz.getRoleByCode('admin'), 'iam-role')
173
263
  if (!adminRole)
174
264
  throw new Error('Administrator role is missing')
@@ -8,14 +8,26 @@ export const APP_DUTIES = {
8
8
  admin: ['app.users.manage', 'app.roles.manage'],
9
9
  audit_admin: ['audit:read', 'audit:backup'],
10
10
  config_admin: ['app.config.manage'],
11
- business_operator: ['feedback:create', 'feedback:read', 'feedback:process', 'feedback:reopen', 'feedback:export'],
11
+ business_operator: [
12
+ 'feedback:create',
13
+ 'feedback:read',
14
+ 'feedback:process',
15
+ 'feedback:reopen',
16
+ 'feedback:export',
17
+ ],
12
18
  user: ['feedback:create'],
13
19
  } as const
14
20
 
15
21
  export async function seedAppDuties(): Promise<HaiResult<void>> {
16
22
  if (iam.config?.rbac.superAdminRole && iam.config.rbac.superAdminRole in APP_DUTIES)
17
23
  return err(HaiCommonError.VALIDATION_ERROR)
18
- for (const code of ['admin', 'audit_admin', 'config_admin', 'business_operator', 'user'] as const) {
24
+ for (const code of [
25
+ 'admin',
26
+ 'audit_admin',
27
+ 'config_admin',
28
+ 'business_operator',
29
+ 'user',
30
+ ] as const) {
19
31
  const permissionIds: string[] = []
20
32
  for (const permissionCode of APP_DUTIES[code]) {
21
33
  let permission = await iam.authz.getPermissionByCode(permissionCode)
@@ -25,8 +37,11 @@ export async function seedAppDuties(): Promise<HaiResult<void>> {
25
37
  permission = await iam.authz.createPermission({ code: permissionCode, name: permissionCode, type: 'api' })
26
38
  if (!permission.success) {
27
39
  permission = await iam.authz.getPermissionByCode(permissionCode)
28
- if (!permission.success || !permission.data)
29
- return permission.success ? err(HaiCommonError.INTERNAL_ERROR) : permission
40
+ if (!permission.success || !permission.data) {
41
+ return permission.success
42
+ ? err(HaiCommonError.INTERNAL_ERROR)
43
+ : permission
44
+ }
30
45
  }
31
46
  }
32
47
  if (permission.data)
@@ -36,16 +51,26 @@ export async function seedAppDuties(): Promise<HaiResult<void>> {
36
51
  if (!role.success)
37
52
  return role
38
53
  if (!role.data) {
39
- role = await iam.authz.createRole({ code, name: serverM(code), isSystem: true })
54
+ role = await iam.authz.createRole({
55
+ code,
56
+ name: serverM(code),
57
+ isSystem: true,
58
+ })
40
59
  if (!role.success) {
41
60
  role = await iam.authz.getRoleByCode(code)
42
- if (!role.success || !role.data)
43
- return role.success ? err(HaiCommonError.INTERNAL_ERROR) : role
61
+ if (!role.success || !role.data) {
62
+ return role.success
63
+ ? err(HaiCommonError.INTERNAL_ERROR)
64
+ : role
65
+ }
44
66
  }
45
67
  }
46
68
  if (!role.data)
47
69
  return err(HaiCommonError.INTERNAL_ERROR)
48
- const updated = await iam.authz.updateRole(role.data.id, { name: serverM(code), permissionIds })
70
+ const updated = await iam.authz.updateRole(role.data.id, {
71
+ name: serverM(code),
72
+ permissionIds,
73
+ })
49
74
  if (!updated.success)
50
75
  return updated
51
76
  }
@@ -58,11 +83,17 @@ export async function bootstrapDutyAccount(username: 'audit_admin' | 'config_adm
58
83
  if (!password)
59
84
  return ok(undefined)
60
85
  const created = await iam.user.createUser({ username, password })
61
- if (!created.success)
62
- return created.error.code === HaiIamError.USER_ALREADY_EXISTS.code ? ok(undefined) : created
86
+ if (!created.success) {
87
+ return created.error.code === HaiIamError.USER_ALREADY_EXISTS.code
88
+ ? ok(undefined)
89
+ : created
90
+ }
63
91
  const role = await iam.authz.getRoleByCode(username)
64
- if (!role.success || !role.data)
65
- return role.success ? err(HaiCommonError.INTERNAL_ERROR) : role
92
+ if (!role.success || !role.data) {
93
+ return role.success
94
+ ? err(HaiCommonError.INTERNAL_ERROR)
95
+ : role
96
+ }
66
97
  const assigned = await iam.authz.syncRoles(created.data.user.id, [role.data.id])
67
98
  if (!assigned.success) {
68
99
  const removed = await iam.user.deleteUser(created.data.user.id)
@@ -34,8 +34,17 @@ interface LoginData { user: { id: string }, tokens: { accessToken: string, refre
34
34
  async function call<T>(route: string, input?: object, token?: string, method = 'POST'): Promise<HaiResult<T>> {
35
35
  const response = await transport.encryptedFetch(`http://localhost${PREFIX}${route}`, {
36
36
  method,
37
- headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
38
- ...(input ? { body: JSON.stringify(input) } : {}),
37
+ headers: {
38
+ 'content-type': 'application/json',
39
+ ...(token
40
+ ? { authorization: `Bearer ${token}` }
41
+ : {}),
42
+ },
43
+ ...(input
44
+ ? {
45
+ body: JSON.stringify(input),
46
+ }
47
+ : {}),
39
48
  })
40
49
  return response.json() as Promise<HaiResult<T>>
41
50
  }
@@ -49,7 +58,10 @@ beforeAll(async () => {
49
58
  const key = crypto.asymmetric.generateKeyPair()
50
59
  if (!key.success)
51
60
  throw new Error(key.error.message)
52
- vi.stubEnv('HAI_CRYPTO_TRANSPORT_SERVERKEYRING', JSON.stringify({ activeKeyId: 'test-v1', keys: { 'test-v1': key.data } }))
61
+ vi.stubEnv('HAI_CRYPTO_TRANSPORT_SERVERKEYRING', JSON.stringify({
62
+ activeKeyId: 'test-v1',
63
+ keys: { 'test-v1': key.data },
64
+ }))
53
65
  app = createServerApp()
54
66
  transport = crypto.transport.createClient({
55
67
  keyExchangeUrl: `http://localhost${PREFIX}/_hai/key-exchange`,
@@ -99,7 +111,12 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
99
111
  if (!result.success)
100
112
  return
101
113
  const config = core.config.getOrThrow<CoreConfig>('core')
102
- expect(result.data).toMatchObject({ name: config.name, version: config.version, env: config.env, defaultLocale: config.defaultLocale })
114
+ expect(result.data).toMatchObject({
115
+ name: config.name,
116
+ version: config.version,
117
+ env: config.env,
118
+ defaultLocale: config.defaultLocale,
119
+ })
103
120
  const invalid = await transport.encryptedFetch(`http://localhost${PREFIX}/app/echo`, {
104
121
  method: 'POST',
105
122
  headers: { 'content-type': 'application/json' },
@@ -129,7 +146,14 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
129
146
  it('mounts public auth routes while keeping default IAM administration routes private', async () => {
130
147
  const config = await transport.encryptedFetch(`http://localhost${PREFIX}/auth/config`)
131
148
  expect(config.status).toBe(200)
132
- for (const route of ['/auth/register', '/auth/login/ldap', '/auth/login/otp', '/auth/otp/send', '/auth/password-reset/request', '/auth/password-reset/confirm']) {
149
+ for (const route of [
150
+ '/auth/register',
151
+ '/auth/login/ldap',
152
+ '/auth/login/otp',
153
+ '/auth/otp/send',
154
+ '/auth/password-reset/request',
155
+ '/auth/password-reset/confirm',
156
+ ]) {
133
157
  const response = await transport.encryptedFetch(`http://localhost${PREFIX}${route}`, {
134
158
  method: 'POST',
135
159
  headers: { 'content-type': 'application/json' },
@@ -144,14 +168,21 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
144
168
  const response = await transport.encryptedFetch(`http://localhost${PREFIX}${route}`, {
145
169
  method,
146
170
  headers: { 'content-type': 'application/json', 'authorization': `Bearer ${adminToken}` },
147
- ...(method === 'POST' ? { body: '{}' } : {}),
171
+ ...(method === 'POST'
172
+ ? { body: '{}' }
173
+ : {}),
148
174
  })
149
175
  expect(response.status).toBe(404)
150
176
  }
151
177
  })
152
178
 
153
179
  it('creates a normal user and enforces guards on every administration API', async () => {
154
- const created = await call<AdminUser>('/admin/users/create', { username: 'normal-user', password: TEST_PASSWORD, role: 'user', accountStatus: 'active' }, adminToken)
180
+ const created = await call<AdminUser>('/admin/users/create', {
181
+ username: 'normal-user',
182
+ password: TEST_PASSWORD,
183
+ role: 'user',
184
+ accountStatus: 'active',
185
+ }, adminToken)
155
186
  expect(created.success).toBe(true)
156
187
  if (!created.success)
157
188
  return
@@ -162,14 +193,35 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
162
193
  return
163
194
  userToken = loggedIn.data.tokens.accessToken
164
195
  for (const [route, input] of [
165
- ['/admin/users/list', { page: 1, pageSize: 20 }],
166
- ['/admin/users/create', { username: 'forbidden', password: TEST_PASSWORD }],
167
- ['/admin/users/update', { id: adminId, accountStatus: 'dormant' }],
168
- ['/admin/users/delete', { id: adminId }],
169
- ['/admin/users/avatar', { id: adminId, contentType: 'image/png', base64: PNG }],
196
+ [
197
+ '/admin/users/list',
198
+ { page: 1, pageSize: 20 },
199
+ ],
200
+ [
201
+ '/admin/users/create',
202
+ { username: 'forbidden', password: TEST_PASSWORD },
203
+ ],
204
+ [
205
+ '/admin/users/update',
206
+ { id: adminId, accountStatus: 'dormant' },
207
+ ],
208
+ [
209
+ '/admin/users/delete',
210
+ { id: adminId },
211
+ ],
212
+ [
213
+ '/admin/users/avatar',
214
+ { id: adminId, contentType: 'image/png', base64: PNG },
215
+ ],
170
216
  ['/admin/roles/list', undefined],
171
- ['/admin/roles/update', { id: 'missing', description: 'x' }],
172
- ['/admin/audit/list', { page: 1, pageSize: 20 }],
217
+ [
218
+ '/admin/roles/update',
219
+ { id: 'missing', description: 'x' },
220
+ ],
221
+ [
222
+ '/admin/audit/list',
223
+ { page: 1, pageSize: 20 },
224
+ ],
173
225
  ] as const) {
174
226
  const denied = await call(route, input, userToken)
175
227
  expect(denied.success).toBe(false)
@@ -179,8 +231,16 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
179
231
  })
180
232
 
181
233
  it('allows refresh-token body transport only for an identified trusted native origin', async () => {
182
- for (const [clientName, origin] of [['{{projectName}}-app', 'capacitor://localhost'], ['{{projectName}}-desktop', 'http://tauri.localhost']] as const) {
183
- const headers = { 'content-type': 'application/json', 'x-request-client': clientName, 'x-hai-token-transport': 'body', origin }
234
+ for (const [clientName, origin] of [
235
+ ['{{projectName}}-app', 'capacitor://localhost'],
236
+ ['{{projectName}}-desktop', 'http://tauri.localhost'],
237
+ ] as const) {
238
+ const headers = {
239
+ 'content-type': 'application/json',
240
+ 'x-request-client': clientName,
241
+ 'x-hai-token-transport': 'body',
242
+ origin,
243
+ }
184
244
  const login = await transport.encryptedFetch(`http://localhost${PREFIX}/auth/login`, {
185
245
  method: 'POST',
186
246
  headers,
@@ -202,10 +262,18 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
202
262
  if (refreshed.success)
203
263
  expect(refreshed.data.tokens.refreshToken).not.toBe(result.data.tokens.refreshToken)
204
264
  }
205
- for (const [clientName, origin] of [['{{projectName}}-app', 'http://untrusted.example'], ['unrecognized-client', 'capacitor://localhost']] as const) {
265
+ for (const [clientName, origin] of [
266
+ ['{{projectName}}-app', 'http://untrusted.example'],
267
+ ['unrecognized-client', 'capacitor://localhost'],
268
+ ] as const) {
206
269
  const response = await transport.encryptedFetch(`http://localhost${PREFIX}/auth/login`, {
207
270
  method: 'POST',
208
- headers: { 'content-type': 'application/json', 'x-request-client': clientName, 'x-hai-token-transport': 'body', origin },
271
+ headers: {
272
+ 'content-type': 'application/json',
273
+ 'x-request-client': clientName,
274
+ 'x-hai-token-transport': 'body',
275
+ origin,
276
+ },
209
277
  body: JSON.stringify({ identifier: 'admin', password: TEST_PASSWORD }),
210
278
  })
211
279
  const result = await response.json() as HaiResult<LoginData>
@@ -222,7 +290,20 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
222
290
  expect(listed.data.items).toHaveLength(1)
223
291
  expect(listed.data.total).toBe(2)
224
292
  }
225
- for (const [route, input] of [['/admin/users/delete', { id: adminId }], ['/admin/users/update', { id: adminId, accountStatus: 'dormant' }], ['/admin/users/update', { id: adminId, role: 'user' }]] as const) {
293
+ for (const [route, input] of [
294
+ [
295
+ '/admin/users/delete',
296
+ { id: adminId },
297
+ ],
298
+ [
299
+ '/admin/users/update',
300
+ { id: adminId, accountStatus: 'dormant' },
301
+ ],
302
+ [
303
+ '/admin/users/update',
304
+ { id: adminId, role: 'user' },
305
+ ],
306
+ ] as const) {
226
307
  const denied = await call(route, input, adminToken)
227
308
  expect(denied.success).toBe(false)
228
309
  if (!denied.success)
@@ -248,7 +329,10 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
248
329
  expect(invalid.success).toBe(false)
249
330
  if (!invalid.success)
250
331
  expect(invalid.error.httpStatus).toBe(400)
251
- const oversized = await call('/profile/avatar', { contentType: 'image/png', base64: Buffer.alloc(512 * 1024 + 1).toString('base64') }, userToken)
332
+ const oversized = await call('/profile/avatar', {
333
+ contentType: 'image/png',
334
+ base64: Buffer.alloc(512 * 1024 + 1).toString('base64'),
335
+ }, userToken)
252
336
  expect(oversized.success).toBe(false)
253
337
  const anonymous = await call('/profile/avatar', { contentType: 'image/png', base64: PNG })
254
338
  expect(anonymous.success).toBe(false)
@@ -296,7 +380,13 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
296
380
  const roles = await call<{ items: { id: string, code: string, userCount: number }[] }>('/admin/roles/list', undefined, adminToken)
297
381
  expect(roles.success).toBe(true)
298
382
  if (roles.success) {
299
- expect(roles.data.items.map(item => item.code).sort()).toEqual(['admin', 'audit_admin', 'business_operator', 'config_admin', 'user'])
383
+ expect(roles.data.items.map(item => item.code).sort()).toEqual([
384
+ 'admin',
385
+ 'audit_admin',
386
+ 'business_operator',
387
+ 'config_admin',
388
+ 'user',
389
+ ])
300
390
  const role = roles.data.items.find(item => item.code === 'user')!
301
391
  expect(role.userCount).toBe(1)
302
392
  expect((await call('/admin/roles/update', { id: role.id, description: 'Updated description' }, adminToken)).success).toBe(true)
@@ -324,7 +414,12 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
324
414
  const before = await iam.user.getUser(user.id)
325
415
  expect(before.success).toBe(true)
326
416
  const sync = vi.spyOn(iam.authz, 'syncRoles').mockResolvedValueOnce(err(HaiCommonError.API_ERROR, 'Injected role persistence failure'))
327
- const failed = await call('/admin/users/update', { id: user.id, role: 'admin', email: 'changed@example.com', displayName: 'Should not change' }, adminToken)
417
+ const failed = await call('/admin/users/update', {
418
+ id: user.id,
419
+ role: 'admin',
420
+ email: 'changed@example.com',
421
+ displayName: 'Should not change',
422
+ }, adminToken)
328
423
  sync.mockRestore()
329
424
  expect(failed.success).toBe(false)
330
425
  const after = await iam.user.getUser(user.id)
@@ -338,7 +433,7 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
338
433
  const log = vi.spyOn(audit, 'log').mockImplementation((input, tx) => input.action === 'update' && input.resource === 'profile'
339
434
  ? Promise.resolve(err(HaiCommonError.API_ERROR, 'Injected audit persistence failure'))
340
435
  : originalLog(input, tx))
341
- const logger = vi.spyOn(core.logger, 'error').mockImplementation(() => {})
436
+ const logger = vi.spyOn(core.logger, 'error').mockImplementation(() => { })
342
437
  try {
343
438
  const result = await call<AdminUser>('/profile/update', { displayName: 'Committed profile' }, userToken)
344
439
  expect(result.success).toBe(true)
@@ -374,9 +469,22 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
374
469
  const snapshot = await iam.session.get(userToken)
375
470
  if (!snapshot.success || !snapshot.data)
376
471
  throw new Error('Session unavailable')
377
- const stale = await iam.session.update(userToken, { roles: ['admin'], permissions: ['*'] }, snapshot.data.version)
472
+ const stale = await iam.session.update(userToken, {
473
+ roles: ['admin'],
474
+ permissions: ['*'],
475
+ }, snapshot.data.version)
378
476
  expect(stale.success).toBe(true)
379
- for (const [route, input] of [['/admin/users/list', { page: 1, pageSize: 20 }], ['/admin/roles/list', undefined], ['/admin/audit/list', { page: 1, pageSize: 20 }]] as const) {
477
+ for (const [route, input] of [
478
+ [
479
+ '/admin/users/list',
480
+ { page: 1, pageSize: 20 },
481
+ ],
482
+ ['/admin/roles/list', undefined],
483
+ [
484
+ '/admin/audit/list',
485
+ { page: 1, pageSize: 20 },
486
+ ],
487
+ ] as const) {
380
488
  const denied = await call(route, input, userToken)
381
489
  expect(denied.success).toBe(false)
382
490
  if (!denied.success)
@@ -415,8 +523,8 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
415
523
  expect(otherLogin.success).toBe(true)
416
524
  if (!otherLogin.success)
417
525
  return
418
- let resume: () => void = () => {}
419
- let entered: () => void = () => {}
526
+ let resume: () => void = () => { }
527
+ let entered: () => void = () => { }
420
528
  const paused = new Promise<void>((resolve) => {
421
529
  resume = resolve
422
530
  })
@@ -467,7 +575,10 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
467
575
  call('/admin/users/update', { id: adminId, accountStatus: 'dormant' }, loggedIn.data.tokens.accessToken),
468
576
  ])
469
577
  expect(results.filter(result => result.success)).toHaveLength(1)
470
- const admins = await iam.user.listUsers({ accountStatus: 'active', include: ['roles'] })
578
+ const admins = await iam.user.listUsers({
579
+ accountStatus: 'active',
580
+ include: ['roles'],
581
+ })
471
582
  expect(admins.success).toBe(true)
472
583
  if (admins.success) {
473
584
  const surviving = admins.data.items.filter(item => item.roles?.some(role => role.code === 'admin'))