create-windy 0.3.19 → 0.4.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 (213) hide show
  1. package/README.md +28 -13
  2. package/dist/cli.js +783 -280
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +18 -8
  7. package/template/README.md +32 -22
  8. package/template/apps/server/Dockerfile +1 -0
  9. package/template/apps/server/package.json +1 -0
  10. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  11. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  12. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  13. package/template/apps/server/src/agent/routes.test.ts +20 -7
  14. package/template/apps/server/src/agent/routes.ts +14 -0
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  16. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  17. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  18. package/template/apps/server/src/app/server-composition.ts +56 -0
  19. package/template/apps/server/src/application-services.ts +2 -0
  20. package/template/apps/server/src/bulk-data/service.ts +14 -0
  21. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  22. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  23. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  24. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  25. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  26. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  27. package/template/apps/server/src/feature/routes.ts +33 -15
  28. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  29. package/template/apps/server/src/foundation.ts +64 -5
  30. package/template/apps/server/src/guards.test.ts +23 -0
  31. package/template/apps/server/src/guards.ts +12 -0
  32. package/template/apps/server/src/index.ts +28 -34
  33. package/template/apps/server/src/installed-business-modules.ts +13 -24
  34. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  35. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  36. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  37. package/template/apps/server/src/license/activation.test.ts +30 -0
  38. package/template/apps/server/src/license/activation.ts +26 -0
  39. package/template/apps/server/src/license/restriction.test.ts +16 -0
  40. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  41. package/template/apps/server/src/license/routes.ts +20 -4
  42. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  43. package/template/apps/server/src/module-bootstrap.ts +14 -23
  44. package/template/apps/server/src/module-composition.test.ts +43 -2
  45. package/template/apps/server/src/module-composition.ts +29 -0
  46. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  47. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  48. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  49. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  50. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  51. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  52. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  53. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  54. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  55. package/template/apps/server/src/module-host/initialize.ts +15 -4
  56. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  57. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  58. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  59. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  60. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  61. package/template/apps/server/src/module-host/request-context.ts +3 -0
  62. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  63. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  64. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  65. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  66. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  67. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  68. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  69. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  70. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  71. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  72. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  73. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  74. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  75. package/template/apps/server/src/module-host.test.ts +2 -0
  76. package/template/apps/server/src/module-preflight.test.ts +289 -0
  77. package/template/apps/server/src/module-preflight.ts +96 -0
  78. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  79. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  80. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  81. package/template/apps/server/src/route-guards.test.ts +22 -0
  82. package/template/apps/server/src/route-guards.ts +10 -3
  83. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  84. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  85. package/template/apps/server/src/runtime-feature.ts +5 -0
  86. package/template/apps/server/src/runtime.ts +10 -49
  87. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  88. package/template/apps/server/src/scheduler/audit.ts +1 -1
  89. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  90. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  91. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  92. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  93. package/template/apps/server/src/scheduler/service.ts +18 -21
  94. package/template/apps/server/src/scheduler/types.ts +5 -1
  95. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  96. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  97. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  98. package/template/apps/server/src/startup-preflight.ts +37 -0
  99. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  100. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  101. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  102. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  103. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  104. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  105. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  106. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  107. package/template/apps/server/src/system/routes.test.ts +21 -11
  108. package/template/apps/server/src/system/routes.ts +11 -2
  109. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  110. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  111. package/template/apps/server/src/work-order/task.test.ts +6 -1
  112. package/template/apps/web/Dockerfile +5 -0
  113. package/template/apps/web/package.json +1 -0
  114. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  115. package/template/apps/web/src/app/web-composition.ts +24 -0
  116. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  117. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  118. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  119. package/template/apps/web/src/layout/navigation.ts +29 -4
  120. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  121. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  122. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  123. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  124. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  125. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  126. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  127. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  128. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  129. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  130. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  131. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  132. package/template/apps/web/src/router/business-routes.ts +32 -45
  133. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  134. package/template/apps/web/src/router/index.ts +64 -54
  135. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  136. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  137. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  138. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  139. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  140. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  141. package/template/apps/web/src/services/work-order-api.ts +16 -0
  142. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  143. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  144. package/template/apps/web/vite.config.ts +22 -1
  145. package/template/docs/architecture/migration-bundle.md +10 -7
  146. package/template/docs/architecture/module-manifest-composition.md +85 -3
  147. package/template/docs/architecture/search-provider-protocol.md +6 -1
  148. package/template/docs/architecture/server-module-interface.md +70 -7
  149. package/template/docs/development/business-web-routes.md +53 -50
  150. package/template/docs/development/module-lifecycle.md +48 -54
  151. package/template/docs/platform/navigation-registry.md +5 -4
  152. package/template/package.json +1 -1
  153. package/template/packages/config/package.json +1 -1
  154. package/template/packages/database/package.json +1 -1
  155. package/template/packages/example-work-order/package.json +6 -0
  156. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  157. package/template/packages/example-work-order/src/admin.ts +35 -0
  158. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  159. package/template/packages/example-work-order/src/manifest.ts +54 -7
  160. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  161. package/template/packages/example-work-order/src/server-module.ts +2 -2
  162. package/template/packages/jobs/package.json +1 -1
  163. package/template/packages/modules/index.ts +4 -0
  164. package/template/packages/modules/package.json +3 -2
  165. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  166. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  167. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  168. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  169. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  170. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  171. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  172. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  173. package/template/packages/modules/src/composition.test.ts +74 -9
  174. package/template/packages/modules/src/composition.ts +8 -0
  175. package/template/packages/modules/src/manifest.ts +1 -0
  176. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  177. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  178. package/template/packages/modules/src/module-host-range.ts +125 -0
  179. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  180. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  181. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  182. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  183. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  184. package/template/packages/modules/src/system-api-binding.ts +43 -9
  185. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  186. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  187. package/template/packages/modules/src/system-api-operational.ts +57 -8
  188. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  189. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  190. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  191. package/template/packages/modules/src/system-data-governance.ts +5 -2
  192. package/template/packages/modules/src/system-features.ts +31 -0
  193. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  194. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  195. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  196. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  197. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  198. package/template/packages/modules/src/system-modules.test.ts +10 -2
  199. package/template/packages/modules/src/system-modules.ts +6 -5
  200. package/template/packages/modules/src/system-permissions.ts +4 -0
  201. package/template/packages/modules/user/module.ts +19 -2
  202. package/template/packages/server-sdk/index.ts +8 -0
  203. package/template/packages/server-sdk/package.json +2 -1
  204. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  205. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  206. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  207. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  208. package/template/packages/shared/package.json +1 -1
  209. package/template/packages/shared/src/audit.test.ts +7 -0
  210. package/template/packages/shared/src/audit.ts +16 -0
  211. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  212. package/template/packages/shared/src/rbac.ts +2 -0
  213. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -0,0 +1,96 @@
