create-windy 0.2.18 → 0.2.20
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.
- package/README.md +4 -0
- package/dist/cli.js +752 -186
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/application-services.ts +2 -2
- package/template/apps/server/src/audit/search-summary.ts +15 -8
- package/template/apps/server/src/example-modules.ts +58 -70
- package/template/apps/server/src/foundation.ts +8 -4
- package/template/apps/server/src/guards.test.ts +13 -0
- package/template/apps/server/src/guards.ts +12 -2
- package/template/apps/server/src/index.ts +48 -91
- package/template/apps/server/src/installed-business-modules.ts +23 -4
- package/template/apps/server/src/module-bootstrap.ts +83 -0
- package/template/apps/server/src/module-composition.test.ts +45 -1
- package/template/apps/server/src/module-composition.ts +45 -0
- package/template/apps/server/src/module-host/initialize.test.ts +123 -0
- package/template/apps/server/src/module-host/initialize.ts +331 -0
- package/template/apps/server/src/module-host/request-context.test.ts +66 -0
- package/template/apps/server/src/module-host/request-context.ts +156 -0
- package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
- package/template/apps/server/src/module-host/role-presets.ts +76 -0
- package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
- package/template/apps/server/src/module-host/route-installer.ts +97 -0
- package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
- package/template/apps/server/src/module-host/search-providers.ts +31 -0
- package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
- package/template/apps/server/src/module-host/storage-port.ts +59 -0
- package/template/apps/server/src/module-host/task-definitions.ts +41 -0
- package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
- package/template/apps/server/src/module-host.test.ts +202 -58
- package/template/apps/server/src/module-host.ts +34 -113
- package/template/apps/server/src/route-guards.test.ts +112 -1
- package/template/apps/server/src/runtime-feature.ts +66 -31
- package/template/apps/server/src/runtime.test.ts +1 -0
- package/template/apps/server/src/runtime.ts +3 -1
- package/template/apps/server/src/system/built-in-roles.ts +1 -1
- package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
- package/template/apps/server/src/work-order/routes.test.ts +30 -18
- package/template/apps/server/src/work-order/routes.ts +104 -135
- package/template/apps/server/src/work-order/search-api.test.ts +14 -3
- package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
- package/template/apps/server/src/work-order/search-provider.ts +17 -12
- package/template/apps/server/src/work-order/task.test.ts +16 -11
- package/template/apps/web/Dockerfile +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
- package/template/docs/architecture/object-storage.md +12 -0
- package/template/packages/config/index.test.ts +57 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +28 -1
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/runner.ts +7 -9
- package/template/packages/example-work-order/index.ts +1 -0
- package/template/packages/example-work-order/package.json +2 -0
- package/template/packages/example-work-order/src/server-module.ts +61 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/types.ts +7 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/catalog-validation.test.ts +212 -0
- package/template/packages/modules/src/catalog-validation.ts +19 -2
- package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
- package/template/packages/server-sdk/index.ts +42 -0
- package/template/packages/server-sdk/package.json +16 -3
- package/template/packages/server-sdk/src/actor.ts +15 -0
- package/template/packages/server-sdk/src/audit-port.ts +22 -0
- package/template/packages/server-sdk/src/jobs-port.ts +24 -0
- package/template/packages/server-sdk/src/module-host.ts +36 -1
- package/template/packages/server-sdk/src/namespace.test.ts +26 -0
- package/template/packages/server-sdk/src/namespace.ts +13 -0
- package/template/packages/server-sdk/src/request-context.ts +21 -0
- package/template/packages/server-sdk/src/role-preset.ts +18 -0
- package/template/packages/server-sdk/src/route-definition.ts +26 -0
- package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
- package/template/packages/server-sdk/src/storage-port.ts +25 -0
- package/template/packages/server-sdk/src/task-registration.ts +18 -0
- package/template/packages/server-sdk/src/tool-registration.ts +29 -0
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/license-catalog.test.ts +73 -1
- package/template/packages/shared/src/license-catalog.ts +95 -0
- package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
- package/template/apps/server/src/work-order/task.ts +0 -30
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
3
3
|
import type { ModuleInitializer } from "@southwind-ai/server-sdk";
|
|
4
4
|
// @windy-module work-order begin
|
|
5
|
-
import {
|
|
5
|
+
import { createWorkOrderModule } from "./example-modules.js";
|
|
6
6
|
// @windy-module work-order end
|
|
7
7
|
import { initializeModuleHosts } from "./module-host.js";
|
|
8
8
|
// @windy-module work-order begin
|
|
@@ -13,88 +13,177 @@ import {
|
|
|
13
13
|
// @windy-module work-order end
|
|
14
14
|
|
|
15
15
|
describe("模块 Host 初始化", () => {
|
|
16
|
-
test("
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
events.push("beta");
|
|
30
|
-
});
|
|
16
|
+
test("任务、工具、角色预设与 Provider 注册强制命名空间与 Manifest 声明", async () => {
|
|
17
|
+
await expect(
|
|
18
|
+
initializeModuleHosts({
|
|
19
|
+
modules: [manifest("known")],
|
|
20
|
+
initializers: [
|
|
21
|
+
initializer("known", (host) => {
|
|
22
|
+
host.registerTaskHandlers([
|
|
23
|
+
{ taskKey: "other.snapshot", handler: () => undefined },
|
|
24
|
+
]);
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
).rejects.toThrow("定时任务不属于模块 known:other.snapshot");
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
await expect(
|
|
31
|
+
initializeModuleHosts({
|
|
32
|
+
modules: [manifest("known")],
|
|
33
|
+
initializers: [
|
|
34
|
+
initializer("known", (host) => {
|
|
35
|
+
host.registerTaskHandlers([
|
|
36
|
+
{ taskKey: "known.snapshot", handler: () => undefined },
|
|
37
|
+
]);
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
41
|
+
).rejects.toThrow("定时任务 Handler 未在 Manifest 声明:known.snapshot");
|
|
42
|
+
|
|
43
|
+
const withTask = manifest("known", {
|
|
44
|
+
tasks: [
|
|
45
|
+
{
|
|
46
|
+
key: "known.snapshot",
|
|
47
|
+
label: "快照",
|
|
48
|
+
description: "",
|
|
49
|
+
permissionKey: "known.item.read",
|
|
50
|
+
featureKey: "known.feature",
|
|
51
|
+
intervalSeconds: 60,
|
|
52
|
+
maxAttempts: 3,
|
|
53
|
+
retryDelaySeconds: 10,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
35
56
|
});
|
|
36
|
-
expect(
|
|
57
|
+
await expect(
|
|
58
|
+
initializeModuleHosts({
|
|
59
|
+
modules: [withTask],
|
|
60
|
+
initializers: [
|
|
61
|
+
initializer("known", (host) => {
|
|
62
|
+
const registration = {
|
|
63
|
+
taskKey: "known.snapshot",
|
|
64
|
+
handler: () => undefined,
|
|
65
|
+
};
|
|
66
|
+
host.registerTaskHandlers([registration, registration]);
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
}),
|
|
70
|
+
).rejects.toThrow("定时任务 Handler 重复注册:known.snapshot");
|
|
37
71
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
72
|
+
await expect(
|
|
73
|
+
initializeModuleHosts({
|
|
74
|
+
modules: [manifest("known")],
|
|
75
|
+
initializers: [
|
|
76
|
+
initializer("known", (host) => {
|
|
77
|
+
host.registerToolHandlers([
|
|
78
|
+
{
|
|
79
|
+
toolKey: "other.list",
|
|
80
|
+
scopeMode: "platform",
|
|
81
|
+
execute: () => ({ items: [], total: 0, page: 1, pageSize: 10 }),
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
}),
|
|
87
|
+
).rejects.toThrow("Agent Tool不属于模块 known:other.list");
|
|
42
88
|
|
|
43
|
-
test("未知模块、错误命名空间和重复注册立即失败", async () => {
|
|
44
89
|
await expect(
|
|
45
90
|
initializeModuleHosts({
|
|
46
|
-
modules: [
|
|
47
|
-
initializers: [
|
|
91
|
+
modules: [manifest("known")],
|
|
92
|
+
initializers: [
|
|
93
|
+
initializer("known", (host) => {
|
|
94
|
+
host.registerToolHandlers([
|
|
95
|
+
{
|
|
96
|
+
toolKey: "known.list",
|
|
97
|
+
scopeMode: "platform",
|
|
98
|
+
execute: () => ({ items: [], total: 0, page: 1, pageSize: 10 }),
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
48
103
|
}),
|
|
49
|
-
).rejects.toThrow("
|
|
104
|
+
).rejects.toThrow("Agent Tool Handler 未在 Manifest 声明:known.list");
|
|
50
105
|
|
|
51
106
|
await expect(
|
|
52
107
|
initializeModuleHosts({
|
|
53
|
-
modules: [
|
|
108
|
+
modules: [
|
|
109
|
+
manifest("known", {
|
|
110
|
+
permissions: [permission("known.item.read", "known")],
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
54
113
|
initializers: [
|
|
55
114
|
initializer("known", (host) => {
|
|
56
|
-
host.
|
|
115
|
+
host.registerRolePresets([
|
|
116
|
+
{
|
|
117
|
+
key: "known.manager",
|
|
118
|
+
label: "管理员",
|
|
119
|
+
permissionKeys: ["other.item.read"],
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
57
122
|
}),
|
|
58
123
|
],
|
|
59
124
|
}),
|
|
60
|
-
).rejects.toThrow("
|
|
125
|
+
).rejects.toThrow("角色预设权限不属于模块 known:other.item.read");
|
|
61
126
|
|
|
62
127
|
await expect(
|
|
63
128
|
initializeModuleHosts({
|
|
64
|
-
modules: [
|
|
129
|
+
modules: [manifest("known")],
|
|
65
130
|
initializers: [
|
|
66
131
|
initializer("known", (host) => {
|
|
67
|
-
host.
|
|
68
|
-
|
|
132
|
+
host.registerRolePresets([
|
|
133
|
+
{
|
|
134
|
+
key: "known.manager",
|
|
135
|
+
label: "管理员",
|
|
136
|
+
permissionKeys: ["known.item.manage"],
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
69
139
|
}),
|
|
70
140
|
],
|
|
71
141
|
}),
|
|
72
|
-
).rejects.toThrow("
|
|
142
|
+
).rejects.toThrow("角色预设引用了模块 known 未声明权限:known.item.manage");
|
|
73
143
|
|
|
74
144
|
await expect(
|
|
75
145
|
initializeModuleHosts({
|
|
76
|
-
modules: [],
|
|
77
|
-
initializers: [
|
|
78
|
-
|
|
146
|
+
modules: [manifest("known")],
|
|
147
|
+
initializers: [
|
|
148
|
+
initializer("known", (host) => {
|
|
149
|
+
host.registerSearchProviders([
|
|
150
|
+
{
|
|
151
|
+
key: "other.search",
|
|
152
|
+
providerVersion: "0.1.0",
|
|
153
|
+
search: () => Promise.resolve({ hits: [] }),
|
|
154
|
+
health: () =>
|
|
155
|
+
Promise.resolve({
|
|
156
|
+
status: "healthy",
|
|
157
|
+
checkedAt: new Date().toISOString(),
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
}),
|
|
162
|
+
],
|
|
79
163
|
}),
|
|
80
|
-
).rejects.toThrow("
|
|
164
|
+
).rejects.toThrow("搜索 Provider不属于模块 known:other.search");
|
|
81
165
|
|
|
82
166
|
await expect(
|
|
83
167
|
initializeModuleHosts({
|
|
84
|
-
modules: [
|
|
168
|
+
modules: [manifest("known")],
|
|
85
169
|
initializers: [
|
|
86
170
|
initializer("known", (host) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
171
|
+
host.registerSearchProviders([
|
|
172
|
+
{
|
|
173
|
+
key: "known.search",
|
|
174
|
+
providerVersion: "0.1.0",
|
|
175
|
+
search: () => Promise.resolve({ hits: [] }),
|
|
176
|
+
health: () =>
|
|
177
|
+
Promise.resolve({
|
|
178
|
+
status: "healthy",
|
|
179
|
+
checkedAt: new Date().toISOString(),
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
94
183
|
}),
|
|
95
184
|
],
|
|
96
185
|
}),
|
|
97
|
-
).rejects.toThrow("
|
|
186
|
+
).rejects.toThrow("搜索 Provider 未在 Manifest 声明:known.search");
|
|
98
187
|
});
|
|
99
188
|
|
|
100
189
|
// @windy-module work-order begin
|
|
@@ -110,9 +199,10 @@ describe("模块 Host 初始化", () => {
|
|
|
110
199
|
),
|
|
111
200
|
).toBe(false);
|
|
112
201
|
|
|
202
|
+
const workOrder = createWorkOrderModule();
|
|
113
203
|
const withWorkOrder = await initializeModuleHosts({
|
|
114
|
-
modules: [
|
|
115
|
-
|
|
204
|
+
modules: [workOrder.manifest],
|
|
205
|
+
installations: [workOrder],
|
|
116
206
|
baseUploadPolicies: platformUploadPolicies,
|
|
117
207
|
});
|
|
118
208
|
const storage = createStorageRuntime(
|
|
@@ -129,6 +219,35 @@ describe("模块 Host 初始化", () => {
|
|
|
129
219
|
});
|
|
130
220
|
expect(session.purpose).toBe("work-order.ticket.attachment");
|
|
131
221
|
});
|
|
222
|
+
|
|
223
|
+
test("work-order 初始化器声明式注册路由、任务、Provider 与角色预设", async () => {
|
|
224
|
+
const workOrder = createWorkOrderModule();
|
|
225
|
+
const initialized = await initializeModuleHosts({
|
|
226
|
+
modules: [workOrder.manifest],
|
|
227
|
+
installations: [workOrder],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(
|
|
231
|
+
initialized.routes.map(
|
|
232
|
+
({ definition }) => `${definition.method} ${definition.path}`,
|
|
233
|
+
),
|
|
234
|
+
).toEqual([
|
|
235
|
+
"GET /api/work-orders",
|
|
236
|
+
"GET /api/work-orders/:id",
|
|
237
|
+
"POST /api/work-orders",
|
|
238
|
+
"PUT /api/work-orders/:id",
|
|
239
|
+
"DELETE /api/work-orders/:id",
|
|
240
|
+
]);
|
|
241
|
+
expect(
|
|
242
|
+
initialized.taskHandlers.map(({ registration }) => registration.taskKey),
|
|
243
|
+
).toEqual(["work-order.ticket.snapshot"]);
|
|
244
|
+
expect(
|
|
245
|
+
initialized.searchProviders.map(({ provider }) => provider.key),
|
|
246
|
+
).toEqual(["work-order.ticket.search"]);
|
|
247
|
+
expect(initialized.rolePresets.map(({ preset }) => preset.key)).toEqual([
|
|
248
|
+
"work-order.ticket-manager",
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
132
251
|
// @windy-module work-order end
|
|
133
252
|
});
|
|
134
253
|
|
|
@@ -139,13 +258,38 @@ function initializer(
|
|
|
139
258
|
return { moduleName, initialize };
|
|
140
259
|
}
|
|
141
260
|
|
|
142
|
-
function
|
|
261
|
+
function manifest(
|
|
262
|
+
name: string,
|
|
263
|
+
overrides: Partial<ModuleManifest> = {},
|
|
264
|
+
): ModuleManifest {
|
|
265
|
+
return {
|
|
266
|
+
name,
|
|
267
|
+
version: "0.1.0",
|
|
268
|
+
label: name,
|
|
269
|
+
description: `${name} 模块`,
|
|
270
|
+
menus: [],
|
|
271
|
+
adminRoutes: [],
|
|
272
|
+
permissions: [],
|
|
273
|
+
apiPermissions: [],
|
|
274
|
+
features: [],
|
|
275
|
+
migrations: [],
|
|
276
|
+
schemaExports: [],
|
|
277
|
+
tasks: [],
|
|
278
|
+
tools: [],
|
|
279
|
+
providers: [],
|
|
280
|
+
auditActions: [],
|
|
281
|
+
...overrides,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function permission(key: string, module: string) {
|
|
143
286
|
return {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
287
|
+
key,
|
|
288
|
+
label: key,
|
|
289
|
+
module,
|
|
290
|
+
moduleLabel: module,
|
|
291
|
+
resource: "item",
|
|
292
|
+
resourceLabel: "事项",
|
|
293
|
+
action: "read" as const,
|
|
150
294
|
};
|
|
151
295
|
}
|
|
@@ -1,113 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (const policy of input.baseUploadPolicies ?? []) {
|
|
36
|
-
if (uploadPurposes.has(policy.purpose)) {
|
|
37
|
-
throw new Error(`上传用途重复注册:${policy.purpose}`);
|
|
38
|
-
}
|
|
39
|
-
uploadPurposes.add(policy.purpose);
|
|
40
|
-
uploadPolicies.push(policy);
|
|
41
|
-
}
|
|
42
|
-
const handlerKeys = new Set<string>();
|
|
43
|
-
const handlerInstallers: HandlerInstaller[] = [];
|
|
44
|
-
|
|
45
|
-
for (const initializer of input.initializers) {
|
|
46
|
-
const moduleName = initializer.moduleName.trim();
|
|
47
|
-
if (!installedNames.has(moduleName)) {
|
|
48
|
-
throw new Error(`模块初始化器与已安装模块不匹配:${moduleName}`);
|
|
49
|
-
}
|
|
50
|
-
if (initializedNames.has(moduleName)) {
|
|
51
|
-
throw new Error(`模块初始化器重复注册:${moduleName}`);
|
|
52
|
-
}
|
|
53
|
-
initializedNames.add(moduleName);
|
|
54
|
-
await initializer.initialize(
|
|
55
|
-
createScopedHost({
|
|
56
|
-
moduleName,
|
|
57
|
-
uploadPolicies,
|
|
58
|
-
uploadPurposes,
|
|
59
|
-
handlerKeys,
|
|
60
|
-
handlerInstallers,
|
|
61
|
-
}),
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
uploadPolicies: [...uploadPolicies],
|
|
67
|
-
installDurableHandlers(registry) {
|
|
68
|
-
for (const install of handlerInstallers) install(registry);
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface ScopedHostState {
|
|
74
|
-
moduleName: string;
|
|
75
|
-
uploadPolicies: ModuleUploadPolicy[];
|
|
76
|
-
uploadPurposes: Set<string>;
|
|
77
|
-
handlerKeys: Set<string>;
|
|
78
|
-
handlerInstallers: HandlerInstaller[];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function createScopedHost(state: ScopedHostState): ModuleHost {
|
|
82
|
-
return {
|
|
83
|
-
moduleName: state.moduleName,
|
|
84
|
-
registerUploadPolicy(policy) {
|
|
85
|
-
requireModuleNamespace("上传用途", policy.purpose, state.moduleName);
|
|
86
|
-
if (state.uploadPurposes.has(policy.purpose)) {
|
|
87
|
-
throw new Error(`上传用途重复注册:${policy.purpose}`);
|
|
88
|
-
}
|
|
89
|
-
state.uploadPurposes.add(policy.purpose);
|
|
90
|
-
state.uploadPolicies.push(policy);
|
|
91
|
-
},
|
|
92
|
-
registerDurableHandler<T>(definition: DurableHandlerDefinition<T>) {
|
|
93
|
-
requireModuleNamespace("任务 handler", definition.key, state.moduleName);
|
|
94
|
-
if (state.handlerKeys.has(definition.key)) {
|
|
95
|
-
throw new Error(`任务 handler 重复注册:${definition.key}`);
|
|
96
|
-
}
|
|
97
|
-
state.handlerKeys.add(definition.key);
|
|
98
|
-
state.handlerInstallers.push((registry) => {
|
|
99
|
-
registry.register(definition);
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function requireModuleNamespace(
|
|
106
|
-
kind: string,
|
|
107
|
-
value: string,
|
|
108
|
-
moduleName: string,
|
|
109
|
-
): void {
|
|
110
|
-
if (value !== moduleName && !value.startsWith(`${moduleName}.`)) {
|
|
111
|
-
throw new Error(`${kind}不属于模块 ${moduleName}:${value}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
initializeModuleHosts,
|
|
3
|
+
type InitializedModuleHosts,
|
|
4
|
+
type InitializeModuleHostsInput,
|
|
5
|
+
type RegisteredModuleRolePreset,
|
|
6
|
+
type RegisteredModuleRoute,
|
|
7
|
+
type RegisteredModuleSearchProvider,
|
|
8
|
+
type RegisteredModuleTaskHandler,
|
|
9
|
+
type RegisteredModuleToolHandler,
|
|
10
|
+
} from "./module-host/initialize.js";
|
|
11
|
+
export {
|
|
12
|
+
buildModuleRequestContext,
|
|
13
|
+
requestActorFromModule,
|
|
14
|
+
toModuleActor,
|
|
15
|
+
} from "./module-host/request-context.js";
|
|
16
|
+
export {
|
|
17
|
+
installModuleRoutes,
|
|
18
|
+
type ModuleRouteInstallContext,
|
|
19
|
+
} from "./module-host/route-installer.js";
|
|
20
|
+
export {
|
|
21
|
+
moduleRolePresetId,
|
|
22
|
+
synchronizeModuleRolePresets,
|
|
23
|
+
} from "./module-host/role-presets.js";
|
|
24
|
+
export {
|
|
25
|
+
adaptModuleSearchProvider,
|
|
26
|
+
adaptModuleSearchProviders,
|
|
27
|
+
} from "./module-host/search-providers.js";
|
|
28
|
+
export {
|
|
29
|
+
createModuleStoragePort,
|
|
30
|
+
type ModuleStorageScope,
|
|
31
|
+
type ModuleStorageServices,
|
|
32
|
+
} from "./module-host/storage-port.js";
|
|
33
|
+
export { createModuleTaskDefinitions } from "./module-host/task-definitions.js";
|
|
34
|
+
export { createModuleToolHandlerMap } from "./module-host/tool-handlers.js";
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
entitlementKey,
|
|
4
|
+
featureKey,
|
|
5
|
+
licenseVersionKey,
|
|
6
|
+
} from "@southwind-ai/shared";
|
|
3
7
|
import { evaluateAccessGuards } from "./access-guards.js";
|
|
4
8
|
import { createServerRuntime } from "./runtime.js";
|
|
5
9
|
|
|
@@ -81,6 +85,113 @@ describe("access guard pipeline", () => {
|
|
|
81
85
|
expect("edition" in context.license!).toBe(false);
|
|
82
86
|
});
|
|
83
87
|
|
|
88
|
+
test("Feature 依赖未启用时拒绝并记录 security.denied,依赖启用后放行", async () => {
|
|
89
|
+
const dependency = {
|
|
90
|
+
key: featureKey("business.base"),
|
|
91
|
+
label: "业务基础",
|
|
92
|
+
module: "business",
|
|
93
|
+
visible: "visible" as const,
|
|
94
|
+
enabled: true,
|
|
95
|
+
stage: "stable" as const,
|
|
96
|
+
};
|
|
97
|
+
const capability = {
|
|
98
|
+
...dependency,
|
|
99
|
+
key: featureKey("business.report"),
|
|
100
|
+
dependencies: [dependency.key],
|
|
101
|
+
};
|
|
102
|
+
let dependencyEnabled = false;
|
|
103
|
+
const runtime = createServerRuntime(
|
|
104
|
+
{},
|
|
105
|
+
{
|
|
106
|
+
actorResolver: { resolve: () => actor(["business.report.read"]) },
|
|
107
|
+
licenseResolver: { resolve: () => ({ entitlements: [] }) },
|
|
108
|
+
featureCatalog: [dependency, capability],
|
|
109
|
+
featureResolver: {
|
|
110
|
+
resolve: (key) => ({
|
|
111
|
+
key,
|
|
112
|
+
enabled: key === dependency.key ? dependencyEnabled : true,
|
|
113
|
+
visible: "visible" as const,
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
const policy = {
|
|
119
|
+
permission: { permissionKey: "business.report.read" },
|
|
120
|
+
feature: capability,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const deniedContext = await runtime.createGuardContext(
|
|
124
|
+
new Request("http://local"),
|
|
125
|
+
);
|
|
126
|
+
const denied = await evaluateAccessGuards(runtime, deniedContext, policy);
|
|
127
|
+
|
|
128
|
+
expect(denied.decision.reason).toBe("dependency-missing");
|
|
129
|
+
expect(denied.feature?.auditEvent?.action).toBe("security.denied");
|
|
130
|
+
const auditEvents = await runtime.listAuditEvents({
|
|
131
|
+
page: 1,
|
|
132
|
+
pageSize: 10,
|
|
133
|
+
});
|
|
134
|
+
expect(
|
|
135
|
+
auditEvents.items.some(
|
|
136
|
+
(event) =>
|
|
137
|
+
event.action === "security.denied" &&
|
|
138
|
+
event.target?.id === "business.report",
|
|
139
|
+
),
|
|
140
|
+
).toBe(true);
|
|
141
|
+
|
|
142
|
+
dependencyEnabled = true;
|
|
143
|
+
const allowedContext = await runtime.createGuardContext(
|
|
144
|
+
new Request("http://local"),
|
|
145
|
+
);
|
|
146
|
+
const allowed = await evaluateAccessGuards(runtime, allowedContext, policy);
|
|
147
|
+
|
|
148
|
+
expect(allowed.decision.reason).toBe("allowed");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("Feature 依赖不在当前 License 版本内时 fail closed", async () => {
|
|
152
|
+
const dependency = {
|
|
153
|
+
key: featureKey("business.advanced-base"),
|
|
154
|
+
label: "高级业务基础",
|
|
155
|
+
module: "business",
|
|
156
|
+
visible: "visible" as const,
|
|
157
|
+
enabled: true,
|
|
158
|
+
stage: "stable" as const,
|
|
159
|
+
allowedLicenseVersions: [licenseVersionKey("advanced")],
|
|
160
|
+
};
|
|
161
|
+
const capability = {
|
|
162
|
+
...dependency,
|
|
163
|
+
key: featureKey("business.report"),
|
|
164
|
+
allowedLicenseVersions: [licenseVersionKey("standard")],
|
|
165
|
+
dependencies: [dependency.key],
|
|
166
|
+
};
|
|
167
|
+
const runtime = createServerRuntime(
|
|
168
|
+
{},
|
|
169
|
+
{
|
|
170
|
+
actorResolver: { resolve: () => actor(["business.report.read"]) },
|
|
171
|
+
licenseResolver: {
|
|
172
|
+
resolve: () => ({
|
|
173
|
+
versionKey: licenseVersionKey("standard"),
|
|
174
|
+
entitlements: [],
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
featureCatalog: [dependency, capability],
|
|
178
|
+
featureResolver: {
|
|
179
|
+
resolve: (key) => ({ key, enabled: true, visible: "visible" }),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
const context = await runtime.createGuardContext(
|
|
184
|
+
new Request("http://local"),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const result = await evaluateAccessGuards(runtime, context, {
|
|
188
|
+
permission: { permissionKey: "business.report.read" },
|
|
189
|
+
feature: capability,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(result.decision.reason).toBe("dependency-missing");
|
|
193
|
+
});
|
|
194
|
+
|
|
84
195
|
test("缺少 Entitlement 时不读取 Feature 状态,开关不能绕过商业授权", async () => {
|
|
85
196
|
let featureResolveCount = 0;
|
|
86
197
|
const runtime = createServerRuntime(
|