create-windy 0.2.33 → 0.3.0

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 (284) hide show
  1. package/dist/cli.js +173 -17
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +9 -0
  6. package/template/README.md +3 -1
  7. package/template/apps/agent-server/Dockerfile +1 -0
  8. package/template/apps/agent-server/README.md +28 -5
  9. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  12. package/template/apps/agent-server/pyproject.toml +1 -1
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
  18. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  21. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  22. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  25. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  26. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  27. package/template/apps/agent-server/tests/test_api.py +16 -1
  28. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  29. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  30. package/template/apps/server/package.json +4 -1
  31. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  32. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  33. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  34. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  35. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  36. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  37. package/template/apps/server/src/agent/run-facade.ts +38 -9
  38. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  39. package/template/apps/server/src/application-services.ts +50 -0
  40. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  41. package/template/apps/server/src/audit/search-summary.ts +2 -6
  42. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  43. package/template/apps/server/src/auth/password.ts +30 -21
  44. package/template/apps/server/src/auth/routes.test.ts +44 -3
  45. package/template/apps/server/src/auth/routes.ts +6 -1
  46. package/template/apps/server/src/auth/runtime.ts +4 -1
  47. package/template/apps/server/src/auth/service.ts +25 -7
  48. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  49. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  50. package/template/apps/server/src/configuration/definition.ts +242 -12
  51. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  52. package/template/apps/server/src/configuration/repository.ts +27 -2
  53. package/template/apps/server/src/configuration/routes.ts +4 -1
  54. package/template/apps/server/src/configuration/service.test.ts +100 -0
  55. package/template/apps/server/src/configuration/service.ts +50 -10
  56. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  57. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  58. package/template/apps/server/src/example-modules.ts +4 -1
  59. package/template/apps/server/src/guards.ts +18 -5
  60. package/template/apps/server/src/http/request-source.test.ts +85 -0
  61. package/template/apps/server/src/http/request-source.ts +90 -0
  62. package/template/apps/server/src/index.ts +45 -45
  63. package/template/apps/server/src/license/routes.ts +2 -0
  64. package/template/apps/server/src/license/runtime-service.ts +10 -11
  65. package/template/apps/server/src/module-bootstrap.ts +2 -2
  66. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  67. package/template/apps/server/src/module-host/initialize.ts +30 -44
  68. package/template/apps/server/src/module-host/request-context.ts +29 -23
  69. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  70. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  71. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  72. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  73. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  74. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  75. package/template/apps/server/src/module-host/storage-scope.ts +89 -0
  76. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  77. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  78. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  79. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  80. package/template/apps/server/src/module-host/user-directory-port.ts +139 -0
  81. package/template/apps/server/src/module-host.ts +4 -0
  82. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  83. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  84. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  85. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  86. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  87. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  88. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  89. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  90. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  91. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  92. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  93. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  94. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  95. package/template/apps/server/src/notification/memory-support.ts +72 -0
  96. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  97. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  98. package/template/apps/server/src/notification/repository.test.ts +171 -0
  99. package/template/apps/server/src/notification/repository.ts +179 -32
  100. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  101. package/template/apps/server/src/notification/route-support.ts +225 -0
  102. package/template/apps/server/src/notification/routes.test.ts +182 -0
  103. package/template/apps/server/src/notification/routes.ts +225 -132
  104. package/template/apps/server/src/notification/service.test.ts +104 -0
  105. package/template/apps/server/src/notification/service.ts +140 -0
  106. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  107. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  108. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  109. package/template/apps/server/src/route-guards.ts +1 -0
  110. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  111. package/template/apps/server/src/runtime-development.ts +2 -0
  112. package/template/apps/server/src/runtime.test.ts +23 -9
  113. package/template/apps/server/src/runtime.ts +39 -2
  114. package/template/apps/server/src/scheduler/audit.ts +2 -11
  115. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  116. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  117. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  118. package/template/apps/server/src/scheduler/routes.ts +2 -7
  119. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  120. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  121. package/template/apps/server/src/search/service.ts +2 -6
  122. package/template/apps/server/src/settings/routes.ts +15 -3
  123. package/template/apps/server/src/settings/runtime.ts +10 -1
  124. package/template/apps/server/src/storage/runtime.ts +13 -0
  125. package/template/apps/server/src/system/audit-query.ts +33 -4
  126. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  127. package/template/apps/server/src/system/audit-routes.ts +6 -5
  128. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  129. package/template/apps/server/src/system/department-organization.ts +320 -0
  130. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  131. package/template/apps/server/src/system/department-routes.ts +114 -33
  132. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  133. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  134. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  135. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  136. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  137. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  138. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  139. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  140. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  141. package/template/apps/server/src/system/route-helpers.ts +2 -5
  142. package/template/apps/server/src/system/route-types.ts +12 -3
  143. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  144. package/template/apps/server/src/system/routes.test.ts +10 -2
  145. package/template/apps/server/src/system/routes.ts +70 -36
  146. package/template/apps/server/src/system/seed.ts +7 -0
  147. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  148. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  149. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  150. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  151. package/template/apps/server/src/work-order/service.ts +42 -10
  152. package/template/apps/web/src/app/error-pages.ts +9 -0
  153. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  154. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  155. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  156. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  157. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  158. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  159. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  160. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  161. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  162. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  163. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  164. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  165. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  166. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  167. package/template/apps/web/src/lib/date-time.ts +22 -10
  168. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  169. package/template/apps/web/src/main.ts +14 -1
  170. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  171. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  172. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  173. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  174. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  175. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  176. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  177. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  178. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  179. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  180. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  181. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  182. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  183. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  184. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  185. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  186. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  187. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  188. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  189. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  190. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  191. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  192. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  193. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  194. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  195. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  196. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  197. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  198. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  199. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  200. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  201. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  202. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  203. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  204. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  205. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  206. package/template/apps/web/src/router/error-navigation.ts +50 -0
  207. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  208. package/template/apps/web/src/router/index.ts +71 -0
  209. package/template/apps/web/src/router/index.webtest.ts +29 -0
  210. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  211. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  212. package/template/apps/web/src/services/auth-api.ts +5 -0
  213. package/template/apps/web/src/services/http.ts +43 -6
  214. package/template/apps/web/src/services/http.webtest.ts +70 -0
  215. package/template/apps/web/src/services/notification-api.ts +85 -2
  216. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  217. package/template/apps/web/src/services/search-api.ts +39 -0
  218. package/template/apps/web/src/services/system-api.ts +18 -0
  219. package/template/apps/web/vitest.config.ts +1 -0
  220. package/template/docker-compose.yml +68 -0
  221. package/template/docs/architecture/ai-egress.md +49 -7
  222. package/template/docs/architecture/ai-runtime.md +34 -6
  223. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  224. package/template/docs/architecture/object-storage.md +22 -2
  225. package/template/docs/architecture/search-provider-protocol.md +38 -2
  226. package/template/docs/development/custom-error-pages.md +78 -0
  227. package/template/docs/development/custom-login-page.md +3 -3
  228. package/template/docs/platform/agent-runtime.md +76 -7
  229. package/template/docs/platform/audit-reliability.md +46 -2
  230. package/template/docs/platform/business-user-directory.md +82 -0
  231. package/template/docs/platform/module-configuration.md +13 -4
  232. package/template/docs/platform/notifications.md +67 -11
  233. package/template/docs/platform/organization-membership.md +17 -3
  234. package/template/docs/platform/system-crud-api.md +16 -0
  235. package/template/docs/platform/web-system-crud.md +6 -2
  236. package/template/package.json +4 -0
  237. package/template/packages/config/index.test.ts +45 -0
  238. package/template/packages/config/package.json +1 -1
  239. package/template/packages/config/src/platform.ts +169 -48
  240. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  241. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  242. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  243. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  244. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  245. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  246. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  247. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  248. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  249. package/template/packages/database/package.json +1 -1
  250. package/template/packages/database/src/schema/identity.ts +7 -0
  251. package/template/packages/database/src/schema/notifications.ts +135 -0
  252. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  253. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  254. package/template/packages/modules/package.json +1 -1
  255. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  256. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  257. package/template/packages/modules/src/manifest.ts +26 -0
  258. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  259. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  260. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  261. package/template/packages/modules/src/system-features.ts +6 -1
  262. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  263. package/template/packages/modules/src/system-module-catalog.ts +28 -4
  264. package/template/packages/modules/src/system-modules.test.ts +13 -1
  265. package/template/packages/modules/src/system-modules.ts +12 -0
  266. package/template/packages/modules/src/system-permissions.ts +11 -5
  267. package/template/packages/server-sdk/index.ts +11 -0
  268. package/template/packages/server-sdk/package.json +3 -2
  269. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  270. package/template/packages/server-sdk/src/module-host.ts +8 -0
  271. package/template/packages/server-sdk/src/request-context.ts +2 -0
  272. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  273. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  274. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
  275. package/template/packages/shared/package.json +1 -1
  276. package/template/packages/shared/src/audit.ts +25 -0
  277. package/template/packages/shared/src/module-configuration.ts +5 -0
  278. package/template/packages/shared/src/notification.ts +92 -1
  279. package/template/packages/shared/src/password.ts +7 -2
  280. package/template/packages/shared/src/platform-settings.ts +2 -0
  281. package/template/packages/storage/package.json +1 -1
  282. package/template/packages/storage/src/upload-service.test.ts +16 -0
  283. package/template/packages/storage/src/upload-service.ts +5 -1
  284. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -1,4 +1,7 @@