1
+ import {
2
+ validateModuleComposition,
3
+ type ModuleManifest,
4
+ } from "@southwind-ai/modules";
5
+ import type {
6
+ InstallableServerModule,
7
+ ModuleUploadPolicy,
8
+ } from "@southwind-ai/server-sdk";
9
+ import { loadFoundationModules } from "./foundation.js";
10
+ import { serverCompositionDeclaration } from "./app/server-composition.js";
11
+ import { initializeModuleHosts } from "./module-host.js";
12
+ import { validateHostedModuleImplementations } from "./module-host/implementation-completeness.js";
13
+ import { validateProjectServerAttachments } from "./module-host/project-attachments.js";
14
+ import {
15
+ runtimeBusinessModules,
16
+ serverComposition,
17
+ } from "./module-host/runtime-composition.js";
18
+ import type {
19
+ WindyServerModuleAdapter,
20
+ WindyServerModuleAttachment,
21
+ } from "@southwind-ai/server-sdk";
22
+
23
+ export async function prepareServerModules(input: {
24
+ readonly modules: readonly ModuleManifest[];
25
+ readonly installations: readonly InstallableServerModule<ModuleManifest>[];
26
+ readonly baseUploadPolicies?: readonly ModuleUploadPolicy[];
27
+ readonly adapters?: readonly WindyServerModuleAdapter<ModuleManifest>[];
28
+ readonly attachments?: readonly WindyServerModuleAttachment<unknown>[];
29
+ readonly enabledModuleNames?: ReadonlySet<string>;
30
+ }) {
31
+ const foundationModules = [...input.modules];
32
+ const canonicalInstallations = input.installations.map((installation) => ({
33
+ source: installation,
34
+ manifest: installation.manifest,
35
+ fingerprint: JSON.stringify(installation.manifest),
36
+ installation: {
37
+ manifest: installation.manifest,
38
+ setup: installation.setup,
39
+ setupSandbox: installation.setupSandbox,
40
+ } satisfies InstallableServerModule<ModuleManifest>,
41
+ }));
42
+ validateModuleComposition(foundationModules);
43
+ validateProjectServerAttachments({
44
+ attachments: input.attachments ?? [],
45
+ adapters: input.adapters ?? [],
46
+ enabledModuleNames: input.enabledModuleNames ?? new Set(),
47
+ });
48
+ const moduleHosts = await initializeModuleHosts({
49
+ modules: foundationModules,
50
+ installations: canonicalInstallations.map(
51
+ ({ installation }) => installation,
52
+ ),
53
+ baseUploadPolicies: input.baseUploadPolicies,
54
+ });
55
+ assertCanonicalManifestIntegrity(canonicalInstallations);
56
+ validateModuleComposition(foundationModules);
57
+ validateHostedModuleImplementations(
58
+ canonicalInstallations.map(({ installation }) => installation),
59
+ moduleHosts,
60
+ );
61
+ return { foundationModules, moduleHosts };
62
+ }
63
+
64
+ function assertCanonicalManifestIntegrity(
65
+ installations: readonly {
66
+ readonly source: InstallableServerModule<ModuleManifest>;
67
+ readonly manifest: ModuleManifest;
68
+ readonly fingerprint: string;
69
+ }[],
70
+ ): void {
71
+ for (const installation of installations) {
72
+ if (installation.source.manifest !== installation.manifest) {
73
+ throw new Error(
74
+ `Server Module 在 setup 期间替换了 canonical Manifest:${installation.manifest.name}`,
75
+ );
76
+ }
77
+ if (JSON.stringify(installation.manifest) !== installation.fingerprint) {
78
+ throw new Error(
79
+ `Server Module 在 setup 期间修改了 canonical Manifest:${installation.manifest.name}`,
80
+ );
81
+ }
82
+ }
83
+ }
84
+
85
+ export function prepareInstalledServerModules(
86
+ baseUploadPolicies: readonly ModuleUploadPolicy[],
87
+ ) {
88
+ return prepareServerModules({
89
+ modules: loadFoundationModules(),
90
+ installations: runtimeBusinessModules,
91
+ baseUploadPolicies,
92
+ adapters: serverCompositionDeclaration.adapters,
93
+ attachments: serverCompositionDeclaration.attachments,
94
+ enabledModuleNames: serverComposition.enabledModuleNames,
95
+ });
96
+ }
@@ -16,6 +16,7 @@ export function validateInstalledRuntime(input: {
16
16
  validateServerModuleComposition({
17
17
  modules: input.modules,
18
18
  routes: input.routes,
19
+ frameworkRoutes: ["GET /api/swagger", "GET /api/swagger/json"],
19
20
  tasks: input.tasks,
20
21
  tools: input.tools,
21
22
  aiOperations,
@@ -0,0 +1,50 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
3
+ import type { AuthRepository } from "./auth/repository.js";
4
+ import type { AuthService } from "./auth/service.js";
5
+ // @windy-module system.license begin
6
+ import type { PlatformLicenseRuntime } from "./license/runtime-service.js";
7
+ import { registerPlatformLicenseRoutes } from "./license/routes.js";
8
+ // @windy-module system.license end
9
+ import {
10
+ createManifestAuthorizedRouteApp,
11
+ createStaticRouteAuthorizer,
12
+ } from "./module-host/static-route-authorization.js";
13
+ import type { createServerPersistence } from "./persistence.js";
14
+ import {
15
+ createPlatformServerApp,
16
+ type PlatformServerAppInput,
17
+ } from "./platform-http-runtime.js";
18
+ import type { createServerRuntime } from "./runtime.js";
19
+ import { registerSessionContextRoute } from "./session/routes.js";
20
+
21
+ interface PlatformAppBootstrapInput extends Omit<
22
+ PlatformServerAppInput,
23
+ "runtime" | "persistence" | "features"
24
+ > {
25
+ runtime: ReturnType<typeof createServerRuntime>;
26
+ persistence: ReturnType<typeof createServerPersistence>;
27
+ authRepository: AuthRepository;
28
+ authService: AuthService;
29
+ features: FeatureFlagDefinition[];
30
+ modules: readonly ModuleManifest[];
31
+ // @windy-module system.license begin
32
+ licenseRuntime: PlatformLicenseRuntime;
33
+ // @windy-module system.license end
34
+ }
35
+
36
+ export async function bootstrapPlatformApp(input: PlatformAppBootstrapInput) {
37
+ const app = await createPlatformServerApp(input);
38
+ const authorizer = createStaticRouteAuthorizer(input.runtime, input.modules);
39
+ // @windy-module system.license begin
40
+ registerPlatformLicenseRoutes(app, input.licenseRuntime, authorizer);
41
+ // @windy-module system.license end
42
+ registerSessionContextRoute(
43
+ createManifestAuthorizedRouteApp(app, authorizer, () => ({
44
+ immutableCoreAccess: true,
45
+ })),
46
+ input.runtime,
47
+ input.features,
48
+ );
49
+ return app;
50
+ }
@@ -15,8 +15,13 @@ import { registerProfileRoutes } from "./profile/routes.js";
15
15
  import type { createServerRuntime } from "./runtime.js";
16
16
  import type { SystemRepositories } from "./system/seed.js";
17
17
  import type { FeatureManifestSyncResult } from "./feature/manifest-feature-repository.js";
18
+ import type { ModuleManifest } from "@southwind-ai/modules";
19
+ import {
20
+ createManifestAuthorizedRouteApp,
21
+ createStaticRouteAuthorizer,
22
+ } from "./module-host/static-route-authorization.js";
18
23
 
19
- interface CreatePlatformServerAppInput {
24
+ export interface PlatformServerAppInput {
20
25
  runtime: ReturnType<typeof createServerRuntime>;
21
26
  persistence: ReturnType<typeof createServerPersistence>;
22
27
  platformConfig: PlatformConfig;
@@ -26,6 +31,7 @@ interface CreatePlatformServerAppInput {
26
31
  features: FeatureFlagDefinition[];
27
32
  logger: StructuredRequestLogger;
28
33
  observability: ReturnType<typeof createPlatformObservability>;
34
+ modules: readonly ModuleManifest[];
29
35
  }
30
36
 
31
37
  export function createHttpObservability(
@@ -44,9 +50,7 @@ export function createHttpObservability(
44
50
  });
45
51
  }
46
52
 
47
- export async function createPlatformServerApp(
48
- input: CreatePlatformServerAppInput,
49
- ) {
53
+ export async function createPlatformServerApp(input: PlatformServerAppInput) {
50
54
  await bootstrapAdminPassword({
51
55
  authService: input.authService,
52
56
  authRepository: input.persistence?.authRepository,
@@ -54,9 +58,22 @@ export async function createPlatformServerApp(
54
58
  password: process.env.WINDY_BOOTSTRAP_ADMIN_PASSWORD,
55
59
  });
56
60
  const app = createPlatformHttpApp(input.runtime, input.observability);
57
- input.observability.register(app);
58
- registerAuthSessionRoute(app);
59
- registerAuthRoutes(app, input.authService, {
61
+ const authorizer = createStaticRouteAuthorizer(input.runtime, input.modules);
62
+ const guardedApp = createManifestAuthorizedRouteApp(
63
+ app,
64
+ authorizer,
65
+ (method, path) =>
66
+ immutableCoreRoutes.has(`${method} /api${path}`)
67
+ ? { immutableCoreAccess: true }
68
+ : undefined,
69
+ );
70
+ input.observability.register(
71
+ createManifestAuthorizedRouteApp(app, authorizer, () => ({
72
+ immutableCoreAccess: true,
73
+ })),
74
+ );
75
+ registerAuthSessionRoute(guardedApp);
76
+ registerAuthRoutes(guardedApp, input.authService, {
60
77
  sessionCookieName: input.platformConfig.security.sessionCookieName,
61
78
  secureCookie: process.env.NODE_ENV === "production",
62
79
  });
@@ -69,6 +86,14 @@ export async function createPlatformServerApp(
69
86
  return app;
70
87
  }
71
88
 
89
+ const immutableCoreRoutes = new Set([
90
+ "GET /api/auth/session",
91
+ "POST /api/auth/login",
92
+ "POST /api/auth/api/login",
93
+ "GET /api/auth/password/policy",
94
+ "POST /api/auth/logout",
95
+ ]);
96
+
72
97
  export function listenPlatformServer(
73
98
  app: ReturnType<typeof createPlatformHttpApp>,
74
99
  ): number {
@@ -5,6 +5,10 @@ import {
5
5
  licenseVersionKey,
6
6
  } from "@southwind-ai/shared";
7
7
  import { evaluateAccessGuards } from "./access-guards.js";
8
+ import {
9
+ systemFeatureKeyForPermission,
10
+ systemFeatureKeyForResource,
11
+ } from "./route-guards.js";
8
12
  import { createServerRuntime } from "./runtime.js";
9
13
 
10
14
  const commercialFeature = {
@@ -18,6 +22,24 @@ const commercialFeature = {
18
22
  };
19
23
 
20
24
  describe("access guard pipeline", () => {
25
+ test("Permission 默认映射到同基础 Key Feature", () => {
26
+ expect(systemFeatureKeyForPermission("system.user.read")).toBe(
27
+ "system.user",
28
+ );
29
+ expect(systemFeatureKeyForPermission("business.report.manage")).toBe(
30
+ "business.report",
31
+ );
32
+ });
33
+
34
+ test("系统资源 Route 使用资源级 Feature", () => {
35
+ expect(systemFeatureKeyForResource("user")).toBe("system.user");
36
+ expect(systemFeatureKeyForResource("role")).toBe("system.role");
37
+ expect(systemFeatureKeyForResource("department")).toBe("system.department");
38
+ expect(systemFeatureKeyForResource("menu")).toBe("system.menu");
39
+ expect(systemFeatureKeyForResource("dict")).toBe("system.dict");
40
+ expect(systemFeatureKeyForResource("ops")).toBe("system.ops");
41
+ });
42
+
21
43
  test.each([
22
44
  {
23
45
  name: "缺少 Entitlement 优先拒绝",
@@ -40,15 +40,22 @@ export function findFeature(
40
40
 
41
41
  export function systemFeatureKeyForResource(resourceName: string): string {
42
42
  const resourceFeatures: Record<string, string> = {
43
+ admin: "system.admin",
43
44
  agent: "system.agent",
44
45
  audit: "system.audit",
46
+ department: "system.department",
47
+ dict: "system.dict",
48
+ feature: "system.feature",
45
49
  license: "system.license",
50
+ menu: "system.menu",
46
51
  ops: "system.ops",
47
52
  notification: "system.notification",
53
+ role: "system.role",
48
54
  scheduler: "system.scheduler",
49
55
  settings: "system.settings",
56
+ user: "system.user",
50
57
  };
51
- return resourceFeatures[resourceName] || "system.rbac";
58
+ return resourceFeatures[resourceName] || `system.${resourceName}`;
52
59
  }
53
60
 
54
61
  export function findSystemFeature(
@@ -59,6 +66,6 @@ export function findSystemFeature(
59
66
  }
60
67
 
61
68
  export function systemFeatureKeyForPermission(permissionKey: string): string {
62
- const resource = permissionKey.split(".").at(1) || "";
63
- return systemFeatureKeyForResource(resource);
69
+ const separator = permissionKey.lastIndexOf(".");
70
+ return separator > 0 ? permissionKey.slice(0, separator) : "";
64
71
  }
@@ -0,0 +1,50 @@
1
+ import type { AuditEvent } from "@southwind-ai/shared";
2
+
3
+ export interface AuditEventListQuery {
4
+ page: number;
5
+ pageSize: number;
6
+ q?: string;
7
+ actorId?: string;
8
+ action?: string;
9
+ resource?: string;
10
+ resourceId?: string;
11
+ occurredAfter?: string;
12
+ occurredBefore?: string;
13
+ includeScheduler?: boolean;
14
+ }
15
+
16
+ export function matchesAuditQuery(
17
+ event: AuditEvent,
18
+ query: AuditEventListQuery,
19
+ ): boolean {
20
+ return (
21
+ (!query.q || matchesAuditKeyword(event, query.q)) &&
22
+ (!query.actorId || event.actor.id === query.actorId) &&
23
+ (!query.action || event.action === query.action) &&
24
+ (!query.resource || event.target?.type === query.resource) &&
25
+ (!query.resourceId || event.target?.id === query.resourceId) &&
26
+ (query.includeScheduler || event.actor.id !== "system_scheduler") &&
27
+ (!query.occurredAfter || event.occurredAt >= query.occurredAfter) &&
28
+ (!query.occurredBefore || event.occurredAt <= query.occurredBefore)
29
+ );
30
+ }
31
+
32
+ function matchesAuditKeyword(event: AuditEvent, query: string): boolean {
33
+ const keyword = query.toLowerCase();
34
+ const values = [
35
+ event.action,
36
+ event.actor.id,
37
+ event.actor.name,
38
+ event.target?.type,
39
+ event.target?.id,
40
+ event.target?.label,
41
+ event.actor.ip,
42
+ event.message,
43
+ event.module,
44
+ event.requestId,
45
+ event.metadata ? JSON.stringify(event.metadata) : undefined,
46
+ ];
47
+ return values.some((value) =>
48
+ value === undefined ? false : String(value).toLowerCase().includes(keyword),
49
+ );
50
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { featureKey, type FeatureFlagDefinition } from "@southwind-ai/shared";
3
+ import { createRequestGuardContext } from "./guards.js";
4
+ import { RuntimeFeatureCatalog } from "./runtime-feature.js";
5
+
6
+ describe("运行时 Feature 的 Module 激活上限", () => {
7
+ test("数据库状态不能重新启用装配清单已停用的 Module", async () => {
8
+ const manifest = feature("system.ops", "system.operations");
9
+ const catalog = new RuntimeFeatureCatalog(
10
+ [manifest],
11
+ {
12
+ resolve: () => ({
13
+ key: manifest.key,
14
+ enabled: true,
15
+ visible: "visible",
16
+ }),
17
+ },
18
+ true,
19
+ new Set(["system.operations"]),
20
+ );
21
+
22
+ await expect(catalog.resolve(context(), manifest)).resolves.toMatchObject({
23
+ enabled: false,
24
+ visible: "hidden",
25
+ disabledReason: "disabled",
26
+ });
27
+ });
28
+ });
29
+
30
+ function feature(key: string, module: string): FeatureFlagDefinition {
31
+ return {
32
+ key: featureKey(key),
33
+ module,
34
+ label: key,
35
+ enabled: true,
36
+ visible: "visible",
37
+ stage: "stable",
38
+ };
39
+ }
40
+
41
+ function context() {
42
+ return createRequestGuardContext(new Request("http://localhost/api/test"), {
43
+ resolveActor: () => undefined,
44
+ resolveLicense: () => undefined,
45
+ });
46
+ }
@@ -38,6 +38,7 @@ export class RuntimeFeatureCatalog {
38
38
  definitions: readonly FeatureFlagDefinition[],
39
39
  private readonly resolver: FeatureFlagRuntimeResolver | undefined,
40
40
  private readonly production: boolean,
41
+ private readonly disabledModuleNames: ReadonlySet<string> = new Set(),
41
42
  ) {
42
43
  for (const definition of definitions) {
43
44
  this.definitions.set(definition.key, definition);
@@ -107,6 +108,9 @@ export class RuntimeFeatureCatalog {
107
108
  private async resolveOwnState(
108
109
  manifestFeature: FeatureFlagDefinition,
109
110
  ): Promise<FeatureFlagDefinition> {
111
+ if (this.disabledModuleNames.has(manifestFeature.module)) {
112
+ return disabledFeature(manifestFeature);
113
+ }
110
114
  if (!this.resolver) {
111
115
  return this.production
112
116
  ? disabledFeature(manifestFeature)
@@ -144,6 +148,7 @@ function isFeatureLicensedForContext(
144
148
  feature: FeatureFlagDefinition,
145
149
  context: RequestGuardContext,
146
150
  ): boolean {
151
+ if (context.licenseEnforced === false) return true;
147
152
  return (
148
153
  isLicenseVersionAllowed(feature, context.license?.versionKey) &&
149
154
  (!feature.requiredEntitlement ||
@@ -32,6 +32,10 @@ import {
32
32
  resolveRequestSource,
33
33
  } from "./http/request-source.js";
34
34
  import { resolveRequestActor } from "./runtime-request-actor.js";
35
+ import {
36
+ matchesAuditQuery,
37
+ type AuditEventListQuery,
38
+ } from "./runtime-audit-query.js";
35
39
  // @windy-module system.agent begin
36
40
  import {
37
41
  createAdkDebugRuntime,
@@ -44,6 +48,7 @@ export type {
44
48
  FeatureFlagRuntimeState,
45
49
  ResolvedFeatureFlag,
46
50
  } from "./runtime-feature.js";
51
+ export type { AuditEventListQuery } from "./runtime-audit-query.js";
47
52
 
48
53
  export interface SessionRecord {
49
54
  token: string;
@@ -61,19 +66,6 @@ export interface AuditEventWriter {
61
66
  status?(): AuditWriteStatus;
62
67
  }
63
68
 
64
- export interface AuditEventListQuery {
65
- page: number;
66
- pageSize: number;
67
- q?: string;
68
- actorId?: string;
69
- action?: string;
70
- resource?: string;
71
- resourceId?: string;
72
- occurredAfter?: string;
73
- occurredBefore?: string;
74
- includeScheduler?: boolean;
75
- }
76
-
77
69
  export interface AuditEventReader {
78
70
  list(query: AuditEventListQuery): Promise<PageResult<AuditEvent>>;
79
71
  }
@@ -81,8 +73,10 @@ export interface AuditEventReader {
81
73
  export interface ServerRuntimeOptions {
82
74
  actorResolver?: RequestActorResolver;
83
75
  licenseResolver?: LicenseEntitlementResolver;
76
+ licenseEnforced?: boolean;
84
77
  featureResolver?: FeatureFlagRuntimeResolver;
85
78
  featureCatalog?: readonly FeatureFlagDefinition[];
79
+ disabledModuleNames?: ReadonlySet<string>;
86
80
  auditWriter?: AuditEventWriter;
87
81
  auditReader?: AuditEventReader;
88
82
  // @windy-module system.agent begin
@@ -225,6 +219,7 @@ export function createServerRuntime(
225
219
  options.featureCatalog || [],
226
220
  options.featureResolver,
227
221
  env.NODE_ENV === "production",
222
+ options.disabledModuleNames ?? new Set(),
228
223
  );
229
224
  const requestSourcePolicy = requestSourcePolicyFromEnv(env);
230
225
 
@@ -254,6 +249,7 @@ export function createServerRuntime(
254
249
  ...requestSource,
255
250
  resolveActor: () => actor,
256
251
  resolveLicense: () => license,
252
+ licenseEnforced: options.licenseEnforced,
257
253
  });
258
254
  },
259
255
  // @windy-module system.agent begin
@@ -276,6 +272,7 @@ export function createServerRuntime(
276
272
  ...requestSource,
277
273
  resolveActor: () => actor,
278
274
  resolveLicense: () => license,
275
+ licenseEnforced: options.licenseEnforced,
279
276
  });
280
277
  },
281
278
  async resolveFeature(
@@ -311,39 +308,3 @@ export function createServerRuntime(
311
308
  },
312
309
  };
313
310
  }
314
-
315
- function matchesAuditQuery(
316
- event: AuditEvent,
317
- query: AuditEventListQuery,
318
- ): boolean {
319
- return (
320
- (!query.q || matchesAuditKeyword(event, query.q)) &&
321
- (!query.actorId || event.actor.id === query.actorId) &&
322
- (!query.action || event.action === query.action) &&
323
- (!query.resource || event.target?.type === query.resource) &&
324
- (!query.resourceId || event.target?.id === query.resourceId) &&
325
- (query.includeScheduler || event.actor.id !== "system_scheduler") &&
326
- (!query.occurredAfter || event.occurredAt >= query.occurredAfter) &&
327
- (!query.occurredBefore || event.occurredAt <= query.occurredBefore)
328
- );
329
- }
330
-
331
- function matchesAuditKeyword(event: AuditEvent, query: string): boolean {
332
- const keyword = query.toLowerCase();
333
- const values = [
334
- event.action,
335
- event.actor.id,
336
- event.actor.name,
337
- event.target?.type,
338
- event.target?.id,
339
- event.target?.label,
340
- event.actor.ip,
341
- event.message,
342
- event.module,
343
- event.requestId,
344
- event.metadata ? JSON.stringify(event.metadata) : undefined,
345
- ];
346
- return values.some((value) =>
347
- value === undefined ? false : String(value).toLowerCase().includes(keyword),
348
- );
349
- }
@@ -0,0 +1,49 @@
1
+ import { expect, test } from "bun:test";
2
+ import { createFoundationSnapshot } from "../foundation.js";
3
+ import { createServerRuntime } from "../runtime.js";
4
+ import { InMemorySchedulerRepository } from "./repository.js";
5
+ import { SchedulerService } from "./service.js";
6
+
7
+ test("任务执行审计使用 Manifest 投影的动作", async () => {
8
+ const feature = createFoundationSnapshot().features.find(
9
+ ({ key }) => key === "system.workflow",
10
+ );
11
+ if (!feature) throw new Error("测试缺少 system.workflow Feature");
12
+ const runtime = createServerRuntime({
13
+ WINDY_DEV_ADMIN_TOKEN: "token",
14
+ });
15
+ const scheduler = new SchedulerService(
16
+ new InMemorySchedulerRepository(),
17
+ [
18
+ {
19
+ key: "system.workflow.timeout-test",
20
+ label: "审批超时测试",
21
+ intervalSeconds: 60,
22
+ maxAttempts: 3,
23
+ retryDelaySeconds: 10,
24
+ permissionKey: "system.workflow.manage",
25
+ auditAction: "workflow.timeout",
26
+ feature,
27
+ handler: () => undefined,
28
+ },
29
+ ],
30
+ runtime,
31
+ );
32
+ await scheduler.initialize();
33
+ const context = await runtime.createGuardContext(
34
+ new Request(
35
+ "http://localhost/api/system/scheduled-tasks/system.workflow.timeout-test/trigger",
36
+ { headers: { authorization: "Bearer token" } },
37
+ ),
38
+ );
39
+
40
+ expect(
41
+ await scheduler.triggerManual("system.workflow.timeout-test", context),
42
+ ).toMatchObject({ ok: true });
43
+ expect(runtime.auditSink.list()).toContainEqual(
44
+ expect.objectContaining({
45
+ action: "workflow.timeout",
46
+ outcome: "success",
47
+ }),
48
+ );
49
+ });
@@ -6,7 +6,7 @@ import type { ScheduledTaskRun } from "./types.js";
6
6
  export function schedulerAuditEvent(
7
7
  context: RequestGuardContext,
8
8
  run: ScheduledTaskRun,
9
- action: "task.trigger" | "task.execute",
9
+ action: AuditEvent["action"],
10
10
  outcome: AuditEvent["outcome"],
11
11
  occurredAt: Date,
12
12
  ): AuditEvent {
@@ -38,7 +38,7 @@ export class SchedulerExecutionRecorder {
38
38
  await this.repository.discardRun(run.id);
39
39
  return completed;
40
40
  }
41
- this.record(context, completed, "task.execute", "success");
41
+ this.record(context, completed, definition.auditAction, "success");
42
42
  return completed;
43
43
  }
44
44
 
@@ -59,14 +59,19 @@ export class SchedulerExecutionRecorder {
59
59
  : undefined,
60
60
  error: sanitizeExecutionError(error),
61
61
  });
62
- this.record(context, completed, "task.execute", "failure");
62
+ this.record(context, completed, definition.auditAction, "failure");
63
63
  return completed;
64
64
  }
65
65
 
66
- denied(context: RequestGuardContext, run: ScheduledTaskRun, reason: string) {
66
+ denied(
67
+ context: RequestGuardContext,
68
+ run: ScheduledTaskRun,
69
+ definition: ScheduledTaskDefinition,
70
+ reason: string,
71
+ ) {
67
72
  if (this.denialReasons.get(run.taskKey) === reason) return;
68
73
  this.denialReasons.set(run.taskKey, reason);
69
- this.record(context, run, "task.execute", "denied");
74
+ this.record(context, run, definition.auditAction, "denied");
70
75
  }
71
76
 
72
77
  trigger(context: RequestGuardContext, run: ScheduledTaskRun) {
@@ -91,7 +96,7 @@ export class SchedulerExecutionRecorder {
91
96
  private record(
92
97
  context: RequestGuardContext,
93
98
  run: ScheduledTaskRun,
94
- action: "task.trigger" | "task.execute",
99
+ action: AuditEvent["action"],
95
100
  outcome: AuditEvent["outcome"],
96
101
  ) {
97
102
  this.runtime.auditSink.recordEvent(
@@ -183,9 +183,10 @@ async function createFixture(options: FixtureOptions = {}) {
183
183
  label: "测试任务",
184
184
  intervalSeconds: 100,
185
185
  maxAttempts: 3,
186
- retryDelaySeconds: 10,
187
- permissionKey: "system.scheduler.manage",
188
- feature: {
186
+ retryDelaySeconds: 10,
187
+ permissionKey: "system.scheduler.manage",
188
+ auditAction: "task.execute",
189
+ feature: {
189
190
  ...feature,
190
191
  requiredEntitlement: entitlementKey("capability.scheduler"),
191
192
  },
@@ -24,9 +24,10 @@ test("自动轮询没有领取任务时不写历史和审计,人工触发仍
24
24
  label: "测试任务",
25
25
  intervalSeconds: 100,
26
26
  maxAttempts: 3,
27
- retryDelaySeconds: 10,
28
- permissionKey: "system.scheduler.manage",
29
- feature: {
27
+ retryDelaySeconds: 10,
28
+ permissionKey: "system.scheduler.manage",
29
+ auditAction: "task.execute",
30
+ feature: {
30
31
  ...feature,
31
32
  requiredEntitlement: entitlementKey("capability.scheduler"),
32
33
  },
@@ -294,6 +294,7 @@ async function createFixture(
294
294
  maxAttempts: 3,
295
295
  retryDelaySeconds: 10,
296
296
  permissionKey: "system.scheduler.manage",
297
+ auditAction: "task.execute",
297
298
  feature: {
298
299
  ...feature,
299
300
  requiredEntitlement: entitlementKey("capability.scheduler"),