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,56 @@
1
+ import moduleAssembly from "../../../../windy.modules.json";
2
+ import {
3
+ defineWindyServerComposition,
4
+ type WindyServerModuleAdapter,
5
+ type WindyServerModuleAttachment,
6
+ } from "@southwind-ai/server-sdk";
7
+ import type { ModuleManifest } from "@southwind-ai/modules";
8
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
9
+ import type { ManagedDepartment } from "../system/entities.js";
10
+ import type { EntityRepository } from "../system/repository.js";
11
+ // @windy-module work-order begin
12
+ import { createWorkOrderModule } from "../example-modules.js";
13
+ // @windy-module work-order end
14
+
15
+ export interface ProjectBusinessModuleRuntime {
16
+ readonly db?: NodePgDatabase;
17
+ readonly departments: EntityRepository<ManagedDepartment>;
18
+ }
19
+
20
+ // @windy-module work-order begin
21
+ const workOrderModule = createWorkOrderModule();
22
+ // @windy-module work-order end
23
+
24
+ const projectAdapters: readonly WindyServerModuleAdapter<ModuleManifest>[] = [
25
+ // @windy-module work-order begin
26
+ {
27
+ name: "work-order",
28
+ hostRange: "^0.1.0",
29
+ runtimeMode: "project-attachment",
30
+ create: () => workOrderModule,
31
+ },
32
+ // @windy-module work-order end
33
+ ];
34
+
35
+ const projectAttachments: readonly WindyServerModuleAttachment<ProjectBusinessModuleRuntime>[] =
36
+ [
37
+ // @windy-module work-order begin
38
+ {
39
+ name: "work-order",
40
+ attach: (runtime) => workOrderModule.attach(runtime),
41
+ },
42
+ // @windy-module work-order end
43
+ ];
44
+
45
+ /**
46
+ * 客户拥有的 Server 组合声明。
47
+ *
48
+ * 标准 package Adapter 由 Host 动态加载;只有需要客户 Repository 的本地 Adapter
49
+ * 才在这里覆盖,并通过显式 attachment 获得项目运行时。动态 package 永远不会收到
50
+ * 数据库或平台内部 Repository。
51
+ */
52
+ export const serverCompositionDeclaration = defineWindyServerComposition({
53
+ assembly: moduleAssembly,
54
+ adapters: projectAdapters,
55
+ attachments: projectAttachments,
56
+ });
@@ -219,6 +219,8 @@ export async function createApplicationServices(
219
219
  : new InMemorySchedulerRepository(),
220
220
  taskDefinitions,
221
221
  runtime: input.runtime,
222
+ resolveActor: (actorId) =>
223
+ resolveCurrentSystemActor(input.systemRepositories, actorId),
222
224
  // @windy-module system.workflow begin
223
225
  workflow,
224
226
  // @windy-module system.workflow end
@@ -28,6 +28,10 @@ import type {
28
28
  } from "./types.js";
29
29
  export * from "./input-validation.js";
30
30
 
31
+ export type BulkResumeAuthorization = (
32
+ job: BulkDataJob,
33
+ ) => Promise<string | undefined>;
34
+
31
35
  export class BulkDataService {
32
36
  private readonly handlers = new Map<string, BulkResourceHandler>();
33
37
  private readonly limits: BulkDataLimits;
@@ -40,6 +44,7 @@ export class BulkDataService {
40
44
  private readonly now: () => Date = () => new Date(),
41
45
  blobStore?: BlobStore,
42
46
  private readonly durable?: LegacyDurableAdapter,
47
+ private readonly authorizeResume?: BulkResumeAuthorization,
43
48
  ) {
44
49
  this.limits = { ...defaultBulkDataLimits, ...limits };
45
50
  this.artifacts = new BulkArtifactStore(repository, blobStore);
@@ -155,6 +160,15 @@ export class BulkDataService {
155
160
  if (!job || job.status !== "running" || job.attempt !== generation) {
156
161
  return;
157
162
  }
163
+ const denial = this.authorizeResume
164
+ ? await this.authorizeResume(job)
165
+ : this.durable
166
+ ? "resume-authorization-unavailable"
167
+ : undefined;
168
+ if (denial) {
169
+ await this.repository.retryOrFail(job.id, denial, this.now());
170
+ return;
171
+ }
158
172
  await this.processClaimed(job);
159
173
  }
160
174
 
@@ -77,7 +77,7 @@ export class SystemGovernedExportAuthorization implements GovernedExportAuthoriz
77
77
  key: request.policyKey,
78
78
  resourceType: request.resourceType,
79
79
  version: request.policyVersion,
80
- featureKey: featureKey("system.rbac"),
80
+ featureKey: featureKey("system.user"),
81
81
  exportPermissionKey: "system.user.export",
82
82
  }
83
83
  : undefined;
@@ -14,7 +14,11 @@ import { DrizzleDurableJobRepository } from "../../jobs/drizzle-repository.js";
14
14
  import { DurableJobService } from "../../jobs/service.js";
15
15
  import { DurableJobWorker } from "../../jobs/worker.js";
16
16
  import { createPlatformObservability } from "../../observability/bootstrap.js";
17
- import { createServerRuntime, type AuditEventWriter } from "../../runtime.js";
17
+ import {
18
+ createServerRuntime,
19
+ type AuditEventWriter,
20
+ type ServerRuntimeOptions,
21
+ } from "../../runtime.js";
18
22
  import { identityRouteConfigs } from "../../system/identity-route-config.js";
19
23
  import { registerSystemRoutes } from "../../system/routes.js";
20
24
  import { createSystemRepositories } from "../../system/seed.js";
@@ -67,7 +71,7 @@ export async function createPostgresApiFixture(
67
71
  const actors = governanceActors();
68
72
  const actorResolver = bearerActorResolver(actors);
69
73
  const logs: Array<Record<string, unknown>> = [];
70
- const runtime = createServerRuntime({}, { actorResolver, auditWriter });
74
+ const runtime = createGovernanceRuntime({ actorResolver, auditWriter });
71
75
  const stack = createGovernanceStack({
72
76
  db,
73
77
  runtime,
@@ -94,6 +98,18 @@ export async function createPostgresApiFixture(
94
98
  };
95
99
  }
96
100
 
101
+ export function createGovernanceRuntime(
102
+ options: Pick<ServerRuntimeOptions, "actorResolver" | "auditWriter">,
103
+ ) {
104
+ return createServerRuntime(
105
+ {},
106
+ {
107
+ ...options,
108
+ featureCatalog: createFoundationSnapshot().features,
109
+ },
110
+ );
111
+ }
112
+
97
113
  export function createGovernanceStack(input: {
98
114
  db: NodePgDatabase;
99
115
  runtime: ReturnType<typeof createServerRuntime>;
@@ -5,7 +5,7 @@ import { Pool } from "pg";
5
5
  import { FileAuditRecoveryQueue } from "../../audit/recovery-queue.js";
6
6
  import { ReliableAuditWriter } from "../../audit/reliable-writer.js";
7
7
  import { DrizzleAuditLogWriter } from "../../audit/drizzle-log-store.js";
8
- import { createServerRuntime, type AuditEventWriter } from "../../runtime.js";
8
+ import type { AuditEventWriter } from "../../runtime.js";
9
9
  import {
10
10
  assertNoCanaries,
11
11
  createExport,
@@ -17,6 +17,7 @@ import {
17
17
  import {
18
18
  apiRequest,
19
19
  approveExport,
20
+ createGovernanceRuntime,
20
21
  createGovernanceStack,
21
22
  createPostgresApiFixture,
22
23
  ownerCanaries,
@@ -249,10 +250,10 @@ async function exerciseRecoveryQueue(
249
250
  join(fixture.storageRoot, "audit-recovery.json"),
250
251
  );
251
252
  const reliable = reliableWriter(primary, queue);
252
- const runtime = createServerRuntime(
253
- {},
254
- { actorResolver: fixture.actorResolver, auditWriter: reliable },
255
- );
253
+ const runtime = createGovernanceRuntime({
254
+ actorResolver: fixture.actorResolver,
255
+ auditWriter: reliable,
256
+ });
256
257
  const stack = createGovernanceStack({
257
258
  db: fixture.db,
258
259
  runtime,
@@ -294,10 +295,10 @@ async function exerciseDoubleFault(
294
295
  },
295
296
  },
296
297
  );
297
- const runtime = createServerRuntime(
298
- {},
299
- { actorResolver: fixture.actorResolver, auditWriter: reliable },
300
- );
298
+ const runtime = createGovernanceRuntime({
299
+ actorResolver: fixture.actorResolver,
300
+ auditWriter: reliable,
301
+ });
301
302
  const stack = createGovernanceStack({
302
303
  db: fixture.db,
303
304
  runtime,
@@ -11,7 +11,8 @@ import { DurableArtifactService } from "../../jobs/artifact-service.js";
11
11
  import { InMemoryDurableExecutionRepository } from "../../jobs/in-memory-execution.js";
12
12
  import { DurableJobService } from "../../jobs/service.js";
13
13
  import { DurableJobWorker } from "../../jobs/worker.js";
14
- import { createServerRuntime, type AuditEventWriter } from "../../runtime.js";
14
+ import type { AuditEventWriter } from "../../runtime.js";
15
+ import { createRouteTestRuntime } from "../../system/route-test-runtime.js";
15
16
  import { FakeRouteApp } from "../../system/route-test-app.js";
16
17
  import type { GovernedExportApprovalPort } from "./approval-port.js";
17
18
  import { GovernedExportJobRuntime } from "./job-runtime.js";
@@ -34,7 +35,9 @@ export async function createFixture(
34
35
  await auditWriter?.write(event, policy);
35
36
  },
36
37
  };
37
- const runtime = createServerRuntime(
38
+ const foundation = createFoundationSnapshot();
39
+ const runtime = createRouteTestRuntime(
40
+ foundation,
38
41
  { WINDY_DEV_ADMIN_TOKEN: "token" },
39
42
  { auditWriter: writer },
40
43
  );
@@ -86,12 +89,7 @@ export async function createFixture(
86
89
  { approvalRequired: true, retentionSeconds: 3600 },
87
90
  );
88
91
  const app = new FakeRouteApp();
89
- registerGovernedUserExportRoutes(
90
- app,
91
- service,
92
- runtime,
93
- createFoundationSnapshot().features,
94
- );
92
+ registerGovernedUserExportRoutes(app, service, runtime, foundation.features);
95
93
  return {
96
94
  app,
97
95
  approval,
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { createFoundationSnapshot } from "../foundation.js";
3
- import { createServerRuntime, type AuditEventWriter } from "../runtime.js";
3
+ import type { AuditEventWriter } from "../runtime.js";
4
+ import { createRouteTestRuntime } from "../system/route-test-runtime.js";
4
5
  import { FakeRouteApp } from "../system/route-test-app.js";
5
6
  import { registerSystemRoutes } from "../system/routes.js";
6
7
  import { createSystemRepositories } from "../system/seed.js";
@@ -249,7 +250,8 @@ describe("用户敏感字段 API", () => {
249
250
  async function fixture(auditWriter?: AuditEventWriter) {
250
251
  const snapshot = createFoundationSnapshot();
251
252
  const repositories = createSystemRepositories(snapshot);
252
- const runtime = createServerRuntime(
253
+ const runtime = createRouteTestRuntime(
254
+ snapshot,
253
255
  { WINDY_DEV_ADMIN_TOKEN: "token" },
254
256
  { auditWriter },
255
257
  );
@@ -95,6 +95,26 @@ describe("Feature Flag 管理授权", () => {
95
95
  expect(item).toMatchObject({ key: "system.rbac", enabled: true });
96
96
  });
97
97
 
98
+ test("Feature 管理核心能力不能关闭或隐藏", async () => {
99
+ const fixture = await createFixture(actor(["developer"], true));
100
+
101
+ for (const body of [
102
+ { enabled: false },
103
+ { visible: "hidden" },
104
+ { licenseVersionOverride: ["advanced"] },
105
+ ]) {
106
+ const response = (await updateFeature(
107
+ fixture,
108
+ body,
109
+ "system.feature",
110
+ )) as Response;
111
+ expect(response.status).toBe(409);
112
+ await expect(response.json()).resolves.toMatchObject({
113
+ error: "FEATURE_CONTROL_IMMUTABLE",
114
+ });
115
+ }
116
+ });
117
+
98
118
  test("Feature Flag 更新明确使用关键审计策略", async () => {
99
119
  const policies: string[] = [];
100
120
  const fixture = await createFixture(actor(["developer"], true), {
@@ -166,7 +186,12 @@ async function createFixture(
166
186
  },
167
187
  );
168
188
  const app = new FakeRouteApp();
169
- registerFeatureFlagRoutes(app, repositories.features, runtime);
189
+ registerFeatureFlagRoutes(
190
+ app,
191
+ repositories.features,
192
+ runtime,
193
+ snapshot.features.find(({ key }) => key === "system.feature")!,
194
+ );
170
195
  const guardContext = await runtime.createGuardContext(
171
196
  new Request("http://localhost/api/system/features/system.profile"),
172
197
  );
@@ -25,21 +25,16 @@ export function registerFeatureFlagRoutes(
25
25
  app: RouteApp,
26
26
  repository: ManifestFeatureRepository,
27
27
  runtime: ServerRuntime,
28
+ feature: FeatureFlagDefinition,
28
29
  ) {
29
30
  app.get("/system/features", async (rawContext) => {
30
31
  const { guardContext, query } = routeContext(rawContext);
31
- return withFeaturePermission(
32
- runtime,
33
- guardContext,
34
- "read",
35
- undefined,
36
- () => {
37
- const listQuery = parseListQuery(query);
38
- return listQuery instanceof Response
39
- ? listQuery
40
- : repository.list(listQuery, ["key", "label", "module"]);
41
- },
42
- );
32
+ return withFeaturePermission(runtime, guardContext, "read", feature, () => {
33
+ const listQuery = parseListQuery(query);
34
+ return listQuery instanceof Response
35
+ ? listQuery
36
+ : repository.list(listQuery, ["key", "label", "module"]);
37
+ });
43
38
  });
44
39
 
45
40
  app.get("/system/features/export", async (rawContext) => {
@@ -48,7 +43,7 @@ export function registerFeatureFlagRoutes(
48
43
  runtime,
49
44
  guardContext,
50
45
  "read",
51
- undefined,
46
+ feature,
52
47
  async () => {
53
48
  const listQuery = parseListQuery(query);
54
49
  if (listQuery instanceof Response) return listQuery;
@@ -78,7 +73,7 @@ export function registerFeatureFlagRoutes(
78
73
  runtime,
79
74
  guardContext,
80
75
  "read",
81
- undefined,
76
+ feature,
82
77
  async () => {
83
78
  const id = requiredId(params);
84
79
  return (await repository.get(id)) || notFound("features", id);
@@ -92,11 +87,21 @@ export function registerFeatureFlagRoutes(
92
87
  runtime,
93
88
  guardContext,
94
89
  "manage",
95
- undefined,
90
+ feature,
96
91
  async () => {
97
92
  const id = requiredId(params);
98
93
  const input = readStatePatch(body);
99
94
  if (input instanceof Response) return input;
95
+ if (id === feature.key && wouldDisableFeatureControl(input)) {
96
+ return Response.json(
97
+ {
98
+ error: "FEATURE_CONTROL_IMMUTABLE",
99
+ message:
100
+ "功能开关管理是核心恢复能力,不能关闭、隐藏或限制 License。",
101
+ },
102
+ { status: 409 },
103
+ );
104
+ }
100
105
  const item = await repository.updateState(
101
106
  id,
102
107
  input,
@@ -156,6 +161,19 @@ function readStatePatch(
156
161
  };
157
162
  }
158
163
 
164
+ function wouldDisableFeatureControl(
165
+ input: Partial<
166
+ Pick<ManagedFeature, "enabled" | "visible" | "licenseVersionOverride">
167
+ >,
168
+ ): boolean {
169
+ return (
170
+ input.enabled === false ||
171
+ input.visible === "hidden" ||
172
+ (input.licenseVersionOverride !== undefined &&
173
+ input.licenseVersionOverride !== null)
174
+ );
175
+ }
176
+
159
177
  function readLicenseVersionOverride(
160
178
  record: Record<string, unknown>,
161
179
  ): LicenseVersionKey[] | null | undefined | Response {
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ // @windy-module system.settings begin
4
+ composeModuleMenus,
5
+ validateModuleComposition,
6
+ // @windy-module system.settings end
7
+ systemModuleManifests,
8
+ } from "@southwind-ai/modules";
9
+ import { applyPlatformModuleActivation } from "./foundation.js";
10
+
11
+ describe("平台 Module 运行时启停", () => {
12
+ // @windy-module system.operations begin
13
+ test("停用只关闭能力与入口,保留静态实现声明用于一致性校验", () => {
14
+ const source = systemModuleManifests().find(
15
+ ({ name }) => name === "system.operations",
16
+ )!;
17
+ const disabled = applyPlatformModuleActivation(source, false);
18
+
19
+ expect(disabled.features.every(({ enabled }) => !enabled)).toBe(true);
20
+ expect(disabled.features.every(({ visible }) => visible === "hidden")).toBe(
21
+ true,
22
+ );
23
+ expect(
24
+ disabled.menus
25
+ .flatMap(({ children }) => children ?? [])
26
+ .every(({ visible }) => !visible),
27
+ ).toBe(true);
28
+ expect(
29
+ disabled.menus
30
+ .filter(({ type }) => type === "catalog")
31
+ .every(({ visible }) => visible),
32
+ ).toBe(true);
33
+ expect(disabled.apiPermissions).toEqual(source.apiPermissions);
34
+ expect(disabled.adminRoutes).toEqual(source.adminRoutes);
35
+ });
36
+ // @windy-module system.operations end
37
+
38
+ // @windy-module system.settings begin
39
+ test("停用一个平台 Module 不改变共享菜单目录壳", () => {
40
+ const modules = systemModuleManifests().map((manifest) =>
41
+ applyPlatformModuleActivation(
42
+ manifest,
43
+ manifest.name !== "system.settings",
44
+ ),
45
+ );
46
+
47
+ expect(() => validateModuleComposition(modules)).not.toThrow();
48
+ expect(
49
+ composeModuleMenus(modules)
50
+ .find(({ key }) => key === "system.configuration")
51
+ ?.children?.find(({ key }) => key === "system.settings")?.visible,
52
+ ).toBe(false);
53
+ });
54
+ // @windy-module system.settings end
55
+
56
+ test("启用时保留原始 Manifest 身份", () => {
57
+ const source = systemModuleManifests()[0]!;
58
+ expect(applyPlatformModuleActivation(source, true)).toBe(source);
59
+ });
60
+ });
@@ -14,7 +14,10 @@ import type {
14
14
  PermissionDefinition,
15
15
  } from "@southwind-ai/shared";
16
16
  import { resolveLicenseCatalog } from "@southwind-ai/shared";
17
- import { installedBusinessModules } from "./installed-business-modules.js";
17
+ import {
18
+ runtimeBusinessModules,
19
+ serverComposition,
20
+ } from "./module-host/runtime-composition.js";
18
21
 
19
22
  export interface FoundationSnapshot {
20
23
  platform: {
@@ -34,12 +37,21 @@ export interface FoundationSnapshot {
34
37
  auditActions: string[];
35
38
  auditActionCatalog: readonly AuditActionDefinition[];
36
39
  licenseCatalog: readonly LicenseCatalogDefinition[];
40
+ disabledModuleNames: readonly string[];
37
41
  }
38
42
 
39
43
  export function loadFoundationModules(): ModuleManifest[] {
40
44
  const registry = createModuleRegistry();
41
- for (const factory of systemModuleFactories) registry.register(factory);
42
- for (const module of installedBusinessModules) {
45
+ for (const factory of systemModuleFactories) {
46
+ const manifest = factory();
47
+ registry.register(() =>
48
+ applyPlatformModuleActivation(
49
+ manifest,
50
+ serverComposition.enabledModuleNames.has(manifest.name),
51
+ ),
52
+ );
53
+ }
54
+ for (const module of runtimeBusinessModules) {
43
55
  registry.register(() => module.manifest);
44
56
  }
45
57
  return registry.list();
@@ -47,10 +59,48 @@ export function loadFoundationModules(): ModuleManifest[] {
47
59
 
48
60
  export function loadSystemModules(): ModuleManifest[] {
49
61
  const registry = createModuleRegistry();
50
- for (const factory of systemModuleFactories) registry.register(factory);
62
+ for (const factory of systemModuleFactories) {
63
+ const manifest = factory();
64
+ registry.register(() =>
65
+ applyPlatformModuleActivation(
66
+ manifest,
67
+ serverComposition.enabledModuleNames.has(manifest.name),
68
+ ),
69
+ );
70
+ }
51
71
  return registry.list();
52
72
  }
53
73
 
74
+ export function applyPlatformModuleActivation(
75
+ manifest: ModuleManifest,
76
+ enabled: boolean,
77
+ ): ModuleManifest {
78
+ if (enabled) return manifest;
79
+ return {
80
+ ...manifest,
81
+ metadata: {
82
+ ...manifest.metadata,
83
+ runtimeActivation: "disabled",
84
+ },
85
+ features: manifest.features.map((feature) => ({
86
+ ...feature,
87
+ enabled: false,
88
+ visible: "hidden",
89
+ })),
90
+ menus: manifest.menus.map(hideMenu),
91
+ };
92
+ }
93
+
94
+ function hideMenu(menu: MenuNode): MenuNode {
95
+ return {
96
+ ...menu,
97
+ visible: menu.type === "catalog" ? menu.visible : false,
98
+ ...(menu.children
99
+ ? { children: menu.children.map((child) => hideMenu(child)) }
100
+ : {}),
101
+ };
102
+ }
103
+
54
104
  function collectMenus(modules: ModuleManifest[]): MenuNode[] {
55
105
  return composeModuleMenus(modules);
56
106
  }
@@ -89,7 +139,13 @@ export function createFoundationSnapshot(
89
139
  name: config.name,
90
140
  enableAI: config.enableAI,
91
141
  databaseDialect: config.database.dialect,
92
- licenseEnabled: config.license.enabled,
142
+ licenseEnabled:
143
+ config.license.enabled &&
144
+ modules.some(
145
+ ({ name, metadata }) =>
146
+ name === "system.license" &&
147
+ metadata?.runtimeActivation !== "disabled",
148
+ ),
93
149
  auditEnabled: config.audit.enabled,
94
150
  ruoyiCompatibilityEnabled: config.compatibility.ruoyiVue.enabled,
95
151
  },
@@ -105,5 +161,8 @@ export function createFoundationSnapshot(
105
161
  auditActions: collectAuditActions(modules),
106
162
  auditActionCatalog: catalogs.auditActions,
107
163
  licenseCatalog: catalogs.licenseVersions,
164
+ disabledModuleNames: modules
165
+ .filter(({ metadata }) => metadata?.runtimeActivation === "disabled")
166
+ .map(({ name }) => name),
108
167
  };
109
168
  }
@@ -8,6 +8,8 @@ import {
8
8
  import {
9
9
  evaluateEntitlementGuard,
10
10
  evaluateFeatureGuard,
11
+ evaluateLicenseRestrictionGuard,
12
+ evaluateLicenseVersionGuard,
11
13
  evaluatePermissionGuard,
12
14
  type RequestGuardContext,
13
15
  } from "./guards.js";
@@ -72,6 +74,27 @@ describe("guards", () => {
72
74
  ).toBe("entitlement-not-granted");
73
75
  });
74
76
 
77
+ test("License 未启用时跳过版本、Entitlement 与限制模式", () => {
78
+ const unrestricted = {
79
+ ...context,
80
+ licenseEnforced: false,
81
+ license: { restricted: true as const, status: "expired" as const },
82
+ };
83
+ const licensedFeature = {
84
+ ...feature,
85
+ requiredEntitlement: entitlementKey("capability.audit"),
86
+ allowedLicenseVersions: [],
87
+ };
88
+
89
+ expect(
90
+ evaluateEntitlementGuard(unrestricted, licensedFeature).allowed,
91
+ ).toBe(true);
92
+ expect(
93
+ evaluateLicenseVersionGuard(unrestricted, licensedFeature).allowed,
94
+ ).toBe(true);
95
+ expect(evaluateLicenseRestrictionGuard(unrestricted).allowed).toBe(true);
96
+ });
97
+
75
98
  test("运行时解析出依赖缺失时拒绝并记录 security.denied", () => {
76
99
  const denied = evaluateFeatureGuard(context, {
77
100
  ...feature,
@@ -58,6 +58,7 @@ export interface RequestGuardContext {
58
58
  status?: "valid" | "expired" | "clock-rollback" | "invalid" | "missing";
59
59
  restricted?: boolean;
60
60
  };
61
+ licenseEnforced?: boolean;
61
62
  }
62
63
 
63
64
  export type GuardDecisionReason =
@@ -84,6 +85,7 @@ export interface RequestGuardContextOptions {
84
85
  resolveLicense?: (
85
86
  request: Request,
86
87
  ) => RequestGuardContext["license"] | undefined;
88
+ licenseEnforced?: boolean;
87
89
  }
88
90
 
89
91
  export type PermissionGuardRequirement = Pick<
@@ -107,6 +109,7 @@ export function createRequestGuardContext(
107
109
  clientIp: options.clientIp,
108
110
  userAgent: options.userAgent,
109
111
  license: options.resolveLicense?.(request),
112
+ licenseEnforced: options.licenseEnforced,
110
113
  };
111
114
  }
112
115
 
@@ -176,6 +179,9 @@ export function evaluateEntitlementGuard(
176
179
  context: RequestGuardContext,
177
180
  feature: FeatureFlagDefinition,
178
181
  ): GuardDecision {
182
+ if (context.licenseEnforced === false) {
183
+ return { allowed: true, reason: "allowed" };
184
+ }
179
185
  if (
180
186
  !feature.requiredEntitlement ||
181
187
  context.license?.entitlements?.includes(feature.requiredEntitlement)
@@ -194,6 +200,9 @@ export function evaluateLicenseVersionGuard(
194
200
  context: RequestGuardContext,
195
201
  feature: FeatureFlagDefinition,
196
202
  ): GuardDecision {
203
+ if (context.licenseEnforced === false) {
204
+ return { allowed: true, reason: "allowed" };
205
+ }
197
206
  if (isLicenseVersionAllowed(feature, context.license?.versionKey)) {
198
207
  return { allowed: true, reason: "allowed" };
199
208
  }
@@ -208,6 +217,9 @@ export function evaluateLicenseRestrictionGuard(
208
217
  context: RequestGuardContext,
209
218
  explicitlyAllowed = false,
210
219
  ): GuardDecision {
220
+ if (context.licenseEnforced === false) {
221
+ return { allowed: true, reason: "allowed" };
222
+ }
211
223
  if (!context.license?.restricted || explicitlyAllowed) {
212
224
  return { allowed: true, reason: "allowed" };
213
225
  }