@oj-module/cli 0.2.18 → 0.2.20

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 (331) hide show
  1. package/devkit/README.md +4 -4
  2. package/devkit/SKILL.md +41 -37
  3. package/devkit/framework-dev.md +6 -5
  4. package/devkit/manual.md +259 -161
  5. package/devkit/menu-rules.md +3 -3
  6. package/devkit/module-dev-and-cdn-playground.md +31 -22
  7. package/devkit/sql-builder-missing-capabilities-pr.md +11 -6
  8. package/package.json +2 -2
  9. package/shell-dist/assets/runtime.js +2 -2
  10. package/shell-dist/index.html +2 -2
  11. package/shell-dist/versions.json +3 -3
  12. package/src/__tests__/build-backend.test.ts +54 -0
  13. package/src/__tests__/layout.test.ts +176 -0
  14. package/src/__tests__/module-list.test.ts +66 -0
  15. package/src/__tests__/oj-caps.test.ts +114 -0
  16. package/src/alias-paths.ts +102 -0
  17. package/src/build.ts +41 -9
  18. package/src/bundle.ts +25 -13
  19. package/src/config.ts +82 -9
  20. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  21. package/src/contract/check.ts +56 -28
  22. package/src/contract/emit-stub.ts +21 -9
  23. package/src/contract/handler-scan.ts +19 -10
  24. package/src/contract/run.ts +14 -4
  25. package/src/dev.ts +22 -7
  26. package/src/index.ts +1 -1
  27. package/src/info.ts +20 -3
  28. package/src/init.ts +91 -21
  29. package/src/layout.ts +282 -24
  30. package/src/lifecycle/__tests__/bundle.test.ts +16 -1
  31. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  32. package/src/market/__tests__/bootstrap.test.ts +17 -9
  33. package/src/market/bootstrap.ts +30 -17
  34. package/src/oj-caps.ts +133 -0
  35. package/src/oj.ts +27 -4
  36. package/src/preview-config.ts +1 -1
  37. package/src/preview.ts +2 -1
  38. package/src/site.ts +2 -2
  39. package/src/usage.ts +6 -6
  40. package/src/vendor.ts +5 -1
  41. package/templates/{api/config.yaml → config.yaml} +13 -8
  42. package/templates/global.d.ts +7 -6
  43. package/templates/init.include.yaml +41 -36
  44. package/templates/package.json +1 -1
  45. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/installer.ts +4 -0
  46. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/smoke.ts +21 -2
  47. package/templates/{api/src/_platform → src/_platform/api}/market/api.ts +25 -15
  48. package/templates/{api/src/_platform → src/_platform/api}/solutions/api.ts +7 -3
  49. package/templates/{api/src/auth → src/auth/api}/_shared/login-throttle.ts +2 -2
  50. package/templates/{api/src/auth → src/auth/api}/_shared/session.ts +2 -2
  51. package/templates/{api/src/auth → src/auth/api}/api.ts +1 -1
  52. package/templates/{api/src/auth → src/auth/api}/ldap-login/api.ts +1 -1
  53. package/templates/src/auth/api/manifest.yaml +6 -0
  54. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/InstallDialog.test.tsx +53 -1
  55. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/marketClient.fake.ts +6 -0
  56. package/templates/{web/src/oj-market → src/oj-market/web}/api/client.ts +5 -0
  57. package/templates/{web/src/oj-market → src/oj-market/web}/api/defaultClient.ts +7 -1
  58. package/templates/{web/src/oj-market → src/oj-market/web}/components/InstallDialog.tsx +80 -5
  59. package/templates/{web/src/oj-market → src/oj-market/web}/hooks/useMarket.ts +6 -1
  60. package/templates/{web/src/oj-market → src/oj-market/web}/locales/en-US.json +10 -1
  61. package/templates/{web/src/oj-market → src/oj-market/web}/locales/zh-CN.json +10 -1
  62. package/templates/{web/src/oj-market → src/oj-market/web}/types.ts +15 -2
  63. package/templates/{api/src/system → src/system/api}/login-methods/api.ts +2 -2
  64. package/templates/src/system/api/manifest.yaml +6 -0
  65. package/templates/{api/src/system → src/system/api}/oplog/api.ts +4 -4
  66. package/templates/{api/src/system → src/system/api}/permissions/api.ts +3 -3
  67. package/templates/{api/src/system → src/system/api}/roles/api.ts +3 -3
  68. package/templates/{api/src/system → src/system/api}/roles/bindings/api.ts +3 -3
  69. package/templates/{api/src/system → src/system/api}/settings/api.ts +3 -3
  70. package/templates/{api/src/system → src/system/api}/users/api.ts +3 -3
  71. package/templates/{api/src/system → src/system/api}/users/forbidden/api.ts +3 -3
  72. package/templates/{api/src/system → src/system/api}/users/password/api.ts +3 -3
  73. package/templates/{api/src/system → src/system/api}/users/roles/api.ts +3 -3
  74. package/templates/src/tsconfig.json +16 -0
  75. package/templates/{api/tests → tests}/auth-ldap.spec.ts +2 -2
  76. package/templates/{api/tests → tests}/auth-session.spec.ts +3 -3
  77. package/templates/{api/tests → tests}/auth-throttle.spec.ts +1 -1
  78. package/templates/tests/fixture.ts +434 -0
  79. package/templates/{api/tests → tests}/guard.spec.ts +1 -1
  80. package/templates/{api/tests → tests}/lifecycle/atomic-state.spec.ts +3 -3
  81. package/templates/{api/tests → tests}/lifecycle/bootstrap.spec.ts +8 -4
  82. package/templates/{api/tests → tests}/lifecycle/deploy-smoke.spec.ts +31 -8
  83. package/templates/{api/tests → tests}/lifecycle/installer.spec.ts +15 -15
  84. package/templates/{api/tests → tests}/lifecycle/market-api.spec.ts +30 -29
  85. package/templates/{api/tests → tests}/lifecycle/migrate.spec.ts +6 -6
  86. package/templates/{api/tests → tests}/lifecycle/preflight.spec.ts +4 -4
  87. package/templates/{api/tests → tests}/lifecycle/register.spec.ts +1 -1
  88. package/templates/{api/tests → tests}/lifecycle/semver.spec.ts +1 -1
  89. package/templates/{api/tests → tests}/lifecycle/sha384.spec.ts +1 -1
  90. package/templates/{api/tests → tests}/lifecycle/sig-fixtures.spec.ts +1 -1
  91. package/templates/{api/tests → tests}/lifecycle/solutions-api.spec.ts +1 -1
  92. package/templates/{api/tests → tests}/lifecycle/steps-fs.spec.ts +12 -12
  93. package/templates/{api/tests → tests}/lifecycle/text-edit.spec.ts +1 -1
  94. package/templates/{api/tests → tests}/lifecycle/tgz-guard.spec.ts +1 -1
  95. package/templates/{api/tests → tests}/lifecycle/trust.spec.ts +5 -5
  96. package/templates/{api/tests → tests}/lifecycle/uninstall.spec.ts +7 -7
  97. package/templates/{api/tests → tests}/lifecycle/verify-checks.spec.ts +9 -6
  98. package/templates/{api/tests → tests}/lifecycle/verify-report.spec.ts +7 -5
  99. package/templates/{api/tests → tests}/query-utils.spec.ts +1 -1
  100. package/templates/{api/tests → tests}/rbac-audit.spec.ts +1 -1
  101. package/templates/{api/tests → tests}/rbac-contacts.spec.ts +2 -2
  102. package/templates/{api/tests → tests}/rbac-guard.spec.ts +1 -1
  103. package/templates/{api/tests → tests}/rbac-roles.spec.ts +2 -2
  104. package/templates/{api/tests → tests}/rbac-seed.spec.ts +1 -1
  105. package/templates/{api/tests → tests}/rbac-settings.spec.ts +2 -2
  106. package/templates/{api/tests → tests}/rbac-solution-perms.spec.ts +1 -1
  107. package/templates/{api/tests → tests}/rbac-users.spec.ts +3 -3
  108. package/templates/tsconfig.json +5 -1
  109. package/templates/tsconfig.paths.json +19 -0
  110. package/templates/types/oj-modules.d.ts +17 -0
  111. package/templates/vitest.config.ts +22 -0
  112. package/templates/web.config.ts +23 -13
  113. package/vendor/host-versions.json +3 -3
  114. package/templates/api/src/auth/manifest.yaml +0 -5
  115. package/templates/api/src/system/manifest.yaml +0 -5
  116. package/templates/api/tests/fixture.ts +0 -58
  117. package/templates/api/tsconfig.json +0 -8
  118. package/templates/api/vitest.config.ts +0 -12
  119. package/templates/logs/task-events.jsonl +0 -0
  120. /package/templates/{api/.ojm-api-exempt.json → .ojm-api-exempt.json} +0 -0
  121. /package/templates/{api/seed.mysql.sql → seed.mysql.sql} +0 -0
  122. /package/templates/{api/seed.pg.sql → seed.pg.sql} +0 -0
  123. /package/templates/{api/src/_platform → src/_platform/api}/README.md +0 -0
  124. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/atomic.ts +0 -0
  125. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/history.ts +0 -0
  126. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/lock.ts +0 -0
  127. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/semver.ts +0 -0
  128. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/sha384.ts +0 -0
  129. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/state.ts +0 -0
  130. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/deploy-web.ts +0 -0
  131. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/download.ts +0 -0
  132. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/extract.ts +0 -0
  133. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/migrate.ts +0 -0
  134. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/preflight.ts +0 -0
  135. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/register.ts +0 -0
  136. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/reload.ts +0 -0
  137. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/verify.ts +0 -0
  138. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tasks.ts +0 -0
  139. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/text-edit.ts +0 -0
  140. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tgz-guard.ts +0 -0
  141. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/trust.ts +0 -0
  142. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/types.ts +0 -0
  143. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/uninstall.ts +0 -0
  144. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-checks.ts +0 -0
  145. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-report.ts +0 -0
  146. /package/templates/{api/src/_platform → src/_platform/api}/_shared/respond.ts +0 -0
  147. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/contacts.ts +0 -0
  148. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/roles.ts +0 -0
  149. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/settings.ts +0 -0
  150. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/solution-perms.ts +0 -0
  151. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/users.ts +0 -0
  152. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/access.ts +0 -0
  153. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/audit.ts +0 -0
  154. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/guard.ts +0 -0
  155. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/oplog.ts +0 -0
  156. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/query.ts +0 -0
  157. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/rate.ts +0 -0
  158. /package/templates/{api/src/_platform → src/_platform/api}/_shared/site-facade.ts +0 -0
  159. /package/templates/{api/src/_platform → src/_platform/api}/contacts/api.ts +0 -0
  160. /package/templates/{api/src/_platform → src/_platform/api}/contacts/detail/api.ts +0 -0
  161. /package/templates/{api/src/_platform → src/_platform/api}/fixtures/l1-test-user.sql +0 -0
  162. /package/templates/{api/src/_platform → src/_platform/api}/manifest.yaml +0 -0
  163. /package/templates/{api/src/_platform → src/_platform/api}/market/bootstrap.ts +0 -0
  164. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.mysql.sql +0 -0
  165. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.pg.sql +0 -0
  166. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.sql +0 -0
  167. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  168. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.sql +0 -0
  169. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  170. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.sql +0 -0
  171. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.mysql.sql +0 -0
  172. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.pg.sql +0 -0
  173. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.sql +0 -0
  174. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.mysql.sql +0 -0
  175. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.sql +0 -0
  176. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  177. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.sql +0 -0
  178. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0007__users_email.sql +0 -0
  179. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  180. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  181. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.sql +0 -0
  182. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.mysql.sql +0 -0
  183. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.pg.sql +0 -0
  184. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.sql +0 -0
  185. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  186. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.pg.sql +0 -0
  187. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.sql +0 -0
  188. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  189. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.sql +0 -0
  190. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.mysql.sql +0 -0
  191. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.pg.sql +0 -0
  192. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.sql +0 -0
  193. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0013__users_profile.sql +0 -0
  194. /package/templates/{api/src/_platform → src/_platform/api}/schema.yaml +0 -0
  195. /package/templates/{api/src/_platform → src/_platform/api}/seed.mysql.sql +0 -0
  196. /package/templates/{api/src/_platform → src/_platform/api}/seed.pg.sql +0 -0
  197. /package/templates/{api/src/_platform → src/_platform/api}/seed.sql +0 -0
  198. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.schemas.ts +0 -0
  199. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.ts +0 -0
  200. /package/templates/{web/src/about → src/about/web}/entry.ts +0 -0
  201. /package/templates/{web/src/about → src/about/web}/locales/en-US.json +0 -0
  202. /package/templates/{web/src/about → src/about/web}/locales/zh-CN.json +0 -0
  203. /package/templates/{web/src/about → src/about/web}/pages/constants.ts +0 -0
  204. /package/templates/{web/src/about → src/about/web}/pages/index.tsx +0 -0
  205. /package/templates/{web/src/access → src/access/web}/entry.ts +0 -0
  206. /package/templates/{web/src/access → src/access/web}/locales/en-US.json +0 -0
  207. /package/templates/{web/src/access → src/access/web}/locales/zh-CN.json +0 -0
  208. /package/templates/{web/src/access → src/access/web}/pages/access-mode/index.tsx +0 -0
  209. /package/templates/{web/src/access → src/access/web}/pages/admin-visible/index.tsx +0 -0
  210. /package/templates/{web/src/access → src/access/web}/pages/button-control/index.tsx +0 -0
  211. /package/templates/{web/src/access → src/access/web}/pages/common-visible/index.tsx +0 -0
  212. /package/templates/{web/src/access → src/access/web}/pages/page-control/index.tsx +0 -0
  213. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap-login.ts +0 -0
  214. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap.ts +0 -0
  215. /package/templates/{web/src/auth → src/auth/web}/__client/api.schemas.ts +0 -0
  216. /package/templates/{web/src/auth → src/auth/web}/__client/api.ts +0 -0
  217. /package/templates/{api/src/demo → src/demo/api}/manifest.yaml +0 -0
  218. /package/templates/{api/src/demo → src/demo/api}/migrations/0001__create_todos.sql +0 -0
  219. /package/templates/{api/src/demo → src/demo/api}/schema.yaml +0 -0
  220. /package/templates/{api/src/demo → src/demo/api}/seed.sql +0 -0
  221. /package/templates/{api/src/demo → src/demo/api}/todos/api.ts +0 -0
  222. /package/templates/{web/src/demo → src/demo/web}/__client/api.schemas.ts +0 -0
  223. /package/templates/{web/src/demo → src/demo/web}/__client/api.ts +0 -0
  224. /package/templates/{web/src/demo → src/demo/web}/entry.ts +0 -0
  225. /package/templates/{web/src/demo → src/demo/web}/locales/en-US.json +0 -0
  226. /package/templates/{web/src/demo → src/demo/web}/locales/zh-CN.json +0 -0
  227. /package/templates/{web/src/demo → src/demo/web}/pages/index.tsx +0 -0
  228. /package/templates/{web/src/exception → src/exception/web}/entry.ts +0 -0
  229. /package/templates/{web/src/exception → src/exception/web}/locales/en-US.json +0 -0
  230. /package/templates/{web/src/exception → src/exception/web}/locales/zh-CN.json +0 -0
  231. /package/templates/{web/src/exception → src/exception/web}/pages/403/index.tsx +0 -0
  232. /package/templates/{web/src/exception → src/exception/web}/pages/404/index.tsx +0 -0
  233. /package/templates/{web/src/exception → src/exception/web}/pages/500/index.tsx +0 -0
  234. /package/templates/{web/src/exception → src/exception/web}/pages/unknown-component/index.tsx +0 -0
  235. /package/templates/{api/src/home → src/home/api}/line/api.ts +0 -0
  236. /package/templates/{api/src/home → src/home/api}/manifest.yaml +0 -0
  237. /package/templates/{api/src/home → src/home/api}/migrations/0001__create_home_tables.sql +0 -0
  238. /package/templates/{api/src/home → src/home/api}/pie/api.ts +0 -0
  239. /package/templates/{api/src/home → src/home/api}/schema.yaml +0 -0
  240. /package/templates/{api/src/home → src/home/api}/seed.sql +0 -0
  241. /package/templates/{web/src/home → src/home/web}/__client/api.schemas.ts +0 -0
  242. /package/templates/{web/src/home → src/home/web}/__client/api.ts +0 -0
  243. /package/templates/{web/src/home → src/home/web}/entry.ts +0 -0
  244. /package/templates/{web/src/home → src/home/web}/locales/en-US.json +0 -0
  245. /package/templates/{web/src/home → src/home/web}/locales/zh-CN.json +0 -0
  246. /package/templates/{web/src/home → src/home/web}/pages/components/bar-chart.tsx +0 -0
  247. /package/templates/{web/src/home → src/home/web}/pages/components/card-list.tsx +0 -0
  248. /package/templates/{web/src/home → src/home/web}/pages/components/line-chart.tsx +0 -0
  249. /package/templates/{web/src/home → src/home/web}/pages/components/pie-chart.tsx +0 -0
  250. /package/templates/{web/src/home → src/home/web}/pages/index.tsx +0 -0
  251. /package/templates/{web/src/login → src/login/web}/entry.ts +0 -0
  252. /package/templates/{web/src/login → src/login/web}/locales/en-US.json +0 -0
  253. /package/templates/{web/src/login → src/login/web}/locales/zh-CN.json +0 -0
  254. /package/templates/{web/src/login → src/login/web}/pages/change-password.tsx +0 -0
  255. /package/templates/{web/src/login → src/login/web}/pages/login.tsx +0 -0
  256. /package/templates/{api/src/notification → src/notification/api}/_shared/notify.ts +0 -0
  257. /package/templates/{api/src/notification → src/notification/api}/api.ts +0 -0
  258. /package/templates/{api/src/notification → src/notification/api}/del/api.ts +0 -0
  259. /package/templates/{api/src/notification → src/notification/api}/manifest.yaml +0 -0
  260. /package/templates/{api/src/notification → src/notification/api}/migrations/0001__create_notifications.sql +0 -0
  261. /package/templates/{api/src/notification → src/notification/api}/migrations/0002__notifications_user.sql +0 -0
  262. /package/templates/{api/src/notification → src/notification/api}/migrations/0003__notifications_broadcast.sql +0 -0
  263. /package/templates/{api/src/notification → src/notification/api}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  264. /package/templates/{api/src/notification → src/notification/api}/schema.yaml +0 -0
  265. /package/templates/{api/src/notification → src/notification/api}/seed.sql +0 -0
  266. /package/templates/{web/src/notification → src/notification/web}/__client/api.schemas.ts +0 -0
  267. /package/templates/{web/src/notification → src/notification/web}/__client/api.ts +0 -0
  268. /package/templates/{web/src/notification → src/notification/web}/entry.ts +0 -0
  269. /package/templates/{web/src/notification → src/notification/web}/locales/en-US.json +0 -0
  270. /package/templates/{web/src/notification → src/notification/web}/locales/zh-CN.json +0 -0
  271. /package/templates/{web/src/notification → src/notification/web}/pages/center/index.tsx +0 -0
  272. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/CardGrid.test.tsx +0 -0
  273. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/HistoryDrawer.test.tsx +0 -0
  274. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/RollbackDialog.test.tsx +0 -0
  275. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/UninstallConfirm.test.tsx +0 -0
  276. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/poll.test.ts +0 -0
  277. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/render.tsx +0 -0
  278. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/runtime-stub.ts +0 -0
  279. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/semver.test.ts +0 -0
  280. /package/templates/{web/src/oj-market → src/oj-market/web}/api/MarketClientProvider.tsx +0 -0
  281. /package/templates/{web/src/oj-market → src/oj-market/web}/api/context.ts +0 -0
  282. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardGrid.tsx +0 -0
  283. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardItem.tsx +0 -0
  284. /package/templates/{web/src/oj-market → src/oj-market/web}/components/DetailDrawer.tsx +0 -0
  285. /package/templates/{web/src/oj-market → src/oj-market/web}/components/HistoryDrawer.tsx +0 -0
  286. /package/templates/{web/src/oj-market → src/oj-market/web}/components/RollbackDialog.tsx +0 -0
  287. /package/templates/{web/src/oj-market → src/oj-market/web}/components/UninstallConfirm.tsx +0 -0
  288. /package/templates/{web/src/oj-market → src/oj-market/web}/entry.ts +0 -0
  289. /package/templates/{web/src/oj-market → src/oj-market/web}/pages/MarketApp.tsx +0 -0
  290. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/poll.ts +0 -0
  291. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/semver.ts +0 -0
  292. /package/templates/{web/src/outside → src/outside/web}/entry.ts +0 -0
  293. /package/templates/{web/src/outside → src/outside/web}/locales/en-US.json +0 -0
  294. /package/templates/{web/src/outside → src/outside/web}/locales/zh-CN.json +0 -0
  295. /package/templates/{api/src/personal-center → src/personal-center/api}/manifest.yaml +0 -0
  296. /package/templates/{api/src/personal-center → src/personal-center/api}/profile/api.ts +0 -0
  297. /package/templates/{api/src/personal-center → src/personal-center/api}/upload/api.ts +0 -0
  298. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.schemas.ts +0 -0
  299. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.ts +0 -0
  300. /package/templates/{web/src/personal-center → src/personal-center/web}/entry.ts +0 -0
  301. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/en-US.json +0 -0
  302. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/zh-CN.json +0 -0
  303. /package/templates/{web/src/personal-center → src/personal-center/web}/pages/my-profile/index.tsx +0 -0
  304. /package/templates/{web/src/route-nest → src/route-nest/web}/entry.ts +0 -0
  305. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/en-US.json +0 -0
  306. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/zh-CN.json +0 -0
  307. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-1/index.tsx +0 -0
  308. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-2/index.tsx +0 -0
  309. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu2/index.tsx +0 -0
  310. /package/templates/{web/src → src}/styles.css +0 -0
  311. /package/templates/{web/src/system → src/system/web}/__client/api.schemas.ts +0 -0
  312. /package/templates/{web/src/system → src/system/web}/__client/api.ts +0 -0
  313. /package/templates/{web/src/system → src/system/web}/entry.ts +0 -0
  314. /package/templates/{web/src/system → src/system/web}/locales/en-US.json +0 -0
  315. /package/templates/{web/src/system → src/system/web}/locales/zh-CN.json +0 -0
  316. /package/templates/{web/src/system → src/system/web}/pages/errors.ts +0 -0
  317. /package/templates/{web/src/system → src/system/web}/pages/forbidden.tsx +0 -0
  318. /package/templates/{web/src/system → src/system/web}/pages/login-methods/index.tsx +0 -0
  319. /package/templates/{web/src/system → src/system/web}/pages/oplog/index.tsx +0 -0
  320. /package/templates/{web/src/system → src/system/web}/pages/roles/index.tsx +0 -0
  321. /package/templates/{web/src/system → src/system/web}/pages/settings/index.tsx +0 -0
  322. /package/templates/{web/src/system → src/system/web}/pages/users/index.tsx +0 -0
  323. /package/templates/{web/src/system → src/system/web}/pages/users/roles.tsx +0 -0
  324. /package/templates/{api/src/web → src/web/api}/get-async-routes/api.ts +0 -0
  325. /package/templates/{api/src/web → src/web/api}/hello/api.ts +0 -0
  326. /package/templates/{api/src/web → src/web/api}/manifest.yaml +0 -0
  327. /package/templates/{api/src/web → src/web/api}/user-info/api.ts +0 -0
  328. /package/templates/{web/src → src/web}/web/__client/api.schemas.ts +0 -0
  329. /package/templates/{web/src → src/web}/web/__client/api.ts +0 -0
  330. /package/templates/{api/tests → tests}/l1-builder.test.ts +0 -0
  331. /package/templates/{api/tests → tests}/rbac-tables.spec.ts +0 -0
