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
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import { validateSearchProviderRegistration } from "./search-provider-registration.js";
4
+
5
+ describe("SearchProvider Host 注册", () => {
6
+ test("Provider 版本必须与 Manifest 一致", () => {
7
+ expect(() =>
8
+ validateSearchProviderRegistration(manifest("1", "1.0.0"), {
9
+ key: "known.search",
10
+ providerVersion: "2.0.0",
11
+ }),
12
+ ).toThrow("版本与 Manifest 不一致");
13
+ });
14
+
15
+ test("只接受当前支持的搜索协议", () => {
16
+ expect(() =>
17
+ validateSearchProviderRegistration(manifest("2", "1.0.0"), {
18
+ key: "known.search",
19
+ providerVersion: "1.0.0",
20
+ }),
21
+ ).toThrow("协议版本不受支持");
22
+ });
23
+ });
24
+
25
+ function manifest(
26
+ contractVersion: string,
27
+ providerVersion: string,
28
+ ): ModuleManifest {
29
+ return {
30
+ name: "known",
31
+ version: "1.0.0",
32
+ label: "已知模块",
33
+ description: "测试模块",
34
+ menus: [],
35
+ adminRoutes: [],
36
+ permissions: [],
37
+ apiPermissions: [],
38
+ features: [],
39
+ migrations: [],
40
+ schemaExports: [],
41
+ tasks: [],
42
+ tools: [],
43
+ providers: [providerDefinition(contractVersion, providerVersion)],
44
+ auditActions: [],
45
+ };
46
+ }
47
+
48
+ function providerDefinition(contractVersion: string, providerVersion: string) {
49
+ return {
50
+ key: "known.search",
51
+ kind: "search",
52
+ contractVersion,
53
+ providerVersion,
54
+ label: "搜索",
55
+ enabledByDefault: true,
56
+ permissionKey: "known.item.read",
57
+ featureKey: "known.item",
58
+ deepLink: {
59
+ surface: "business",
60
+ routeTemplate: "/known/:id",
61
+ idParameter: "id",
62
+ },
63
+ } as unknown as ModuleManifest["providers"][number];
64
+ }
@@ -0,0 +1,35 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import {
3
+ SEARCH_PROVIDER_CONTRACT_VERSION,
4
+ type SearchProviderManifestDefinition,
5
+ } from "@southwind-ai/shared";
6
+
7
+ interface SearchProviderRegistration {
8
+ readonly key: string;
9
+ readonly providerVersion: string;
10
+ }
11
+
12
+ export function validateSearchProviderRegistration(
13
+ manifest: ModuleManifest,
14
+ provider: SearchProviderRegistration,
15
+ ): void {
16
+ const definition = manifest.providers.find(({ key }) => key === provider.key);
17
+ if (!definition) {
18
+ throw new Error(`搜索 Provider 未在 Manifest 声明:${provider.key}`);
19
+ }
20
+ if (
21
+ !isSearchProviderDefinition(definition) ||
22
+ definition.contractVersion !== SEARCH_PROVIDER_CONTRACT_VERSION
23
+ ) {
24
+ throw new Error(`SearchProvider ${provider.key} 协议版本不受支持`);
25
+ }
26
+ if (definition.providerVersion !== provider.providerVersion) {
27
+ throw new Error(`SearchProvider ${provider.key} 版本与 Manifest 不一致`);
28
+ }
29
+ }
30
+
31
+ function isSearchProviderDefinition(
32
+ value: ModuleManifest["providers"][number],
33
+ ): value is SearchProviderManifestDefinition {
34
+ return value.kind === "search" && "providerVersion" in value;
35
+ }
@@ -0,0 +1,99 @@
1
+ import { expect, test } from "bun:test";
2
+ import { systemModuleManifests } from "@southwind-ai/modules";
3
+ import { createServerRuntime } from "../runtime.js";
4
+ import {
5
+ createManifestAuthorizedRouteApp,
6
+ createStaticRouteAuthorizer,
7
+ } from "./static-route-authorization.js";
8
+
9
+ // @windy-module system.license begin
10
+ test("平台静态 Route 使用 Manifest Feature 与 Permission Guard", async () => {
11
+ const modules = systemModuleManifests();
12
+ const runtime = createServerRuntime(
13
+ { WINDY_DEV_ADMIN_TOKEN: "token_1" },
14
+ { disabledModuleNames: new Set(["system.license"]) },
15
+ );
16
+ const authorizer = createStaticRouteAuthorizer(runtime, modules);
17
+ const context = await runtime.createGuardContext(
18
+ new Request("http://localhost/api/license/runtime", {
19
+ headers: { authorization: "Bearer token_1" },
20
+ }),
21
+ );
22
+
23
+ const denied = await authorizer.authorize(
24
+ context,
25
+ "GET",
26
+ "/api/license/runtime",
27
+ );
28
+
29
+ expect(denied?.status).toBe(403);
30
+ await expect(denied?.json()).resolves.toMatchObject({
31
+ reason: "feature-disabled",
32
+ });
33
+ await expect(
34
+ authorizer.authorize(context, "GET", "/api/unknown"),
35
+ ).rejects.toThrow("平台静态 Route 缺少 Manifest 声明:GET /api/unknown");
36
+ });
37
+ // @windy-module system.license end
38
+
39
+ test("静态 Route 包装器阻断已停用 Module,核心恢复入口显式豁免", async () => {
40
+ const modules = systemModuleManifests();
41
+ const runtime = createServerRuntime(
42
+ {},
43
+ {
44
+ featureCatalog: modules.flatMap(({ features }) => features),
45
+ disabledModuleNames: new Set(["system.identity", "system"]),
46
+ },
47
+ );
48
+ const authorizer = createStaticRouteAuthorizer(runtime, modules);
49
+ let handler: ((context: unknown) => unknown) | undefined;
50
+ let calls = 0;
51
+ const app = routeApp((registered) => {
52
+ handler = registered;
53
+ });
54
+ const guarded = createManifestAuthorizedRouteApp(app, authorizer);
55
+ guarded.get("/auth/sessions", () => {
56
+ calls += 1;
57
+ return Response.json({ ok: true });
58
+ });
59
+ const context = await runtime.createGuardContextForActor(
60
+ new Request("http://localhost/api/auth/sessions"),
61
+ actor("system.profile.read"),
62
+ );
63
+
64
+ const denied = await handler!({ guardContext: context });
65
+ expect((denied as Response).status).toBe(403);
66
+ expect(calls).toBe(0);
67
+
68
+ const core = createManifestAuthorizedRouteApp(app, authorizer, () => ({
69
+ immutableCoreAccess: true,
70
+ }));
71
+ core.get("/auth/session", () => {
72
+ calls += 1;
73
+ return Response.json({ ok: true });
74
+ });
75
+ const allowed = await handler!({ guardContext: context });
76
+ expect((allowed as Response).status).toBe(200);
77
+ expect(calls).toBe(1);
78
+ });
79
+
80
+ function routeApp(
81
+ capture: (handler: (context: unknown) => unknown) => void,
82
+ ): Parameters<typeof createManifestAuthorizedRouteApp>[0] {
83
+ return {
84
+ get: (_path, handler) => capture(handler),
85
+ post: (_path, handler) => capture(handler),
86
+ put: (_path, handler) => capture(handler),
87
+ delete: (_path, handler) => capture(handler),
88
+ };
89
+ }
90
+
91
+ function actor(permissionKey: string) {
92
+ return {
93
+ id: "user_1",
94
+ type: "user" as const,
95
+ name: "测试用户",
96
+ roleCodes: [],
97
+ permissionKeys: [permissionKey],
98
+ };
99
+ }
@@ -0,0 +1,90 @@
1
+ import { apiRouteKey, type ModuleManifest } from "@southwind-ai/modules";
2
+ import type { RequestGuardContext } from "../guards.js";
3
+ import { evaluateRouteGuards, findFeature } from "../route-guards.js";
4
+ import type { createServerRuntime } from "../runtime.js";
5
+ import { forbidden } from "../system/route-helpers.js";
6
+ import type { RouteApp } from "../system/route-types.js";
7
+
8
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
9
+
10
+ export interface StaticRouteAuthorizer {
11
+ authorize(
12
+ context: RequestGuardContext,
13
+ method: string,
14
+ path: string,
15
+ options?: {
16
+ readonly restrictedModeAccess?: "backup-export" | "admin-admission";
17
+ readonly immutableCoreAccess?: boolean;
18
+ },
19
+ ): Promise<Response | undefined>;
20
+ }
21
+
22
+ /**
23
+ * 平台静态 Route 与 Module Host Route 使用相同的 Manifest Guard 事实源。
24
+ */
25
+ export function createStaticRouteAuthorizer(
26
+ runtime: ServerRuntime,
27
+ modules: readonly ModuleManifest[],
28
+ ): StaticRouteAuthorizer {
29
+ const bindings = new Map(
30
+ modules.flatMap((module) =>
31
+ module.apiPermissions.map(
32
+ (binding) => [apiRouteKey(binding), binding] as const,
33
+ ),
34
+ ),
35
+ );
36
+ const features = modules.flatMap((module) => module.features);
37
+ return {
38
+ async authorize(context, method, path, options) {
39
+ const key = apiRouteKey({ method, path });
40
+ const binding = bindings.get(key);
41
+ if (!binding) {
42
+ throw new Error(`平台静态 Route 缺少 Manifest 声明:${key}`);
43
+ }
44
+ if (options?.immutableCoreAccess) return undefined;
45
+ const feature = binding.featureKey
46
+ ? findFeature(features, binding.featureKey)
47
+ : undefined;
48
+ const decision = await evaluateRouteGuards(runtime, context, {
49
+ permission: binding,
50
+ feature,
51
+ restrictedModeAccess: options?.restrictedModeAccess,
52
+ });
53
+ return decision.allowed ? undefined : forbidden(decision.reason);
54
+ },
55
+ };
56
+ }
57
+
58
+ export function createManifestAuthorizedRouteApp(
59
+ app: RouteApp,
60
+ authorizer: StaticRouteAuthorizer,
61
+ options?: (
62
+ method: string,
63
+ path: string,
64
+ ) => Parameters<StaticRouteAuthorizer["authorize"]>[3],
65
+ ): RouteApp {
66
+ const wrap =
67
+ (method: string, path: string, handler: (context: unknown) => unknown) =>
68
+ async (raw: unknown) => {
69
+ const context = raw as { guardContext: RequestGuardContext };
70
+ const denied = await authorizer.authorize(
71
+ context.guardContext,
72
+ method,
73
+ apiPath(path),
74
+ options?.(method, path),
75
+ );
76
+ return denied ?? handler(raw);
77
+ };
78
+ return {
79
+ get: (path, handler) => app.get(path, wrap("GET", path, handler)),
80
+ post: (path, handler, options) =>
81
+ app.post(path, wrap("POST", path, handler), options),
82
+ put: (path, handler, options) =>
83
+ app.put(path, wrap("PUT", path, handler), options),
84
+ delete: (path, handler) => app.delete(path, wrap("DELETE", path, handler)),
85
+ };
86
+ }
87
+
88
+ function apiPath(path: string): string {
89
+ return path.startsWith("/api/") ? path : `/api${path}`;
90
+ }
@@ -31,6 +31,7 @@ export function createModuleTaskDefinitions(
31
31
  maxAttempts: task.maxAttempts,
32
32
  retryDelaySeconds: task.retryDelaySeconds,
33
33
  permissionKey: task.permissionKey,
34
+ auditAction: task.auditAction,
34
35
  feature,
35
36
  handler: registration.handler,
36
37
  isIdleResult: registration.isIdleResult,
@@ -24,6 +24,8 @@ test("Composition Root 向业务 Handler 注入用户目录 Port", async () => {
24
24
  method: "GET",
25
25
  path: "/api/case/members",
26
26
  permissionKey: "case.member.read",
27
+ featureKey: "case.member",
28
+ auditAction: "case.member.read",
27
29
  handler: (context) => context.userDirectory.list(),
28
30
  },
29
31
  ]);
