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
@@ -17,6 +17,7 @@ import { createWorkOrderRepository } from "./work-order/repository.js";
17
17
  import { createDrizzleWorkOrderRepository } from "./work-order/drizzle-repository.js";
18
18
  import { createWorkOrderRouteDefinitions } from "./work-order/routes.js";
19
19
  import { WorkOrderSearchProvider } from "./work-order/search-provider.js";
20
+ import { createWorkOrderOpenSearchIndex } from "./work-order/opensearch-index.js";
20
21
  import { WorkOrderService } from "./work-order/service.js";
21
22
 
22
23
  export interface WorkOrderModuleDependencies {
@@ -42,6 +43,7 @@ export interface WorkOrderModule {
42
43
  */
43
44
  export function createWorkOrderModule(): WorkOrderModule {
44
45
  let service: WorkOrderService | undefined;
46
+ const searchIndex = createWorkOrderOpenSearchIndex(process.env);
45
47
  const requireService = () => {
46
48
  if (!service) throw new Error("示例工单模块尚未挂载");
47
49
  return service;
@@ -52,7 +54,7 @@ export function createWorkOrderModule(): WorkOrderModule {
52
54
  requireService().operationalSnapshot(
53
55
  platformDataActor("task_work_order_snapshot", "工单运营快照任务"),
54
56
  ),
55
- searchProvider: new WorkOrderSearchProvider(requireService),
57
+ searchProvider: new WorkOrderSearchProvider(requireService, searchIndex),
56
58
  });
57
59
  return {
58
60
  ...serverModule,
@@ -67,6 +69,7 @@ export function createWorkOrderModule(): WorkOrderModule {
67
69
  ? createDrizzleWorkOrderRepository(dependencies.db)
68
70
  : createWorkOrderRepository()),
69
71
  dependencies.departments,
72
+ searchIndex,
70
73
  );
71
74
  },
72
75
  };
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ApiPermissionBinding,
3
+ AuditActor,
3
4
  AuditEvent,
4
5
  EntitlementKey,
5
6
  FeatureFlagDefinition,
@@ -33,6 +34,7 @@ export interface RequestGuardContext {
33
34
  actor: RequestActor;
34
35
  startedAt: string;
35
36
  clientIp?: string;
37
+ userAgent?: string;
36
38
  license?: {
37
39
  versionKey?: LicenseVersionKey;
38
40
  /** V1 兼容字段。新 License 不再包含 Entitlement。 */
@@ -61,6 +63,7 @@ export interface GuardDecision {
61
63
 
62
64
  export interface RequestGuardContextOptions {
63
65
  clientIp?: string;
66
+ userAgent?: string;
64
67
  resolveActor?: (request: Request) => RequestActor | undefined;
65
68
  resolveLicense?: (
66
69
  request: Request,
@@ -86,10 +89,24 @@ export function createRequestGuardContext(
86
89
  actor: options.resolveActor?.(request) || anonymousActor(),
87
90
  startedAt: new Date().toISOString(),
88
91
  clientIp: options.clientIp,
92
+ userAgent: options.userAgent,
89
93
  license: options.resolveLicense?.(request),
90
94
  };
91
95
  }
92
96
 
97
+ export function auditActor(
98
+ context: Pick<RequestGuardContext, "actor" | "clientIp" | "userAgent">,
99
+ ): AuditActor {
100
+ return {
101
+ id: context.actor.id,
102
+ type: context.actor.type === "anonymous" ? "user" : context.actor.type,
103
+ name: context.actor.name,
104
+ roleCodes: context.actor.roleCodes,
105
+ ip: context.clientIp,
106
+ userAgent: context.userAgent,
107
+ };
108
+ }
109
+
93
110
  export function evaluatePermissionGuard(
94
111
  context: RequestGuardContext,
95
112
  binding: PermissionGuardRequirement,
@@ -207,11 +224,7 @@ function deniedDecision(
207
224
  id: randomUUIDv7(),
208
225
  action: "security.denied",
209
226
  outcome: "denied",
210
- actor: {
211
- id: context.actor.id,
212
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
213
- name: context.actor.name,
214
- },
227
+ actor: auditActor(context),
215
228
  target: {
216
229
  type: "guard",
217
230
  id: targetId,
@@ -0,0 +1,85 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ requestSourcePolicyFromEnv,
4
+ resolveRequestSource,
5
+ } from "./request-source.js";
6
+
7
+ describe("平台请求来源信任边界", () => {
8
+ test("默认使用直接对端并忽略客户端伪造的转发头", () => {
9
+ const source = resolveRequestSource(
10
+ request("203.0.113.66"),
11
+ "198.51.100.10",
12
+ requestSourcePolicyFromEnv({}),
13
+ );
14
+
15
+ expect(source).toEqual({
16
+ clientIp: "198.51.100.10",
17
+ userAgent: "Windy-Test/1.0",
18
+ });
19
+ });
20
+
21
+ test("仅显式启用且直接对端受信时采用单一合法转发来源", () => {
22
+ const policy = requestSourcePolicyFromEnv({
23
+ WINDY_TRUST_PROXY: "true",
24
+ WINDY_TRUSTED_PROXY_IPS: "10.0.0.8",
25
+ });
26
+
27
+ expect(
28
+ resolveRequestSource(request("203.0.113.66"), "::1", policy).clientIp,
29
+ ).toBe("203.0.113.66");
30
+ expect(
31
+ resolveRequestSource(request("203.0.113.67"), "10.0.0.8", policy)
32
+ .clientIp,
33
+ ).toBe("203.0.113.67");
34
+ expect(
35
+ resolveRequestSource(request("203.0.113.68"), "10.0.0.9", policy)
36
+ .clientIp,
37
+ ).toBe("10.0.0.9");
38
+ });
39
+
40
+ test("多段或非法转发来源退回直接对端", () => {
41
+ const policy = requestSourcePolicyFromEnv({
42
+ WINDY_TRUST_PROXY: "true",
43
+ });
44
+ for (const forwardedFor of [
45
+ "203.0.113.66, 198.51.100.9",
46
+ "not-an-ip",
47
+ "203.0.113.66:443",
48
+ ]) {
49
+ expect(
50
+ resolveRequestSource(request(forwardedFor), "127.0.0.1", policy)
51
+ .clientIp,
52
+ ).toBe("127.0.0.1");
53
+ }
54
+ });
55
+
56
+ test("代理配置默认关闭且错误配置 fail closed", () => {
57
+ expect(requestSourcePolicyFromEnv({})).toEqual({
58
+ trustProxy: false,
59
+ trustedProxyIps: [],
60
+ });
61
+ expect(() =>
62
+ requestSourcePolicyFromEnv({
63
+ WINDY_TRUSTED_PROXY_IPS: "10.0.0.8",
64
+ }),
65
+ ).toThrow("WINDY_TRUST_PROXY=true");
66
+ expect(() =>
67
+ requestSourcePolicyFromEnv({ WINDY_TRUST_PROXY: "yes" }),
68
+ ).toThrow("只允许 true 或 false");
69
+ expect(() =>
70
+ requestSourcePolicyFromEnv({
71
+ WINDY_TRUST_PROXY: "true",
72
+ WINDY_TRUSTED_PROXY_IPS: "10.0.0.999",
73
+ }),
74
+ ).toThrow("无效 IP");
75
+ });
76
+ });
77
+
78
+ function request(forwardedFor: string): Request {
79
+ return new Request("https://windy.example.test/api/system/users", {
80
+ headers: {
81
+ "user-agent": "Windy-Test/1.0",
82
+ "x-forwarded-for": forwardedFor,
83
+ },
84
+ });
85
+ }
@@ -0,0 +1,90 @@
1
+ import { isIP } from "node:net";
2
+
3
+ export interface RequestSourcePolicy {
4
+ trustProxy: boolean;
5
+ trustedProxyIps: readonly string[];
6
+ }
7
+
8
+ export interface RequestSource {
9
+ clientIp?: string;
10
+ userAgent?: string;
11
+ }
12
+
13
+ export function requestSourcePolicyFromEnv(
14
+ env: Record<string, string | undefined>,
15
+ ): RequestSourcePolicy {
16
+ const trustProxy = readBoolean(env.WINDY_TRUST_PROXY);
17
+ const configuredIps = (env.WINDY_TRUSTED_PROXY_IPS || "")
18
+ .split(",")
19
+ .map((value) => value.trim())
20
+ .filter(Boolean);
21
+ if (!trustProxy && configuredIps.length) {
22
+ throw new Error(
23
+ "配置 WINDY_TRUSTED_PROXY_IPS 时必须显式设置 WINDY_TRUST_PROXY=true",
24
+ );
25
+ }
26
+ return {
27
+ trustProxy,
28
+ trustedProxyIps: configuredIps.map((value) => {
29
+ const normalized = normalizeIp(value);
30
+ if (!normalized) {
31
+ throw new Error(`WINDY_TRUSTED_PROXY_IPS 包含无效 IP:${value}`);
32
+ }
33
+ return normalized;
34
+ }),
35
+ };
36
+ }
37
+
38
+ export function resolveRequestSource(
39
+ request: Request,
40
+ peerIp: string | undefined,
41
+ policy: RequestSourcePolicy,
42
+ ): RequestSource {
43
+ const normalizedPeerIp = normalizeIp(peerIp);
44
+ const forwardedIp = resolveForwardedIp(request, normalizedPeerIp, policy);
45
+ return {
46
+ clientIp: forwardedIp || normalizedPeerIp,
47
+ userAgent: normalizeUserAgent(request.headers.get("user-agent")),
48
+ };
49
+ }
50
+
51
+ function resolveForwardedIp(
52
+ request: Request,
53
+ peerIp: string | undefined,
54
+ policy: RequestSourcePolicy,
55
+ ): string | undefined {
56
+ if (!policy.trustProxy || !peerIp || !isTrustedProxy(peerIp, policy)) {
57
+ return undefined;
58
+ }
59
+ const header = request.headers.get("x-forwarded-for")?.trim();
60
+ if (!header || header.includes(",")) return undefined;
61
+ return normalizeIp(header);
62
+ }
63
+
64
+ function isTrustedProxy(peerIp: string, policy: RequestSourcePolicy): boolean {
65
+ if (isLoopback(peerIp)) return true;
66
+ return policy.trustedProxyIps.some((value) => normalizeIp(value) === peerIp);
67
+ }
68
+
69
+ function isLoopback(ip: string): boolean {
70
+ return ip === "::1" || (isIP(ip) === 4 && ip.startsWith("127."));
71
+ }
72
+
73
+ function normalizeIp(value: string | undefined): string | undefined {
74
+ const candidate = value?.trim().toLowerCase();
75
+ if (!candidate) return undefined;
76
+ const mapped = candidate.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
77
+ const normalized = mapped && isIP(mapped) === 4 ? mapped : candidate;
78
+ return isIP(normalized) ? normalized : undefined;
79
+ }
80
+
81
+ function normalizeUserAgent(value: string | null): string | undefined {
82
+ const normalized = value?.trim();
83
+ return normalized ? normalized.slice(0, 1_024) : undefined;
84
+ }
85
+
86
+ function readBoolean(value: string | undefined): boolean {
87
+ if (value === undefined || value === "" || value === "false") return false;
88
+ if (value === "true") return true;
89
+ throw new Error("WINDY_TRUST_PROXY 只允许 true 或 false");
90
+ }
@@ -26,10 +26,7 @@ import { GovernedToolRegistry } from "./agent/tool-registry.js";
26
26
  import { PlatformMcpAdapter } from "./mcp/adapter.js";
27
27
  import { registerMcpRoutes } from "./mcp/routes.js";
28
28
  // @windy-module system.agent end
29
- import { registerAuthRoutes, registerAuthSessionRoute } from "./auth/routes.js";
30
29
  import { createAuthRuntime } from "./auth/runtime.js";
31
- import { createPlatformHttpApp } from "./http-app.js";
32
- import { bootstrapAdminPassword } from "./auth/bootstrap-admin.js";
33
30
  import { createServerPersistence } from "./persistence.js";
34
31
  // @windy-module system.license begin
35
32
  import { registerPlatformLicenseRoutes } from "./license/routes.js";
@@ -51,7 +48,6 @@ import { SchedulerWorker } from "./scheduler/worker.js";
51
48
  // @windy-module system.settings begin
52
49
  import { createPlatformSettingsRuntime } from "./settings/runtime.js";
53
50
  // @windy-module system.settings end
54
- import { registerProfileRoutes } from "./profile/routes.js";
55
51
  // @windy-module system.notification begin
56
52
  import { registerNotificationRoutes } from "./notification/routes.js";
57
53
  // @windy-module system.notification end
@@ -76,11 +72,13 @@ import { createSearchHost } from "./search/host.js";
76
72
  // @windy-module system.workflow begin
77
73
  import { registerWorkflowRoutes } from "./workflow/routes.js";
78
74
  // @windy-module system.workflow end
79
- import {
80
- createPlatformObservability,
81
- logPlatformStartup,
82
- } from "./observability/bootstrap.js";
83
75
  import { bootstrapServerModules } from "./module-bootstrap.js";
76
+ import {
77
+ createHttpObservability,
78
+ createPlatformServerApp,
79
+ listenPlatformServer,
80
+ logServerStartup,
81
+ } from "./platform-http-runtime.js";
84
82
 
85
83
  const logger = pino({ name: "windy-server" });
86
84
  const platformConfig = loadPlatformConfig();
@@ -128,20 +126,17 @@ const runtime = createServerRuntime(process.env, {
128
126
  auditWriter: persistence?.auditWriter,
129
127
  // @windy-module system.audit end
130
128
  });
131
- const observability = createPlatformObservability({
132
- databaseCheck: persistence
133
- ? async () => void (await persistence.pool.query("select 1"))
134
- : undefined,
135
- databaseRequired: process.env.NODE_ENV === "production",
136
- auditStatus: () => runtime.auditSink.status(),
137
- logger,
138
- });
129
+ const observability = createHttpObservability(runtime, persistence, logger);
139
130
  // @windy-module system.license begin
140
131
  platformLicenseRuntime.setAuditSink(runtime.auditSink);
141
132
  // @windy-module system.license end
142
133
  const { authRepository, authService } = createAuthRuntime({
143
134
  repository: persistence?.authRepository,
144
- sessionTtlSeconds: platformConfig.security.sessionTtlSeconds,
135
+ sessionTtlSeconds: () => platformConfig.security.sessionTtlSeconds,
136
+ passwordPolicy: () => ({
137
+ minLength: platformConfig.security.passwordMinLength,
138
+ maxLength: platformConfig.security.passwordMaxLength,
139
+ }),
145
140
  auditWriter: (event) => runtime.auditSink.recordCriticalEvent(event),
146
141
  // @windy-module system.audit begin
147
142
  auditReader: persistence?.auditReader,
@@ -153,12 +148,17 @@ const storageRuntime = createStorageRuntime(
153
148
  persistence?.db,
154
149
  moduleHosts.uploadPolicies,
155
150
  );
151
+ configurationRuntime.bindStorage(storageRuntime.uploads);
156
152
  // @windy-module system.storage end
157
153
  // @windy-module system.settings begin
158
- const platformSettings = createPlatformSettingsRuntime(persistence?.db, {
159
- softwareName: platformConfig.name,
160
- logoUrl: platformConfig.logoUrl,
161
- });
154
+ const platformSettings = createPlatformSettingsRuntime(
155
+ persistence?.db,
156
+ {
157
+ softwareName: platformConfig.name,
158
+ logoUrl: platformConfig.logoUrl,
159
+ },
160
+ () => ({ timeZone: platformConfig.regional.timeZone }),
161
+ );
162
162
  // @windy-module system.settings end
163
163
  // @windy-module system.search begin
164
164
  const searchHost = createSearchHost(
@@ -177,6 +177,7 @@ const {
177
177
  scheduler,
178
178
  taskDefinitions: scheduledTaskDefinitions,
179
179
  notifications: notificationRepository,
180
+ notificationService,
180
181
  workflow: workflowService,
181
182
  governedExportService,
182
183
  durable: durableJobRuntime,
@@ -188,6 +189,7 @@ const {
188
189
  storage: storageRuntime,
189
190
  moduleTasks: moduleTaskDefinitions,
190
191
  bulkRetentionDays: readPositiveInt(process.env.BULK_DATA_RETENTION_DAYS, 7),
192
+ mailConfig: platformConfig.mail,
191
193
  installModuleDurableHandlers: moduleHosts.installDurableHandlers,
192
194
  });
193
195
  const schedulerWorker = new SchedulerWorker(
@@ -197,20 +199,17 @@ const schedulerWorker = new SchedulerWorker(
197
199
  (result) => observability.metrics.observeSchedulerTick(result),
198
200
  );
199
201
  // @windy-module system.scheduler end
200
- await bootstrapAdminPassword({
202
+ const app = await createPlatformServerApp({
203
+ runtime,
204
+ persistence,
205
+ platformConfig,
206
+ authRepository,
201
207
  authService,
202
- authRepository: persistence?.authRepository,
203
208
  repositories: systemRepositories,
204
- password: process.env.WINDY_BOOTSTRAP_ADMIN_PASSWORD,
205
- });
206
- const app = createPlatformHttpApp(runtime, observability);
207
- observability.register(app);
208
- registerAuthSessionRoute(app);
209
- registerAuthRoutes(app, authService, {
210
- sessionCookieName: platformConfig.security.sessionCookieName,
211
- secureCookie: process.env.NODE_ENV === "production",
209
+ features,
210
+ logger,
211
+ observability,
212
212
  });
213
- registerProfileRoutes(app, authRepository, runtime, features);
214
213
 
215
214
  // @windy-module system.license begin
216
215
  registerPlatformLicenseRoutes(app, platformLicenseRuntime);
@@ -220,7 +219,13 @@ registerPlatformLicenseRoutes(app, platformLicenseRuntime);
220
219
  platformSettings.register(app, runtime, features);
221
220
  // @windy-module system.settings end
222
221
  // @windy-module system.notification begin
223
- registerNotificationRoutes(app, notificationRepository, runtime, features);
222
+ registerNotificationRoutes(
223
+ app,
224
+ notificationRepository,
225
+ runtime,
226
+ features,
227
+ notificationService,
228
+ );
224
229
  // @windy-module system.notification end
225
230
  // @windy-module system.workflow begin
226
231
  registerWorkflowRoutes(app, workflowService, runtime);
@@ -337,13 +342,8 @@ validateInstalledRuntime({
337
342
  hosts: moduleHosts,
338
343
  });
339
344
 
340
- const port = process.env.PORT ? Number(process.env.PORT) : 9_747;
341
- app.listen(port);
345
+ const port = listenPlatformServer(app);
342
346
  // @windy-module system.scheduler begin
343
347
  schedulerWorker.start();
344
348
  // @windy-module system.scheduler end
345
- logPlatformStartup(logger, {
346
- featureSync,
347
- persistenceEnabled: Boolean(persistence),
348
- port,
349
- });
349
+ logServerStartup(logger, featureSync, Boolean(persistence), port);
@@ -52,6 +52,8 @@ export function registerPlatformLicenseRoutes(
52
52
  const installed = await licenseRuntime.install(rawLicense, {
53
53
  actor: context.guardContext.actor,
54
54
  requestId: context.guardContext.requestId,
55
+ clientIp: context.guardContext.clientIp,
56
+ userAgent: context.guardContext.userAgent,
55
57
  });
56
58
  if (installed.result.status === "rejected") {
57
59
  return Response.json(
@@ -17,7 +17,11 @@ import type {
17
17
  LicenseFailureReason,
18
18
  SignedLicense,
19
19
  } from "@southwind-ai/shared";
20
- import type { RequestActor } from "../guards.js";
20
+ import {
21
+ auditActor,
22
+ type RequestActor,
23
+ type RequestGuardContext,
24
+ } from "../guards.js";
21
25
  import type { InMemoryAuditSink } from "../runtime.js";
22
26
  import type {
23
27
  LicenseEntitlementResolver,
@@ -57,10 +61,10 @@ export interface PlatformLicenseRuntimeOptions {
57
61
  now?: () => Date;
58
62
  }
59
63
 
60
- export interface LicenseInstallActorContext {
61
- actor: RequestActor;
62
- requestId: string;
63
- }
64
+ export type LicenseInstallActorContext = Pick<
65
+ RequestGuardContext,
66
+ "actor" | "requestId" | "clientIp" | "userAgent"
67
+ >;
64
68
 
65
69
  export class PlatformLicenseRuntime implements LicenseEntitlementResolver {
66
70
  private readonly stateStorage: LicenseStorage;
@@ -285,12 +289,7 @@ function installAuditEvent(
285
289
  id: crypto.randomUUID(),
286
290
  action: "license.update",
287
291
  outcome: success ? "success" : "failure",
288
- actor: {
289
- id: context.actor.id,
290
- type: "user",
291
- name: context.actor.name,
292
- roleCodes: context.actor.roleCodes,
293
- },
292
+ actor: auditActor(context),
294
293
  target: {
295
294
  type: "runtime-license",
296
295
  id: result.license?.claims.licenseId || candidate?.claims.licenseId,
@@ -1,6 +1,7 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { MCP_API_PATH } from "@southwind-ai/shared";
4
5
  import { createFoundationSnapshot, loadSystemModules } from "../foundation.js";
5
6
  import { createServerRuntime } from "../runtime.js";
6
7
  import { createSystemRepositories } from "../system/seed.js";
@@ -150,7 +151,7 @@ async function connectClient(
150
151
  token: string,
151
152
  ): Promise<Client> {
152
153
  const transport = new StreamableHTTPClientTransport(
153
- new URL("http://localhost/api/mcp"),
154
+ new URL(`http://localhost${MCP_API_PATH}`),
154
155
  {
155
156
  requestInit: {
156
157
  headers: { authorization: `Bearer ${token}` },
@@ -175,7 +176,7 @@ function mcpRequest(token?: string, origin?: string): Request {
175
176
  });
176
177
  if (token) headers.set("authorization", `Bearer ${token}`);
177
178
  if (origin) headers.set("origin", origin);
178
- return new Request("http://localhost/api/mcp", {
179
+ return new Request(`http://localhost${MCP_API_PATH}`, {
179
180
  method: "POST",
180
181
  headers,
181
182
  body: JSON.stringify({
@@ -10,7 +10,10 @@ import type {
10
10
  ModuleToolInput,
11
11
  ModuleToolResult,
12
12
  } from "@southwind-ai/server-sdk";
13
- import type { FeatureFlagDefinition } from "@southwind-ai/shared";
13
+ import {
14
+ MCP_SERVER_INFO,
15
+ type FeatureFlagDefinition,
16
+ } from "@southwind-ai/shared";
14
17
  import { evaluateAccessGuards } from "../access-guards.js";
15
18
  import type { RequestGuardContext } from "../guards.js";
16
19
  import { findFeature } from "../route-guards.js";
@@ -69,14 +72,11 @@ export class PlatformMcpAdapter {
69
72
  }
70
73
 
71
74
  private createServer(context: RequestGuardContext): Server {
72
- const server = new Server(
73
- { name: "windy-platform", version: "0.1.0" },
74
- {
75
- capabilities: { tools: {} },
76
- instructions:
77
- "仅调用当前账号可见的只读业务工具;权限和数据范围由 Windy 平台逐次校验。",
78
- },
79
- );
75
+ const server = new Server(MCP_SERVER_INFO, {
76
+ capabilities: { tools: {} },
77
+ instructions:
78
+ "仅调用当前账号可见的只读业务工具;权限和数据范围由 Windy 平台逐次校验。",
79
+ });
80
80
 
81
81
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
82
82
  tools: await this.allowedQueryTools(context),
@@ -1,3 +1,4 @@
1
+ import { MCP_ROUTE_PATH } from "@southwind-ai/shared";
1
2
  import type { RouteApp } from "../system/routes.js";
2
3
  import { PlatformMcpAdapter } from "./adapter.js";
3
4
 
@@ -15,7 +16,7 @@ export function registerMcpRoutes(
15
16
  const context = rawContext as McpRouteContext;
16
17
  return adapter.handle(context.request, context.guardContext, context.body);
17
18
  };
18
- app.get("/mcp", handle);
19
- app.post("/mcp", handle);
20
- app.delete("/mcp", handle);
19
+ app.get(MCP_ROUTE_PATH, handle);
20
+ app.post(MCP_ROUTE_PATH, handle);
21
+ app.delete(MCP_ROUTE_PATH, handle);
21
22
  }
@@ -17,7 +17,7 @@ import {
17
17
  } from "./module-host.js";
18
18
  import type { ServerPersistence } from "./persistence.js";
19
19
  // @windy-module system.storage begin
20
- import { platformUploadPolicies } from "./storage/runtime.js";
20
+ import { createPlatformUploadPolicies } from "./storage/runtime.js";
21
21
  // @windy-module system.storage end
22
22
  import {
23
23
  createSystemRepositories,
@@ -51,7 +51,7 @@ export async function bootstrapServerModules(
51
51
  installations: installedBusinessModules,
52
52
  baseUploadPolicies: [
53
53
  // @windy-module system.storage begin
54
- ...platformUploadPolicies,
54
+ ...createPlatformUploadPolicies(platformConfig),
55
55
  // @windy-module system.storage end
56
56
  ],
57
57
  });
@@ -15,7 +15,11 @@ import {
15
15
  } from "@southwind-ai/server-sdk";
16
16
  import type { AuditEvent } from "@southwind-ai/shared";
17
17
  import type { ModuleManifest } from "@southwind-ai/modules";
18
- import type { RequestActor, RequestGuardContext } from "../guards.js";
18
+ import {
19
+ auditActor,
20
+ type RequestActor,
21
+ type RequestGuardContext,
22
+ } from "../guards.js";
19
23
  import type { createServerRuntime } from "../runtime.js";
20
24
  import { unavailableModuleStoragePort } from "./storage-port.js";
21
25
 
@@ -130,11 +134,7 @@ function buildAuditEvent(
130
134
  id: randomUUIDv7(),
131
135
  action,
132
136
  outcome: "success",
133
- actor: {
134
- id: context.actor.id,
135
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
136
- name: context.actor.name,
137
- },
137
+ actor: auditActor(context),
138
138
  target: input.target,
139
139
  requestId: context.requestId,
140
140
  metadata: input.metadata,
@@ -6,7 +6,7 @@ import type {
6
6
  } from "@southwind-ai/server-sdk";
7
7
  import { evaluateAccessGuards } from "../access-guards.js";
8
8
  import { isServerScopeContext } from "../data-access/context.js";
9
- import type { RequestGuardContext } from "../guards.js";
9
+ import { auditActor, type RequestGuardContext } from "../guards.js";
10
10
  import type { createServerRuntime } from "../runtime.js";
11
11
  import { findFeature } from "../route-guards.js";
12
12
  import type { ModuleStorageScope } from "./storage-port.js";
@@ -73,14 +73,7 @@ function resourceFileEvent(
73
73
  id: randomUUIDv7(),
74
74
  action: policy.auditAction,
75
75
  outcome,
76
- actor: {
77
- id: input.guardContext.actor.id,
78
- type:
79
- input.guardContext.actor.type === "anonymous"
80
- ? ("user" as const)
81
- : input.guardContext.actor.type,
82
- name: input.guardContext.actor.name,
83
- },
76
+ actor: auditActor(input.guardContext),
84
77
  target: {
85
78
  type: request.resourceType,
86
79
  id: request.resourceId,
@@ -8,7 +8,7 @@ import {
8
8
  import type { AuditEvent, FeatureFlagDefinition } from "@southwind-ai/shared";
9
9
  import { DataScopedResourceService } from "../data-access/scoped-resource.js";
10
10
  import { departmentDescendantResolver } from "../data-access/department-tree.js";
11
- import type { RequestGuardContext } from "../guards.js";
11
+ import { auditActor, type RequestGuardContext } from "../guards.js";
12
12
  import { evaluateRouteGuards, findFeature } from "../route-guards.js";
13
13
  import type { createServerRuntime } from "../runtime.js";
14
14
  import type { SystemRepositories } from "../system/seed.js";
@@ -125,11 +125,7 @@ function queryAuditEvent(
125
125
  id: randomUUIDv7(),
126
126
  action: auditAction,
127
127
  outcome: "success",
128
- actor: {
129
- id: context.actor.id,
130
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
131
- name: context.actor.name,
132
- },
128
+ actor: auditActor(context),
133
129
  target: { type: "system.user-directory", id: "enabled-users" },
134
130
  requestId: context.requestId,
135
131
  metadata: {