create-windy 0.2.32 → 0.2.34
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/dist/cli.js +9 -1
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +11 -0
- package/template/apps/agent-server/README.md +10 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +62 -1
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
- package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
- package/template/apps/agent-server/tests/test_api.py +16 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +2 -0
- package/template/apps/server/src/agent/run-facade.test.ts +52 -2
- package/template/apps/server/src/agent/run-facade.ts +36 -2
- package/template/apps/server/src/index.ts +28 -45
- package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
- package/template/apps/server/src/module-host/initialize.ts +30 -44
- package/template/apps/server/src/module-host/request-context.ts +23 -17
- package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
- package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
- package/template/apps/server/src/module-host/route-declaration.ts +45 -0
- package/template/apps/server/src/module-host/route-installer.ts +27 -3
- package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
- package/template/apps/server/src/module-host/storage-port.ts +134 -1
- package/template/apps/server/src/module-host/storage-scope.ts +96 -0
- package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
- package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
- package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
- package/template/apps/server/src/module-host/user-directory-port.ts +143 -0
- package/template/apps/server/src/module-host.ts +4 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +6 -0
- package/template/apps/server/src/settings/watermark-routes.test.ts +13 -3
- package/template/apps/server/src/settings/watermark-routes.ts +41 -3
- package/template/apps/server/src/system/built-in-roles.ts +1 -0
- package/template/apps/web/src/app/auth/login-page.ts +4 -0
- package/template/apps/web/src/composables/useLoginFlow.webtest.ts +91 -0
- package/template/apps/web/src/composables/useWatermarkSettings.ts +7 -3
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
- package/template/apps/web/src/composables/watermark-settings.ts +131 -9
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +73 -7
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +3 -1
- package/template/apps/web/src/layout/GlobalWatermark.vue +24 -11
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +25 -11
- package/template/apps/web/src/pages/auth/login-page-extension.ts +9 -0
- package/template/apps/web/src/pages/auth/login-page-extension.webtest.ts +10 -0
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +102 -4
- package/template/apps/web/src/router/index.ts +2 -2
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +3 -2
- package/template/docker-compose.yml +2 -0
- package/template/docs/architecture/ai-egress.md +49 -7
- package/template/docs/architecture/ai-runtime.md +20 -1
- package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
- package/template/docs/architecture/object-storage.md +22 -2
- package/template/docs/development/custom-login-page.md +90 -0
- package/template/docs/platform/agent-runtime.md +44 -1
- package/template/docs/platform/business-user-directory.md +82 -0
- package/template/docs/platform/platform-shell-settings.md +14 -9
- package/template/docs/platform/web-system-crud.md +1 -1
- package/template/packages/database/drizzle/0034_funny_mesmero.sql +5 -0
- package/template/packages/database/drizzle/meta/0034_snapshot.json +7253 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/src/schema/platform-settings.ts +13 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
- package/template/packages/modules/src/ai-operation-composition.ts +115 -0
- package/template/packages/modules/src/manifest.ts +26 -0
- package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
- package/template/packages/modules/src/system-audit-actions.ts +5 -0
- package/template/packages/modules/src/system-features.ts +5 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
- package/template/packages/modules/src/system-module-catalog.ts +26 -3
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +3 -2
- package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
- package/template/packages/server-sdk/src/module-host.ts +8 -0
- package/template/packages/server-sdk/src/request-context.ts +2 -0
- package/template/packages/server-sdk/src/storage-port.ts +66 -0
- package/template/packages/server-sdk/src/tool-registration.ts +6 -0
- package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
- package/template/packages/shared/src/platform-settings.ts +17 -1
|
@@ -6,7 +6,7 @@ import type { ModuleManifest } from "@southwind-ai/modules";
|
|
|
6
6
|
import {
|
|
7
7
|
requireModuleNamespace,
|
|
8
8
|
type ModuleHost,
|
|
9
|
-
type
|
|
9
|
+
type ModuleResourceFileAccessPolicy,
|
|
10
10
|
type ModuleUploadPolicy,
|
|
11
11
|
} from "@southwind-ai/server-sdk";
|
|
12
12
|
import type {
|
|
@@ -19,6 +19,11 @@ import type {
|
|
|
19
19
|
RegisteredModuleTaskHandler,
|
|
20
20
|
RegisteredModuleToolHandler,
|
|
21
21
|
} from "./initialize-contracts.js";
|
|
22
|
+
import {
|
|
23
|
+
registerResourceFileAccessPolicies,
|
|
24
|
+
validateResourceFileAccessPurposes,
|
|
25
|
+
} from "./resource-file-policies.js";
|
|
26
|
+
import { validateRouteDeclaration } from "./route-declaration.js";
|
|
22
27
|
|
|
23
28
|
export type {
|
|
24
29
|
InitializedModuleHosts,
|
|
@@ -53,6 +58,15 @@ export async function initializeModuleHosts(
|
|
|
53
58
|
uploadPolicies,
|
|
54
59
|
uploadPurposes,
|
|
55
60
|
modulePurposes: new Map<string, Set<string>>(),
|
|
61
|
+
moduleDependencies: new Map(
|
|
62
|
+
input.modules.map((module) => [module.name, module.dependencies ?? []]),
|
|
63
|
+
),
|
|
64
|
+
resourceFileAccessPolicies: [],
|
|
65
|
+
moduleResourceFileAccessPolicies: new Map<
|
|
66
|
+
string,
|
|
67
|
+
ModuleResourceFileAccessPolicy[]
|
|
68
|
+
>(),
|
|
69
|
+
resourceFileAccessPolicyKeys: new Set<string>(),
|
|
56
70
|
handlerKeys: new Set<string>(),
|
|
57
71
|
handlerInstallers: [],
|
|
58
72
|
routes: [],
|
|
@@ -104,10 +118,12 @@ export async function initializeModuleHosts(
|
|
|
104
118
|
}
|
|
105
119
|
await installation.setup(createScopedHost(manifest, state));
|
|
106
120
|
}
|
|
121
|
+
validateResourceFileAccessPurposes(state);
|
|
107
122
|
|
|
108
123
|
return {
|
|
109
124
|
manifests: input.modules,
|
|
110
125
|
uploadPolicies: [...state.uploadPolicies],
|
|
126
|
+
resourceFileAccessPolicies: [...state.resourceFileAccessPolicies],
|
|
111
127
|
routes: [...state.routes],
|
|
112
128
|
taskHandlers: [...state.taskHandlers],
|
|
113
129
|
toolHandlers: [...state.toolHandlers],
|
|
@@ -122,6 +138,9 @@ export async function initializeModuleHosts(
|
|
|
122
138
|
uploadPurposesOf(moduleName) {
|
|
123
139
|
return state.modulePurposes.get(moduleName) ?? new Set<string>();
|
|
124
140
|
},
|
|
141
|
+
resourceFileAccessPoliciesOf(moduleName) {
|
|
142
|
+
return state.moduleResourceFileAccessPolicies.get(moduleName) ?? [];
|
|
143
|
+
},
|
|
125
144
|
installDurableHandlers(registry) {
|
|
126
145
|
for (const install of state.handlerInstallers) install(registry);
|
|
127
146
|
},
|
|
@@ -132,6 +151,13 @@ interface HostCollections {
|
|
|
132
151
|
uploadPolicies: ModuleUploadPolicy[];
|
|
133
152
|
uploadPurposes: Set<string>;
|
|
134
153
|
modulePurposes: Map<string, Set<string>>;
|
|
154
|
+
moduleDependencies: ReadonlyMap<string, readonly string[]>;
|
|
155
|
+
resourceFileAccessPolicies: ModuleResourceFileAccessPolicy[];
|
|
156
|
+
moduleResourceFileAccessPolicies: Map<
|
|
157
|
+
string,
|
|
158
|
+
ModuleResourceFileAccessPolicy[]
|
|
159
|
+
>;
|
|
160
|
+
resourceFileAccessPolicyKeys: Set<string>;
|
|
135
161
|
handlerKeys: Set<string>;
|
|
136
162
|
handlerInstallers: HandlerInstaller[];
|
|
137
163
|
routes: RegisteredModuleRoute[];
|
|
@@ -167,6 +193,9 @@ function createScopedHost(
|
|
|
167
193
|
purposes.add(policy.purpose);
|
|
168
194
|
state.modulePurposes.set(moduleName, purposes);
|
|
169
195
|
},
|
|
196
|
+
registerResourceFileAccessPolicies(policies) {
|
|
197
|
+
registerResourceFileAccessPolicies(manifest, state, policies);
|
|
198
|
+
},
|
|
170
199
|
registerDurableHandler<T>(definition: DurableHandlerDefinition<T>) {
|
|
171
200
|
requireModuleNamespace("任务 handler", definition.key, moduleName);
|
|
172
201
|
if (state.handlerKeys.has(definition.key)) {
|
|
@@ -283,46 +312,3 @@ function createScopedHost(
|
|
|
283
312
|
},
|
|
284
313
|
};
|
|
285
314
|
}
|
|
286
|
-
|
|
287
|
-
function validateRouteDeclaration(
|
|
288
|
-
manifest: ModuleManifest,
|
|
289
|
-
definition: ModuleRouteDefinition,
|
|
290
|
-
): void {
|
|
291
|
-
const binding = manifest.apiPermissions.find(
|
|
292
|
-
(item) =>
|
|
293
|
-
item.method === definition.method && item.path === definition.path,
|
|
294
|
-
);
|
|
295
|
-
const route = `${definition.method} ${definition.path}`;
|
|
296
|
-
if (!binding) {
|
|
297
|
-
throw new Error(`模块路由未在 Manifest 声明:${route}`);
|
|
298
|
-
}
|
|
299
|
-
if (binding.permissionKey !== definition.permissionKey) {
|
|
300
|
-
throw new Error(`模块路由权限与 Manifest 声明不一致:${route}`);
|
|
301
|
-
}
|
|
302
|
-
if (
|
|
303
|
-
definition.featureKey !== undefined &&
|
|
304
|
-
binding.featureKey !== definition.featureKey
|
|
305
|
-
) {
|
|
306
|
-
throw new Error(`模块路由 Feature 与 Manifest 声明不一致:${route}`);
|
|
307
|
-
}
|
|
308
|
-
if (
|
|
309
|
-
binding.featureKey &&
|
|
310
|
-
!manifest.features.some(({ key }) => key === binding.featureKey)
|
|
311
|
-
) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`模块路由 Feature 未在 Manifest 注册:${binding.featureKey}`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
if (
|
|
317
|
-
definition.auditAction &&
|
|
318
|
-
!manifest.auditActions.some((action) =>
|
|
319
|
-
typeof action === "string"
|
|
320
|
-
? action === definition.auditAction
|
|
321
|
-
: action.key === definition.auditAction,
|
|
322
|
-
)
|
|
323
|
-
) {
|
|
324
|
-
throw new Error(
|
|
325
|
-
`模块路由审计动作未在 Manifest 注册:${definition.auditAction}`,
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
@@ -7,12 +7,17 @@ import type {
|
|
|
7
7
|
ModuleJobSubmitter,
|
|
8
8
|
ModuleRequestContext,
|
|
9
9
|
ModuleStoragePort,
|
|
10
|
+
ModuleUserDirectoryPort,
|
|
11
|
+
} from "@southwind-ai/server-sdk";
|
|
12
|
+
import {
|
|
13
|
+
ModuleUserDirectoryError,
|
|
14
|
+
requireModuleNamespace,
|
|
10
15
|
} from "@southwind-ai/server-sdk";
|
|
11
|
-
import { requireModuleNamespace } from "@southwind-ai/server-sdk";
|
|
12
16
|
import type { AuditEvent } from "@southwind-ai/shared";
|
|
13
17
|
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
14
18
|
import type { RequestActor, RequestGuardContext } from "../guards.js";
|
|
15
19
|
import type { createServerRuntime } from "../runtime.js";
|
|
20
|
+
import { unavailableModuleStoragePort } from "./storage-port.js";
|
|
16
21
|
|
|
17
22
|
type ServerRuntime = ReturnType<typeof createServerRuntime>;
|
|
18
23
|
|
|
@@ -58,6 +63,7 @@ export interface ModuleRequestContextInput {
|
|
|
58
63
|
runtime: ServerRuntime;
|
|
59
64
|
jobs?: DurableJobs;
|
|
60
65
|
storage?: ModuleStoragePort;
|
|
66
|
+
userDirectory?: ModuleUserDirectoryPort;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export function buildModuleRequestContext(
|
|
@@ -71,7 +77,22 @@ export function buildModuleRequestContext(
|
|
|
71
77
|
body: input.body,
|
|
72
78
|
audit: createModuleAuditEmitter(input),
|
|
73
79
|
jobs: createModuleJobSubmitter(input),
|
|
74
|
-
storage: input.storage ??
|
|
80
|
+
storage: input.storage ?? unavailableModuleStoragePort(input.moduleName),
|
|
81
|
+
userDirectory:
|
|
82
|
+
input.userDirectory ?? unavailableModuleUserDirectory(input.moduleName),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function unavailableModuleUserDirectory(
|
|
87
|
+
moduleName: string,
|
|
88
|
+
): ModuleUserDirectoryPort {
|
|
89
|
+
return {
|
|
90
|
+
async list() {
|
|
91
|
+
throw new ModuleUserDirectoryError(
|
|
92
|
+
"USER_DIRECTORY_UNAVAILABLE",
|
|
93
|
+
`模块 ${moduleName} 的用户目录 Port 未挂载`,
|
|
94
|
+
);
|
|
95
|
+
},
|
|
75
96
|
};
|
|
76
97
|
}
|
|
77
98
|
|
|
@@ -139,18 +160,3 @@ function createModuleJobSubmitter(
|
|
|
139
160
|
},
|
|
140
161
|
};
|
|
141
162
|
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 存储 Port 未挂载时的防御性实现:任何访问都以明确错误失败,
|
|
145
|
-
* 而不是让模块悄悄绕过 purpose / owner 约束。
|
|
146
|
-
*/
|
|
147
|
-
function unavailableModuleStorage(moduleName: string): ModuleStoragePort {
|
|
148
|
-
const fail = (): never => {
|
|
149
|
-
throw new Error(`模块 ${moduleName} 的存储 Port 未挂载`);
|
|
150
|
-
};
|
|
151
|
-
return {
|
|
152
|
-
createUploadSession: () => fail(),
|
|
153
|
-
openFile: () => fail(),
|
|
154
|
-
openPublicFile: () => fail(),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { featureKey } from "@southwind-ai/shared";
|
|
3
|
+
import type {
|
|
4
|
+
ModuleInitializer,
|
|
5
|
+
ModuleResourceFileAccessPolicy,
|
|
6
|
+
} from "@southwind-ai/server-sdk";
|
|
7
|
+
import { initializeModuleHosts } from "./initialize.js";
|
|
8
|
+
import { manifest } from "./test-fixtures.js";
|
|
9
|
+
|
|
10
|
+
describe("模块资源文件访问策略注册", () => {
|
|
11
|
+
test("组合保留同 Manifest 的 purpose、Feature、Permission 与 Audit 契约", async () => {
|
|
12
|
+
const declared = declaredManifest();
|
|
13
|
+
const policy = accessPolicy();
|
|
14
|
+
const hosts = await initializeModuleHosts({
|
|
15
|
+
modules: [declared],
|
|
16
|
+
initializers: [
|
|
17
|
+
initializer((host) => {
|
|
18
|
+
host.registerResourceFileAccessPolicies([policy]);
|
|
19
|
+
host.registerUploadPolicy(uploadPolicy());
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(hosts.resourceFileAccessPoliciesOf("evidence")).toEqual([policy]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("未注册 purpose、未知 Manifest 引用与重复策略均 fail-fast", async () => {
|
|
28
|
+
await expect(
|
|
29
|
+
initializeModuleHosts({
|
|
30
|
+
modules: [declaredManifest()],
|
|
31
|
+
initializers: [
|
|
32
|
+
initializer((host) => {
|
|
33
|
+
host.registerResourceFileAccessPolicies([accessPolicy()]);
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
).rejects.toThrow(
|
|
38
|
+
"资源文件访问策略用途未由模块 evidence 或其声明依赖注册:evidence.original",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await expect(
|
|
42
|
+
initializeModuleHosts({
|
|
43
|
+
modules: [declaredManifest()],
|
|
44
|
+
initializers: [
|
|
45
|
+
initializer((host) => {
|
|
46
|
+
host.registerUploadPolicy(uploadPolicy());
|
|
47
|
+
host.registerResourceFileAccessPolicies([
|
|
48
|
+
{ ...accessPolicy(), permissionKey: "evidence.unknown" },
|
|
49
|
+
]);
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
}),
|
|
53
|
+
).rejects.toThrow("资源文件访问权限未在 Manifest 声明:evidence.unknown");
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
initializeModuleHosts({
|
|
57
|
+
modules: [declaredManifest()],
|
|
58
|
+
initializers: [
|
|
59
|
+
initializer((host) => {
|
|
60
|
+
host.registerUploadPolicy(uploadPolicy());
|
|
61
|
+
host.registerResourceFileAccessPolicies([
|
|
62
|
+
accessPolicy(),
|
|
63
|
+
accessPolicy(),
|
|
64
|
+
]);
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
}),
|
|
68
|
+
).rejects.toThrow("资源文件访问策略重复注册");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("资源类型、用途和治理 Key 不能越过模块命名空间", async () => {
|
|
72
|
+
await expect(
|
|
73
|
+
initializeModuleHosts({
|
|
74
|
+
modules: [declaredManifest()],
|
|
75
|
+
initializers: [
|
|
76
|
+
initializer((host) => {
|
|
77
|
+
host.registerUploadPolicy(uploadPolicy());
|
|
78
|
+
host.registerResourceFileAccessPolicies([
|
|
79
|
+
{ ...accessPolicy(), resourceType: "other.file" },
|
|
80
|
+
]);
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
).rejects.toThrow("资源文件类型不属于模块 evidence:other.file");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("消费者可显式读取 Manifest 依赖模块拥有的 purpose", async () => {
|
|
88
|
+
const evidence = manifest("evidence");
|
|
89
|
+
const media = manifest("media", {
|
|
90
|
+
dependencies: ["evidence"],
|
|
91
|
+
permissions: [
|
|
92
|
+
{
|
|
93
|
+
key: "media.read",
|
|
94
|
+
label: "读取媒体",
|
|
95
|
+
module: "media",
|
|
96
|
+
moduleLabel: "媒体",
|
|
97
|
+
resource: "file",
|
|
98
|
+
resourceLabel: "文件",
|
|
99
|
+
action: "read",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
features: [
|
|
103
|
+
{
|
|
104
|
+
key: featureKey("media.catalog"),
|
|
105
|
+
label: "媒体目录",
|
|
106
|
+
module: "media",
|
|
107
|
+
visible: "visible",
|
|
108
|
+
enabled: true,
|
|
109
|
+
stage: "stable",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
auditActions: ["media.file.read"],
|
|
113
|
+
});
|
|
114
|
+
const policy: ModuleResourceFileAccessPolicy = {
|
|
115
|
+
purpose: "evidence.original",
|
|
116
|
+
resourceType: "media.evidence-file",
|
|
117
|
+
featureKey: "media.catalog",
|
|
118
|
+
permissionKey: "media.read",
|
|
119
|
+
auditAction: "media.file.read",
|
|
120
|
+
authorize: () => true,
|
|
121
|
+
};
|
|
122
|
+
const hosts = await initializeModuleHosts({
|
|
123
|
+
modules: [evidence, media],
|
|
124
|
+
installations: [
|
|
125
|
+
{
|
|
126
|
+
manifest: evidence,
|
|
127
|
+
setup(host) {
|
|
128
|
+
host.registerUploadPolicy(uploadPolicy());
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
manifest: media,
|
|
133
|
+
setup(host) {
|
|
134
|
+
host.registerResourceFileAccessPolicies([policy]);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(hosts.resourceFileAccessPoliciesOf("media")).toEqual([policy]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function initializer(
|
|
145
|
+
initialize: ModuleInitializer["initialize"],
|
|
146
|
+
): ModuleInitializer {
|
|
147
|
+
return { moduleName: "evidence", initialize };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function declaredManifest() {
|
|
151
|
+
return manifest("evidence", {
|
|
152
|
+
permissions: [
|
|
153
|
+
{
|
|
154
|
+
key: "evidence.read",
|
|
155
|
+
label: "读取物证",
|
|
156
|
+
module: "evidence",
|
|
157
|
+
moduleLabel: "物证",
|
|
158
|
+
resource: "file",
|
|
159
|
+
resourceLabel: "文件",
|
|
160
|
+
action: "read",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
features: [
|
|
164
|
+
{
|
|
165
|
+
key: featureKey("evidence.catalog"),
|
|
166
|
+
label: "物证目录",
|
|
167
|
+
module: "evidence",
|
|
168
|
+
visible: "visible",
|
|
169
|
+
enabled: true,
|
|
170
|
+
stage: "stable",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
auditActions: ["evidence.file.read"],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function accessPolicy(): ModuleResourceFileAccessPolicy {
|
|
178
|
+
return {
|
|
179
|
+
purpose: "evidence.original",
|
|
180
|
+
resourceType: "evidence.file",
|
|
181
|
+
featureKey: "evidence.catalog",
|
|
182
|
+
permissionKey: "evidence.read",
|
|
183
|
+
auditAction: "evidence.file.read",
|
|
184
|
+
authorize: () => true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function uploadPolicy() {
|
|
189
|
+
return {
|
|
190
|
+
purpose: "evidence.original",
|
|
191
|
+
maxByteSize: 1024,
|
|
192
|
+
maxPartByteSize: 512,
|
|
193
|
+
sessionTtlMs: 60_000,
|
|
194
|
+
allowedTypes: { "image/jpeg": ["jpg"] },
|
|
195
|
+
signature: "none" as const,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
2
|
+
import {
|
|
3
|
+
requireModuleNamespace,
|
|
4
|
+
type ModuleResourceFileAccessPolicy,
|
|
5
|
+
} from "@southwind-ai/server-sdk";
|
|
6
|
+
|
|
7
|
+
export interface ResourceFilePolicyCollections {
|
|
8
|
+
readonly modulePurposes: Map<string, Set<string>>;
|
|
9
|
+
readonly moduleDependencies: ReadonlyMap<string, readonly string[]>;
|
|
10
|
+
readonly resourceFileAccessPolicies: ModuleResourceFileAccessPolicy[];
|
|
11
|
+
readonly moduleResourceFileAccessPolicies: Map<
|
|
12
|
+
string,
|
|
13
|
+
ModuleResourceFileAccessPolicy[]
|
|
14
|
+
>;
|
|
15
|
+
readonly resourceFileAccessPolicyKeys: Set<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerResourceFileAccessPolicies(
|
|
19
|
+
manifest: ModuleManifest,
|
|
20
|
+
state: ResourceFilePolicyCollections,
|
|
21
|
+
policies: readonly ModuleResourceFileAccessPolicy[],
|
|
22
|
+
): void {
|
|
23
|
+
for (const policy of policies) {
|
|
24
|
+
validateNamespace(manifest, policy);
|
|
25
|
+
validateManifest(manifest, policy);
|
|
26
|
+
const key = `${manifest.name}:${policy.purpose}:${policy.resourceType}`;
|
|
27
|
+
if (state.resourceFileAccessPolicyKeys.has(key)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`资源文件访问策略重复注册:${policy.purpose} / ${policy.resourceType}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
state.resourceFileAccessPolicyKeys.add(key);
|
|
33
|
+
state.resourceFileAccessPolicies.push(policy);
|
|
34
|
+
const modulePolicies =
|
|
35
|
+
state.moduleResourceFileAccessPolicies.get(manifest.name) ?? [];
|
|
36
|
+
modulePolicies.push(policy);
|
|
37
|
+
state.moduleResourceFileAccessPolicies.set(manifest.name, modulePolicies);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateResourceFileAccessPurposes(
|
|
42
|
+
state: ResourceFilePolicyCollections,
|
|
43
|
+
): void {
|
|
44
|
+
for (const [moduleName, policies] of state.moduleResourceFileAccessPolicies) {
|
|
45
|
+
for (const policy of policies) {
|
|
46
|
+
const owner = [...state.modulePurposes].find(([, purposes]) =>
|
|
47
|
+
purposes.has(policy.purpose),
|
|
48
|
+
)?.[0];
|
|
49
|
+
const allowedOwners = new Set([
|
|
50
|
+
moduleName,
|
|
51
|
+
...(state.moduleDependencies.get(moduleName) ?? []),
|
|
52
|
+
]);
|
|
53
|
+
if (!owner || !allowedOwners.has(owner)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`资源文件访问策略用途未由模块 ${moduleName} 或其声明依赖注册:${policy.purpose}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateNamespace(
|
|
63
|
+
manifest: ModuleManifest,
|
|
64
|
+
policy: ModuleResourceFileAccessPolicy,
|
|
65
|
+
): void {
|
|
66
|
+
const moduleName = manifest.name;
|
|
67
|
+
if (
|
|
68
|
+
![moduleName, ...(manifest.dependencies ?? [])].some(
|
|
69
|
+
(owner) =>
|
|
70
|
+
policy.purpose === owner || policy.purpose.startsWith(`${owner}.`),
|
|
71
|
+
)
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`资源文件用途不属于模块 ${moduleName} 或其声明依赖:${policy.purpose}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
requireModuleNamespace("资源文件类型", policy.resourceType, moduleName);
|
|
78
|
+
requireModuleNamespace("资源文件权限", policy.permissionKey, moduleName);
|
|
79
|
+
requireModuleNamespace("资源文件 Feature", policy.featureKey, moduleName);
|
|
80
|
+
requireModuleNamespace("资源文件审计动作", policy.auditAction, moduleName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateManifest(
|
|
84
|
+
manifest: ModuleManifest,
|
|
85
|
+
policy: ModuleResourceFileAccessPolicy,
|
|
86
|
+
): void {
|
|
87
|
+
if (!manifest.permissions.some(({ key }) => key === policy.permissionKey)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`资源文件访问权限未在 Manifest 声明:${policy.permissionKey}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!manifest.features.some(({ key }) => key === policy.featureKey)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`资源文件访问 Feature 未在 Manifest 声明:${policy.featureKey}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
!manifest.auditActions.some((action) =>
|
|
99
|
+
typeof action === "string"
|
|
100
|
+
? action === policy.auditAction
|
|
101
|
+
: action.key === policy.auditAction,
|
|
102
|
+
)
|
|
103
|
+
) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`资源文件访问审计动作未在 Manifest 声明:${policy.auditAction}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
2
|
+
import type { ModuleRouteDefinition } from "@southwind-ai/server-sdk";
|
|
3
|
+
|
|
4
|
+
export function validateRouteDeclaration(
|
|
5
|
+
manifest: ModuleManifest,
|
|
6
|
+
definition: ModuleRouteDefinition,
|
|
7
|
+
): void {
|
|
8
|
+
const binding = manifest.apiPermissions.find(
|
|
9
|
+
(item) =>
|
|
10
|
+
item.method === definition.method && item.path === definition.path,
|
|
11
|
+
);
|
|
12
|
+
const route = `${definition.method} ${definition.path}`;
|
|
13
|
+
if (!binding) {
|
|
14
|
+
throw new Error(`模块路由未在 Manifest 声明:${route}`);
|
|
15
|
+
}
|
|
16
|
+
if (binding.permissionKey !== definition.permissionKey) {
|
|
17
|
+
throw new Error(`模块路由权限与 Manifest 声明不一致:${route}`);
|
|
18
|
+
}
|
|
19
|
+
if (
|
|
20
|
+
definition.featureKey !== undefined &&
|
|
21
|
+
binding.featureKey !== definition.featureKey
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(`模块路由 Feature 与 Manifest 声明不一致:${route}`);
|
|
24
|
+
}
|
|
25
|
+
if (
|
|
26
|
+
binding.featureKey &&
|
|
27
|
+
!manifest.features.some(({ key }) => key === binding.featureKey)
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`模块路由 Feature 未在 Manifest 注册:${binding.featureKey}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
definition.auditAction &&
|
|
35
|
+
!manifest.auditActions.some((action) =>
|
|
36
|
+
typeof action === "string"
|
|
37
|
+
? action === definition.auditAction
|
|
38
|
+
: action.key === definition.auditAction,
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`模块路由审计动作未在 Manifest 注册:${definition.auditAction}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
createModuleStoragePort,
|
|
11
11
|
type ModuleStorageServices,
|
|
12
12
|
} from "./storage-port.js";
|
|
13
|
+
import {
|
|
14
|
+
createModuleUserDirectoryPort,
|
|
15
|
+
type ModuleUserDirectoryServices,
|
|
16
|
+
} from "./user-directory-port.js";
|
|
17
|
+
import { createModuleStorageScope } from "./storage-scope.js";
|
|
13
18
|
|
|
14
19
|
type ServerRuntime = ReturnType<typeof createServerRuntime>;
|
|
15
20
|
|
|
@@ -21,6 +26,8 @@ export interface ModuleRouteInstallContext {
|
|
|
21
26
|
jobs?: DurableJobs;
|
|
22
27
|
/** 平台存储服务;提供后模块获得受限 Upload/BlobRead Port。 */
|
|
23
28
|
storage?: ModuleStorageServices;
|
|
29
|
+
/** 平台身份目录;Adapter 会绑定当前请求 Actor 与服务端数据范围。 */
|
|
30
|
+
userDirectory?: Omit<ModuleUserDirectoryServices, "runtime" | "features">;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
@@ -69,11 +76,28 @@ export function installModuleRoutes(
|
|
|
69
76
|
jobs: context.jobs,
|
|
70
77
|
storage: context.storage
|
|
71
78
|
? createModuleStoragePort(context.storage, {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
...createModuleStorageScope({
|
|
80
|
+
moduleName,
|
|
81
|
+
purposes: hosts.uploadPurposesOf(moduleName),
|
|
82
|
+
resourceFileAccessPolicies:
|
|
83
|
+
hosts.resourceFileAccessPoliciesOf(moduleName),
|
|
84
|
+
guardContext: route.guardContext,
|
|
85
|
+
runtime: context.runtime,
|
|
86
|
+
features: context.features,
|
|
87
|
+
signal: new AbortController().signal,
|
|
88
|
+
}),
|
|
75
89
|
})
|
|
76
90
|
: undefined,
|
|
91
|
+
userDirectory: context.userDirectory
|
|
92
|
+
? createModuleUserDirectoryPort(
|
|
93
|
+
{
|
|
94
|
+
...context.userDirectory,
|
|
95
|
+
runtime: context.runtime,
|
|
96
|
+
features: context.features,
|
|
97
|
+
},
|
|
98
|
+
route.guardContext,
|
|
99
|
+
)
|
|
100
|
+
: undefined,
|
|
77
101
|
});
|
|
78
102
|
const result = await definition.handler(requestContext);
|
|
79
103
|
if (definition.auditAction) {
|