1
- import { platformConfigDefinition, type PlatformConfig } from "@southwind-ai/config";
1
+ import {
2
+ platformConfigDefinition,
3
+ type PlatformConfig,
4
+ } from "@southwind-ai/config";
2
5
  import type {
3
6
  ModuleConfigFieldDescriptor,
4
7
  ModuleConfigValues,
@@ -18,21 +21,68 @@ export interface PlatformConfigApplyListener {
18
21
  applied(config: PlatformConfig): void;
19
22
  }
20
23
 
21
- const managedPaths = ["enableAI", "cache.healthTimeoutMs"] as const;
22
- const platformValuesSchema = z.strictObject({
23
- enableAI: z.boolean(),
24
- "cache.healthTimeoutMs": z.number().int().min(50).max(10_000),
25
- });
24
+ const managedPaths = [
25
+ "enableAI",
26
+ "cache.healthTimeoutMs",
27
+ "security.sessionTtlSeconds",
28
+ "security.passwordMinLength",
29
+ "security.passwordMaxLength",
30
+ "storage.bulkDataMaxByteSize",
31
+ "storage.brandingLogoMaxByteSize",
32
+ "mail.enabled",
33
+ "mail.host",
34
+ "mail.port",
35
+ "mail.secure",
36
+ "mail.fromAddress",
37
+ "mail.connectTimeoutMs",
38
+ "regional.timeZone",
39
+ ] as const;
40
+ const platformValuesSchema = z
41
+ .strictObject({
42
+ enableAI: z.boolean(),
43
+ "cache.healthTimeoutMs": z.number().int().min(50).max(10_000),
44
+ "security.sessionTtlSeconds": z.number().int().min(60).max(2_592_000),
45
+ "security.passwordMinLength": z.number().int().min(8).max(128),
46
+ "security.passwordMaxLength": z.number().int().min(12).max(512),
47
+ "storage.bulkDataMaxByteSize": z
48
+ .number()
49
+ .int()
50
+ .min(1024)
51
+ .max(10 * 1024 ** 3),
52
+ "storage.brandingLogoMaxByteSize": z
53
+ .number()
54
+ .int()
55
+ .min(1024)
56
+ .max(50 * 1024 ** 2),
57
+ "mail.enabled": z.boolean(),
58
+ "mail.host": z.string().trim().max(253),
59
+ "mail.port": z.number().int().min(1).max(65_535),
60
+ "mail.secure": z.boolean(),
61
+ "mail.fromAddress": z.string().trim().max(320),
62
+ "mail.connectTimeoutMs": z.number().int().min(100).max(120_000),
63
+ "regional.timeZone": z
64
+ .string()
65
+ .trim()
66
+ .min(1)
67
+ .max(100)
68
+ .refine(isSupportedTimeZone),
69
+ })
70
+ .refine(
71
+ (values) =>
72
+ values["security.passwordMinLength"] <=
73
+ values["security.passwordMaxLength"],
74
+ { message: "密码最小长度不能大于最大长度" },
75
+ );
26
76
 
27
77
  export class PlatformConfigApplyAdapter {
28
- private listener?: PlatformConfigApplyListener;
78
+ private readonly listeners = new Set<PlatformConfigApplyListener>();
29
79
 
30
80
  constructor(private readonly config: PlatformConfig) {
31
81
  assertSafeManagedPaths();
32
82
  }
33
83
 
34
- setListener(listener: PlatformConfigApplyListener): void {
35
- this.listener = listener;
84
+ addListener(listener: PlatformConfigApplyListener): void {
85
+ this.listeners.add(listener);
36
86
  }
37
87
 
38
88
  definition(): ManagedModuleConfigDefinition {
@@ -45,6 +95,7 @@ export class PlatformConfigApplyAdapter {
45
95
  label: "AI 能力总开关",
46
96
  description: "控制 foundation 快照中的平台 AI 能力声明。",
47
97
  input: "boolean",
98
+ applyMode: "live",
48
99
  },
49
100
  {
50
101
  path: "cache.healthTimeoutMs",
@@ -53,29 +104,208 @@ export class PlatformConfigApplyAdapter {
53
104
  input: "integer",
54
105
  minimum: 50,
55
106
  maximum: 10_000,
107
+ applyMode: "live",
108
+ },
109
+ integerField(
110
+ "security.sessionTtlSeconds",
111
+ "会话有效期",
112
+ "控制新创建会话的有效期,单位为秒;已存在会话保持原过期时间。",
113
+ 60,
114
+ 2_592_000,
115
+ ),
116
+ integerField(
117
+ "security.passwordMinLength",
118
+ "密码最小长度",
119
+ "修改或重置密码时立即使用的新最小长度。",
120
+ 8,
121
+ 128,
122
+ ),
123
+ integerField(
124
+ "security.passwordMaxLength",
125
+ "密码最大长度",
126
+ "修改或重置密码时立即使用的新最大长度。",
127
+ 12,
128
+ 512,
129
+ ),
130
+ integerField(
131
+ "storage.bulkDataMaxByteSize",
132
+ "批量数据文件上限",
133
+ "批量导入导出上传的最大字节数。",
134
+ 1024,
135
+ 10 * 1024 ** 3,
136
+ ),
137
+ integerField(
138
+ "storage.brandingLogoMaxByteSize",
139
+ "品牌 Logo 文件上限",
140
+ "品牌 Logo 上传的最大字节数。",
141
+ 1024,
142
+ 50 * 1024 ** 2,
143
+ ),
144
+ restartBooleanField(
145
+ "mail.enabled",
146
+ "启用邮件传输配置",
147
+ "控制邮件传输配置是否启用;仍需安装业务邮件发送 Adapter。",
148
+ ),
149
+ restartTextField("mail.host", "SMTP 主机", "SMTP 服务主机名。"),
150
+ restartIntegerField(
151
+ "mail.port",
152
+ "SMTP 端口",
153
+ "SMTP 服务端口。",
154
+ 1,
155
+ 65_535,
156
+ ),
157
+ restartBooleanField(
158
+ "mail.secure",
159
+ "SMTP TLS 直连",
160
+ "启用后使用 TLS 直连;STARTTLS 由后续邮件 Adapter 处理。",
161
+ ),
162
+ restartTextField(
163
+ "mail.fromAddress",
164
+ "默认发件地址",
165
+ "邮件 Adapter 使用的默认 From 地址。",
166
+ ),
167
+ restartIntegerField(
168
+ "mail.connectTimeoutMs",
169
+ "SMTP 连接超时",
170
+ "邮件 Adapter 建立连接的超时时间,单位为毫秒。",
171
+ 100,
172
+ 120_000,
173
+ ),
174
+ {
175
+ path: "regional.timeZone",
176
+ label: "显示时区",
177
+ description: "平台日期时间展示使用的 IANA 时区,例如 Asia/Shanghai。",
178
+ input: "text",
179
+ applyMode: "live",
56
180
  },
57
181
  ],
58
- validate: (values) => platformValuesSchema.parse(values),
182
+ validate: (values) =>
183
+ this.parse({
184
+ ...this.values(),
185
+ ...(isRecord(values) ? values : {}),
186
+ }),
59
187
  apply: (values) => this.apply(values),
60
188
  baseValues: () => this.values(),
61
189
  };
62
190
  }
63
191
 
64
192
  private apply(values: ModuleConfigValues): void {
65
- const parsed = platformValuesSchema.parse(values);
193
+ const parsed = this.parse(values);
66
194
  this.config.enableAI = parsed.enableAI;
67
195
  this.config.cache.healthTimeoutMs = parsed["cache.healthTimeoutMs"];
68
- this.listener?.applied(this.config);
196
+ this.config.security.sessionTtlSeconds =
197
+ parsed["security.sessionTtlSeconds"];
198
+ this.config.security.passwordMinLength =
199
+ parsed["security.passwordMinLength"];
200
+ this.config.security.passwordMaxLength =
201
+ parsed["security.passwordMaxLength"];
202
+ this.config.storage.bulkDataMaxByteSize =
203
+ parsed["storage.bulkDataMaxByteSize"];
204
+ this.config.storage.brandingLogoMaxByteSize =
205
+ parsed["storage.brandingLogoMaxByteSize"];
206
+ this.config.mail.enabled = parsed["mail.enabled"];
207
+ this.config.mail.host = parsed["mail.host"];
208
+ this.config.mail.port = parsed["mail.port"];
209
+ this.config.mail.secure = parsed["mail.secure"];
210
+ this.config.mail.fromAddress = parsed["mail.fromAddress"];
211
+ this.config.mail.connectTimeoutMs = parsed["mail.connectTimeoutMs"];
212
+ this.config.regional.timeZone = parsed["regional.timeZone"];
213
+ for (const listener of this.listeners) listener.applied(this.config);
214
+ }
215
+
216
+ private parse(values: unknown) {
217
+ const parsed = platformValuesSchema.parse(values);
218
+ if (
219
+ parsed["mail.enabled"] &&
220
+ (!parsed["mail.fromAddress"] ||
221
+ (!parsed["mail.host"] && !this.config.mail.smtpUrl))
222
+ ) {
223
+ throw new Error("邮件传输配置不完整");
224
+ }
225
+ return parsed;
69
226
  }
70
227
 
71
228
  private values(): ModuleConfigValues {
72
229
  return {
73
230
  enableAI: this.config.enableAI,
74
231
  "cache.healthTimeoutMs": this.config.cache.healthTimeoutMs,
232
+ "security.sessionTtlSeconds": this.config.security.sessionTtlSeconds,
233
+ "security.passwordMinLength": this.config.security.passwordMinLength,
234
+ "security.passwordMaxLength": this.config.security.passwordMaxLength,
235
+ "storage.bulkDataMaxByteSize": this.config.storage.bulkDataMaxByteSize,
236
+ "storage.brandingLogoMaxByteSize":
237
+ this.config.storage.brandingLogoMaxByteSize,
238
+ "mail.enabled": this.config.mail.enabled,
239
+ "mail.host": this.config.mail.host,
240
+ "mail.port": this.config.mail.port,
241
+ "mail.secure": this.config.mail.secure,
242
+ "mail.fromAddress": this.config.mail.fromAddress,
243
+ "mail.connectTimeoutMs": this.config.mail.connectTimeoutMs,
244
+ "regional.timeZone": this.config.regional.timeZone,
75
245
  };
76
246
  }
77
247
  }
78
248
 
249
+ function integerField(
250
+ path: string,
251
+ label: string,
252
+ description: string,
253
+ minimum: number,
254
+ maximum: number,
255
+ ): ModuleConfigFieldDescriptor {
256
+ return {
257
+ path,
258
+ label,
259
+ description,
260
+ input: "integer",
261
+ applyMode: "live",
262
+ minimum,
263
+ maximum,
264
+ };
265
+ }
266
+
267
+ function restartIntegerField(
268
+ path: string,
269
+ label: string,
270
+ description: string,
271
+ minimum: number,
272
+ maximum: number,
273
+ ): ModuleConfigFieldDescriptor {
274
+ return {
275
+ ...integerField(path, label, description, minimum, maximum),
276
+ applyMode: "restart",
277
+ };
278
+ }
279
+
280
+ function restartBooleanField(
281
+ path: string,
282
+ label: string,
283
+ description: string,
284
+ ): ModuleConfigFieldDescriptor {
285
+ return { path, label, description, input: "boolean", applyMode: "restart" };
286
+ }
287
+
288
+ function restartTextField(
289
+ path: string,
290
+ label: string,
291
+ description: string,
292
+ ): ModuleConfigFieldDescriptor {
293
+ return { path, label, description, input: "text", applyMode: "restart" };
294
+ }
295
+
296
+ function isSupportedTimeZone(value: string): boolean {
297
+ try {
298
+ new Intl.DateTimeFormat("zh-CN", { timeZone: value });
299
+ return true;
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+
305
+ function isRecord(value: unknown): value is Record<string, unknown> {
306
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
307
+ }
308
+
79
309
  function assertSafeManagedPaths(): void {
80
310
  const fields = new Map(
81
311
  platformConfigDefinition.environment.map((field) => [field.path, field]),
@@ -1,7 +1,7 @@
1
1
  import { randomUUIDv7 } from "bun";
2
2
  import { moduleConfigVersions } from "@southwind-ai/database";
3
3
  import type { ModuleConfigVersion } from "@southwind-ai/shared";
4
- import { and, desc, eq, ne, sql } from "drizzle-orm";
4
+ import { and, desc, eq, inArray, ne, sql } from "drizzle-orm";
5
5
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
6
6
  import type {
7
7
  BeginConfigVersionInput,
@@ -58,7 +58,10 @@ export class DrizzleModuleConfigVersionRepository implements ModuleConfigVersion
58
58
  .where(
59
59
  and(
60
60
  eq(moduleConfigVersions.moduleKey, candidate.moduleKey),
61
- eq(moduleConfigVersions.applyStatus, "effective"),
61
+ inArray(moduleConfigVersions.applyStatus, [
62
+ "effective",
63
+ "pending-restart",
64
+ ]),
62
65
  ne(moduleConfigVersions.id, id),
63
66
  ),
64
67
  );
@@ -84,6 +87,37 @@ export class DrizzleModuleConfigVersionRepository implements ModuleConfigVersion
84
87
  return mapRequired(updated);
85
88
  }
86
89
 
90
+ async markPendingRestart(id: string): Promise<ModuleConfigVersion> {
91
+ return this.db.transaction(async (tx) => {
92
+ const [candidate] = await tx
93
+ .select()
94
+ .from(moduleConfigVersions)
95
+ .where(eq(moduleConfigVersions.id, id))
96
+ .limit(1);
97
+ if (!candidate) throw new Error("配置版本不存在");
98
+ await tx
99
+ .update(moduleConfigVersions)
100
+ .set({ applyStatus: "superseded" })
101
+ .where(
102
+ and(
103
+ eq(moduleConfigVersions.moduleKey, candidate.moduleKey),
104
+ eq(moduleConfigVersions.applyStatus, "pending-restart"),
105
+ ne(moduleConfigVersions.id, id),
106
+ ),
107
+ );
108
+ const [updated] = await tx
109
+ .update(moduleConfigVersions)
110
+ .set({
111
+ applyStatus: "pending-restart",
112
+ applyError: null,
113
+ appliedAt: null,
114
+ })
115
+ .where(eq(moduleConfigVersions.id, id))
116
+ .returning();
117
+ return mapRequired(updated);
118
+ });
119
+ }
120
+
87
121
  latest(moduleKey: string): Promise<ModuleConfigVersion | undefined> {
88
122
  return this.first(moduleKey);
89
123
  }
@@ -18,6 +18,7 @@ export interface BeginConfigVersionInput {
18
18
  export interface ModuleConfigVersionRepository {
19
19
  begin(input: BeginConfigVersionInput): Promise<ModuleConfigVersion>;
20
20
  markEffective(id: string, appliedAt: Date): Promise<ModuleConfigVersion>;
21
+ markPendingRestart(id: string): Promise<ModuleConfigVersion>;
21
22
  markFailed(
22
23
  id: string,
23
24
  applyError: string,
@@ -61,7 +62,8 @@ export class InMemoryModuleConfigVersionRepository implements ModuleConfigVersio
61
62
  for (const candidate of this.versions) {
62
63
  if (
63
64
  candidate.moduleKey === version.moduleKey &&
64
- candidate.applyStatus === "effective"
65
+ (candidate.applyStatus === "effective" ||
66
+ candidate.applyStatus === "pending-restart")
65
67
  ) {
66
68
  candidate.applyStatus = "superseded";
67
69
  }
@@ -84,6 +86,23 @@ export class InMemoryModuleConfigVersionRepository implements ModuleConfigVersio
84
86
  return cloneVersion(version);
85
87
  }
86
88
 
89
+ async markPendingRestart(id: string): Promise<ModuleConfigVersion> {
90
+ const version = this.required(id);
91
+ for (const candidate of this.versions) {
92
+ if (
93
+ candidate.id !== id &&
94
+ candidate.moduleKey === version.moduleKey &&
95
+ candidate.applyStatus === "pending-restart"
96
+ ) {
97
+ candidate.applyStatus = "superseded";
98
+ }
99
+ }
100
+ version.applyStatus = "pending-restart";
101
+ version.applyError = undefined;
102
+ version.appliedAt = undefined;
103
+ return cloneVersion(version);
104
+ }
105
+
87
106
  async latest(moduleKey: string): Promise<ModuleConfigVersion | undefined> {
88
107
  return this.sorted(moduleKey)[0];
89
108
  }
@@ -134,5 +153,11 @@ export function cloneVersion(
134
153
  }
135
154
 
136
155
  export function isApplyStatus(value: string): value is ModuleConfigApplyStatus {
137
- return ["applying", "effective", "failed", "superseded"].includes(value);
156
+ return [
157
+ "applying",
158
+ "effective",
159
+ "failed",
160
+ "pending-restart",
161
+ "superseded",
162
+ ].includes(value);
138
163
  }
@@ -176,7 +176,10 @@ function recordResult(
176
176
  version: ModuleConfigVersion,
177
177
  ): void {
178
178
  const outcome: AuditOutcome =
179
- version.applyStatus === "effective" ? "success" : "failure";
179
+ version.applyStatus === "effective" ||
180
+ version.applyStatus === "pending-restart"
181
+ ? "success"
182
+ : "failure";
180
183
  runtime.auditSink.recordEvent({
181
184
  ...resourceEvent(
182
185
  context,
@@ -81,6 +81,106 @@ describe("模块配置版本服务", () => {
81
81
  expect(serialized).not.toContain("secret-value-marker");
82
82
  expect(serialized).not.toContain("SecretRef");
83
83
  });
84
+
85
+ test("重启字段保存为等待状态并在下一次启动提升为生效版本", async () => {
86
+ const repository = new InMemoryModuleConfigVersionRepository();
87
+ const current = { host: "old.smtp.local" };
88
+ const definition: ManagedModuleConfigDefinition = {
89
+ moduleKey: "mail.runtime",
90
+ label: "邮件传输配置",
91
+ fields: [
92
+ {
93
+ path: "host",
94
+ label: "SMTP 主机",
95
+ description: "重启后应用",
96
+ input: "text",
97
+ applyMode: "restart",
98
+ },
99
+ ],
100
+ validate(values) {
101
+ const input = values as Record<string, unknown>;
102
+ return { host: String(input.host || current.host) };
103
+ },
104
+ apply(values) {
105
+ current.host = values.host as string;
106
+ },
107
+ baseValues: () => ({ ...current }),
108
+ };
109
+ const service = new ModuleConfigurationService(repository, [definition]);
110
+
111
+ const created = await service.create(
112
+ "mail.runtime",
113
+ { host: "new.smtp.local" },
114
+ "user-1",
115
+ );
116
+
117
+ expect(created).toMatchObject({
118
+ ok: true,
119
+ version: { applyStatus: "pending-restart" },
120
+ });
121
+ expect(current.host).toBe("old.smtp.local");
122
+
123
+ const replacement = await service.create(
124
+ "mail.runtime",
125
+ { host: "corrected.smtp.local" },
126
+ "user-1",
127
+ );
128
+ expect(replacement).toMatchObject({
129
+ ok: true,
130
+ version: { version: 2, applyStatus: "pending-restart" },
131
+ });
132
+ expect((await service.summaries())[0]?.pendingValues).toEqual({
133
+ host: "corrected.smtp.local",
134
+ });
135
+ expect(
136
+ (await service.list("mail.runtime"))?.map((item) => item.applyStatus),
137
+ ).toEqual(["pending-restart", "superseded"]);
138
+
139
+ await new ModuleConfigurationService(repository, [definition]).initialize();
140
+
141
+ expect(current.host).toBe("corrected.smtp.local");
142
+ expect((await repository.latest("mail.runtime"))?.applyStatus).toBe(
143
+ "effective",
144
+ );
145
+ });
146
+
147
+ test("旧版本缺少新增字段时使用当前基线补齐", async () => {
148
+ const current = { enabled: false, timeoutMs: 300 };
149
+ const schema = z.strictObject({
150
+ enabled: z.boolean(),
151
+ timeoutMs: z.number().int(),
152
+ });
153
+ const definition: ManagedModuleConfigDefinition = {
154
+ moduleKey: "compatible.runtime",
155
+ label: "兼容配置",
156
+ fields: [],
157
+ validate(values) {
158
+ return schema.parse({
159
+ ...current,
160
+ ...(values as Record<string, unknown>),
161
+ });
162
+ },
163
+ apply(values) {
164
+ current.enabled = values.enabled as boolean;
165
+ current.timeoutMs = values.timeoutMs as number;
166
+ },
167
+ baseValues: () => ({ ...current }),
168
+ };
169
+ const repository = new InMemoryModuleConfigVersionRepository();
170
+ const version = await repository.begin({
171
+ moduleKey: definition.moduleKey,
172
+ values: { enabled: true },
173
+ diff: [],
174
+ actorId: "legacy",
175
+ });
176
+ await repository.markEffective(version.id, new Date());
177
+ const service = new ModuleConfigurationService(repository, [definition]);
178
+
179
+ await service.initialize();
180
+
181
+ expect(current).toEqual({ enabled: true, timeoutMs: 300 });
182
+ expect((await service.summaries())[0]?.effectiveValues).toEqual(current);
183
+ });
84
184
  });
85
185
 
86
186
  function createFixture(failApply = false) {
@@ -36,14 +36,21 @@ export class ModuleConfigurationService {
36
36
 
37
37
  async initialize(): Promise<void> {
38
38
  for (const definition of this.definitions.values()) {
39
- const effective = await this.repository.effective(definition.moduleKey);
40
- if (!effective) continue;
39
+ const latest = await this.repository.latest(definition.moduleKey);
40
+ const candidate =
41
+ latest?.applyStatus === "pending-restart"
42
+ ? latest
43
+ : await this.repository.effective(definition.moduleKey);
44
+ if (!candidate) continue;
41
45
  try {
42
- const values = definition.validate(effective.values);
46
+ const values = definition.validate(candidate.values);
43
47
  await definition.apply(values);
48
+ if (candidate.applyStatus === "pending-restart") {
49
+ await this.repository.markEffective(candidate.id, new Date());
50
+ }
44
51
  } catch (error) {
45
52
  await this.repository.markFailed(
46
- effective.id,
53
+ candidate.id,
47
54
  sanitizeApplyError(error),
48
55
  new Date(),
49
56
  );
@@ -62,9 +69,13 @@ export class ModuleConfigurationService {
62
69
  moduleKey: definition.moduleKey,
63
70
  label: definition.label,
64
71
  fields: definition.fields.map((field) => ({ ...field })),
65
- effectiveValues: {
66
- ...(effective?.values || definition.baseValues()),
67
- },
72
+ effectiveValues: definition.validate(
73
+ effective?.values || definition.baseValues(),
74
+ ),
75
+ pendingValues:
76
+ latest?.applyStatus === "pending-restart"
77
+ ? definition.validate(latest.values)
78
+ : undefined,
68
79
  effectiveSource: effective ? "version" : "environment",
69
80
  effectiveVersion: effective?.version,
70
81
  latestVersion: latest?.version,
@@ -129,16 +140,33 @@ export class ModuleConfigurationService {
129
140
  reason?: string,
130
141
  rollbackOfVersion?: number,
131
142
  ): Promise<ConfigVersionResult> {
132
- const effective = await this.repository.effective(definition.moduleKey);
133
- const before = effective?.values || definition.baseValues();
143
+ const [effective, latest] = await Promise.all([
144
+ this.repository.effective(definition.moduleKey),
145
+ this.repository.latest(definition.moduleKey),
146
+ ]);
147
+ const before = definition.validate(
148
+ latest?.applyStatus === "pending-restart"
149
+ ? latest.values
150
+ : effective?.values || definition.baseValues(),
151
+ );
152
+ const diff = configDiff(before, values);
134
153
  const version = await this.repository.begin({
135
154
  moduleKey: definition.moduleKey,
136
155
  values,
137
- diff: configDiff(before, values),
156
+ diff,
138
157
  rollbackOfVersion,
139
158
  reason,
140
159
  actorId,
141
160
  });
161
+ if (
162
+ latest?.applyStatus === "pending-restart" ||
163
+ requiresRestart(definition, diff)
164
+ ) {
165
+ return {
166
+ ok: true,
167
+ version: await this.repository.markPendingRestart(version.id),
168
+ };
169
+ }
142
170
  try {
143
171
  await definition.apply(values);
144
172
  return {
@@ -158,6 +186,18 @@ export class ModuleConfigurationService {
158
186
  }
159
187
  }
160
188
 
189
+ function requiresRestart(
190
+ definition: ManagedModuleConfigDefinition,
191
+ diff: ModuleConfigDiffEntry[],
192
+ ): boolean {
193
+ const restartPaths = new Set(
194
+ definition.fields
195
+ .filter((field) => field.applyMode === "restart")
196
+ .map((field) => field.path),
197
+ );
198
+ return diff.some((entry) => restartPaths.has(entry.path));
199
+ }
200
+
161
201
  export function configDiff(
162
202
  before: ModuleConfigValues,
163
203
  after: ModuleConfigValues,
@@ -133,9 +133,10 @@ function createRepositories(pool: Pool) {
133
133
  async function seedDepartments(
134
134
  repository: ReturnType<typeof createRepositories>["departments"],
135
135
  ) {
136
- await repository.create(department("dept_a"), setupActor);
136
+ await repository.create(department("org_root"), setupActor);
137
+ await repository.create(department("dept_a", "org_root"), setupActor);
137
138
  await repository.create(department("dept_a_child", "dept_a"), setupActor);
138
- await repository.create(department("dept_b"), setupActor);
139
+ await repository.create(department("dept_b", "org_root"), setupActor);
139
140
  }
140
141
 
141
142
  async function seedUsers(
@@ -1,6 +1,6 @@
1
1
  import type { GovernedExportQuerySnapshot } from "@southwind-ai/database";
2
2
  import type { AuditEvent } from "@southwind-ai/shared";
3
- import type { RequestGuardContext } from "../../guards.js";
3
+ import { auditActor, type RequestGuardContext } from "../../guards.js";
4
4
  import {
5
5
  DurableArtifactError,
6
6
  type DurableArtifactService,
@@ -310,11 +310,7 @@ function event(
310
310
  id: Bun.randomUUIDv7(),
311
311
  action,
312
312
  outcome: "success",
313
- actor: {
314
- id: context.actor.id,
315
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
316
- name: context.actor.name,
317
- },
313
+ actor: auditActor(context),
318
314
  target: { type: "system.user.governed-export", id },
319
315
  requestId: context.requestId,
320
316
  metadata,