@@ -9,7 +9,7 @@
9
9
  "preview": "ojm preview",
10
10
  "info": "ojm info",
11
11
  "typecheck": "tsc --noEmit -p tsconfig.json",
12
- "typecheck:api": "tsc --noEmit -p api/tsconfig.json"
12
+ "typecheck:api": "tsc --noEmit -p src/tsconfig.json"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@ant-design/cssinjs": "catalog:",
@@ -89,6 +89,8 @@ export interface InstallRequest extends InstallContext {
89
89
  expected?: string[]
90
90
  /** 安装器内部通道(外部调用方忽略):终态审计读 status/failedStage/taskId + migrate 捕获 appliedSeq */
91
91
  outcome?: { taskId?: string, status?: TaskStatus, failedStage?: InstallStage, appliedSeq?: number | null, cancelled?: boolean }
92
+ /** 安装器内部通道(reloading → smoke):本次新追加进 auth.anonymous_paths 的路径(restart_pending,探活对其 401 判待重启) */
93
+ deferredAnonymous?: string[]
92
94
  /**
93
95
  * 取消安装令牌(U5,P3-8):运行期外部置位(端点 `?cancel=<taskId>` 或任务状态 `cancelling` 驱动的闭包),
94
96
  * 返回 true 即中止——PIPELINE 循环每段前探测 + migrating 段 spawn 流逐块探测;幂等、可重入
@@ -347,6 +349,8 @@ async function reloadStage(req: InstallRequest): Promise<StageResult> {
347
349
  const anonymous = Array.isArray(api.anonymous) ? api.anonymous.filter((x): x is string => typeof x === "string") : [];
348
350
  const patched = anonymous.length > 0 ? appendAnonymousPaths(bumped.content, anonymous) : { content: bumped.content, added: [] };
349
351
  await atomicWriteText(path, patched.content);
352
+ // 交接给 smoke:本次真正新增的匿名路径在重启前恒 401(restart_pending)
353
+ req.deferredAnonymous = patched.added;
350
354
  }
351
355
  catch (e) {
352
356
  return { stage: "reloading", ok: false, retryable: true, message: `热更失败:config.yaml 安装序号自增/匿名端点接线失败(${String(e)})——reload unchanged 判定基于 config 文档 sha,无变更则热更不可能生效(B-11,PRD E5 口径)` };
@@ -30,6 +30,12 @@ export interface SmokeContext extends InstallContext {
30
30
  modulesJsonPath: string
31
31
  /** 注册快照查询;缺省回落 queryRegistered(isolate db 全局;测试注入 stub) */
32
32
  query?: (name: string) => Promise<SolutionSnapshot | null>
33
+ /**
34
+ * 本次安装**新追加**进 config.yaml `auth.anonymous_paths` 的路径(reloading 段注入,带前导 /)。
35
+ * 匿名接线属 restart_pending(reload 段已明示),本进程内尚未生效——探活对这些路径的
36
+ * 401 判「待重启」而非失败;已在配置里却仍 401 才是真失败(说明接线或守卫有问题)。
37
+ */
38
+ deferredAnonymous?: string[]
33
39
  }