@@ -104,6 +106,7 @@ function businessManifest(): ModuleManifest {
104
106
  path: "/api/case/members",
105
107
  permissionKey: "case.member.read",
106
108
  featureKey: "case.member",
109
+ auditAction: "case.member.read",
107
110
  },
108
111
  ],
109
112
  features: [
@@ -121,6 +124,6 @@ function businessManifest(): ModuleManifest {
121
124
  tasks: [],
122
125
  tools: [],
123
126
  providers: [],
124
- auditActions: [],
127
+ auditActions: ["case.member.read"],
125
128
  };
126
129
  }
@@ -48,11 +48,13 @@ describe("模块 Host 初始化", () => {
48
48
  description: "",
49
49
  permissionKey: "known.item.read",
50
50
  featureKey: "known.feature",
51
+ auditAction: "task.execute",
51
52
  intervalSeconds: 60,
52
53
  maxAttempts: 3,
53
54
  retryDelaySeconds: 10,
54
55
  },
55
56
  ],
57
+ auditActions: ["task.execute"],
56
58
  });
57
59
  await expect(
58
60
  initializeModuleHosts({
@@ -0,0 +1,289 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ systemModuleManifests,
4
+ type ModuleManifest,
5
+ } from "@southwind-ai/modules";
6
+ import type {
7
+ ModuleGovernedResourceRegistration,
8
+ ModuleHost,
9
+ } from "@southwind-ai/server-sdk";
10
+ import { featureKey } from "@southwind-ai/shared";
11
+ import { prepareServerModules } from "./module-preflight.js";
12
+
13
+ describe("Server Module 构建与启动前门禁", () => {
14
+ test("Host 实现不完整时在持久化操作前 fail closed", async () => {
15
+ const module = routeModule();
16
+ await expect(
17
+ prepareServerModules({
18
+ modules: [...systemModuleManifests(), module.manifest],
19
+ installations: [{ manifest: module.manifest, setup() {} }],
20
+ }),
21
+ ).rejects.toThrow("API Route GET /api/catalog/items 缺少 Handler");
22
+ });
23
+
24
+ test("setup 不能在首次校验后原地变异 Manifest", async () => {
25
+ const module = routeModule();
26
+ const manifest = {
27
+ ...module.manifest,
28
+ features: [...module.manifest.features],
29
+ };
30
+ await expect(
31
+ prepareServerModules({
32
+ modules: [...systemModuleManifests(), manifest],
33
+ installations: [
34
+ {
35
+ manifest,
36
+ setup() {
37
+ manifest.features.push(manifest.features[0]!);
38
+ },
39
+ },
40
+ ],
41
+ }),
42
+ ).rejects.toThrow(
43
+ "Server Module 在 setup 期间修改了 canonical Manifest:catalog",
44
+ );
45
+ });
46
+
47
+ test("setup 不能替换安装单元的 canonical Manifest 引用", async () => {
48
+ const module = routeModule();
49
+ const mutable = module as {
50
+ manifest: ModuleManifest;
51
+ setup: typeof module.setup;
52
+ };
53
+ await expect(
54
+ prepareServerModules({
55
+ modules: [...systemModuleManifests(), module.manifest],
56
+ installations: [
57
+ {
58
+ get manifest() {
59
+ return mutable.manifest;
60
+ },
61
+ setup() {
62
+ mutable.manifest = {
63
+ ...module.manifest,
64
+ apiPermissions: [],
65
+ };
66
+ },
67
+ },
68
+ ],
69
+ }),
70
+ ).rejects.toThrow(
71
+ "Server Module 在 setup 期间替换了 canonical Manifest:catalog",
72
+ );
73
+ });
74
+
75
+ test("attachment 结构错误时不执行任何 Module setup", async () => {
76
+ const manifest = governedManifest();
77
+ let setupCalled = false;
78
+ await expect(
79
+ prepareServerModules({
80
+ modules: [manifest],
81
+ installations: [
82
+ {
83
+ manifest,
84
+ setup() {
85
+ setupCalled = true;
86
+ },
87
+ },
88
+ ],
89
+ adapters: [
90
+ {
91
+ name: "people",
92
+ hostRange: "^0.1.0",
93
+ runtimeMode: "self-contained",
94
+ create: () => ({ manifest, setup() {} }),
95
+ },
96
+ ],
97
+ attachments: [{ name: "people", attach() {} }],
98
+ enabledModuleNames: new Set(["people"]),
99
+ }),
100
+ ).rejects.toThrow("不能挂载 self-contained Adapter");
101
+ expect(setupCalled).toBe(false);
102
+ });
103
+
104
+ test("返回已经完成声明与实现校验的 Host", async () => {
105
+ const module = routeModule();
106
+ const result = await prepareServerModules({
107
+ modules: [...systemModuleManifests(), module.manifest],
108
+ installations: [module],
109
+ });
110
+
111
+ expect(result.moduleHosts.routes).toHaveLength(1);
112
+ expect(result.foundationModules).toContain(module.manifest);
113
+ });
114
+
115
+ test("合法治理资源注册满足派生路由实现门禁", async () => {
116
+ const manifest = governedManifest();
117
+ const result = await prepareServerModules({
118
+ modules: [manifest],
119
+ installations: [
120
+ {
121
+ manifest,
122
+ setup(host) {
123
+ host.registerGovernedResources([governedRegistration()]);
124
+ },
125
+ },
126
+ ],
127
+ });
128
+
129
+ expect(result.moduleHosts.governedResources).toHaveLength(1);
130
+ });
131
+ });
132
+
133
+ function routeModule() {
134
+ const capability = featureKey("catalog.item");
135
+ const manifest: ModuleManifest = {
136
+ name: "catalog",
137
+ version: "1.0.0",
138
+ label: "目录",
139
+ description: "目录测试模块",
140
+ menus: [],
141
+ adminRoutes: [],
142
+ permissions: [
143
+ {
144
+ key: "catalog.item.read",
145
+ label: "查看目录",
146
+ module: "catalog",
147
+ moduleLabel: "目录",
148
+ resource: "item",
149
+ resourceLabel: "目录项",
150
+ action: "read",
151
+ },
152
+ ],
153
+ apiPermissions: [
154
+ {
155
+ method: "GET",
156
+ path: "/api/catalog/items",
157
+ permissionKey: "catalog.item.read",
158
+ featureKey: capability,
159
+ auditAction: "data.read",
160
+ },
161
+ ],
162
+ features: [
163
+ {
164
+ key: capability,
165
+ module: "catalog",
166
+ label: "目录项",
167
+ enabled: true,
168
+ visible: "visible",
169
+ stage: "stable",
170
+ },
171
+ ],
172
+ migrations: [],
173
+ schemaExports: [],
174
+ tasks: [],
175
+ tools: [],
176
+ providers: [],
177
+ auditActions: ["data.read"],
178
+ };
179
+ return {
180
+ manifest,
181
+ setup(host: ModuleHost) {
182
+ host.registerRoutes([
183
+ {
184
+ method: "GET",
185
+ path: "/api/catalog/items",
186
+ permissionKey: "catalog.item.read",
187
+ featureKey: capability,
188
+ handler: () => ({ items: [] }),
189
+ },
190
+ ]);
191
+ },
192
+ };
193
+ }
194
+
195
+ function governedRegistration(): ModuleGovernedResourceRegistration {
196
+ return {
197
+ policyKey: "people.person.fields",
198
+ readPath: "/api/people/persons/:id",
199
+ revealPath: "/api/people/persons/:id/plaintext",
200
+ revealAuditAction: "data.plaintext.read",
201
+ authorize: () => true,
202
+ read: () => ({
203
+ id: "person-1",
204
+ publicFields: { name: "张三" },
205
+ governedFields: { phone: "13812348071" },
206
+ }),
207
+ };
208
+ }
209
+
210
+ function governedManifest(): ModuleManifest {
211
+ const capability = featureKey("people.person");
212
+ return {
213
+ name: "people",
214
+ version: "1.0.0",
215
+ label: "人员",
216
+ description: "人员主档",
217
+ menus: [],
218
+ adminRoutes: [],
219
+ permissions: ["read", "reveal", "export"].map((action) => ({
220
+ key: `people.person.${action}`,
221
+ label: action,
222
+ module: "people",
223
+ moduleLabel: "人员",
224
+ resource: "person",
225
+ resourceLabel: "人员",
226
+ action: action as "read" | "reveal" | "export",
227
+ ...(action === "reveal"
228
+ ? { accessRisk: "sensitive-disclosure" as const }
229
+ : action === "export"
230
+ ? { accessRisk: "governed-export" as const }
231
+ : {}),
232
+ })),
233
+ apiPermissions: [
234
+ {
235
+ method: "GET",
236
+ path: "/api/people/persons/:id",
237
+ permissionKey: "people.person.read",
238
+ featureKey: capability,
239
+ auditAction: "data.read",
240
+ },
241
+ {
242
+ method: "POST",
243
+ path: "/api/people/persons/:id/plaintext",
244
+ permissionKey: "people.person.reveal",
245
+ featureKey: capability,
246
+ auditAction: "data.plaintext.read",
247
+ },
248
+ ],
249
+ features: [
250
+ {
251
+ key: capability,
252
+ module: "people",
253
+ label: "人员主档",
254
+ enabled: true,
255
+ visible: "visible",
256
+ stage: "stable",
257
+ },
258
+ ],
259
+ migrations: [],
260
+ schemaExports: [],
261
+ tasks: [],
262
+ tools: [],
263
+ providers: [],
264
+ dataPolicies: [
265
+ {
266
+ key: "people.person.fields",
267
+ resourceType: "people.person",
268
+ version: "1",
269
+ featureKey: capability,
270
+ readPermissionKey: "people.person.read",
271
+ revealPermissionKey: "people.person.reveal",
272
+ exportPermissionKey: "people.person.export",
273
+ display: {
274
+ read: "masked",
275
+ reveal: "plaintext",
276
+ unauthorized: "hidden",
277
+ },
278
+ fields: [
279
+ {
280
+ fieldKey: "phone",
281
+ classification: "sensitive",
282
+ mask: { kind: "partial", prefix: 3, suffix: 4 },
283
+ },
284
+ ],
285
+ },
286
+ ],
287
+ auditActions: ["data.read", "data.plaintext.read"],
288
+ };
289
+ }