create-windy 0.2.34 → 0.3.1

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 (261) hide show
  1. package/README.md +26 -3
  2. package/dist/cli.js +8121 -3746
  3. package/package.json +3 -3
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +5 -0
  6. package/template/README.md +28 -4
  7. package/template/_gitignore +4 -0
  8. package/template/apps/agent-server/Dockerfile +1 -0
  9. package/template/apps/agent-server/README.md +18 -5
  10. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  12. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  13. package/template/apps/agent-server/pyproject.toml +1 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  18. package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
  19. package/template/apps/agent-server/tests/__init__.py +1 -0
  20. package/template/apps/agent-server/tests/openai_provider_test_support.py +131 -0
  21. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  22. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  23. package/template/apps/agent-server/tests/test_openai_provider.py +7 -123
  24. package/template/apps/server/package.json +4 -1
  25. package/template/apps/server/src/agent/run-facade.ts +2 -7
  26. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  27. package/template/apps/server/src/application-services.ts +50 -0
  28. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  29. package/template/apps/server/src/audit/search-summary.ts +2 -6
  30. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  31. package/template/apps/server/src/auth/password.ts +30 -21
  32. package/template/apps/server/src/auth/routes.test-support.ts +94 -0
  33. package/template/apps/server/src/auth/routes.test.ts +51 -78
  34. package/template/apps/server/src/auth/routes.ts +6 -1
  35. package/template/apps/server/src/auth/runtime.ts +4 -1
  36. package/template/apps/server/src/auth/service.ts +25 -7
  37. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  38. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  39. package/template/apps/server/src/configuration/definition.ts +242 -12
  40. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  41. package/template/apps/server/src/configuration/repository.ts +27 -2
  42. package/template/apps/server/src/configuration/routes.ts +4 -1
  43. package/template/apps/server/src/configuration/service.test.ts +100 -0
  44. package/template/apps/server/src/configuration/service.ts +50 -10
  45. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  46. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  47. package/template/apps/server/src/example-modules.ts +4 -1
  48. package/template/apps/server/src/guards.ts +18 -5
  49. package/template/apps/server/src/http/request-source.test.ts +85 -0
  50. package/template/apps/server/src/http/request-source.ts +90 -0
  51. package/template/apps/server/src/index.ts +40 -40
  52. package/template/apps/server/src/license/routes.ts +2 -0
  53. package/template/apps/server/src/license/runtime-service.ts +10 -11
  54. package/template/apps/server/src/mcp/adapter.test.ts +3 -2
  55. package/template/apps/server/src/mcp/adapter.ts +9 -9
  56. package/template/apps/server/src/mcp/routes.ts +4 -3
  57. package/template/apps/server/src/module-bootstrap.ts +2 -2
  58. package/template/apps/server/src/module-host/request-context.ts +6 -6
  59. package/template/apps/server/src/module-host/storage-scope.ts +2 -9
  60. package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
  61. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  62. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  63. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  64. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  65. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  66. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  67. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  68. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  69. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  70. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  71. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  72. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  73. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  74. package/template/apps/server/src/notification/memory-support.ts +72 -0
  75. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  76. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  77. package/template/apps/server/src/notification/repository.test.ts +171 -0
  78. package/template/apps/server/src/notification/repository.ts +179 -32
  79. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  80. package/template/apps/server/src/notification/route-support.ts +225 -0
  81. package/template/apps/server/src/notification/routes.test.ts +182 -0
  82. package/template/apps/server/src/notification/routes.ts +225 -132
  83. package/template/apps/server/src/notification/service.test.ts +104 -0
  84. package/template/apps/server/src/notification/service.ts +140 -0
  85. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  86. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  87. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  88. package/template/apps/server/src/platform-http-runtime.ts +87 -0
  89. package/template/apps/server/src/route-guards.ts +1 -0
  90. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  91. package/template/apps/server/src/runtime-development.ts +1 -0
  92. package/template/apps/server/src/runtime.test.ts +23 -9
  93. package/template/apps/server/src/runtime.ts +39 -2
  94. package/template/apps/server/src/scheduler/audit.ts +2 -11
  95. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  96. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  97. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  98. package/template/apps/server/src/scheduler/routes.ts +2 -7
  99. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  100. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  101. package/template/apps/server/src/search/service.ts +2 -6
  102. package/template/apps/server/src/settings/routes.ts +15 -3
  103. package/template/apps/server/src/settings/runtime.ts +10 -1
  104. package/template/apps/server/src/storage/runtime.ts +13 -0
  105. package/template/apps/server/src/system/audit-query.ts +33 -4
  106. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  107. package/template/apps/server/src/system/audit-routes.ts +6 -5
  108. package/template/apps/server/src/system/department-organization.ts +320 -0
  109. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  110. package/template/apps/server/src/system/department-routes.ts +114 -33
  111. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  112. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  113. package/template/apps/server/src/system/drizzle-system-configs.ts +319 -0
  114. package/template/apps/server/src/system/drizzle-system.ts +59 -311
  115. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  116. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  117. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  118. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  119. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  120. package/template/apps/server/src/system/resource-routes.ts +270 -0
  121. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  122. package/template/apps/server/src/system/route-helpers.ts +2 -5
  123. package/template/apps/server/src/system/route-types.ts +12 -3
  124. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  125. package/template/apps/server/src/system/routes.test.ts +10 -2
  126. package/template/apps/server/src/system/routes.ts +35 -267
  127. package/template/apps/server/src/system/seed.ts +7 -0
  128. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  129. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  130. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  131. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  132. package/template/apps/server/src/work-order/service.ts +42 -10
  133. package/template/apps/web/src/app/error-pages.ts +9 -0
  134. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  135. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  136. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  137. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  138. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  139. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  140. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  141. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  142. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  143. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  144. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  145. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  146. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  147. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  148. package/template/apps/web/src/lib/date-time.ts +22 -10
  149. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  150. package/template/apps/web/src/main.ts +14 -1
  151. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  152. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  153. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  154. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  155. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  156. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  157. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  158. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  159. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  160. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  161. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  162. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  163. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  164. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  165. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  166. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  167. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  168. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  169. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  170. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  171. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  172. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  173. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  174. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  175. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.vue +21 -0
  176. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.webtest.ts +17 -0
  177. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +96 -108
  178. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  179. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  180. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  181. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  182. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  183. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  184. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  185. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  186. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  187. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  188. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  189. package/template/apps/web/src/router/error-navigation.ts +50 -0
  190. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  191. package/template/apps/web/src/router/index.ts +71 -0
  192. package/template/apps/web/src/router/index.webtest.ts +29 -0
  193. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  194. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  195. package/template/apps/web/src/services/auth-api.ts +5 -0
  196. package/template/apps/web/src/services/http.ts +43 -6
  197. package/template/apps/web/src/services/http.webtest.ts +70 -0
  198. package/template/apps/web/src/services/notification-api.ts +85 -2
  199. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  200. package/template/apps/web/src/services/search-api.ts +39 -0
  201. package/template/apps/web/src/services/system-api.ts +18 -0
  202. package/template/apps/web/vitest.config.ts +1 -0
  203. package/template/docker-compose.yml +66 -0
  204. package/template/docs/architecture/ai-runtime.md +14 -5
  205. package/template/docs/architecture/mcp-business-capabilities.md +107 -0
  206. package/template/docs/architecture/search-provider-protocol.md +38 -2
  207. package/template/docs/development/custom-error-pages.md +78 -0
  208. package/template/docs/development/custom-login-page.md +3 -3
  209. package/template/docs/platform/agent-runtime.md +32 -6
  210. package/template/docs/platform/agent-tools.md +4 -0
  211. package/template/docs/platform/audit-reliability.md +46 -2
  212. package/template/docs/platform/module-configuration.md +13 -4
  213. package/template/docs/platform/notifications.md +67 -11
  214. package/template/docs/platform/organization-membership.md +17 -3
  215. package/template/docs/platform/system-crud-api.md +16 -0
  216. package/template/docs/platform/web-system-crud.md +6 -2
  217. package/template/package.json +4 -0
  218. package/template/packages/config/index.test.ts +45 -0
  219. package/template/packages/config/package.json +1 -1
  220. package/template/packages/config/src/platform.ts +169 -48
  221. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  222. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  223. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  224. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  225. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  226. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  227. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  228. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  229. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  230. package/template/packages/database/package.json +1 -1
  231. package/template/packages/database/src/schema/identity.ts +7 -0
  232. package/template/packages/database/src/schema/notifications.ts +135 -0
  233. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  234. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  235. package/template/packages/modules/package.json +1 -1
  236. package/template/packages/modules/src/composition-fixtures.test-support.ts +164 -0
  237. package/template/packages/modules/src/composition.test.ts +5 -158
  238. package/template/packages/modules/src/system-api-operational.ts +234 -0
  239. package/template/packages/modules/src/system-api-permissions.ts +14 -181
  240. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  241. package/template/packages/modules/src/system-features.ts +1 -1
  242. package/template/packages/modules/src/system-module-catalog.ts +2 -1
  243. package/template/packages/modules/src/system-module-tasks.ts +86 -0
  244. package/template/packages/modules/src/system-modules-access.test.ts +60 -0
  245. package/template/packages/modules/src/system-modules.test.ts +13 -54
  246. package/template/packages/modules/src/system-modules.ts +2 -72
  247. package/template/packages/modules/src/system-permissions.ts +9 -5
  248. package/template/packages/server-sdk/package.json +1 -1
  249. package/template/packages/shared/index.ts +1 -0
  250. package/template/packages/shared/package.json +1 -1
  251. package/template/packages/shared/src/audit.ts +25 -0
  252. package/template/packages/shared/src/mcp.test.ts +12 -0
  253. package/template/packages/shared/src/mcp.ts +7 -0
  254. package/template/packages/shared/src/module-configuration.ts +5 -0
  255. package/template/packages/shared/src/notification.ts +92 -1
  256. package/template/packages/shared/src/password.ts +7 -2
  257. package/template/packages/shared/src/platform-settings.ts +2 -0
  258. package/template/packages/storage/package.json +1 -1
  259. package/template/packages/storage/src/upload-service.test.ts +16 -0
  260. package/template/packages/storage/src/upload-service.ts +5 -1
  261. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -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
  }
