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
@@ -20,6 +20,7 @@ import { DrizzleSchedulerRepository } from "./scheduler/drizzle-repository.js";
20
20
  import { InMemorySchedulerRepository } from "./scheduler/repository.js";
21
21
  import type { ScheduledTaskDefinition } from "./scheduler/types.js";
22
22
  import type { createStorageRuntime } from "./storage/runtime.js";
23
+ import type { PlatformConfig } from "@southwind-ai/config";
23
24
  import type { DurableHandlerRegistry } from "@southwind-ai/jobs";
24
25
  import { collectActiveBlobRefs } from "./storage/blob-reference-registry.js";
25
26
  import type { createSystemRepositories } from "./system/seed.js";
@@ -33,6 +34,17 @@ import {
33
34
  } from "./workflow/effects.js";
34
35
  import { InMemoryWorkflowRepository } from "./workflow/repository.js";
35
36
  import { WorkflowService } from "./workflow/service.js";
37
+ import { NotificationChannelRegistry } from "./notification/channel-adapter.js";
38
+ import { DrizzleNotificationDeliveryRepository } from "./notification/delivery-repository.js";
39
+ import { NotificationDeliveryWorker } from "./notification/delivery-worker.js";
40
+ import { InMemoryNotificationDeliveryRepository } from "./notification/memory-delivery-repository.js";
41
+ import {
42
+ DrizzleNotificationPreferenceRepository,
43
+ InMemoryNotificationPreferenceRepository,
44
+ } from "./notification/preference-repository.js";
45
+ import { SystemNotificationRecipientDirectory } from "./notification/recipient-directory.js";
46
+ import { NotificationService } from "./notification/service.js";
47
+ import { SmtpNotificationChannelAdapter } from "./notification/smtp-adapter.js";
36
48
 
37
49
  interface ApplicationServicesInput {
38
50
  persistence: ReturnType<typeof createServerPersistence>;
@@ -42,6 +54,7 @@ interface ApplicationServicesInput {
42
54
  storage: ReturnType<typeof createStorageRuntime>;
43
55
  moduleTasks: ScheduledTaskDefinition[];
44
56
  bulkRetentionDays: number;
57
+ mailConfig?: PlatformConfig["mail"];
45
58
  installModuleDurableHandlers?: (registry: DurableHandlerRegistry) => void;
46
59
  }
47
60
 
@@ -52,6 +65,32 @@ export async function createApplicationServices(
52
65
  const notifications = persistence
53
66
  ? new DrizzleNotificationRepository(persistence.db)
54
67
  : new InMemoryNotificationRepository();
68
+ const notificationPreferences = persistence
69
+ ? new DrizzleNotificationPreferenceRepository(persistence.db)
70
+ : new InMemoryNotificationPreferenceRepository();
71
+ const notificationDeliveries = persistence
72
+ ? new DrizzleNotificationDeliveryRepository(persistence.db)
73
+ : new InMemoryNotificationDeliveryRepository();
74
+ const notificationRecipients = new SystemNotificationRecipientDirectory(
75
+ input.systemRepositories,
76
+ );
77
+ const notificationChannels = new NotificationChannelRegistry(
78
+ input.mailConfig?.enabled
79
+ ? [new SmtpNotificationChannelAdapter(input.mailConfig)]
80
+ : [],
81
+ );
82
+ const notificationService = new NotificationService(
83
+ notifications,
84
+ notificationPreferences,
85
+ notificationDeliveries,
86
+ notificationRecipients,
87
+ { mandatoryCategories: ["安全"] },
88
+ );
89
+ const notificationDeliveryWorker = new NotificationDeliveryWorker(
90
+ notificationDeliveries,
91
+ notificationRecipients,
92
+ notificationChannels,
93
+ );
55
94
  const deferredTimeout = new DeferredWorkflowTimeoutScheduler();
56
95
  const workflow = new WorkflowService(
57
96
  persistence
@@ -89,6 +128,15 @@ export async function createApplicationServices(
89
128
  const result = await governedExportDispatch.runNext?.();
90
129
  return { claimed: Boolean(result && result.status !== "idle") };
91
130
  },
131
+ notificationDispatch: async () => {
132
+ const scheduled = await notificationService.dispatchScheduled();
133
+ const delivery = await notificationDeliveryWorker.runNext();
134
+ return {
135
+ claimed: scheduled.claimed + delivery.claimed > 0,
136
+ scheduled,
137
+ delivery,
138
+ };
139
+ },
92
140
  }),
93
141
  ...input.moduleTasks,
94
142
  ];
