create-windy 0.2.33 → 0.3.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.
- package/dist/cli.js +173 -17
- 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 +9 -0
- package/template/README.md +3 -1
- package/template/apps/agent-server/Dockerfile +1 -0
- package/template/apps/agent-server/README.md +28 -5
- package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
- package/template/apps/agent-server/pyproject.toml +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
- package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
- 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_adk_adapter.py +305 -0
- package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -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/package.json +4 -1
- 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 +38 -9
- package/template/apps/server/src/agent/tool-registry.ts +2 -6
- package/template/apps/server/src/application-services.ts +50 -0
- package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
- package/template/apps/server/src/audit/search-summary.ts +2 -6
- package/template/apps/server/src/auth/credential-restriction.ts +1 -0
- package/template/apps/server/src/auth/password.ts +30 -21
- package/template/apps/server/src/auth/routes.test.ts +44 -3
- package/template/apps/server/src/auth/routes.ts +6 -1
- package/template/apps/server/src/auth/runtime.ts +4 -1
- package/template/apps/server/src/auth/service.ts +25 -7
- package/template/apps/server/src/configuration/bootstrap.ts +20 -1
- package/template/apps/server/src/configuration/definition.test.ts +71 -0
- package/template/apps/server/src/configuration/definition.ts +242 -12
- package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
- package/template/apps/server/src/configuration/repository.ts +27 -2
- package/template/apps/server/src/configuration/routes.ts +4 -1
- package/template/apps/server/src/configuration/service.test.ts +100 -0
- package/template/apps/server/src/configuration/service.ts +50 -10
- package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
- package/template/apps/server/src/data-governance/export/service.ts +2 -6
- package/template/apps/server/src/example-modules.ts +4 -1
- package/template/apps/server/src/guards.ts +18 -5
- package/template/apps/server/src/http/request-source.test.ts +85 -0
- package/template/apps/server/src/http/request-source.ts +90 -0
- package/template/apps/server/src/index.ts +45 -45
- package/template/apps/server/src/license/routes.ts +2 -0
- package/template/apps/server/src/license/runtime-service.ts +10 -11
- package/template/apps/server/src/module-bootstrap.ts +2 -2
- 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 +29 -23
- 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 +89 -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 +139 -0
- package/template/apps/server/src/module-host.ts +4 -0
- package/template/apps/server/src/notification/channel-adapter.ts +46 -0
- package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
- package/template/apps/server/src/notification/delivery-policy.ts +37 -0
- package/template/apps/server/src/notification/delivery-repository.ts +198 -0
- package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
- package/template/apps/server/src/notification/delivery-worker.ts +88 -0
- package/template/apps/server/src/notification/drizzle-management.ts +180 -0
- package/template/apps/server/src/notification/drizzle-query.ts +160 -0
- package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
- package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
- package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
- package/template/apps/server/src/notification/extension-routes.ts +111 -0
- package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
- package/template/apps/server/src/notification/memory-support.ts +72 -0
- package/template/apps/server/src/notification/preference-repository.ts +95 -0
- package/template/apps/server/src/notification/recipient-directory.ts +104 -0
- package/template/apps/server/src/notification/repository.test.ts +171 -0
- package/template/apps/server/src/notification/repository.ts +179 -32
- package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
- package/template/apps/server/src/notification/route-support.ts +225 -0
- package/template/apps/server/src/notification/routes.test.ts +182 -0
- package/template/apps/server/src/notification/routes.ts +225 -132
- package/template/apps/server/src/notification/service.test.ts +104 -0
- package/template/apps/server/src/notification/service.ts +140 -0
- package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
- package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
- package/template/apps/server/src/persistence.integration.test.ts +41 -2
- package/template/apps/server/src/route-guards.ts +1 -0
- package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
- package/template/apps/server/src/runtime-development.ts +2 -0
- package/template/apps/server/src/runtime.test.ts +23 -9
- package/template/apps/server/src/runtime.ts +39 -2
- package/template/apps/server/src/scheduler/audit.ts +2 -11
- package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
- package/template/apps/server/src/scheduler/definitions.ts +7 -0
- package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
- package/template/apps/server/src/scheduler/routes.ts +2 -7
- package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
- package/template/apps/server/src/search/opensearch-client.ts +145 -0
- package/template/apps/server/src/search/service.ts +2 -6
- package/template/apps/server/src/settings/routes.ts +15 -3
- package/template/apps/server/src/settings/runtime.ts +10 -1
- package/template/apps/server/src/storage/runtime.ts +13 -0
- package/template/apps/server/src/system/audit-query.ts +33 -4
- package/template/apps/server/src/system/audit-routes.test.ts +58 -4
- package/template/apps/server/src/system/audit-routes.ts +6 -5
- package/template/apps/server/src/system/built-in-roles.ts +1 -0
- package/template/apps/server/src/system/department-organization.ts +320 -0
- package/template/apps/server/src/system/department-routes.test.ts +207 -48
- package/template/apps/server/src/system/department-routes.ts +114 -33
- package/template/apps/server/src/system/drizzle-repository.ts +15 -7
- package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
- package/template/apps/server/src/system/drizzle-system.ts +37 -9
- package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
- package/template/apps/server/src/system/identity-route-config.ts +7 -2
- package/template/apps/server/src/system/mutation-runner.ts +23 -0
- package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
- package/template/apps/server/src/system/resource-access-response.ts +12 -6
- package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
- package/template/apps/server/src/system/route-helpers.ts +2 -5
- package/template/apps/server/src/system/route-types.ts +12 -3
- package/template/apps/server/src/system/routes-validation.test.ts +26 -0
- package/template/apps/server/src/system/routes.test.ts +10 -2
- package/template/apps/server/src/system/routes.ts +70 -36
- package/template/apps/server/src/system/seed.ts +7 -0
- package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
- package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
- package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
- package/template/apps/server/src/work-order/search-provider.ts +60 -1
- package/template/apps/server/src/work-order/service.ts +42 -10
- package/template/apps/web/src/app/error-pages.ts +9 -0
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
- package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
- package/template/apps/web/src/components/common/FormDialog.vue +16 -2
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
- package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
- package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
- package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
- package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
- package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
- package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
- package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
- package/template/apps/web/src/lib/date-time.ts +22 -10
- package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
- package/template/apps/web/src/main.ts +14 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
- package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
- package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
- package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
- package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
- package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
- package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
- package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
- package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
- package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
- package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
- package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
- package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
- package/template/apps/web/src/pages/system/resource-config.ts +12 -4
- package/template/apps/web/src/router/error-navigation.ts +50 -0
- package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
- package/template/apps/web/src/router/index.ts +71 -0
- package/template/apps/web/src/router/index.webtest.ts +29 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/auth-api.ts +5 -0
- package/template/apps/web/src/services/http.ts +43 -6
- package/template/apps/web/src/services/http.webtest.ts +70 -0
- package/template/apps/web/src/services/notification-api.ts +85 -2
- package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
- package/template/apps/web/src/services/search-api.ts +39 -0
- package/template/apps/web/src/services/system-api.ts +18 -0
- package/template/apps/web/vitest.config.ts +1 -0
- package/template/docker-compose.yml +68 -0
- package/template/docs/architecture/ai-egress.md +49 -7
- package/template/docs/architecture/ai-runtime.md +34 -6
- package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
- package/template/docs/architecture/object-storage.md +22 -2
- package/template/docs/architecture/search-provider-protocol.md +38 -2
- package/template/docs/development/custom-error-pages.md +78 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +76 -7
- package/template/docs/platform/audit-reliability.md +46 -2
- package/template/docs/platform/business-user-directory.md +82 -0
- package/template/docs/platform/module-configuration.md +13 -4
- package/template/docs/platform/notifications.md +67 -11
- package/template/docs/platform/organization-membership.md +17 -3
- package/template/docs/platform/system-crud-api.md +16 -0
- package/template/docs/platform/web-system-crud.md +6 -2
- package/template/package.json +4 -0
- package/template/packages/config/index.test.ts +45 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +169 -48
- package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
- package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
- package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
- package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
- package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
- package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
- package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
- package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/identity.ts +7 -0
- package/template/packages/database/src/schema/notifications.ts +135 -0
- package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
- package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
- 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-api-permissions.ts +54 -0
- package/template/packages/modules/src/system-audit-actions.ts +15 -0
- package/template/packages/modules/src/system-features.ts +6 -1
- package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
- package/template/packages/modules/src/system-module-catalog.ts +28 -4
- package/template/packages/modules/src/system-modules.test.ts +13 -1
- package/template/packages/modules/src/system-modules.ts +12 -0
- package/template/packages/modules/src/system-permissions.ts +11 -5
- 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/package.json +1 -1
- package/template/packages/shared/src/audit.ts +25 -0
- package/template/packages/shared/src/module-configuration.ts +5 -0
- package/template/packages/shared/src/notification.ts +92 -1
- package/template/packages/shared/src/password.ts +7 -2
- package/template/packages/shared/src/platform-settings.ts +2 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/upload-service.test.ts +16 -0
- package/template/packages/storage/src/upload-service.ts +5 -1
- package/template/scripts/notification-postgres-regression.ts +27 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Transporter } from "nodemailer";
|
|
3
|
+
import {
|
|
4
|
+
NotificationChannelError,
|
|
5
|
+
type NotificationChannelMessage,
|
|
6
|
+
} from "./channel-adapter.js";
|
|
7
|
+
import { SmtpNotificationChannelAdapter } from "./smtp-adapter.js";
|
|
8
|
+
|
|
9
|
+
describe("SmtpNotificationChannelAdapter", () => {
|
|
10
|
+
test("使用类型化 mail 配置发件且不向消息暴露 smtpUrl", async () => {
|
|
11
|
+
const sent: unknown[] = [];
|
|
12
|
+
const transporter = {
|
|
13
|
+
async sendMail(message: unknown) {
|
|
14
|
+
sent.push(message);
|
|
15
|
+
return { messageId: "mail-1" };
|
|
16
|
+
},
|
|
17
|
+
} as unknown as Transporter;
|
|
18
|
+
const adapter = new SmtpNotificationChannelAdapter(config, transporter);
|
|
19
|
+
expect(await adapter.deliver(message)).toEqual({
|
|
20
|
+
providerMessageId: "mail-1",
|
|
21
|
+
});
|
|
22
|
+
expect(sent[0]).toMatchObject({
|
|
23
|
+
from: "notify@example.com",
|
|
24
|
+
to: "user@example.com",
|
|
25
|
+
subject: "测试通知",
|
|
26
|
+
text: "正文",
|
|
27
|
+
});
|
|
28
|
+
expect(JSON.stringify(sent[0])).not.toContain("smtp://");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("认证错误标记为不可重试且只保留标准化错误码", async () => {
|
|
32
|
+
const transporter = {
|
|
33
|
+
async sendMail() {
|
|
34
|
+
throw Object.assign(new Error("包含服务端敏感响应"), { code: "EAUTH" });
|
|
35
|
+
},
|
|
36
|
+
} as unknown as Transporter;
|
|
37
|
+
const adapter = new SmtpNotificationChannelAdapter(config, transporter);
|
|
38
|
+
try {
|
|
39
|
+
await adapter.deliver(message);
|
|
40
|
+
throw new Error("expected rejection");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
expect(error).toBeInstanceOf(NotificationChannelError);
|
|
43
|
+
expect(error).toMatchObject({ code: "EAUTH", retryable: false });
|
|
44
|
+
expect(String(error)).not.toContain("敏感响应");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const config = {
|
|
50
|
+
enabled: true,
|
|
51
|
+
host: "smtp.example.com",
|
|
52
|
+
port: 587,
|
|
53
|
+
secure: false,
|
|
54
|
+
fromAddress: "notify@example.com",
|
|
55
|
+
connectTimeoutMs: 5_000,
|
|
56
|
+
smtpUrl: "smtp://secret@example.com",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const message: NotificationChannelMessage = {
|
|
60
|
+
deliveryId: "delivery-1",
|
|
61
|
+
userId: "user-1",
|
|
62
|
+
address: "user@example.com",
|
|
63
|
+
subject: "测试通知",
|
|
64
|
+
content: "正文",
|
|
65
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
import type { Transporter } from "nodemailer";
|
|
3
|
+
import type { PlatformConfig } from "@southwind-ai/config";
|
|
4
|
+
import {
|
|
5
|
+
NotificationChannelError,
|
|
6
|
+
type NotificationChannelAdapter,
|
|
7
|
+
type NotificationChannelMessage,
|
|
8
|
+
} from "./channel-adapter.js";
|
|
9
|
+
|
|
10
|
+
export class SmtpNotificationChannelAdapter implements NotificationChannelAdapter {
|
|
11
|
+
readonly channel = "email" as const;
|
|
12
|
+
private readonly transporter: Transporter;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly config: PlatformConfig["mail"],
|
|
16
|
+
transporter?: Transporter,
|
|
17
|
+
) {
|
|
18
|
+
if (!config.enabled || !config.fromAddress) {
|
|
19
|
+
throw new Error("SMTP 通知渠道未启用或未配置发件地址");
|
|
20
|
+
}
|
|
21
|
+
this.transporter =
|
|
22
|
+
transporter ||
|
|
23
|
+
nodemailer.createTransport(
|
|
24
|
+
config.smtpUrl || {
|
|
25
|
+
host: config.host,
|
|
26
|
+
port: config.port,
|
|
27
|
+
secure: config.secure,
|
|
28
|
+
connectionTimeout: config.connectTimeoutMs,
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async deliver(message: NotificationChannelMessage) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await this.transporter.sendMail({
|
|
36
|
+
from: this.config.fromAddress,
|
|
37
|
+
to: message.address,
|
|
38
|
+
subject: message.subject,
|
|
39
|
+
text: message.content,
|
|
40
|
+
headers: { "X-Windy-Delivery-Id": message.deliveryId },
|
|
41
|
+
});
|
|
42
|
+
return { providerMessageId: result.messageId || undefined };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const code = smtpErrorCode(error);
|
|
45
|
+
throw new NotificationChannelError(code, isRetryableSmtpError(error));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function smtpErrorCode(error: unknown) {
|
|
51
|
+
if (!error || typeof error !== "object") return "SMTP_DELIVERY_FAILED";
|
|
52
|
+
const value = error as { code?: unknown; responseCode?: unknown };
|
|
53
|
+
const code =
|
|
54
|
+
typeof value.code === "string" ? value.code.toUpperCase() : undefined;
|
|
55
|
+
const responseCode =
|
|
56
|
+
typeof value.responseCode === "number" ? value.responseCode : undefined;
|
|
57
|
+
return (
|
|
58
|
+
code || (responseCode ? `SMTP_${responseCode}` : "SMTP_DELIVERY_FAILED")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRetryableSmtpError(error: unknown) {
|
|
63
|
+
if (!error || typeof error !== "object") return true;
|
|
64
|
+
const value = error as { code?: unknown; responseCode?: unknown };
|
|
65
|
+
if (typeof value.responseCode === "number")
|
|
66
|
+
return value.responseCode >= 400 && value.responseCode < 500;
|
|
67
|
+
return !["EAUTH", "EENVELOPE", "EMESSAGE"].includes(String(value.code || ""));
|
|
68
|
+
}
|
|
@@ -4,7 +4,10 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
5
5
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
6
6
|
import { Pool } from "pg";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DrizzleAuditLogReader,
|
|
9
|
+
DrizzleAuditLogWriter,
|
|
10
|
+
} from "./audit/drizzle-log-store.js";
|
|
8
11
|
|
|
9
12
|
const databaseUrl = process.env.AUDIT_TEST_DATABASE_URL;
|
|
10
13
|
const describePostgres = databaseUrl ? describe : describe.skip;
|
|
@@ -12,6 +15,7 @@ const describePostgres = databaseUrl ? describe : describe.skip;
|
|
|
12
15
|
describePostgres("PostgreSQL audit writer", () => {
|
|
13
16
|
let pool: Pool;
|
|
14
17
|
let writer: DrizzleAuditLogWriter;
|
|
18
|
+
let reader: DrizzleAuditLogReader;
|
|
15
19
|
|
|
16
20
|
beforeAll(async () => {
|
|
17
21
|
pool = new Pool({ connectionString: databaseUrl });
|
|
@@ -25,10 +29,13 @@ describePostgres("PostgreSQL audit writer", () => {
|
|
|
25
29
|
migrationsTable: "__audit01_test_migrations",
|
|
26
30
|
});
|
|
27
31
|
writer = new DrizzleAuditLogWriter(db);
|
|
32
|
+
reader = new DrizzleAuditLogReader(db);
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
afterAll(async () => {
|
|
31
|
-
await pool?.query(
|
|
36
|
+
await pool?.query(
|
|
37
|
+
"delete from audit_logs where id in ('audit_pg_1', 'audit_pg_search_1')",
|
|
38
|
+
);
|
|
32
39
|
await pool?.end();
|
|
33
40
|
});
|
|
34
41
|
|
|
@@ -51,4 +58,36 @@ describePostgres("PostgreSQL audit writer", () => {
|
|
|
51
58
|
);
|
|
52
59
|
expect(result.rows[0]?.count).toBe("1");
|
|
53
60
|
});
|
|
61
|
+
|
|
62
|
+
test("关键字与分钟范围在 PostgreSQL Reader 中共同生效", async () => {
|
|
63
|
+
await writer.write({
|
|
64
|
+
id: "audit_pg_search_1",
|
|
65
|
+
action: "case.review",
|
|
66
|
+
outcome: "success",
|
|
67
|
+
actor: {
|
|
68
|
+
id: "user_1",
|
|
69
|
+
type: "user",
|
|
70
|
+
name: "审计员",
|
|
71
|
+
ip: "203.0.113.8",
|
|
72
|
+
},
|
|
73
|
+
target: { type: "case", id: "case_1", label: "案件甲" },
|
|
74
|
+
module: "case-workbench",
|
|
75
|
+
requestId: "request_pg_1",
|
|
76
|
+
message: "完成复核",
|
|
77
|
+
metadata: { note: "线索已确认" },
|
|
78
|
+
occurredAt: "2026-07-20T01:15:30.000Z",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await reader.list({
|
|
82
|
+
page: 1,
|
|
83
|
+
pageSize: 10,
|
|
84
|
+
q: "案件甲",
|
|
85
|
+
actorId: "user_1",
|
|
86
|
+
action: "case.review",
|
|
87
|
+
occurredAfter: "2026-07-20T01:15:00.000Z",
|
|
88
|
+
occurredBefore: "2026-07-20T01:15:59.999Z",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.items.map(({ id }) => id)).toContain("audit_pg_search_1");
|
|
92
|
+
});
|
|
54
93
|
});
|
|
@@ -44,6 +44,7 @@ export function systemFeatureKeyForResource(resourceName: string): string {
|
|
|
44
44
|
audit: "system.audit",
|
|
45
45
|
license: "system.license",
|
|
46
46
|
ops: "system.ops",
|
|
47
|
+
notification: "system.notification",
|
|
47
48
|
scheduler: "system.scheduler",
|
|
48
49
|
settings: "system.settings",
|
|
49
50
|
};
|
|
@@ -95,4 +95,43 @@ describe("server runtime audit list", () => {
|
|
|
95
95
|
expect(hidden.items.map(({ id }) => id)).toEqual(["audit_user"]);
|
|
96
96
|
expect(included.total).toBe(2);
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
test("无数据库时关键字覆盖动作、操作者、对象、IP 和已脱敏上下文", async () => {
|
|
100
|
+
const runtime = createServerRuntime();
|
|
101
|
+
runtime.auditSink.recordEvent({
|
|
102
|
+
id: "audit_keyword",
|
|
103
|
+
action: "case.review",
|
|
104
|
+
outcome: "success",
|
|
105
|
+
actor: {
|
|
106
|
+
id: "user_1",
|
|
107
|
+
type: "user",
|
|
108
|
+
name: "审计员",
|
|
109
|
+
ip: "203.0.113.8",
|
|
110
|
+
},
|
|
111
|
+
target: { type: "case", id: "case_1", label: "案件甲" },
|
|
112
|
+
module: "case-workbench",
|
|
113
|
+
requestId: "request_1",
|
|
114
|
+
message: "完成复核",
|
|
115
|
+
metadata: { note: "线索已确认", token: "[REDACTED]" },
|
|
116
|
+
occurredAt: "2026-07-14T01:00:00.000Z",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
for (const q of [
|
|
120
|
+
"case.review",
|
|
121
|
+
"审计员",
|
|
122
|
+
"案件甲",
|
|
123
|
+
"203.0.113.8",
|
|
124
|
+
"完成复核",
|
|
125
|
+
"case-workbench",
|
|
126
|
+
"request_1",
|
|
127
|
+
"线索已确认",
|
|
128
|
+
]) {
|
|
129
|
+
const result = await runtime.listAuditEvents({
|
|
130
|
+
page: 1,
|
|
131
|
+
pageSize: 10,
|
|
132
|
+
q,
|
|
133
|
+
});
|
|
134
|
+
expect(result.items.map(({ id }) => id)).toEqual(["audit_keyword"]);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
98
137
|
});
|
|
@@ -25,6 +25,7 @@ export function developmentAdminActor(): RequestActor {
|
|
|
25
25
|
"system.menu.read",
|
|
26
26
|
"system.user.read",
|
|
27
27
|
"system.user.manage",
|
|
28
|
+
"system.user-directory.read",
|
|
28
29
|
"system.role.read",
|
|
29
30
|
"system.role.manage",
|
|
30
31
|
"system.department.read",
|
|
@@ -51,6 +52,7 @@ export function developmentAdminActor(): RequestActor {
|
|
|
51
52
|
"system.scheduler.read",
|
|
52
53
|
"system.scheduler.manage",
|
|
53
54
|
"system.notification.read",
|
|
55
|
+
"system.notification.update",
|
|
54
56
|
"system.notification.manage",
|
|
55
57
|
"system.bulk-data.manage",
|
|
56
58
|
"system.storage.read",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type FeatureFlagDefinition,
|
|
8
8
|
} from "@southwind-ai/shared";
|
|
9
9
|
import { createFoundationSnapshot } from "./foundation.js";
|
|
10
|
+
import { auditActor } from "./guards.js";
|
|
10
11
|
import { createServerRuntime } from "./runtime.js";
|
|
11
12
|
import { AuditWriteUnavailableError } from "./audit/contracts.js";
|
|
12
13
|
|
|
@@ -136,30 +137,43 @@ describe("server runtime", () => {
|
|
|
136
137
|
});
|
|
137
138
|
|
|
138
139
|
test("外部 actor resolver 优先提供数据库角色权限来源", async () => {
|
|
140
|
+
const sessionActor = {
|
|
141
|
+
id: "user_from_db",
|
|
142
|
+
type: "user" as const,
|
|
143
|
+
name: "数据库用户",
|
|
144
|
+
roleCodes: ["operator"],
|
|
145
|
+
permissionKeys: ["system.audit.read"],
|
|
146
|
+
};
|
|
139
147
|
const runtime = createServerRuntime(
|
|
140
148
|
{ WINDY_DEV_ADMIN_TOKEN: "token_1" },
|
|
141
149
|
{
|
|
142
150
|
actorResolver: {
|
|
143
151
|
async resolve() {
|
|
144
|
-
return
|
|
145
|
-
id: "user_from_db",
|
|
146
|
-
type: "user",
|
|
147
|
-
name: "数据库用户",
|
|
148
|
-
roleCodes: ["operator"],
|
|
149
|
-
permissionKeys: ["system.audit.read"],
|
|
150
|
-
};
|
|
152
|
+
return sessionActor;
|
|
151
153
|
},
|
|
152
154
|
},
|
|
153
155
|
},
|
|
154
156
|
);
|
|
155
157
|
const request = new Request("http://localhost/api/system/audit-logs", {
|
|
156
|
-
headers: {
|
|
158
|
+
headers: {
|
|
159
|
+
authorization: "Bearer token_1",
|
|
160
|
+
"user-agent": "Windy-Audit-Test/1.0",
|
|
161
|
+
"x-forwarded-for": "203.0.113.99",
|
|
162
|
+
},
|
|
157
163
|
});
|
|
158
164
|
|
|
159
|
-
const context = await runtime.createGuardContext(request
|
|
165
|
+
const context = await runtime.createGuardContext(request, {
|
|
166
|
+
clientIp: "198.51.100.10",
|
|
167
|
+
});
|
|
160
168
|
|
|
161
169
|
expect(context.actor.id).toBe("user_from_db");
|
|
162
170
|
expect(context.actor.permissionKeys).toEqual(["system.audit.read"]);
|
|
171
|
+
expect(auditActor(context)).toMatchObject({
|
|
172
|
+
ip: "198.51.100.10",
|
|
173
|
+
userAgent: "Windy-Audit-Test/1.0",
|
|
174
|
+
});
|
|
175
|
+
expect(sessionActor).not.toHaveProperty("ip");
|
|
176
|
+
expect(sessionActor).not.toHaveProperty("userAgent");
|
|
163
177
|
});
|
|
164
178
|
|
|
165
179
|
test("外部 license resolver 优先提供真实 License 授权状态", async () => {
|
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
type FeatureFlagRuntimeResolver,
|
|
28
28
|
type ResolvedFeatureFlag,
|
|
29
29
|
} from "./runtime-feature.js";
|
|
30
|
+
import {
|
|
31
|
+
requestSourcePolicyFromEnv,
|
|
32
|
+
resolveRequestSource,
|
|
33
|
+
} from "./http/request-source.js";
|
|
30
34
|
|
|
31
35
|
export type {
|
|
32
36
|
FeatureFlagRuntimeResolver,
|
|
@@ -53,6 +57,7 @@ export interface AuditEventWriter {
|
|
|
53
57
|
export interface AuditEventListQuery {
|
|
54
58
|
page: number;
|
|
55
59
|
pageSize: number;
|
|
60
|
+
q?: string;
|
|
56
61
|
actorId?: string;
|
|
57
62
|
action?: string;
|
|
58
63
|
resource?: string;
|
|
@@ -198,6 +203,7 @@ export function createServerRuntime(
|
|
|
198
203
|
options.featureResolver,
|
|
199
204
|
env.NODE_ENV === "production",
|
|
200
205
|
);
|
|
206
|
+
const requestSourcePolicy = requestSourcePolicyFromEnv(env);
|
|
201
207
|
|
|
202
208
|
return {
|
|
203
209
|
auditSink,
|
|
@@ -205,13 +211,18 @@ export function createServerRuntime(
|
|
|
205
211
|
request: Request,
|
|
206
212
|
metadata: { clientIp?: string } = {},
|
|
207
213
|
) {
|
|
214
|
+
const requestSource = resolveRequestSource(
|
|
215
|
+
request,
|
|
216
|
+
metadata.clientIp,
|
|
217
|
+
requestSourcePolicy,
|
|
218
|
+
);
|
|
208
219
|
const actor =
|
|
209
220
|
(await options.actorResolver?.resolve(request)) ||
|
|
210
221
|
sessionStore.resolve(request);
|
|
211
222
|
const license =
|
|
212
223
|
(await options.licenseResolver?.resolve()) || licenseStore.resolve();
|
|
213
224
|
return createRequestGuardContext(request, {
|
|
214
|
-
|
|
225
|
+
...requestSource,
|
|
215
226
|
resolveActor: () => actor,
|
|
216
227
|
resolveLicense: () => license,
|
|
217
228
|
});
|
|
@@ -221,10 +232,15 @@ export function createServerRuntime(
|
|
|
221
232
|
actor: RequestActor,
|
|
222
233
|
metadata: { clientIp?: string } = {},
|
|
223
234
|
) {
|
|
235
|
+
const requestSource = resolveRequestSource(
|
|
236
|
+
request,
|
|
237
|
+
metadata.clientIp,
|
|
238
|
+
requestSourcePolicy,
|
|
239
|
+
);
|
|
224
240
|
const license =
|
|
225
241
|
(await options.licenseResolver?.resolve()) || licenseStore.resolve();
|
|
226
242
|
return createRequestGuardContext(request, {
|
|
227
|
-
|
|
243
|
+
...requestSource,
|
|
228
244
|
resolveActor: () => actor,
|
|
229
245
|
resolveLicense: () => license,
|
|
230
246
|
});
|
|
@@ -268,6 +284,7 @@ function matchesAuditQuery(
|
|
|
268
284
|
query: AuditEventListQuery,
|
|
269
285
|
): boolean {
|
|
270
286
|
return (
|
|
287
|
+
(!query.q || matchesAuditKeyword(event, query.q)) &&
|
|
271
288
|
(!query.actorId || event.actor.id === query.actorId) &&
|
|
272
289
|
(!query.action || event.action === query.action) &&
|
|
273
290
|
(!query.resource || event.target?.type === query.resource) &&
|
|
@@ -277,3 +294,23 @@ function matchesAuditQuery(
|
|
|
277
294
|
(!query.occurredBefore || event.occurredAt <= query.occurredBefore)
|
|
278
295
|
);
|
|
279
296
|
}
|
|
297
|
+
|
|
298
|
+
function matchesAuditKeyword(event: AuditEvent, query: string): boolean {
|
|
299
|
+
const keyword = query.toLowerCase();
|
|
300
|
+
const values = [
|
|
301
|
+
event.action,
|
|
302
|
+
event.actor.id,
|
|
303
|
+
event.actor.name,
|
|
304
|
+
event.target?.type,
|
|
305
|
+
event.target?.id,
|
|
306
|
+
event.target?.label,
|
|
307
|
+
event.actor.ip,
|
|
308
|
+
event.message,
|
|
309
|
+
event.module,
|
|
310
|
+
event.requestId,
|
|
311
|
+
event.metadata ? JSON.stringify(event.metadata) : undefined,
|
|
312
|
+
];
|
|
313
|
+
return values.some((value) =>
|
|
314
|
+
value === undefined ? false : String(value).toLowerCase().includes(keyword),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUIDv7 } from "bun";
|
|
2
2
|
import type { AuditEvent } from "@southwind-ai/shared";
|
|
3
|
-
import
|
|
3
|
+
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
4
4
|
import type { ScheduledTaskRun } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export function schedulerAuditEvent(
|
|
@@ -14,7 +14,7 @@ export function schedulerAuditEvent(
|
|
|
14
14
|
id: randomUUIDv7(),
|
|
15
15
|
action,
|
|
16
16
|
outcome,
|
|
17
|
-
actor: auditActor(context
|
|
17
|
+
actor: auditActor(context),
|
|
18
18
|
target: { type: "scheduled-task", id: run.taskKey },
|
|
19
19
|
requestId: context.requestId,
|
|
20
20
|
metadata: {
|
|
@@ -27,12 +27,3 @@ export function schedulerAuditEvent(
|
|
|
27
27
|
occurredAt: occurredAt.toISOString(),
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
function auditActor(actor: RequestActor): AuditEvent["actor"] {
|
|
32
|
-
return {
|
|
33
|
-
id: actor.id,
|
|
34
|
-
type: actor.type === "anonymous" ? "user" : actor.type,
|
|
35
|
-
name: actor.name,
|
|
36
|
-
roleCodes: actor.roleCodes,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -13,6 +13,7 @@ describe("平台任务 Manifest Adapter", () => {
|
|
|
13
13
|
storageCleanup: () => ({ deleted: 0 }),
|
|
14
14
|
workflowTimeoutDispatch: () => ({ claimed: false }),
|
|
15
15
|
governedExportDispatch: () => ({ claimed: false }),
|
|
16
|
+
notificationDispatch: () => ({ claimed: false }),
|
|
16
17
|
},
|
|
17
18
|
);
|
|
18
19
|
|
|
@@ -31,5 +32,10 @@ describe("平台任务 Manifest Adapter", () => {
|
|
|
31
32
|
.find(({ key }) => key === "system.governed-export.dispatch")
|
|
32
33
|
?.isIdleResult?.({ claimed: false }),
|
|
33
34
|
).toBe(true);
|
|
35
|
+
expect(
|
|
36
|
+
definitions
|
|
37
|
+
.find(({ key }) => key === "system.notification.dispatch")
|
|
38
|
+
?.isIdleResult?.({ claimed: false }),
|
|
39
|
+
).toBe(true);
|
|
34
40
|
});
|
|
35
41
|
});
|
|
@@ -8,6 +8,7 @@ export interface PlatformTaskHandlers {
|
|
|
8
8
|
storageCleanup?: ScheduledTaskDefinition["handler"];
|
|
9
9
|
workflowTimeoutDispatch?: ScheduledTaskDefinition["handler"];
|
|
10
10
|
governedExportDispatch?: ScheduledTaskDefinition["handler"];
|
|
11
|
+
notificationDispatch?: ScheduledTaskDefinition["handler"];
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function createPlatformTaskDefinitions(
|
|
@@ -33,6 +34,11 @@ export function createPlatformTaskDefinitions(
|
|
|
33
34
|
"system.governed-export.dispatch",
|
|
34
35
|
handlers.governedExportDispatch,
|
|
35
36
|
);
|
|
37
|
+
if (handlers.notificationDispatch)
|
|
38
|
+
handlerByKey.set(
|
|
39
|
+
"system.notification.dispatch",
|
|
40
|
+
handlers.notificationDispatch,
|
|
41
|
+
);
|
|
36
42
|
|
|
37
43
|
return systemModuleManifests()
|
|
38
44
|
.flatMap((module) => module.tasks)
|
|
@@ -49,6 +55,7 @@ export function createPlatformTaskDefinitions(
|
|
|
49
55
|
isIdleResult:
|
|
50
56
|
task.key === "system.bulk-data.dispatch" ||
|
|
51
57
|
task.key === "system.workflow.timeout-dispatch" ||
|
|
58
|
+
task.key === "system.notification.dispatch" ||
|
|
52
59
|
task.key === "system.governed-export.dispatch"
|
|
53
60
|
? (value) => value?.claimed === false
|
|
54
61
|
: undefined,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUIDv7 } from "bun";
|
|
2
2
|
import type { AuditEvent } from "@southwind-ai/shared";
|
|
3
3
|
import { runGuardedBackgroundTask } from "../background/task-guard.js";
|
|
4
|
-
import
|
|
4
|
+
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
5
5
|
import type { createServerRuntime } from "../runtime.js";
|
|
6
6
|
import type { SchedulerRepository } from "./repository.js";
|
|
7
7
|
import { sanitizeRecoveryReason } from "./sensitive-text.js";
|
|
@@ -116,7 +116,7 @@ function auditEvent(
|
|
|
116
116
|
id: randomUUIDv7(),
|
|
117
117
|
action: "task.recover",
|
|
118
118
|
outcome,
|
|
119
|
-
actor: auditActor(context
|
|
119
|
+
actor: auditActor(context),
|
|
120
120
|
target: { type: "scheduled-task-run", id: recovery.runId },
|
|
121
121
|
requestId: context.requestId,
|
|
122
122
|
message: phase === "intent" ? "人工恢复意图已确认" : "人工恢复状态转换结果",
|
|
@@ -141,12 +141,3 @@ function policy(definition: ScheduledTaskDefinition) {
|
|
|
141
141
|
feature: definition.feature,
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
function auditActor(actor: RequestActor): AuditEvent["actor"] {
|
|
146
|
-
return {
|
|
147
|
-
id: actor.id,
|
|
148
|
-
type: actor.type === "anonymous" ? "user" : actor.type,
|
|
149
|
-
name: actor.name,
|
|
150
|
-
roleCodes: actor.roleCodes,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
FeatureFlagDefinition,
|
|
5
5
|
HttpMethod,
|
|
6
6
|
} from "@southwind-ai/shared";
|
|
7
|
-
import type
|
|
7
|
+
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
8
8
|
import { evaluateRouteGuards, findSystemFeature } from "../route-guards.js";
|
|
9
9
|
import type { createServerRuntime } from "../runtime.js";
|
|
10
10
|
import { routeContext } from "../system/route-helpers.js";
|
|
@@ -209,12 +209,7 @@ function readAudit(context: RequestGuardContext, resource: string): AuditEvent {
|
|
|
209
209
|
id: randomUUIDv7(),
|
|
210
210
|
action: "task.read",
|
|
211
211
|
outcome: "success",
|
|
212
|
-
actor:
|
|
213
|
-
id: context.actor.id,
|
|
214
|
-
type: context.actor.type === "anonymous" ? "user" : context.actor.type,
|
|
215
|
-
name: context.actor.name,
|
|
216
|
-
roleCodes: context.actor.roleCodes,
|
|
217
|
-
},
|
|
212
|
+
actor: auditActor(context),
|
|
218
213
|
target: { type: resource },
|
|
219
214
|
requestId: context.requestId,
|
|
220
215
|
occurredAt: new Date().toISOString(),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { OpenSearchClient, readOpenSearchConfig } from "./opensearch-client.js";
|
|
3
|
+
|
|
4
|
+
describe("OpenSearch Client 配置", () => {
|
|
5
|
+
test("未配置 URL 时保持默认无外部搜索依赖", () => {
|
|
6
|
+
expect(readOpenSearchConfig({})).toBeUndefined();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("拒绝 URL 内嵌凭据与不完整认证配置", () => {
|
|
10
|
+
const config = readOpenSearchConfig({
|
|
11
|
+
OPENSEARCH_URL: "http://user:secret@localhost:9200",
|
|
12
|
+
});
|
|
13
|
+
expect(() => new OpenSearchClient(config!)).toThrow("不含凭据、路径和参数");
|
|
14
|
+
expect(
|
|
15
|
+
() =>
|
|
16
|
+
new OpenSearchClient({
|
|
17
|
+
url: "http://localhost:9200",
|
|
18
|
+
username: "windy",
|
|
19
|
+
requestTimeoutMs: 1_000,
|
|
20
|
+
}),
|
|
21
|
+
).toThrow("必须同时配置");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("认证只进入 Server 到 OpenSearch 的请求头", async () => {
|
|
25
|
+
const requests: Request[] = [];
|
|
26
|
+
const client = new OpenSearchClient(
|
|
27
|
+
{
|
|
28
|
+
url: "http://opensearch:9200",
|
|
29
|
+
username: "windy",
|
|
30
|
+
password: "secret",
|
|
31
|
+
requestTimeoutMs: 1_000,
|
|
32
|
+
},
|
|
33
|
+
async (input, init) => {
|
|
34
|
+
requests.push(new Request(input, init));
|
|
35
|
+
return Response.json({ status: "green" });
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(await client.health()).toEqual({ status: "green" });
|
|
40
|
+
expect(requests[0]?.url).toBe(
|
|
41
|
+
"http://opensearch:9200/_cluster/health?local=true",
|
|
42
|
+
);
|
|
43
|
+
expect(requests[0]?.headers.get("authorization")).toStartWith("Basic ");
|
|
44
|
+
});
|
|
45
|
+
});
|