@@ -0,0 +1,94 @@
1
+ import { expect } from "bun:test";
2
+ import type { AuditEvent, PasswordPolicy } from "@southwind-ai/shared";
3
+ import { Elysia } from "elysia";
4
+ import { createServerRuntime } from "../runtime.js";
5
+ import { RepositoryActorResolver } from "./actor-resolver.js";
6
+ import { InMemoryAuthRepository, type AuthUserRecord } from "./repository.js";
7
+ import { registerAuthRoutes, registerAuthSessionRoute } from "./routes.js";
8
+ import { AuthService } from "./service.js";
9
+
10
+ export function authFixture(
11
+ users: AuthUserRecord[],
12
+ passwordPolicy?: () => PasswordPolicy,
13
+ sessionTtlSeconds?: () => number,
14
+ ) {
15
+ const repository = new InMemoryAuthRepository(users);
16
+ const resolver = new RepositoryActorResolver(repository, {
17
+ sessionCookieName: "windy.sid",
18
+ });
19
+ const events: AuditEvent[] = [];
20
+ const runtime = createServerRuntime({}, { actorResolver: resolver });
21
+ const service = new AuthService(repository, {
22
+ passwordPolicy,
23
+ sessionTtlSeconds,
24
+ auditWriter: {
25
+ write(event) {
26
+ events.push(event);
27
+ },
28
+ },
29
+ });
30
+ const app = new Elysia({ prefix: "/api" }).derive(async ({ request }) => ({
31
+ guardContext: await runtime.createGuardContext(request),
32
+ }));
33
+ registerAuthSessionRoute(app);
34
+ registerAuthRoutes(app, service, { secureCookie: false });
35
+ return { app, events, repository, runtime };
36
+ }
37
+
38
+ export function createActiveUser(passwordHash: string): AuthUserRecord {
39
+ return {
40
+ id: "user_admin",
41
+ username: "admin",
42
+ displayName: "平台管理员",
43
+ email: "admin@windy.local",
44
+ phone: "13800138000",
45
+ passwordHash,
46
+ credentialState: "active",
47
+ status: "active",
48
+ roleCodes: ["developer"],
49
+ permissionKeys: ["system.user.manage", "system.audit.read"],
50
+ };
51
+ }
52
+
53
+ export async function loginWithPassword(
54
+ app: { handle(request: Request): Response | Promise<Response> },
55
+ username: string,
56
+ password: string,
57
+ ) {
58
+ const response = await app.handle(
59
+ jsonRequest("/api/auth/api/login", {
60
+ username,
61
+ password,
62
+ }),
63
+ );
64
+ expect(response.status).toBe(200);
65
+ return (await response.json()) as {
66
+ token: string;
67
+ sessionId: string;
68
+ expiresAt: string;
69
+ };
70
+ }
71
+
72
+ export function readCookieValue(header: string, name: string): string {
73
+ const value = header.split(";")[0]?.slice(`${name}=`.length);
74
+ return value ? decodeURIComponent(value) : "";
75
+ }
76
+
77
+ export function jsonRequest(path: string, body: unknown): Request {
78
+ return new Request(`http://localhost${path}`, {
79
+ method: "POST",
80
+ headers: { "content-type": "application/json" },
81
+ body: JSON.stringify(body),
82
+ });
83
+ }
84
+
85
+ export function apiRequest(
86
+ path: string,
87
+ token: string,
88
+ method = "GET",
89
+ ): Request {
90
+ return new Request(`http://localhost${path}`, {
91
+ method,
92
+ headers: { authorization: `Bearer ${token}` },
93
+ });
94
+ }
@@ -1,12 +1,14 @@
1
1
  import { beforeAll, describe, expect, test } from "bun:test";
