create-windy 0.3.18 → 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 (216) hide show
  1. package/README.md +28 -13
  2. package/dist/cli.js +788 -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/agent-server/src/southwind_agent_server/adk_debug.py +38 -5
  9. package/template/apps/agent-server/tests/test_adk_debug_result_polling.py +139 -0
  10. package/template/apps/server/Dockerfile +1 -0
  11. package/template/apps/server/package.json +1 -0
  12. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  13. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  14. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  15. package/template/apps/server/src/agent/routes.test.ts +20 -7
  16. package/template/apps/server/src/agent/routes.ts +14 -0
  17. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  18. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  19. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  20. package/template/apps/server/src/app/server-composition.ts +56 -0
  21. package/template/apps/server/src/application-services.ts +2 -0
  22. package/template/apps/server/src/bulk-data/service.ts +14 -0
  23. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  24. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  25. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  26. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  27. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  28. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  29. package/template/apps/server/src/feature/routes.ts +33 -15
  30. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  31. package/template/apps/server/src/foundation.ts +64 -5
  32. package/template/apps/server/src/guards.test.ts +23 -0
  33. package/template/apps/server/src/guards.ts +12 -0
  34. package/template/apps/server/src/index.ts +28 -34
  35. package/template/apps/server/src/installed-business-modules.ts +13 -24
  36. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  37. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  38. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  39. package/template/apps/server/src/license/activation.test.ts +30 -0
  40. package/template/apps/server/src/license/activation.ts +26 -0
  41. package/template/apps/server/src/license/restriction.test.ts +16 -0
  42. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  43. package/template/apps/server/src/license/routes.ts +20 -4
  44. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  45. package/template/apps/server/src/module-bootstrap.ts +14 -23
  46. package/template/apps/server/src/module-composition.test.ts +43 -2
  47. package/template/apps/server/src/module-composition.ts +29 -0
  48. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  49. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  50. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  51. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  52. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  53. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  54. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  55. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  56. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  57. package/template/apps/server/src/module-host/initialize.ts +15 -4
  58. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  59. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  60. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  61. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  62. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  63. package/template/apps/server/src/module-host/request-context.ts +3 -0
  64. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  65. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  66. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  67. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  68. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  69. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  70. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  71. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  72. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  73. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  74. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  75. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  76. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  77. package/template/apps/server/src/module-host.test.ts +2 -0
  78. package/template/apps/server/src/module-preflight.test.ts +289 -0
  79. package/template/apps/server/src/module-preflight.ts +96 -0
  80. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  81. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  82. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  83. package/template/apps/server/src/route-guards.test.ts +22 -0
  84. package/template/apps/server/src/route-guards.ts +10 -3
  85. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  86. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  87. package/template/apps/server/src/runtime-feature.ts +5 -0
  88. package/template/apps/server/src/runtime.ts +10 -49
  89. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  90. package/template/apps/server/src/scheduler/audit.ts +1 -1
  91. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  92. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  93. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  94. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  95. package/template/apps/server/src/scheduler/service.ts +18 -21
  96. package/template/apps/server/src/scheduler/types.ts +5 -1
  97. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  98. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  99. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  100. package/template/apps/server/src/startup-preflight.ts +37 -0
  101. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  102. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  103. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  104. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  105. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  106. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  107. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  108. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  109. package/template/apps/server/src/system/routes.test.ts +21 -11
  110. package/template/apps/server/src/system/routes.ts +11 -2
  111. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  112. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  113. package/template/apps/server/src/work-order/task.test.ts +6 -1
  114. package/template/apps/web/Dockerfile +5 -0
  115. package/template/apps/web/package.json +1 -0
  116. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  117. package/template/apps/web/src/app/web-composition.ts +24 -0
  118. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  119. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  120. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  121. package/template/apps/web/src/layout/navigation.ts +29 -4
  122. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  123. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  124. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  125. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  126. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  127. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  128. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  129. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  130. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  131. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  132. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  133. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  134. package/template/apps/web/src/router/business-routes.ts +32 -45
  135. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  136. package/template/apps/web/src/router/index.ts +64 -54
  137. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  138. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  139. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  140. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  141. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  142. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  143. package/template/apps/web/src/services/work-order-api.ts +16 -0
  144. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  145. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  146. package/template/apps/web/vite.config.ts +22 -1
  147. package/template/docs/architecture/migration-bundle.md +10 -7
  148. package/template/docs/architecture/module-manifest-composition.md +85 -3
  149. package/template/docs/architecture/search-provider-protocol.md +6 -1
  150. package/template/docs/architecture/server-module-interface.md +70 -7
  151. package/template/docs/development/business-web-routes.md +53 -50
  152. package/template/docs/development/module-lifecycle.md +48 -54
  153. package/template/docs/platform/agent-runtime.md +2 -0
  154. package/template/docs/platform/navigation-registry.md +5 -4
  155. package/template/package.json +1 -1
  156. package/template/packages/config/package.json +1 -1
  157. package/template/packages/database/package.json +1 -1
  158. package/template/packages/example-work-order/package.json +6 -0
  159. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  160. package/template/packages/example-work-order/src/admin.ts +35 -0
  161. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  162. package/template/packages/example-work-order/src/manifest.ts +54 -7
  163. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  164. package/template/packages/example-work-order/src/server-module.ts +2 -2
  165. package/template/packages/jobs/package.json +1 -1
  166. package/template/packages/modules/index.ts +4 -0
  167. package/template/packages/modules/package.json +3 -2
  168. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  169. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  170. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  171. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  172. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  173. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  174. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  175. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  176. package/template/packages/modules/src/composition.test.ts +74 -9
  177. package/template/packages/modules/src/composition.ts +8 -0
  178. package/template/packages/modules/src/manifest.ts +1 -0
  179. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  180. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  181. package/template/packages/modules/src/module-host-range.ts +125 -0
  182. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  183. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  184. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  185. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  186. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  187. package/template/packages/modules/src/system-api-binding.ts +43 -9
  188. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  189. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  190. package/template/packages/modules/src/system-api-operational.ts +57 -8
  191. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  192. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  193. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  194. package/template/packages/modules/src/system-data-governance.ts +5 -2
  195. package/template/packages/modules/src/system-features.ts +31 -0
  196. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  197. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  198. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  199. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  200. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  201. package/template/packages/modules/src/system-modules.test.ts +10 -2
  202. package/template/packages/modules/src/system-modules.ts +6 -5
  203. package/template/packages/modules/src/system-permissions.ts +4 -0
  204. package/template/packages/modules/user/module.ts +19 -2
  205. package/template/packages/server-sdk/index.ts +8 -0
  206. package/template/packages/server-sdk/package.json +2 -1
  207. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  208. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  209. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  210. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  211. package/template/packages/shared/package.json +1 -1
  212. package/template/packages/shared/src/audit.test.ts +7 -0
  213. package/template/packages/shared/src/audit.ts +16 -0
  214. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  215. package/template/packages/shared/src/rbac.ts +2 -0
  216. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -42,7 +42,7 @@ describe("客户 License 离线激活码路由", () => {
42
42
  requestId: "request_1",
43
43
  },
44
44
  }));
