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,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
+ }