2
- import type { AuditEvent } from "@southwind-ai/shared";
3
- import { Elysia } from "elysia";
4
- import { createServerRuntime } from "../runtime.js";
5
- import { RepositoryActorResolver } from "./actor-resolver.js";
2
+ import type { PasswordPolicy } from "@southwind-ai/shared";
6
3
  import { argon2PasswordHasher } from "./password.js";
7
- import { InMemoryAuthRepository, type AuthUserRecord } from "./repository.js";
8
- import { registerAuthRoutes, registerAuthSessionRoute } from "./routes.js";
9
- import { AuthService } from "./service.js";
4
+ import {
5
+ apiRequest,
6
+ authFixture,
7
+ createActiveUser,
8
+ jsonRequest,
9
+ loginWithPassword,
10
+ readCookieValue,
11
+ } from "./routes.test-support.js";
10
12
 
11
13
  let passwordHash = "";
12
14
 
@@ -14,7 +16,49 @@ beforeAll(async () => {
14
16
  passwordHash = await argon2PasswordHasher.hash("test-only-password");
15
17
  });
16
18
 
19
+ function activeUser() {
20
+ return createActiveUser(passwordHash);
21
+ }
22
+
23
+ function login(
24
+ app: { handle(request: Request): Response | Promise<Response> },
25
+ username: string,
26
+ ) {
27
+ return loginWithPassword(app, username, "test-only-password");
28
+ }
29
+
17
30
  describe("platform auth API", () => {
31
+ test("密码策略端点返回当前运行时策略且禁止缓存", async () => {
32
+ let policy: PasswordPolicy = { minLength: 16, maxLength: 64 };
33
+ const fixture = authFixture([activeUser()], () => policy);
34
+
35
+ const first = await fixture.app.handle(
36
+ new Request("http://localhost/api/auth/password/policy"),
37
+ );
38
+ expect(first.headers.get("cache-control")).toBe("no-store");
39
+ await expect(first.json()).resolves.toEqual(policy);
40
+
41
+ policy = { minLength: 20, maxLength: 80 };
42
+ const second = await fixture.app.handle(
43
+ new Request("http://localhost/api/auth/password/policy"),
44
+ );
45
+ await expect(second.json()).resolves.toEqual(policy);
46
+ });
47
+
48
+ test("会话有效期配置即时影响后续签发会话", async () => {
49
+ let ttlSeconds = 60;
50
+ const fixture = authFixture([activeUser()], undefined, () => ttlSeconds);
51
+ const first = await login(fixture.app, "admin");
52
+
53
+ ttlSeconds = 120;
54
+ const second = await login(fixture.app, "admin");
55
+
56
+ expect(
57
+ new Date(second.expiresAt).getTime() -
58
+ new Date(first.expiresAt).getTime(),
59
+ ).toBeGreaterThan(59_000);
60
+ });
61
+
18
62
  test("会话探测只返回当前 Actor,不暴露 Token", async () => {
19
63
  const fixture = authFixture([activeUser()]);
20
64
  const anonymous = await fixture.app.handle(
@@ -277,74 +321,3 @@ describe("platform auth API", () => {
277
321
  );
278
322
  });
279
323
  });
280
-
281
- function authFixture(users: AuthUserRecord[]) {
282
- const repository = new InMemoryAuthRepository(users);
283
- const resolver = new RepositoryActorResolver(repository, {
284
- sessionCookieName: "windy.sid",
285
- });
286
- const events: AuditEvent[] = [];
287
- const runtime = createServerRuntime({}, { actorResolver: resolver });
288
- const service = new AuthService(repository, {
289
- auditWriter: {
290
- write(event) {
291
- events.push(event);
292
- },
293
- },
294
- });
295
- const app = new Elysia({ prefix: "/api" }).derive(async ({ request }) => ({
296
- guardContext: await runtime.createGuardContext(request),
297
- }));
298
- registerAuthSessionRoute(app);
299
- registerAuthRoutes(app, service, { secureCookie: false });
300
- return { app, events, repository, runtime };
301
- }
302
-
303
- function activeUser(): AuthUserRecord {
304
- return {
305
- id: "user_admin",
306
- username: "admin",
307
- displayName: "平台管理员",
308
- email: "admin@windy.local",
309
- phone: "13800138000",
310
- passwordHash,
311
- credentialState: "active",
312
- status: "active",
313
- roleCodes: ["developer"],
314
- permissionKeys: ["system.user.manage", "system.audit.read"],
315
- };
316
- }
317
-
318
- async function login(
319
- app: { handle(request: Request): Response | Promise<Response> },
320
- username: string,
321
- ) {
322
- const response = await app.handle(
323
- jsonRequest("/api/auth/api/login", {
324
- username,
325
- password: "test-only-password",
326
- }),
327
- );
328
- expect(response.status).toBe(200);
329
- return (await response.json()) as { token: string; sessionId: string };
330
- }
331
-
332
- function readCookieValue(header: string, name: string): string {
333
- const value = header.split(";")[0]?.slice(`${name}=`.length);
334
- return value ? decodeURIComponent(value) : "";
335
- }
336
-
337
- function jsonRequest(path: string, body: unknown): Request {
338
- return new Request(`http://localhost${path}`, {
339
- method: "POST",
340
- headers: { "content-type": "application/json" },
341
- body: JSON.stringify(body),
342
- });
343
- }
344
-
345
- function apiRequest(path: string, token: string, method = "GET"): Request {
346
- return new Request(`http://localhost${path}`, {
347
- method,
348
- headers: { authorization: `Bearer ${token}` },
349
- });
350
- }
@@ -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
+ });