@@ -149,6 +197,8 @@ export async function createApplicationServices(
149
197
  return {
150
198
  ...services,
151
199
  notifications,
200
+ notificationService,
201
+ notificationDeliveryWorker,
152
202
  workflow,
153
203
  governedExports,
154
204
  governedExportService,
@@ -74,6 +74,7 @@ export class DrizzleAuditLogReader {
74
74
 
75
75
  function auditWhere(query: AuditEventListQuery): SQL | undefined {
76
76
  const clauses = [
77
+ query.q ? keywordClause(query.q) : undefined,
77
78
  query.includeScheduler
78
79
  ? undefined
79
80
  : ne(auditLogs.actorId, "system_scheduler"),
@@ -91,6 +92,30 @@ function auditWhere(query: AuditEventListQuery): SQL | undefined {
91
92
  return clauses.length ? and(...clauses) : undefined;
92
93
  }
93
94
 
95
+ function keywordClause(query: string): SQL {
96
+ const pattern = `%${escapeLike(query.toLowerCase())}%`;
97
+ return sql`lower(concat_ws(' ',
98
+ ${auditLogs.action},
99
+ ${auditLogs.actorId},
100
+ ${auditLogs.resource},
101
+ ${auditLogs.resourceId},
102
+ ${auditLogs.ip},
103
+ coalesce(${auditLogs.metadata} ->> 'actorName', ''),
104
+ coalesce(${auditLogs.metadata} ->> 'targetLabel', ''),
105
+ coalesce(${auditLogs.metadata} ->> 'message', ''),
106
+ coalesce(${auditLogs.metadata} ->> 'module', ''),
107
+ coalesce(${auditLogs.metadata} ->> 'requestId', ''),
108
+ coalesce(${auditLogs.metadata}::text, '')
109
+ )) like ${pattern} escape '\\'`;
110
+ }
111
+
112
+ function escapeLike(value: string): string {
113
+ return value
114
+ .replaceAll("\\", "\\\\")
115
+ .replaceAll("%", "\\%")
116
+ .replaceAll("_", "\\_");
117
+ }
118
+
94
119
  function rowToAuditEvent(row: typeof auditLogs.$inferSelect): AuditEvent {
95
120
  const metadata = readMetadata(row.metadata);
96
121
  const outcome = readString(metadata.outcome, "success") as AuditOutcome;
@@ -1,6 +1,6 @@
1
1
  import type { AuditEvent } from "@southwind-ai/shared";
2
2
  import { randomUUIDv7 } from "bun";
3
- import type { RequestGuardContext } from "../guards.js";
3
+ import { auditActor, type RequestGuardContext } from "../guards.js";
4
4
 
5
5
  export function searchAuditMetadata(
6
6
  query: string,
@@ -26,11 +26,7 @@ export function searchAuditEvent(
26
26
  id: randomUUIDv7(),
27
27
  action: "data.search",
28
28
  outcome: "success",
29
- actor: {
30
- id: context.actor.id,
31
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
32
- name: context.actor.name,
33
- },
29
+ actor: auditActor(context),
34
30
  target: { type: resourceName },
35
31
  requestId: context.requestId,
36
32
  metadata: searchAuditMetadata(query, matched),
@@ -2,6 +2,7 @@ import type { RequestGuardContext } from "../guards.js";
2
2
 
3
3
  const PASSWORD_CHANGE_ALLOWLIST = new Set([
4
4
  "GET /api/auth/session",
5
+ "GET /api/auth/password/policy",
5
6
  "POST /api/auth/password/change",
6
7
  "POST /api/auth/logout",
7
8
  ]);
@@ -1,5 +1,5 @@
1
1
  import argon2, { argon2id } from "argon2";
2
- import { PASSWORD_POLICY } from "@southwind-ai/shared";
2
+ import { PASSWORD_POLICY, type PasswordPolicy } from "@southwind-ai/shared";
3
3
 
4
4
  export { PASSWORD_POLICY } from "@southwind-ai/shared";
5
5
 
@@ -15,30 +15,39 @@ export interface PasswordHasher {
15
15
  verify(hash: string, password: string): Promise<boolean>;
16
16
  }
17
17
 
18
- export const argon2PasswordHasher: PasswordHasher = {
19
- async hash(password) {
20
- assertPasswordPolicy(password);
21
- return argon2.hash(password, ARGON2_OPTIONS);
22
- },
23
- async verify(hash, password) {
24
- if (!password || password.length > PASSWORD_POLICY.maxLength) {
25
- return false;
26
- }
27
- try {
28
- return await argon2.verify(hash, password);
29
- } catch {
30
- return false;
31
- }
32
- },
33
- };
18
+ export const argon2PasswordHasher = createArgon2PasswordHasher();
34
19
 
35
- export function assertPasswordPolicy(password: string): void {
20
+ export function createArgon2PasswordHasher(
21
+ policyProvider: () => PasswordPolicy = () => PASSWORD_POLICY,
22
+ ): PasswordHasher {
23
+ return {
24
+ async hash(password) {
25
+ assertPasswordPolicy(password, policyProvider());
26
+ return argon2.hash(password, ARGON2_OPTIONS);
27
+ },
28
+ async verify(hash, password) {
29
+ if (!password || password.length > 512) {
30
+ return false;
31
+ }
32
+ try {
33
+ return await argon2.verify(hash, password);
34
+ } catch {
35
+ return false;
36
+ }
37
+ },
38
+ };
39
+ }
40
+
41
+ export function assertPasswordPolicy(
42
+ password: string,
43
+ policy: PasswordPolicy = PASSWORD_POLICY,
44
+ ): void {
36
45
  if (
37
- password.length < PASSWORD_POLICY.minLength ||
38
- password.length > PASSWORD_POLICY.maxLength
46
+ password.length < policy.minLength ||
47
+ password.length > policy.maxLength
39
48
  ) {
40
49
  throw new Error(
41
- `密码长度必须为 ${PASSWORD_POLICY.minLength}-${PASSWORD_POLICY.maxLength} 个字符`,
50
+ `密码长度必须为 ${policy.minLength}-${policy.maxLength} 个字符`,
42
51
  );
43
52
  }
44
53
  }
@@ -1,5 +1,5 @@
1
1
  import { beforeAll, describe, expect, test } from "bun:test";
2
- import type { AuditEvent } from "@southwind-ai/shared";
2
+ import type { AuditEvent, PasswordPolicy } from "@southwind-ai/shared";
3
3
  import { Elysia } from "elysia";
4
4
  import { createServerRuntime } from "../runtime.js";
5
5
  import { RepositoryActorResolver } from "./actor-resolver.js";
@@ -15,6 +15,37 @@ beforeAll(async () => {
15
15
  });
16
16
 
17
17
  describe("platform auth API", () => {
18
+ test("密码策略端点返回当前运行时策略且禁止缓存", async () => {
19
+ let policy: PasswordPolicy = { minLength: 16, maxLength: 64 };
20
+ const fixture = authFixture([activeUser()], () => policy);
21
+
22
+ const first = await fixture.app.handle(
23
+ new Request("http://localhost/api/auth/password/policy"),
24
+ );
25
+ expect(first.headers.get("cache-control")).toBe("no-store");
26
+ await expect(first.json()).resolves.toEqual(policy);
27
+
28
+ policy = { minLength: 20, maxLength: 80 };
29
+ const second = await fixture.app.handle(
30
+ new Request("http://localhost/api/auth/password/policy"),
31
+ );
32
+ await expect(second.json()).resolves.toEqual(policy);
33
+ });
34
+
35
+ test("会话有效期配置即时影响后续签发会话", async () => {
36
+ let ttlSeconds = 60;
37
+ const fixture = authFixture([activeUser()], undefined, () => ttlSeconds);
38
+ const first = await login(fixture.app, "admin");
39
+
40
+ ttlSeconds = 120;
41
+ const second = await login(fixture.app, "admin");
42
+
43
+ expect(
44
+ new Date(second.expiresAt).getTime() -
45
+ new Date(first.expiresAt).getTime(),
46
+ ).toBeGreaterThan(59_000);
47
+ });
48
+
18
49
  test("会话探测只返回当前 Actor,不暴露 Token", async () => {
19
50
  const fixture = authFixture([activeUser()]);
20
51
  const anonymous = await fixture.app.handle(
@@ -278,7 +309,11 @@ describe("platform auth API", () => {
278
309
  });
279
310
  });
280
311
 
281
- function authFixture(users: AuthUserRecord[]) {
312
+ function authFixture(
313
+ users: AuthUserRecord[],
314
+ passwordPolicy?: () => PasswordPolicy,
315
+ sessionTtlSeconds?: () => number,
316
+ ) {
282
317
  const repository = new InMemoryAuthRepository(users);
283
318
  const resolver = new RepositoryActorResolver(repository, {
284
319
  sessionCookieName: "windy.sid",
@@ -286,6 +321,8 @@ function authFixture(users: AuthUserRecord[]) {
286
321
  const events: AuditEvent[] = [];
287
322
  const runtime = createServerRuntime({}, { actorResolver: resolver });
288
323
  const service = new AuthService(repository, {
324
+ passwordPolicy,
325
+ sessionTtlSeconds,
289
326
  auditWriter: {
290
327
  write(event) {
291
328
  events.push(event);
@@ -326,7 +363,11 @@ async function login(
326
363
  }),
327
364
  );
328
365
  expect(response.status).toBe(200);
329
- return (await response.json()) as { token: string; sessionId: string };
366
+ return (await response.json()) as {
367
+ token: string;
368
+ sessionId: string;
369
+ expiresAt: string;
370
+ };
330
371
  }
331
372
 
332
373
  function readCookieValue(header: string, name: string): string {
@@ -53,6 +53,10 @@ export function registerAuthRoutes(
53
53
  registerLoginRoutes(app, authService, options);
54
54
  registerOwnSessionRoutes(app, authService, options);
55
55
 
56
+ app.get("/auth/password/policy", () =>
57
+ json(authService.getPasswordPolicy(), 200, { "cache-control": "no-store" }),
58
+ );
59
+
56
60
  app.post("/auth/logout", async (rawContext) => {
57
61
  const context = rawContext as AuthRouteContext;
58
62
  const token = readRequestSessionToken(context.request, cookieName);
@@ -112,10 +116,11 @@ export function registerAuthRoutes(
112
116
  INVALID_PASSWORD: 400,
113
117
  PASSWORD_UNCHANGED: 400,
114
118
  } as const;
119
+ const policy = authService.getPasswordPolicy();
115
120
  const messages = {
116
121
  USER_NOT_FOUND: "当前账号不存在",
117
122
  INVALID_CURRENT_PASSWORD: "当前密码错误",
118
- INVALID_PASSWORD: "新密码必须为 12-128 个字符",
123
+ INVALID_PASSWORD: `新密码必须为 ${policy.minLength}-${policy.maxLength} 个字符`,
119
124
  PASSWORD_UNCHANGED: "新密码不能与当前密码相同",
120
125
  } as const;
121
126
  return json(
@@ -1,10 +1,12 @@
1
1
  import type { AuditEventReader, AuditEventWriter } from "../runtime.js";
2
+ import type { PasswordPolicy } from "@southwind-ai/shared";
2
3
  import { InMemoryAuthRepository, type AuthRepository } from "./repository.js";
3
4
  import { AuthService } from "./service.js";
4
5
 
5
6
  interface AuthRuntimeInput {
6
7
  repository?: AuthRepository;
7
- sessionTtlSeconds: number;
8
+ sessionTtlSeconds: number | (() => number);
9
+ passwordPolicy?: () => PasswordPolicy;
8
10
  auditWriter: AuditEventWriter["write"];
9
11
  auditReader?: AuditEventReader;
10
12
  }
@@ -13,6 +15,7 @@ export function createAuthRuntime(input: AuthRuntimeInput) {
13
15
  const authRepository = input.repository ?? new InMemoryAuthRepository();
14
16
  const authService = new AuthService(authRepository, {
15
17
  sessionTtlSeconds: input.sessionTtlSeconds,
18
+ passwordPolicy: input.passwordPolicy,
16
19
  auditWriter: { write: input.auditWriter },
17
20
  auditReader: input.auditReader,
18
21
  });
@@ -1,6 +1,10 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { randomUUIDv7 } from "bun";
3
- import type { AuditEvent } from "@southwind-ai/shared";
3
+ import {
4
+ PASSWORD_POLICY,
5
+ type AuditEvent,
6
+ type PasswordPolicy,
7
+ } from "@southwind-ai/shared";
4
8
  import type { RequestActor } from "../guards.js";
5
9
  import type { AuditEventReader, AuditEventWriter } from "../runtime.js";
6
10
  import { hashSessionToken } from "./actor-resolver.js";
@@ -8,7 +12,7 @@ import {
8
12
  AuthenticationProviderRegistry,
9
13
  LocalArgon2AuthenticationAdapter,
10
14
  } from "./authentication-provider.js";
11
- import { argon2PasswordHasher, type PasswordHasher } from "./password.js";
15
+ import { createArgon2PasswordHasher, type PasswordHasher } from "./password.js";
12
16
  import type { AuthRepository, AuthSessionRecord } from "./repository.js";
13
17
  import {
14
18
  CredentialService,
@@ -42,7 +46,8 @@ export type RevokeResult =
42
46
  | { ok: false; code: "SESSION_NOT_FOUND" | "FORBIDDEN" };
43
47
 
44
48
  export interface AuthServiceOptions {
45
- sessionTtlSeconds?: number;
49
+ sessionTtlSeconds?: number | (() => number);
50
+ passwordPolicy?: () => PasswordPolicy;
46
51
  now?: () => Date;
47
52
  tokenFactory?: () => string;
48
53
  passwordHasher?: PasswordHasher;
@@ -52,7 +57,8 @@ export interface AuthServiceOptions {
52
57
  }
53
58
 
54
59
  export class AuthService {
55
- private readonly sessionTtlSeconds: number;
60
+ private readonly sessionTtlSeconds: () => number;
61
+ private readonly passwordPolicy: () => PasswordPolicy;
56
62
  private readonly now: () => Date;
57
63
  private readonly tokenFactory: () => string;
58
64
  private readonly passwordHasher: PasswordHasher;
@@ -64,11 +70,17 @@ export class AuthService {
64
70
  private readonly repository: AuthRepository,
65
71
  private readonly options: AuthServiceOptions,
66
72
  ) {
67
- this.sessionTtlSeconds = options.sessionTtlSeconds || 60 * 60 * 8;
73
+ const sessionTtlSeconds = options.sessionTtlSeconds;
74
+ this.sessionTtlSeconds =
75
+ typeof sessionTtlSeconds === "function"
76
+ ? sessionTtlSeconds
77
+ : () => sessionTtlSeconds || 60 * 60 * 8;
78
+ this.passwordPolicy = options.passwordPolicy || (() => PASSWORD_POLICY);
68
79
  this.now = options.now || (() => new Date());
69
80
  this.tokenFactory =
70
81
  options.tokenFactory || (() => randomBytes(32).toString("base64url"));
71
- this.passwordHasher = options.passwordHasher || argon2PasswordHasher;
82
+ this.passwordHasher =
83
+ options.passwordHasher || createArgon2PasswordHasher(this.passwordPolicy);
72
84
  this.authenticationProviders =
73
85
  options.authenticationProviders ||
74
86
  new AuthenticationProviderRegistry(
@@ -140,7 +152,9 @@ export class AuthService {
140
152
  tokenHash: hashSessionToken(token),
141
153
  status: "active",
142
154
  issuedAt,
143
- expiresAt: new Date(issuedAt.getTime() + this.sessionTtlSeconds * 1_000),
155
+ expiresAt: new Date(
156
+ issuedAt.getTime() + this.sessionTtlSeconds() * 1_000,
157
+ ),
144
158
  ip: metadata.ip,
145
159
  userAgent: metadata.userAgent,
146
160
  };
@@ -211,6 +225,10 @@ export class AuthService {
211
225
  );
212
226
  }
213
227
 
228
+ getPasswordPolicy(): PasswordPolicy {
229
+ return { ...this.passwordPolicy() };
230
+ }
231
+
214
232
  async revokeSession(
215
233
  sessionId: string,
216
234
  actor: RequestActor,
@@ -3,6 +3,10 @@ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
3
3
  import type { FoundationSnapshot } from "../foundation.js";
4
4
  import type { createServerRuntime } from "../runtime.js";
5
5
  import type { RouteApp } from "../system/route-types.js";
6
+ // @windy-module system.storage begin
7
+ import type { UploadService } from "@southwind-ai/storage";
8
+ import { createPlatformUploadPolicies } from "../storage/runtime.js";
9
+ // @windy-module system.storage end
6
10
  import {
7
11
  createRedisHealthProbe,
8
12
  type RedisHealthProbe,
@@ -21,6 +25,9 @@ export async function bootstrapModuleConfiguration(
21
25
  ): Promise<{
22
26
  service: ModuleConfigurationService;
23
27
  bindFoundation(snapshot: FoundationSnapshot): void;
28
+ // @windy-module system.storage begin
29
+ bindStorage(uploads: Pick<UploadService, "setPolicy">): void;
30
+ // @windy-module system.storage end
24
31
  redisHealthProbe: RedisHealthProbe;
25
32
  register(
26
33
  app: Pick<RouteApp, "get" | "post">,
@@ -37,12 +44,24 @@ export async function bootstrapModuleConfiguration(
37
44
  return {
38
45
  service,
39
46
  bindFoundation(snapshot) {
40
- adapter.setListener({
47
+ snapshot.platform.enableAI = config.enableAI;
48
+ adapter.addListener({
41
49
  applied(current) {
42
50
  snapshot.platform.enableAI = current.enableAI;
43
51
  },
44
52
  });
45
53
  },
54
+ // @windy-module system.storage begin
55
+ bindStorage(uploads: Pick<UploadService, "setPolicy">) {
56
+ adapter.addListener({
57
+ applied(current) {
58
+ for (const policy of createPlatformUploadPolicies(current)) {
59
+ uploads.setPolicy(policy);
60
+ }
61
+ },
62
+ });
63
+ },
64
+ // @windy-module system.storage end
46
65
  redisHealthProbe: createLiveRedisHealthProbe(config),
47
66
  register(app, runtime, features) {
48
67
  registerModuleConfigurationRoutes(app, service, runtime, features);
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { loadPlatformConfig } from "@southwind-ai/config";
3
+ import { PlatformConfigApplyAdapter } from "./definition.js";
4
+
5
+ describe("平台类型化运行配置", () => {
6
+ test("白名单不包含 SMTP Secret 且旧版本缺失字段由环境基线补齐", () => {
7
+ const config = loadPlatformConfig({
8
+ MAIL_SMTP_URL: "smtp://secret-marker@example.invalid",
9
+ });
10
+ const definition = new PlatformConfigApplyAdapter(config).definition();
11
+
12
+ expect(definition.fields.map((field) => field.path)).not.toContain(
13
+ "mail.smtpUrl",
14
+ );
15
+ expect(JSON.stringify(definition.baseValues())).not.toContain(
16
+ "secret-marker",
17
+ );
18
+ expect(
19
+ definition.validate({
20
+ enableAI: true,
21
+ "cache.healthTimeoutMs": 500,
22
+ }),
23
+ ).toMatchObject({
24
+ enableAI: true,
25
+ "cache.healthTimeoutMs": 500,
26
+ "security.passwordMinLength": 12,
27
+ "security.passwordMaxLength": 128,
28
+ "regional.timeZone": "Asia/Shanghai",
29
+ });
30
+ expect(() =>
31
+ definition.validate({
32
+ ...definition.baseValues(),
33
+ "mail.smtpUrl": "smtp://must-not-persist",
34
+ }),
35
+ ).toThrow();
36
+ expect(() =>
37
+ definition.validate({
38
+ ...definition.baseValues(),
39
+ "mail.enabled": true,
40
+ }),
41
+ ).toThrow();
42
+ });
43
+
44
+ test("即时字段应用到共享运行配置对象", async () => {
45
+ const config = loadPlatformConfig({});
46
+ const definition = new PlatformConfigApplyAdapter(config).definition();
47
+
48
+ await definition.apply(
49
+ definition.validate({
50
+ ...definition.baseValues(),
51
+ "security.sessionTtlSeconds": 3_600,
52
+ "security.passwordMinLength": 16,
53
+ "security.passwordMaxLength": 64,
54
+ "storage.bulkDataMaxByteSize": 1_048_576,
55
+ "storage.brandingLogoMaxByteSize": 262_144,
56
+ "regional.timeZone": "UTC",
57
+ }),
58
+ );
59
+
60
+ expect(config.security).toMatchObject({
61
+ sessionTtlSeconds: 3_600,
62
+ passwordMinLength: 16,
63
+ passwordMaxLength: 64,
64
+ });
65
+ expect(config.storage).toEqual({
66
+ bulkDataMaxByteSize: 1_048_576,
67
+ brandingLogoMaxByteSize: 262_144,
68
+ });
69
+ expect(config.regional.timeZone).toBe("UTC");
70
+ });
71
+ });