34
40
 
35
41
  export interface StageResult {
@@ -116,6 +122,7 @@ export async function smoke(ctx: SmokeContext): Promise<StageResult> {
116
122
  const web = isObj(m.web) ? m.web : undefined;
117
123
 
118
124
  // ① 探活:health 优先(2xx),缺省首个非匿名路由(连通即可)
125
+ const notes: string[] = [];
119
126
  try {
120
127
  const health = typeof m.health === "string" && m.health !== "" ? m.health : undefined;
121
128
  const target = health ?? firstNonAnonymousRoute(m);
@@ -123,7 +130,18 @@ export async function smoke(ctx: SmokeContext): Promise<StageResult> {
123
130
  const url = probeUrlOf(ctx.apiPrefix, target);
124
131
  const res = await ctx.fetch(url);
125
132
  if (health !== undefined && !res.ok) {
126
- failures.push({ item: "探活", retryable: true, reason: `health 端点 ${url} 返回 HTTP ${res.status}(C2-33 冒烟探针要求 2xx)` });
133
+ // 本次安装新增的匿名接线尚未生效(restart_pending):此处的 401 反映的是
134
+ // 「宿主还没重启」,不是包或站点的问题——判待重启并落人话提示,不判失败。
135
+ const rel = (s: string) => s.replace(/^\/+/, "");
136
+ const healthRel = rel(health);
137
+ const pending = (ctx.deferredAnonymous ?? []).some(a => healthRel === rel(a) || healthRel.startsWith(`${rel(a)}/`));
138
+ const deferred = res.status === 401 && pending;
139
+ if (deferred) {
140
+ notes.push(`探活 ${url} 返回 401:该端点是本次安装新增的匿名路径(auth.anonymous_paths),匿名接线属 restart_pending——重启宿主后生效,不计入失败`);
141
+ }
142
+ else {
143
+ failures.push({ item: "探活", retryable: true, reason: `health 端点 ${url} 返回 HTTP ${res.status}(C2-33 冒烟探针要求 2xx)` });
144
+ }
127
145
  }
128
146
  }
129
147
  }
@@ -205,7 +223,8 @@ export async function smoke(ctx: SmokeContext): Promise<StageResult> {
205
223
  // ④ entry 浏览器加载不在安装器验(设计 §4.1)——无判定,口径钉于成功 message。
206
224
 
207
225
  if (failures.length === 0) {
208
- return { stage: "smoke", ok: true, message: "冒烟通过:探活 / integrity / 注册一致性 三项全过(entry 浏览器加载不在安装器验——设计 §4.1,宿主 error boundary + P2 深检兜底)" };
226
+ const passed = "冒烟通过:探活 / integrity / 注册一致性 三项全过(entry 浏览器加载不在安装器验——设计 §4.1,宿主 error boundary + P2 深检兜底)";
227
+ return { stage: "smoke", ok: true, message: notes.length === 0 ? passed : `${passed}\n${notes.map(n => `- ${n}`).join("\n")}` };
209
228
  }
210
229
  return {
211
230
  stage: "smoke",
@@ -52,9 +52,9 @@ export interface MarketEnv {
52
52
  trustPath: string
53
53
  /** sqlite 库文件路径(R14 备份源;模板 config.yaml `sqlite://db.sqlite` → 站点根 db.sqlite) */
54
54
  dbPath: string
55
- /** 站点 web 模块目录(<dist>/modules) */
55
+ /** 站点 web 模块目录(站点产物根下的 modules/) */
56
56
  siteModulesDir: string
57
- /** 站点 modules.json 路径(与 siteModulesDir 同属 <dist>) */
57
+ /** 站点 modules.json 路径(与 siteModulesDir 同属站点产物根) */
58
58
  modulesJsonPath: string
59
59
  /** API 前缀(smoke 探针 URL 前缀) */
60
60
  apiPrefix: string
@@ -78,17 +78,20 @@ export interface MarketEnv {
78
78
  /** 迁移账本 head 真源(P1-16 接线①:缺省读 oj journal 表 `_oj_migrations` max(version),查询失败 → null 不猜) */
79
79
  ledgerHeadOf?: (name: string) => Promise<number | bigint | null>
80
80
  /**
81
- * 宿主 versions.json(C2-09 peerRuntime 比对真源,键 = 包名)。缺省读 <dist>/versions.json
82
- * (ojm 宿主构建产物,与 modules.json 同目录,B-11 接线);不可得 → undefined,manifest 声明
83
- * peerRuntime 即预检 fail-closed 拒装(C2-09 语义保持)。
81
+ * 宿主 versions.json(C2-09 peerRuntime 比对真源,键 = 包名)。缺省读 `<sharedRoot>/versions.json`
82
+ * (ojm 宿主构建产物:与 manifests.yaml 同层,即 release 根,B-11 接线);不可得 → undefined,
83
+ * manifest 声明 peerRuntime 即预检 fail-closed 拒装(C2-09 语义保持)。
84
84
  */
85
85
  hostVersions?: Record<string, string>
86
86
  }
87
87
 
88
- /** 宿主 versions.json 布局推导(B-11):与 modules.json 同目录(<dist>/versions.json) */
89
- export function versionsJsonPathOf(modulesJsonPath: string): string {
90
- const slash = modulesJsonPath.lastIndexOf("/");
91
- return slash === -1 ? "versions.json" : `${modulesJsonPath.slice(0, slash)}/versions.json`;
88
+ /**
89
+ * 宿主 versions.json 布局推导(B-11):与 manifests.yaml 同层(release 根 = `sharedRoot`),
90
+ * 不是站点产物根——两处根在 `src/<name>/{api,web}` 布局下已分流(`api/dist` vs `web/dist`)。
91
+ */
92
+ export function versionsJsonPathOf(releaseRoot: string): string {
93
+ const trimmed = releaseRoot.replace(/\/+$/, "");
94
+ return trimmed === "" ? "versions.json" : `${trimmed}/versions.json`;
92
95
  }
93
96
 
94
97
  /** 宿主 versions.json 解析(纯函数):{packageName: version} 全字符串表;任何违约 → undefined(fail-closed 不猜) */
@@ -113,30 +116,37 @@ export function parseVersionsJson(text: string): Record<string, string> | undefi
113
116
  return out;
114
117
  }
115
118
 
116
- /** hostVersions 注入缺省:显式注入优先,否则读 <dist>/versions.json;不可得 → undefined(C2-09 fail-closed) */
119
+ /** hostVersions 注入缺省:显式注入优先,否则读 `<sharedRoot>/versions.json`;不可得 → undefined(C2-09 fail-closed) */
117
120
  async function hostVersionsOf(env: MarketEnv): Promise<Record<string, string> | undefined> {
118
121
  if (env.hostVersions !== undefined) {
119
122
  return env.hostVersions;
120
123
  }
121
124
  try {
122
- return parseVersionsJson(new TextDecoder().decode(await fsFace().readFile(versionsJsonPathOf(env.modulesJsonPath))));
125
+ return parseVersionsJson(new TextDecoder().decode(await fsFace().readFile(versionsJsonPathOf(env.sharedRoot))));
123
126
  }
124
127
  catch {
125
128
  return undefined;
126
129
  }
127
130
  }
128
131
 
129
- /** oj serve release 布局生产缺省(P1-16 B3/B4;全字段可覆盖) */
132
+ /**
133
+ * oj serve release 布局生产缺省(P1-16 B3/B4;全字段可覆盖)。
134
+ *
135
+ * 两处根在 `src/<name>/{api,web}` 布局下分流,且都相对 fs jail 根(= config.yaml 所在目录):
136
+ * release 根 `api/dist` —— 版本目录 / manifests.yaml / versions.json / migrations(后端产物)
137
+ * 站点根 `web/dist` —— modules/ + modules.json(前端产物,安装器 upsert 的落点)
138
+ * 暂存 / 任务 / 状态 / 信任库 / db 仍在 jail 根下:它们是安装器私有面,不属任何产物根。
139
+ */
130
140
  export function marketEnv(): MarketEnv {
131
141
  return {
132
- sharedRoot: "dist",
142
+ sharedRoot: "api/dist",
133
143
  staging: "modules/.staging",
134
144
  taskDir: "modules/.install-tasks",
135
145
  statePath: "modules/.solutions/state.yaml",
136
146
  trustPath: ".solutions/trusted-keys.yaml",
137
147
  dbPath: "db.sqlite",
138
- siteModulesDir: "dist/modules",
139
- modulesJsonPath: "dist/modules.json",
148
+ siteModulesDir: "web/dist/modules",
149
+ modulesJsonPath: "web/dist/modules.json",
140
150
  apiPrefix: "/api",
141
151
  ledgerHeadOf: journalHeadOf,
142
152
  };
@@ -30,12 +30,16 @@ import { journalHeadOf, siteOrigin } from "../_shared/site-facade";
30
30
  /** 线格式行(与 packages/runtime/src/api/rbac/types.ts 的 SolutionRouteRow 对齐) */
31
31
  export type { SolutionRouteRow } from "../_lifecycle/verify-checks";
32
32
 
33
- /** oj serve release 布局生产缺省 */
33
+ /**
34
+ * oj serve release 布局生产缺省(与 marketEnv 同口径,见其注释):
35
+ * sharedRoot = 后端 release 根(版本目录 / migrations)
36
+ * modulesJsonPath = 站点产物根下的 modules.json(web 入口 integrity 真源)
37
+ */
34
38
  export function solutionsEnv(): SolutionsEnv {
35
39
  return {
36
- sharedRoot: "dist",
40
+ sharedRoot: "api/dist",
37
41
  statePath: "modules/.solutions/state.yaml",
38
- modulesJsonPath: "dist/modules.json",
42
+ modulesJsonPath: "web/dist/modules.json",
39
43
  apiPrefix: "/api",
40
44
  };
41
45
  }
@@ -1,7 +1,7 @@
1
1
  // 登录限速编排(B5/AC1-26):auth/login 与 auth/ldap-login 共用的 (IP, username) 双维度守卫 + 失败记账。
2
2
  // 状态全在 DB(sys_login_throttle/sys_rate_window,迁移 0009)——oj 每请求重建模块,进程内方案恒失效(§9.3-3)。
3
- import { auditOp } from "../../_platform/_shared/service/infra/audit";
4
- import { loginBlocked, loginFailed, loginSucceeded, rateLimit } from "../../_platform/_shared/service/infra/rate";
3
+ import { auditOp } from "#/_platform/_shared/service/infra/audit";
4
+ import { loginBlocked, loginFailed, loginSucceeded, rateLimit } from "#/_platform/_shared/service/infra/rate";
5
5
 
6
6
  /** 客户端 IP:优先 X-Forwarded-For(反代后),回落 X-Real-IP。 */
7
7
  export async function clientIp(): Promise<string> {
@@ -4,8 +4,8 @@
4
4
  // B4:access JWT 仅载 sub(roles 走登录响应/资料接口,不进 claims)。
5
5
  // B3:refresh = 不透明随机串 + DB 存 SHA-256 哈希(sys_refresh_token,迁移 0008);
6
6
  // 轮换作旧 + 滑动窗口(绝对 TTL 封顶 min(旧 exp, now+30d));reuse 检测全族吊销 + 审计。
7
- import { invalidateUser } from "../../_platform/_shared/service/infra/access";
8
- import { recordOp } from "../../_platform/_shared/service/infra/audit";
7
+ import { invalidateUser } from "#/_platform/_shared/service/infra/access";
8
+ import { recordOp } from "#/_platform/_shared/service/infra/audit";
9
9
 
10
10
  export interface SessionDeps {
11
11
  randomHex: (bytes: number) => string
@@ -1,4 +1,4 @@
1
- import { respond } from "../_platform/_shared/respond";
1
+ import { respond } from "#/_platform/_shared/respond";
2
2
  // oj 嵌套 handler 组:组名 = 路径段(login → POST /api/auth/login,路由不变)。
3
3
  // 阶段 B 登录链(D14/AC1-12/13/27):领域逻辑全在 _shared/session.ts(createSession 注入 deps),
4
4
  // handler 只编排:取参 → 调 service → json 信封;B5 限速收敛在 _shared/login-throttle.ts(与 ldap-login 共用)。
@@ -1,6 +1,6 @@
1
1
  // POST /api/auth/ldap-login —— LDAP 目录登录(D1/AC1-12;匿名端点,E6:anonymous_paths 写 /auth/ldap-login)。
2
2
  // 编排:B5 限速守卫(与 auth/login 同维度)→ ldapLogin 九步 → respond 统一异常出口(§9.3-1)。
3
- import { respond } from "../../_platform/_shared/respond";
3
+ import { respond } from "#/_platform/_shared/respond";
4
4
  import { createLdapLogin, runtimeLdapGateway } from "../_shared/ldap-login";
5
5
  import { clientIp, guardLoginThrottle, noteLoginFailed, noteLoginSucceeded } from "../_shared/login-throttle";
6
6
  import { createSession } from "../_shared/session";
@@ -0,0 +1,6 @@
1
+ name: auth
2
+ desc: JWT 鉴权端点(业务路由):login/refresh/logout;Bearer 守卫由 oj-auth 提供
3
+ version: 0.1.0
4
+ deps: # 两处都用:login/refresh 读 _platform.users(ownership_guard: deny,§3/S003);
5
+ # 跨模块别名 `#/_platform/…` 的依赖门禁(oj build S008)
6
+ _platform: ^0.1.0
@@ -1,5 +1,6 @@
1
1
  import type { IndexRow, TaskRecord } from "../types";
2
- import { screen, waitFor } from "@testing-library/react";
2
+ import { fireEvent, screen, waitFor } from "@testing-library/react";
3
+ import { useState } from "react";
3
4
  import { describe, expect, it } from "vitest";
4
5
  import { InstallDialog } from "../components/InstallDialog";
5
6
  import { InMemoryMarketClient } from "./marketClient.fake";
@@ -69,4 +70,55 @@ describe("installDialog 安装进度(BDD)", () => {
69
70
  expect(client.calls).toContain("install:alpha@1.1.0");
70
71
  expect(screen.getByText("回滚中")).toBeTruthy();
71
72
  });
73
+
74
+ it("given needs_trust 终态 When 打开 Then 渲染指纹与确认按钮,点击后入库并重试安装", async () => {
75
+ const pending: TaskRecord = {
76
+ id: "t1",
77
+ name: "alpha",
78
+ version: "1.2.0",
79
+ status: "needs_trust",
80
+ stage: "verifying",
81
+ progress: 18,
82
+ message: "待信任确认",
83
+ stages: [],
84
+ trust: { key_id: "alpha.author", public_key: "ab".repeat(32), fingerprint: "6afc".repeat(16) },
85
+ startedAt: 0,
86
+ };
87
+ const client = new InMemoryMarketClient({ taskRecord: pending, installResult: { task_id: "t1" } });
88
+ renderWithProviders(<InstallDialog row={row} open onClose={() => {}} />, client);
89
+ const button = await screen.findByText("指纹一致,信任并重试");
90
+ expect(screen.getByTestId("install-trust-fingerprint").textContent).toContain("6afc".repeat(16));
91
+ fireEvent.click(button);
92
+ await waitFor(() => expect(client.calls).toContain("trust:t1"));
93
+ // 确认入库后自动重试同一安装(staging 未清场,服务端续跑)
94
+ await waitFor(() => expect(client.calls.filter(c => c.startsWith("install:")).length).toBe(2));
95
+ });
96
+
97
+ it("given 终态 When 父级每次渲染都换 onDone 身份 Then 只通告一次(防 onDone→重渲染→onDone 死循环)", async () => {
98
+ const done: TaskRecord = {
99
+ id: "t1",
100
+ name: "alpha",
101
+ version: "1.2.0",
102
+ status: "done",
103
+ stage: "done",
104
+ progress: 100,
105
+ message: "ok",
106
+ stages: [],
107
+ startedAt: 0,
108
+ };
109
+ const client = new InMemoryMarketClient({ taskRecord: done, installResult: { task_id: "t1" } });
110
+ function Harness() {
111
+ const [n, setN] = useState(0);
112
+ return (
113
+ <>
114
+ <span data-testid="done-count">{n}</span>
115
+ <InstallDialog row={row} open onClose={() => {}} onDone={() => setN(v => v + 1)} />
116
+ </>
117
+ );
118
+ }
119
+ renderWithProviders(<Harness />, client);
120
+ await waitFor(() => expect(screen.getByTestId("install-message").textContent).toBe("ok"), { timeout: 5000 });
121
+ await new Promise(resolve => setTimeout(resolve, 50));
122
+ expect(screen.getByTestId("done-count").textContent).toBe("1");
123
+ });
72
124
  });
@@ -11,6 +11,7 @@ export interface FakeState {
11
11
  enableResult?: { ok: boolean, message: string }
12
12
  uninstallResult?: { ok: boolean, message: string }
13
13
  installResult?: { task_id: string }
14
+ trustResult?: { result: string, key_id: string, fingerprint: string }
14
15
  }
15
16
 
16
17
  export class InMemoryMarketClient implements MarketClient {
@@ -49,6 +50,11 @@ export class InMemoryMarketClient implements MarketClient {
49
50
  } as TaskRecord);
50
51
  }
51
52
 
53
+ async trust(taskId: string) {
54
+ this.calls.push(`trust:${taskId}`);
55
+ return this.state.trustResult ?? { result: "added", key_id: "k1", fingerprint: "f".repeat(64) };
56
+ }
57
+
52
58
  async enable(input: EnableInput) {
53
59
  this.calls.push(`enable:${input.name}:${input.enabled}`);
54
60
  return this.state.enableResult ?? { ok: true, message: "ok" };
@@ -24,6 +24,11 @@ export interface MarketClient {
24
24
  install: (input: InstallInput) => Promise<{ task_id: string }>
25
25
  /** 任务轮询消费面(query 形态 ?id=) */
26
26
  task: (id: string) => Promise<TaskRecord>
27
+ /**
28
+ * TOFU 确认:把 needs_trust 任务记录里的发布方 key 写入宿主信任库(服务端自任务记录重取
29
+ * 公钥复算指纹比对,防混淆代理)。确认成功后重试同一安装即可续跑(staging 未清场)。
30
+ */
31
+ trust: (taskId: string) => Promise<{ result: string, key_id: string, fingerprint: string }>
27
32
  /** 生命周期开关(同步 setEnabled,无任务模型;{ok,message} 200 渲染 ok 位) */
28
33
  enable: (input: EnableInput) => Promise<{ ok: boolean, message: string }>
29
34
  /** 市场操作历史(_platform 只读端点 /_platform/market/history) */
@@ -25,9 +25,10 @@ import {
25
25
  platformMarketInstallPost,
26
26
  platformMarketMetaGet,
27
27
  platformMarketTaskGet,
28
+ platformMarketTrustPost,
28
29
  platformMarketUninstallImpactGet,
29
30
  platformMarketUninstallPost,
30
- } from "../../_platform/__client/api";
31
+ } from "../../../_platform/web/__client/api";
31
32
 
32
33
  // 绑定运行时 request 到生成 client 的模块级单槽(生成 client 是静态单例,必须由某处
33
34
  // bindRequest;此处兜底确保市场子应用无论挂载顺序都能拿到请求实例)。与 _platform 模块
@@ -96,6 +97,11 @@ export const httpMarketClient: MarketClient = {
96
97
  // 运行时形状由服务端真源 + DEV zod 校验保证,TS 侧以领域类型为准。
97
98
  return (await platformMarketTaskGet({ id })) as unknown as TaskRecord;
98
99
  },
100
+ async trust(taskId: string) {
101
+ // 服务端自任务记录重取公钥复算指纹比对(防混淆代理),前端只传 task_id,不传公钥
102
+ const data = await platformMarketTrustPost({ task_id: taskId });
103
+ return data as { result: string, key_id: string, fingerprint: string };
104
+ },
99
105
  async enable(input: EnableInput) {
100
106
  const data = await platformMarketEnablePost({ name: input.name, enabled: input.enabled });
101
107
  return data as { ok: boolean, message: string };
@@ -1,6 +1,6 @@
1
1
  import type { IndexRow, TaskRecord } from "../types";
2
2
  import { Alert, Button, Modal, Progress, Space, Typography } from "antd";
3
- import React, { useEffect, useState } from "react";
3
+ import React, { useEffect, useRef, useState } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import { useMarketClient } from "../api/context";
6
6
  import { useInstallTask } from "../hooks/useMarket";
@@ -18,6 +18,17 @@ export interface InstallDialogProps {
18
18
 
19
19
  const isTerminal = (r: TaskRecord | null) => r != null && r.status !== "running";
20
20
 
21
+ /**
22
+ * 阶段失败指引(契约附录 A 的 `lifecycle.stage.*` key)→ 本模块文案。
23
+ * 附录 A 的这批 key 在模板 locales 里尚未落地,直接 t() 会把裸 key 显示给运维,
24
+ * 故此处显式映射;未收录的 key 保持原样(不吞信息)。
25
+ */
26
+ const HINT_KEYS: Record<string, string> = {
27
+ "lifecycle.stage.smoke.hint": "oj-market:task:hintSmoke",
28
+ "lifecycle.stage.cancelled": "oj-market:task:hintCancelled",
29
+ "lifecycle.stage.reloading.hint": "oj-market:task:hintReloading",
30
+ };
31
+
21
32
  /**
22
33
  * 安装/升级/回滚对话框:触发 POST install → 800ms 轮询 TaskRecord 终态渲染(reason+retryable+hintKey 双语)。
23
34
  * 回滚 = 对已装旧版降级安装(显式 version);降版迁移差异校验(R12)的可读失败由终态 error 渲染。
@@ -27,7 +38,13 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
27
38
  const client = useMarketClient();
28
39
  const [taskId, setTaskId] = useState<string | null>(null);
29
40
  const [started, setStarted] = useState(false);
41
+ const [trusting, setTrusting] = useState(false);
42
+ const [trustError, setTrustError] = useState<string | null>(null);
30
43
  const { record, done } = useInstallTask(taskId);
44
+ // onDone 对同一任务只通告一次:父级每次渲染都会换 onDone 身份,若只靠 deps 触发,
45
+ // 终态后 onDone → index.retry() → 父级重渲染 → onDone 身份变 → 再触发,会形成
46
+ // 无限请求循环(浏览器侧表现为 ERR_INSUFFICIENT_RESOURCES)。
47
+ const notifiedTaskId = useRef<string | null>(null);
31
48
 
32
49
  const targetVersion = version ?? row?.latest ?? null;
33
50
  const title
@@ -49,15 +66,43 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
49
66
  if (!open) {
50
67
  setTaskId(null);
51
68
  setStarted(false);
69
+ setTrustError(null);
70
+ notifiedTaskId.current = null;
52
71
  }
53
72
  }, [open, row, started, client, targetVersion]);
54
73
 
55
74
  useEffect(() => {
56
- if (record && isTerminal(record) && done)
57
- onDone?.(record);
75
+ if (!record || !isTerminal(record) || !done)
76
+ return;
77
+ if (notifiedTaskId.current === record.id)
78
+ return;
79
+ notifiedTaskId.current = record.id;
80
+ onDone?.(record);
58
81
  }, [record, done, onDone]);
59
82
 
83
+ // TOFU 确认:确认入库后重试同一安装(服务端 staging 未清场,续跑不重下)
84
+ const handleTrustConfirm = async () => {
85
+ if (taskId === null || taskId.startsWith("__error__"))
86
+ return;
87
+ setTrusting(true);
88
+ setTrustError(null);
89
+ try {
90
+ await client.trust(taskId);
91
+ notifiedTaskId.current = null;
92
+ setTaskId(null);
93
+ setStarted(false);
94
+ }
95
+ catch (e) {
96
+ setTrustError(e instanceof Error ? e.message : String(e));
97
+ }
98
+ finally {
99
+ setTrusting(false);
100
+ }
101
+ };
102
+
60
103
  const terminal = isTerminal(record);
104
+ const needsTrust = record?.status === "needs_trust";
105
+ const succeeded = record?.status === "done";
61
106
  const failed = record?.status === "failed" || record?.status === "partial";
62
107
  const errorTask = typeof taskId === "string" && taskId.startsWith("__error__");
63
108
 
@@ -78,9 +123,35 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
78
123
  )}
79
124
  {record && (
80
125
  <>
81
- <Progress percent={record.progress} status={terminal ? (failed ? "exception" : "success") : "active"} />
126
+ <Progress percent={record.progress} status={terminal ? (succeeded ? "success" : "exception") : "active"} />
82
127
  <div data-testid="install-stage">{record.stage ? t(`oj-market:task:stage.${record.stage}`, record.stage) : record.stage}</div>
83
128
  <div data-testid="install-message">{record.message}</div>
129
+ {needsTrust && record?.trust && (
130
+ <Alert
131
+ style={{ marginTop: 12 }}
132
+ type="warning"
133
+ showIcon
134
+ message={t("oj-market:task:trustTitle")}
135
+ description={(
136
+ <Space direction="vertical" size={4}>
137
+ <span>{t("oj-market:task:trustBody", { keyId: record.trust.key_id })}</span>
138
+ <span data-testid="install-trust-fingerprint">
139
+ {t("oj-market:task:trustFingerprint")}
140
+ <Typography.Text code copyable={{ text: record.trust.fingerprint }}>
141
+ {record.trust.fingerprint}
142
+ </Typography.Text>
143
+ </span>
144
+ <span>{t("oj-market:task:trustHint")}</span>
145
+ </Space>
146
+ )}
147
+ />
148
+ )}
149
+ {needsTrust && trustError && <Alert style={{ marginTop: 8 }} type="error" showIcon message={trustError} />}
150
+ {needsTrust && (
151
+ <Button type="primary" style={{ marginTop: 12 }} loading={trusting} onClick={handleTrustConfirm}>
152
+ {t("oj-market:task:trustConfirm")}
153
+ </Button>
154
+ )}
84
155
  {failed && record.error && (
85
156
  <Alert
86
157
  type="error"
@@ -89,7 +160,11 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
89
160
  description={(
90
161
  <Space direction="vertical">
91
162
  <span>{record.error.retryable ? t("oj-market:task:retryable") : t("oj-market:task:notRetryable")}</span>
92
- {record.error.hintKey && <span data-testid="install-hint">{t(record.error.hintKey, record.error.hintKey)}</span>}
163
+ {record.error.hintKey && (
164
+ <span data-testid="install-hint">
165
+ {t(HINT_KEYS[record.error.hintKey] ?? record.error.hintKey, record.error.hintKey)}
166
+ </span>
167
+ )}
93
168
  </Space>
94
169
  )}
95
170
  />
@@ -40,8 +40,13 @@ export function useInstallTask(taskId: string | null): {
40
40
  const [done, setDone] = useState(false);
41
41
 
42
42
  useEffect(() => {
43
- if (!taskId)
43
+ // 无任务 id = 尚无任务:清空上一次任务的记录,避免「重新安装/信任后重试」时
44
+ // 旧终态(可能是 needs_trust)残留在弹窗里被误读成本次结果。
45
+ if (!taskId) {
46
+ setRecord(null);
47
+ setDone(false);
44
48
  return;
49
+ }
45
50
  let active = true;
46
51
  setRecord(null);
47
52
  setDone(false);
@@ -49,12 +49,21 @@
49
49
  "running": "Running",
50
50
  "done": "Done",
51
51
  "failed": "Failed",
52
- "partial": "Partial"
52
+ "partial": "Partial",
53
+ "needs_trust": "Awaiting trust"
53
54
  },
55
+ "trustTitle": "Trust confirmation required: publisher key not in host trust store",
56
+ "trustBody": "The signing key of publisher \"{{keyId}}\" is not trusted by this host yet (first-use TOFU).",
57
+ "trustFingerprint": "Key fingerprint: ",
58
+ "trustHint": "Compare this fingerprint with the one published by the vendor; confirm only if they match. Confirming retries this install automatically and reuses the staged files.",
59
+ "trustConfirm": "Fingerprint matches — trust and retry",
54
60
  "reason": "Reason",
55
61
  "retryable": "Retryable",
56
62
  "notRetryable": "Not retryable",
57
63
  "hint": "Hint",
64
+ "hintSmoke": "Smoke probe failures usually mean one of two things: (1) the anonymous endpoints added by this install (auth.anonymous_paths) are pending a restart — restart the host and retry; (2) in dev the host already ships a module with the same name, so the route belongs to the source module and the probe is not hitting the freshly installed package.",
65
+ "hintCancelled": "This install was cancelled: lock file, version directory and staging were restored to their pre-install state. You can retry directly.",
66
+ "hintReloading": "Config hot-reload was partial: hot-applicable sections (vars, ...) are applied, while the auth section (including anonymous endpoints) needs a host restart.",
58
67
  "close": "Close",
59
68
  "viewLog": "View log"
60
69
  },
@@ -49,12 +49,21 @@
49
49
  "running": "进行中",
50
50
  "done": "完成",
51
51
  "failed": "失败",
52
- "partial": "部分完成"
52
+ "partial": "部分完成",
53
+ "needs_trust": "待信任确认"
53
54
  },
55
+ "trustTitle": "待信任确认:发布方密钥不在宿主信任库",
56
+ "trustBody": "发布方「{{keyId}}」的签名密钥尚未被本宿主信任(首次信任 TOFU)。",
57
+ "trustFingerprint": "密钥指纹:",
58
+ "trustHint": "请与发布方公布的公钥指纹逐字核对,一致才确认。确认后会自动重试本次安装,已暂存的内容不会重新下载。",
59
+ "trustConfirm": "指纹一致,信任并重试",
54
60
  "reason": "原因",
55
61
  "retryable": "可重试",
56
62
  "notRetryable": "不可重试",
57
63
  "hint": "提示",
64
+ "hintSmoke": "冒烟探活失败常见于两类原因:① 本次安装新增的匿名端点(auth.anonymous_paths)属「待重启生效」,重启宿主后再点重试即可;② dev 形态下宿主已内置同名模块,该路由由源码模块占位,探活打到的不是刚装的包。",
65
+ "hintCancelled": "本次安装已被取消:锁、版本目录与 staging 均已回到发起前状态,可直接重试。",
66
+ "hintReloading": "配置热更未完全生效:可热改段(vars 等)已应用,auth 段(含匿名端点)需重启宿主后生效。",
58
67
  "close": "关闭",
59
68
  "viewLog": "查看日志"
60
69
  },
@@ -5,8 +5,12 @@
5
5
  /** 安装阶段名(契约附录 A 十阶段逐字 + lock_commit/audit/done 终态) */
6
6
  export type InstallStage = "preflight" | "downloading" | "verifying" | "extracting" | "migrating" | "lock_commit" | "registering" | "deploying_web" | "reloading" | "smoke" | "audit" | "done";
7
7
 
8
- /** 任务状态(§10.2 三态 + partial:锁后失败部分生效) */
9
- export type TaskStatus = "running" | "done" | "failed" | "partial";
8
+ /**
9
+ * 任务状态(§10.2 三态 + partial:锁后失败部分生效 + needs_trust:未知发布方 key 待人工
10
+ * TOFU 确认,staging 不清场,确认入库后重试同一安装即可续跑)。
11
+ * needs_trust 是非成功态:消费面不得按绿色成功渲染(真源 api/src/_platform/_lifecycle/types.ts:64)。
12
+ */
13
+ export type TaskStatus = "running" | "done" | "failed" | "partial" | "needs_trust";
10
14
 
11
15
  /** 阶段明细日志(UI 详情日志区数据源) */
12
16
  export interface TaskStageLog {
@@ -22,6 +26,13 @@ export interface TaskError {
22
26
  hintKey?: string
23
27
  }
24
28
 
29
+ /** needs_trust 终态:待确认的发布方 key(指纹 = sha256(public_key) 十六进制,供人核对) */
30
+ export interface TaskTrust {
31
+ key_id: string
32
+ public_key: string
33
+ fingerprint: string
34
+ }
35
+
25
36
  /** 安装任务记录(§10.2 + stages 明细日志;落 .install-tasks/<id>.json,800ms 轮询消费面) */
26
37
  export interface TaskRecord {
27
38
  id: string
@@ -34,6 +45,8 @@ export interface TaskRecord {
34
45
  message: string
35
46
  stages: TaskStageLog[]
36
47
  error?: TaskError
48
+ /** needs_trust 终态才有:待确认的发布方 key(POST /market/trust {task_id} 消费) */
49
+ trust?: TaskTrust
37
50
  startedAt: number
38
51
  finishedAt?: number
39
52
  appliedSeq?: number
@@ -1,7 +1,7 @@
1
- import { respond } from "../../_platform/_shared/respond";
1
+ import { respond } from "#/_platform/_shared/respond";
2
2
  // GET /api/system/login-methods —— 登录方式发现(C5/D2;匿名端点:登录前无 token,
3
3
  // 项目 config.yaml auth.anonymous_paths 必须列 /system/login-methods,漏列则握手 401 登录页必坏)。
4
- import { listLoginMethods } from "../../_platform/_shared/service/domain/settings";
4
+ import { listLoginMethods } from "#/_platform/_shared/service/domain/settings";
5
5
 
6
6
  async function get() {
7
7
  await respond(async () => {
@@ -0,0 +1,6 @@
1
+ name: system
2
+ desc: 框架管理域端点(/api/system/*):角色/权限点管理(assertSystemAdmin 判权,H2)
3
+ version: 0.1.0
4
+ deps: # 两处都用:roles/bindings/permissions 读写 _platform 的 sys_* 表(ownership_guard: deny);
5
+ # 跨模块别名 `#/_platform/…` 的依赖门禁(oj build S008)
6
+ _platform: ^0.1.0
@@ -1,9 +1,9 @@
1
- import { respond } from "../../_platform/_shared/respond";
1
+ import { respond } from "#/_platform/_shared/respond";
2
2
  // GET /api/system/oplog?action=&username=&module=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
3
3
  // POST /api/system/oplog —— 操作日志写入(C3;登录态即可,解决方案模块记录自身操作)。
4
- import { assertSystemAdmin, currentAccess } from "../../_platform/_shared/service/infra/access";
5
- import { recordOp } from "../../_platform/_shared/service/infra/audit";
6
- import { searchOplog } from "../../_platform/_shared/service/infra/oplog";
4
+ import { assertSystemAdmin, currentAccess } from "#/_platform/_shared/service/infra/access";
5
+ import { recordOp } from "#/_platform/_shared/service/infra/audit";
6
+ import { searchOplog } from "#/_platform/_shared/service/infra/oplog";
7
7
 
8
8
  async function get() {
9
9
  await respond(async () => {