45
- registerPlatformLicenseRoutes(app, runtime);
45
+ registerPlatformLicenseRoutes(app, runtime, allowRoutes);
46
46
 
47
47
  const accepted = await app.handle(post({ activationCode }));
48
48
  expect(accepted.status).toBe(200);
@@ -150,10 +150,14 @@ function createApp(runtime: PlatformLicenseRuntime) {
150
150
  requestId: "request_1",
151
151
  },
152
152
  }));
153
- registerPlatformLicenseRoutes(app, runtime);
153
+ registerPlatformLicenseRoutes(app, runtime, allowRoutes);
154
154
  return app;
155
155
  }
156
156
 
157
+ const allowRoutes = {
158
+ authorize: async () => undefined,
159
+ };
160
+
157
161
  function fixedLicense(): SignedLicenseV1 {
158
162
  return {
159
163
  formatVersion: 1,
@@ -1,6 +1,7 @@
1
1
  import type { RequestGuardContext } from "../guards.js";
2
2
  import type { RouteApp } from "../system/routes.js";
3
3
  import type { PlatformLicenseRuntime } from "./runtime-service.js";
4
+ import type { StaticRouteAuthorizer } from "../module-host/static-route-authorization.js";
4
5
  import {
5
6
  boundedJsonBodyErrorResponse,
6
7
  decodeOfflineActivationCode,
@@ -17,10 +18,18 @@ interface LicenseRouteContext {
17
18
  export function registerPlatformLicenseRoutes(
18
19
  app: Pick<RouteApp, "get" | "post">,
19
20
  licenseRuntime: PlatformLicenseRuntime,
21
+ authorizer: StaticRouteAuthorizer,
20
22
  ) {
21
23
  app.get("/license/runtime", async (rawContext) => {
22
24
  const context = rawContext as LicenseRouteContext;
23
25
  if (context.guardContext.actor.type !== "user") return unauthorized();
26
+ const denied = await authorizer.authorize(
27
+ context.guardContext,
28
+ "GET",
29
+ "/api/license/runtime",
30
+ { restrictedModeAccess: "admin-admission" },
31
+ );
32
+ if (denied) return denied;
24
33
  return licenseRuntime.snapshotForActor(context.guardContext.actor);
25
34
  });
26
35
 
@@ -29,6 +38,13 @@ export function registerPlatformLicenseRoutes(
29
38
  async (rawContext) => {
30
39
  const context = rawContext as LicenseRouteContext;
31
40
  if (context.guardContext.actor.type !== "user") return unauthorized();
41
+ const denied = await authorizer.authorize(
42
+ context.guardContext,
43
+ "POST",
44
+ "/api/license/runtime/install",
45
+ { restrictedModeAccess: "admin-admission" },
46
+ );
47
+ if (denied) return denied;
32
48
  let rawLicense: string;
33
49
  try {
34
50
  rawLicense = readInstallCandidate(
@@ -82,6 +98,10 @@ export function registerPlatformLicenseRoutes(
82
98
  return app;
83
99
  }
84
100
 
101
+ function unauthorized(): Response {
102
+ return Response.json({ error: "UNAUTHORIZED" }, { status: 401 });
103
+ }
104
+
85
105
  function readInstallCandidate(body: unknown): string {
86
106
  if (!body || typeof body !== "object" || Array.isArray(body)) return "";
87
107
  const record = body as Record<string, unknown>;
@@ -95,7 +115,3 @@ function readInstallCandidate(body: unknown): string {
95
115
  ? record.license
96
116
  : "";
97
117
  }
98
-
99
- function unauthorized(): Response {
100
- return Response.json({ error: "UNAUTHORIZED" }, { status: 401 });
101
- }
@@ -33,6 +33,7 @@ import {
33
33
  requestStatus as status,
34
34
  } from "./runtime-service-test-http.js";
35
35
  import { buildScopeContext } from "../data-access/context.js";
36
+ import { createStaticRouteAuthorizer } from "../module-host/static-route-authorization.js";
36
37
 
37
38
  const NOW = new Date("2026-08-01T00:00:00.000Z");
38
39
 
@@ -240,6 +241,7 @@ function createFixture(
240
241
  {
241
242
  actorResolver: { resolve: (current) => actorFrom(current) },
242
243
  licenseResolver: licenseRuntime,
244
+ featureCatalog: snapshot.features,
243
245
  featureResolver: {
244
246
  resolve: (key) => ({ key, enabled: true, visible: "visible" }),
245
247
  },
@@ -258,15 +260,18 @@ function createFixture(
258
260
  }
259
261
  })
260
262
  .get("/health", () => ({ status: "ok" }));
261
- registerPlatformLicenseRoutes(app, licenseRuntime);
263
+ const modules = loadSystemModules();
264
+ const routeAuthorizer = createStaticRouteAuthorizer(runtime, modules);
265
+ registerPlatformLicenseRoutes(app, licenseRuntime, routeAuthorizer);
262
266
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
263
267
  registerAgentRoutes(
264
268
  app,
265
269
  new GovernedToolRegistry(
266
- loadSystemModules(),
270
+ modules,
267
271
  createSystemToolHandlers(repositories, runtime),
268
272
  runtime,
269
273
  ),
274
+ routeAuthorizer,
270
275
  );
271
276
  return { app, runtime, licenseRuntime };
272
277
  }
@@ -283,6 +288,9 @@ function actorFrom(current: Request) {
283
288
  permissionKeys: [
284
289
  "system.user.read",
285
290
  "system.audit.read",
291
+ "system.agent.read",
292
+ "system.license.read",
293
+ "system.license.manage",
286
294
  "system.menu.read",
287
295
  ],
288
296
  scopeContext: buildScopeContext("user_1", undefined, [
@@ -1,20 +1,13 @@
1
1
  import type { PlatformConfig } from "@southwind-ai/config";
2
+ import type { ModuleUploadPolicy } from "@southwind-ai/server-sdk";
2
3
  // @windy-module system.configuration begin
3
4
  import { bootstrapModuleConfiguration } from "./configuration/bootstrap.js";
4
5
  // @windy-module system.configuration end
5
- import {
6
- createFoundationSnapshot,
7
- loadFoundationModules,
8
- } from "./foundation.js";
6
+ import { createFoundationSnapshot } from "./foundation.js";
9
7
  import { runInstalledModuleMigrations } from "./module-migrations.js";
10
- import {
11
- attachInstalledBusinessModules,
12
- installedBusinessModules,
13
- } from "./installed-business-modules.js";
14
- import {
15
- initializeModuleHosts,
16
- synchronizeModuleRolePresets,
17
- } from "./module-host.js";
8
+ import { attachRuntimeBusinessModules } from "./module-host/runtime-composition.js";
9
+ import { synchronizeModuleRolePresets } from "./module-host.js";
10
+ import { prepareInstalledServerModules } from "./module-preflight.js";
18
11
  import type { ServerPersistence } from "./persistence.js";
19
12
  // @windy-module system.storage begin
20
13
  import { createPlatformUploadPolicies } from "./storage/runtime.js";
@@ -27,8 +20,15 @@ import {
27
20
  export async function bootstrapServerModules(
28
21
  platformConfig: PlatformConfig,
29
22
  persistence: ServerPersistence | undefined,
23
+ prepared?: Awaited<ReturnType<typeof prepareInstalledServerModules>>,
30
24
  ) {
31
- const foundationModules = loadFoundationModules();
25
+ const baseUploadPolicies: ModuleUploadPolicy[] = [
26
+ // @windy-module system.storage begin
27
+ ...createPlatformUploadPolicies(platformConfig),
28
+ // @windy-module system.storage end
29
+ ];
30
+ const { foundationModules, moduleHosts } =
31
+ prepared ?? (await prepareInstalledServerModules(baseUploadPolicies));
32
32
  const foundationSnapshot = createFoundationSnapshot(
33
33
  platformConfig,
34
34
  foundationModules,
@@ -46,15 +46,6 @@ export async function bootstrapServerModules(
46
46
  // @windy-module system.configuration begin
47
47
  configurationRuntime.bindFoundation(foundationSnapshot);
48
48
  // @windy-module system.configuration end
49
- const moduleHosts = await initializeModuleHosts({
50
- modules: foundationModules,
51
- installations: installedBusinessModules,
52
- baseUploadPolicies: [
53
- // @windy-module system.storage begin
54
- ...createPlatformUploadPolicies(platformConfig),
55
- // @windy-module system.storage end
56
- ],
57
- });
58
49
  const systemRepositories = createSystemRepositories(foundationSnapshot, {
59
50
  db: persistence?.db,
60
51
  });
@@ -74,7 +65,7 @@ export async function bootstrapServerModules(
74
65
  foundationSnapshot,
75
66
  moduleHosts.rolePresets,
76
67
  );
77
- attachInstalledBusinessModules({
68
+ await attachRuntimeBusinessModules({
78
69
  db: persistence?.db,
79
70
  departments: systemRepositories.departments,
80
71
  });
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import { featureKey } from "@southwind-ai/shared";
3
4
  import { Elysia } from "elysia";
4
5
  import { validateServerModuleComposition } from "./module-composition.js";
5
6
 
@@ -83,6 +84,35 @@ describe("Server Module Composition", () => {
83
84
  "AI Operation Handler已注册但未在 Manifest 声明:system.unknown-operation",
84
85
  );
85
86
  });
87
+
88
+ test("平台静态 HTTP Route 也必须在 Manifest 声明", () => {
89
+ expect(() =>
90
+ validateServerModuleComposition({
91
+ modules: [fixture()],
92
+ routes: [
93
+ {
94
+ method: "POST",
95
+ path: "/api/auth/users/:id/password",
96
+ },
97
+ { method: "GET", path: "/api/demo/ghost" },
98
+ ],
99
+ }),
100
+ ).toThrow("HTTP Route 已注册但未在 Manifest 声明:GET /api/demo/ghost");
101
+
102
+ expect(() =>
103
+ validateServerModuleComposition({
104
+ modules: [fixture()],
105
+ routes: [
106
+ {
107
+ method: "POST",
108
+ path: "/api/auth/users/:id/password",
109
+ },
110
+ { method: "GET", path: "/api/openapi.json" },
111
+ ],
112
+ frameworkRoutes: ["GET /api/openapi.json"],
113
+ }),
114
+ ).not.toThrow();
115
+ });
86
116
  });
87
117
 
88
118
  function fixture(): ModuleManifest {
@@ -110,14 +140,25 @@ function fixture(): ModuleManifest {
110
140
  method: "POST",
111
141
  path: "/api/auth/users/:id/password",
112
142
  permissionKey: "system.auth.manage",
143
+ featureKey: "system.auth",
144
+ auditAction: "auth.password.update",
145
+ },
146
+ ],
147
+ features: [
148
+ {
149
+ key: featureKey("system.auth"),
150
+ label: "账号认证",
151
+ module: "system",
152
+ visible: "visible",
153
+ enabled: true,
154
+ stage: "stable",
113
155
  },
114
156
  ],
115
- features: [],
116
157
  migrations: [],
117
158
  schemaExports: [],
118
159
  tasks: [],
119
160
  tools: [],
120
161
  providers: [],
121
- auditActions: [],
162
+ auditActions: ["auth.password.update"],
122
163
  };
123
164
  }
@@ -23,6 +23,11 @@ export interface ModuleImplementationRegistrations {
23
23
  export interface ServerCompositionInput {
24
24
  modules: readonly ModuleManifest[];
25
25
  routes: readonly RegisteredHttpRoute[];
26
+ /**
27
+ * 仅用于框架生成且不属于产品 API 的路由,例如 OpenAPI 文档。
28
+ * 产品、运维、认证与恢复入口仍必须进入 Manifest。
29
+ */
30
+ frameworkRoutes?: readonly string[];
26
31
  tasks?: readonly { key: string }[];
27
32
  tools?: ReadonlyMap<string, unknown>;
28
33
  aiOperations?: readonly string[];
@@ -33,6 +38,11 @@ export interface ServerCompositionInput {
33
38
  export function validateServerModuleComposition(
34
39
  input: ServerCompositionInput,
35
40
  ): void {
41
+ validateHttpRouteRegistration(
42
+ input.modules,
43
+ input.routes.map(apiRouteKey),
44
+ input.frameworkRoutes ?? [],
45
+ );
36
46
  validateModuleComposition(input.modules, {
37
47
  apiRoutes: input.routes.map(apiRouteKey),
38
48
  tasks: (input.tasks || []).map(({ key }) => key),
@@ -45,6 +55,25 @@ export function validateServerModuleComposition(
45
55
  }
46
56
  }
47
57
 
58
+ function validateHttpRouteRegistration(
59
+ modules: readonly ModuleManifest[],
60
+ registeredRoutes: readonly string[],
61
+ frameworkRoutes: readonly string[],
62
+ ): void {
63
+ const declared = new Set(
64
+ modules.flatMap((module) => module.apiPermissions.map(apiRouteKey)),
65
+ );
66
+ const exemptions = new Set(frameworkRoutes);
67
+ const unknown = registeredRoutes.filter(
68
+ (route) => !declared.has(route) && !exemptions.has(route),
69
+ );
70
+ if (unknown.length) {
71
+ throw new Error(
72
+ `HTTP Route 已注册但未在 Manifest 声明:${[...new Set(unknown)].join("、")}`,
73
+ );
74
+ }
75
+ }
76
+
48
77
  function validateRegistrationConsistency(
49
78
  modules: readonly ModuleManifest[],
50
79
  registrations: ModuleImplementationRegistrations,
@@ -0,0 +1,172 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { DurableHandlerContext } from "@southwind-ai/jobs";
3
+ import { featureKey } from "@southwind-ai/shared";
4
+ import { buildActorScopeContexts } from "../data-access/context.js";
5
+ import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
6
+ import type { RequestActor } from "../guards.js";
7
+ import { createServerRuntime } from "../runtime.js";
8
+ import { createModuleDurableContextualizer } from "./durable-context.js";
9
+ import { initializeModuleHosts } from "./initialize.js";
10
+ import { manifest } from "./test-fixtures.js";
11
+
12
+ const feature = {
13
+ key: featureKey("evidence.catalog"),
14
+ label: "电子物证",
15
+ module: "evidence",
16
+ visible: "visible" as const,
17
+ enabled: true,
18
+ stage: "stable" as const,
19
+ };
20
+ const moduleManifest = manifest("evidence", {
21
+ features: [feature],
22
+ permissions: [
23
+ {
24
+ key: "evidence.read",
25
+ label: "读取物证",
26
+ module: "evidence",
27
+ moduleLabel: "物证",
28
+ resource: "file",
29
+ resourceLabel: "文件",
30
+ action: "read",
31
+ },
32
+ ],
33
+ tasks: [
34
+ {
35
+ key: "evidence.refresh",
36
+ label: "刷新物证",
37
+ description: "刷新物证目录。",
38
+ permissionKey: "evidence.read",
39
+ featureKey: "evidence.catalog",
40
+ auditAction: "evidence.file.read",
41
+ intervalSeconds: 60,
42
+ maxAttempts: 3,
43
+ retryDelaySeconds: 5,
44
+ },
45
+ ],
46
+ auditActions: ["evidence.file.read"],
47
+ });
48
+
49
+ describe("模块 Durable Handler 逐次 Guard", () => {
50
+ test("重试与恢复会重新读取当前 Feature 和 License 状态", async () => {
51
+ let featureEnabled = true;
52
+ const featureChanged = await fixture({
53
+ featureResolver: {
54
+ resolve: (key) => ({
55
+ key,
56
+ enabled: featureEnabled,
57
+ visible: "visible",
58
+ }),
59
+ },
60
+ });
61
+ const persistedJob = job(featureChanged.actor);
62
+ await featureChanged.contextualize({
63
+ claim: { job: persistedJob },
64
+ moduleName: "evidence",
65
+ context: baseContext(1),
66
+ });
67
+ featureEnabled = false;
68
+ await expect(
69
+ featureChanged.contextualize({
70
+ claim: { job: persistedJob },
71
+ moduleName: "evidence",
72
+ context: baseContext(2),
73
+ }),
74
+ ).rejects.toMatchObject({
75
+ code: "MODULE_DURABLE_GUARD_DENIED",
76
+ details: { reason: "feature-disabled" },
77
+ });
78
+
79
+ const restricted = await fixture({
80
+ licenseEnforced: true,
81
+ licenseResolver: {
82
+ resolve: () => ({ restricted: true, status: "invalid" }),
83
+ },
84
+ });
85
+ await expect(
86
+ restricted.contextualize({
87
+ claim: { job: job(restricted.actor) },
88
+ moduleName: "evidence",
89
+ context: baseContext(3),
90
+ }),
91
+ ).rejects.toMatchObject({
92
+ code: "MODULE_DURABLE_GUARD_DENIED",
93
+ details: { reason: "license-restricted" },
94
+ });
95
+ });
96
+ });
97
+
98
+ async function fixture(
99
+ runtimeOptions: NonNullable<Parameters<typeof createServerRuntime>[1]>,
100
+ ) {
101
+ const hosts = await initializeModuleHosts({
102
+ modules: [moduleManifest],
103
+ installations: [
104
+ {
105
+ manifest: moduleManifest,
106
+ setup(host) {
107
+ host.registerDurableHandler({
108
+ key: "evidence.refresh",
109
+ featureKey: "evidence.catalog",
110
+ permissionKey: "evidence.read",
111
+ auditAction: "evidence.file.read",
112
+ payload: { parse: () => ({}) },
113
+ execute: () => undefined,
114
+ });
115
+ },
116
+ },
117
+ ],
118
+ });
119
+ const runtime = createServerRuntime(
120
+ { NODE_ENV: "test" },
121
+ { ...runtimeOptions, featureCatalog: [feature] },
122
+ );
123
+ const actor = currentActor();
124
+ return {
125
+ actor,
126
+ contextualize: createModuleDurableContextualizer({
127
+ hosts,
128
+ runtime,
129
+ features: [feature],
130
+ resolveActor: async () => actor,
131
+ }),
132
+ };
133
+ }
134
+
135
+ function currentActor(): RequestActor {
136
+ return {
137
+ id: "user-reader",
138
+ type: "user",
139
+ name: "物证读取者",
140
+ roleCodes: ["investigator"],
141
+ permissionKeys: ["evidence.read"],
142
+ ...buildActorScopeContexts("user-reader", "dept-case", [
143
+ {
144
+ id: "role-investigator",
145
+ code: "investigator",
146
+ dataScope: "all",
147
+ departmentIds: [],
148
+ },
149
+ ]),
150
+ };
151
+ }
152
+
153
+ function job(actor: RequestActor) {
154
+ return {
155
+ id: "job-evidence",
156
+ handlerKey: "evidence.refresh",
157
+ requestedBy: actor.id,
158
+ authorizationScope: captureAuthorizationScope(actor.scopeContext).snapshot,
159
+ };
160
+ }
161
+
162
+ function baseContext(attemptNumber: number): DurableHandlerContext {
163
+ return {
164
+ jobId: "job-evidence",
165
+ stageKey: "main",
166
+ attemptNumber,
167
+ signal: new AbortController().signal,
168
+ heartbeat: async () => undefined,
169
+ progress: async () => undefined,
170
+ publishArtifact: async () => undefined,
171
+ };
172
+ }
@@ -6,6 +6,8 @@ import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
6
6
  import type { RequestActor } from "../guards.js";
7
7
  import { createServerRuntime } from "../runtime.js";
8
8
  import { createModuleDurableContextualizer } from "./durable-context.js";
9
+ import { initializeModuleHosts } from "./initialize.js";
10
+ import { manifest } from "./test-fixtures.js";
9
11
 
10
12
  const feature = {
11
13
  key: featureKey("evidence.catalog"),
@@ -19,8 +21,54 @@ const feature = {
19
21
  describe("模块 Durable Handler 可选端口", () => {
20
22
  test("未挂载 Storage 时仍重建 Actor,并对文件读取 fail-closed", async () => {
21
23
  const actor = currentActor();
24
+ const moduleManifest = manifest("evidence", {
25
+ features: [feature],
26
+ permissions: [
27
+ {
28
+ key: "evidence.read",
29
+ label: "读取物证",
30
+ module: "evidence",
31
+ moduleLabel: "物证",
32
+ resource: "file",
33
+ resourceLabel: "文件",
34
+ action: "read",
35
+ },
36
+ ],
37
+ tasks: [
38
+ {
39
+ key: "evidence.refresh",
40
+ label: "刷新物证",
41
+ description: "刷新物证目录。",
42
+ permissionKey: "evidence.read",
43
+ featureKey: "evidence.catalog",
44
+ auditAction: "evidence.file.read",
45
+ intervalSeconds: 60,
46
+ maxAttempts: 3,
47
+ retryDelaySeconds: 5,
48
+ },
49
+ ],
50
+ auditActions: ["evidence.file.read"],
51
+ });
52
+ const hosts = await initializeModuleHosts({
53
+ modules: [moduleManifest],
54
+ installations: [
55
+ {
56
+ manifest: moduleManifest,
57
+ setup(host) {
58
+ host.registerDurableHandler({
59
+ key: "evidence.refresh",
60
+ featureKey: "evidence.catalog",
61
+ permissionKey: "evidence.read",
62
+ auditAction: "evidence.file.read",
63
+ payload: { parse: () => ({}) },
64
+ execute: () => undefined,
65
+ });
66
+ },
67
+ },
68
+ ],
69
+ });
22
70
  const contextualize = createModuleDurableContextualizer({
23
- hosts: {} as never,
71
+ hosts,
24
72
  runtime: createServerRuntime(
25
73
  { NODE_ENV: "test" },
26
74
  { featureCatalog: [feature] },
@@ -66,6 +114,7 @@ function currentActor(): RequestActor {
66
114
  function job(actor: RequestActor) {
67
115
  return {
68
116
  id: "job-evidence",
117
+ handlerKey: "evidence.refresh",
69
118
  requestedBy: actor.id,
70
119
  authorizationScope: captureAuthorizationScope(actor.scopeContext).snapshot,
71
120
  };
@@ -34,6 +34,19 @@ const evidenceManifest = manifest("evidence", {
34
34
  },
35
35
  ],
36
36
  auditActions: ["evidence.file.read"],
37
+ tasks: [
38
+ {
39
+ key: "evidence.refresh",
40
+ label: "刷新物证",
41
+ description: "刷新物证目录。",
42
+ permissionKey: "evidence.read",
43
+ featureKey: "evidence.catalog",
44
+ auditAction: "evidence.file.read",
45
+ intervalSeconds: 60,
46
+ maxAttempts: 3,
47
+ retryDelaySeconds: 5,
48
+ },
49
+ ],
37
50
  });
38
51
 
39
52
  describe("模块 Durable Handler 授权上下文", () => {
@@ -137,7 +150,7 @@ describe("模块 Durable Handler 授权上下文", () => {
137
150
  // @windy-module system.storage end
138
151
 
139
152
  // @windy-module system.storage begin
140
- test("Scope 变化、权限撤销与资源绑定漂移均 fail-closed", async () => {
153
+ test("Scope 变化与权限撤销均在执行前 fail-closed", async () => {
141
154
  const stable = currentActor();
142
155
  const scopeChanged = currentActor("self");
143
156
  const setupScopeChanged = await setup(() => scopeChanged);
@@ -152,37 +165,32 @@ describe("模块 Durable Handler 授权上下文", () => {
152
165
  retryable: false,
153
166
  });
154
167
 
155
- const permissionRevoked = { ...stable, permissionKeys: [] };
156
- const revoked = await setup(() => permissionRevoked);
157
- const file = await seedFile(revoked.storage);
158
- const context = await revoked.contextualize({
159
- claim: { job: job(stable) },
168
+ let permissionGranted = true;
169
+ const revoked = await setup(() => ({
170
+ ...stable,
171
+ permissionKeys: permissionGranted ? ["evidence.read"] : [],
172
+ }));
173
+ const persistedJob = job(stable);
174
+ await revoked.contextualize({
175
+ claim: { job: persistedJob },
160
176
  moduleName: "evidence",
161
177
  context: baseContext(1),
162
178
  });
163
- const storage = (
164
- context as typeof context & {
165
- storage: {
166
- openResourceFile(input: {
167
- fileId: string;
168
- purpose: string;
169
- resourceType: string;
170
- resourceId: string;
171
- }): Promise<unknown>;
172
- };
173
- }
174
- ).storage;
179
+ permissionGranted = false;
175
180
  await expect(
176
- storage.openResourceFile({
177
- fileId: file.id,
178
- purpose: "evidence.raw",
179
- resourceType: "evidence.evidence-file",
180
- resourceId: "evidence-1",
181
+ revoked.contextualize({
182
+ claim: { job: persistedJob },
183
+ moduleName: "evidence",
184
+ context: baseContext(2),
181
185
  }),
182
- ).rejects.toThrow("资源文件访问未授权");
186
+ ).rejects.toMatchObject({
187
+ code: "MODULE_DURABLE_GUARD_DENIED",
188
+ retryable: false,
189
+ details: { reason: "missing-permission" },
190
+ });
183
191
  expect(revoked.runtime.auditSink.list()).toContainEqual(
184
192
  expect.objectContaining({
185
- action: "evidence.file.read",
193
+ action: "security.denied",
186
194
  outcome: "denied",
187
195
  }),
188
196
  );
@@ -222,6 +230,14 @@ async function setup(
222
230
  context.actor.id === "user-reader",
223
231
  },
224
232
  ]);
233
+ host.registerDurableHandler({
234
+ key: "evidence.refresh",
235
+ featureKey: "evidence.catalog",
236
+ permissionKey: "evidence.read",
237
+ auditAction: "evidence.file.read",
238
+ payload: { parse: () => ({}) },
239
+ execute: () => undefined,
240
+ });
225
241
  },
226
242
  },
227
243
  ],
@@ -278,6 +294,7 @@ function currentActor(scope: "all" | "self" = "all"): RequestActor {
278
294
  function job(actor: RequestActor) {
279
295
  return {
280
296
  id: "job-evidence",
297
+ handlerKey: "evidence.refresh",
281
298
  requestedBy: actor.id,
282
299
  authorizationScope: captureAuthorizationScope(actor.scopeContext).snapshot,
283
300
  };