create-windy 0.3.2 → 0.3.5
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 +27 -9
- package/dist/cli.js +825 -117
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +18 -11
- package/template/_gitignore +1 -0
- package/template/apps/agent-server/README.md +2 -0
- package/template/apps/agent-server/pyproject.toml +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
- package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
- package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
- package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
- package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
- package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
- package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
- package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
- package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
- package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
- package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
- package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
- package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
- package/template/apps/agent-server/tests/test_api.py +24 -1
- package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
- package/template/apps/agent-server/tests/test_config.py +4 -0
- package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
- package/template/apps/agent-server/tests/test_multimodal.py +51 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
- package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
- package/template/apps/agent-server/tests/test_telemetry.py +25 -0
- package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
- package/template/apps/agent-server/uv.lock +165 -0
- package/template/apps/document-server/Dockerfile +19 -0
- package/template/apps/document-server/pyproject.toml +43 -0
- package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
- package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
- package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
- package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
- package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
- package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
- package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
- package/template/apps/document-server/tests/__init__.py +1 -0
- package/template/apps/document-server/tests/test_config.py +42 -0
- package/template/apps/document-server/tests/test_service.py +140 -0
- package/template/apps/document-server/uv.lock +430 -0
- package/template/apps/embedding-server/Dockerfile +17 -0
- package/template/apps/embedding-server/pyproject.toml +44 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
- package/template/apps/embedding-server/tests/__init__.py +1 -0
- package/template/apps/embedding-server/tests/test_adapter.py +92 -0
- package/template/apps/embedding-server/tests/test_config.py +38 -0
- package/template/apps/embedding-server/uv.lock +449 -0
- package/template/apps/server/src/agent/background-operation-port.ts +37 -0
- package/template/apps/server/src/agent/background-operation.test.ts +231 -0
- package/template/apps/server/src/agent/background-operation.ts +252 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
- package/template/apps/server/src/agent/execution-grants.ts +7 -1
- package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
- package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
- package/template/apps/server/src/agent/idempotency-key.ts +11 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
- package/template/apps/server/src/agent/remote-runtime.ts +173 -5
- package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
- package/template/apps/server/src/agent/run-authorization.ts +180 -0
- package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
- package/template/apps/server/src/agent/run-contracts.ts +88 -1
- package/template/apps/server/src/agent/run-definition-support.ts +58 -0
- package/template/apps/server/src/agent/run-errors.ts +25 -0
- package/template/apps/server/src/agent/run-facade.test.ts +57 -46
- package/template/apps/server/src/agent/run-facade.ts +185 -65
- package/template/apps/server/src/agent/run-release.ts +78 -0
- package/template/apps/server/src/agent/run-route-input.ts +169 -0
- package/template/apps/server/src/agent/run-routes.ts +63 -76
- package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
- package/template/apps/server/src/agent/sandbox-client.ts +200 -0
- package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
- package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
- package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
- package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
- package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
- package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
- package/template/apps/server/src/agent/tool-approval.ts +115 -0
- package/template/apps/server/src/agent/tool-audit.ts +62 -0
- package/template/apps/server/src/agent/tool-host.test.ts +39 -11
- package/template/apps/server/src/agent/tool-host.ts +184 -17
- package/template/apps/server/src/agent/tool-registry.ts +52 -47
- package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
- package/template/apps/server/src/agent/vision-input.ts +120 -0
- package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
- package/template/apps/server/src/ai-artifact/codec.ts +137 -0
- package/template/apps/server/src/ai-artifact/routes.ts +37 -0
- package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
- package/template/apps/server/src/application-services.ts +62 -2
- package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
- package/template/apps/server/src/auth/reauthentication.ts +19 -0
- package/template/apps/server/src/auth/runtime.ts +9 -1
- package/template/apps/server/src/data-access/context.ts +6 -4
- package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
- package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
- package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
- package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
- package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
- package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
- package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
- package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
- package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
- package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
- package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
- package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
- package/template/apps/server/src/data-governance/export/routes.ts +11 -1
- package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
- package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
- package/template/apps/server/src/data-governance/export/service.ts +90 -113
- package/template/apps/server/src/data-governance/export/source.ts +101 -0
- package/template/apps/server/src/data-governance/export/types.ts +27 -0
- package/template/apps/server/src/document/contracts.ts +76 -0
- package/template/apps/server/src/document/facade.ts +339 -0
- package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
- package/template/apps/server/src/document/remote-runtime.ts +223 -0
- package/template/apps/server/src/document/routes.ts +193 -0
- package/template/apps/server/src/embedding/contracts.ts +83 -0
- package/template/apps/server/src/embedding/error.ts +10 -0
- package/template/apps/server/src/embedding/facade.ts +217 -0
- package/template/apps/server/src/embedding/input.ts +172 -0
- package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
- package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
- package/template/apps/server/src/embedding/routes.ts +152 -0
- package/template/apps/server/src/index.ts +40 -43
- package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
- package/template/apps/server/src/jobs/artifact-service.ts +10 -0
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
- package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
- package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
- package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +17 -0
- package/template/apps/server/src/jobs/execution.test.ts +34 -0
- package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
- package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
- package/template/apps/server/src/jobs/repository.ts +3 -0
- package/template/apps/server/src/jobs/worker.ts +80 -41
- package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
- package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
- package/template/apps/server/src/module-host/durable-context.ts +124 -0
- package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
- package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
- package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
- package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
- package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
- package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
- package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
- package/template/apps/server/src/module-host/governed-resources.ts +203 -0
- package/template/apps/server/src/module-host/http-runtime.ts +95 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +49 -1
- package/template/apps/server/src/module-host/initialize.ts +39 -16
- package/template/apps/server/src/module-host/job-port.test.ts +262 -0
- package/template/apps/server/src/module-host/job-port.ts +334 -0
- package/template/apps/server/src/module-host/request-context.test.ts +88 -2
- package/template/apps/server/src/module-host/request-context.ts +56 -22
- package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
- package/template/apps/server/src/module-host/route-installer.ts +36 -1
- package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/module-runtime-validation.ts +7 -3
- package/template/apps/server/src/speech/contracts.ts +76 -0
- package/template/apps/server/src/speech/facade.ts +316 -0
- package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
- package/template/apps/server/src/speech/remote-runtime.ts +227 -0
- package/template/apps/server/src/speech/routes.ts +180 -0
- package/template/apps/server/src/system/actor.ts +39 -0
- package/template/apps/server/src/system/route-helpers.ts +1 -0
- package/template/apps/server/src/workflow/service.ts +2 -1
- package/template/apps/speech-server/Dockerfile +22 -0
- package/template/apps/speech-server/pyproject.toml +43 -0
- package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
- package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
- package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
- package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
- package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
- package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
- package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
- package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
- package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
- package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
- package/template/apps/speech-server/tests/__init__.py +1 -0
- package/template/apps/speech-server/tests/test_config.py +33 -0
- package/template/apps/speech-server/tests/test_service.py +154 -0
- package/template/apps/speech-server/uv.lock +430 -0
- package/template/apps/web/src/app/business-routes.ts +22 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
- package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
- package/template/apps/web/src/components/common/FormDialog.vue +6 -0
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
- package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
- package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
- package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
- package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
- package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
- package/template/apps/web/src/layout/AppShell.vue +28 -25
- package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
- package/template/apps/web/src/router/business-routes.ts +68 -0
- package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
- package/template/apps/web/src/router/index.ts +7 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/access-api.ts +5 -1
- package/template/apps/web/src/services/access-api.webtest.ts +35 -0
- package/template/docker-compose.yml +127 -0
- package/template/docs/architecture/ai-egress.md +12 -9
- package/template/docs/architecture/ai-runtime.md +23 -11
- package/template/docs/architecture/durable-jobs.md +25 -3
- package/template/docs/architecture/foundation-infrastructure.md +163 -0
- package/template/docs/architecture/module-host.md +115 -0
- package/template/docs/architecture/server-module-interface.md +39 -0
- package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
- package/template/docs/development/business-web-routes.md +66 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/development/module-lifecycle.md +71 -0
- package/template/docs/operations/observability.md +5 -1
- package/template/docs/platform/agent-runtime.md +21 -11
- package/template/docs/platform/agent-tool-sandbox.md +58 -0
- package/template/docs/platform/agent-tools.md +5 -0
- package/template/docs/platform/document-ai-runtime.md +204 -0
- package/template/docs/platform/governed-export-renderers.md +132 -0
- package/template/docs/platform/navigation-registry.md +5 -0
- package/template/docs/platform/organization-membership.md +2 -1
- package/template/docs/platform/speech-ai-runtime.md +156 -0
- package/template/docs/platform/system-crud-api.md +7 -1
- package/template/docs/platform/vision-ai-runtime.md +97 -0
- package/template/docs/platform/web-system-crud.md +6 -0
- package/template/package.json +16 -2
- package/template/packages/ai-client/index.ts +4 -0
- package/template/packages/ai-client/package.json +5 -2
- package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
- package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
- package/template/packages/ai-client/src/agent/client/client.ts +119 -7
- package/template/packages/ai-client/src/agent/client/run.ts +14 -0
- package/template/packages/ai-client/src/agent/client/types.ts +18 -0
- package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
- package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
- package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
- package/template/packages/ai-client/src/agent/index.ts +1 -0
- package/template/packages/ai-client/src/artifact-ref.ts +8 -0
- package/template/packages/ai-client/src/document/client.test.ts +114 -0
- package/template/packages/ai-client/src/document/client.ts +134 -0
- package/template/packages/ai-client/src/document/contracts.ts +152 -0
- package/template/packages/ai-client/src/document/error.ts +43 -0
- package/template/packages/ai-client/src/document/index.ts +3 -0
- package/template/packages/ai-client/src/document/run.ts +104 -0
- package/template/packages/ai-client/src/document/validation.ts +115 -0
- package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
- package/template/packages/ai-client/src/embedding/client.ts +96 -0
- package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
- package/template/packages/ai-client/src/embedding/error.ts +61 -0
- package/template/packages/ai-client/src/embedding/index.ts +3 -0
- package/template/packages/ai-client/src/embedding/validation.ts +117 -0
- package/template/packages/ai-client/src/speech/client.test.ts +135 -0
- package/template/packages/ai-client/src/speech/client.ts +104 -0
- package/template/packages/ai-client/src/speech/contracts.ts +129 -0
- package/template/packages/ai-client/src/speech/error.ts +43 -0
- package/template/packages/ai-client/src/speech/index.ts +3 -0
- package/template/packages/ai-client/src/speech/run.ts +133 -0
- package/template/packages/ai-client/src/speech/validation.ts +132 -0
- package/template/packages/config/package.json +2 -2
- package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
- package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
- package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
- package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
- package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
- package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
- package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
- package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +149 -0
- package/template/packages/database/src/schema/common.ts +2 -14
- package/template/packages/database/src/schema/durable-jobs.ts +5 -0
- package/template/packages/database/src/schema/governed-exports.ts +17 -5
- package/template/packages/database/src/schema/index.ts +3 -0
- package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
- package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
- package/template/packages/database/src/schema.test.ts +6 -23
- package/template/packages/jobs/package.json +3 -2
- package/template/packages/jobs/src/handlers.test.ts +13 -0
- package/template/packages/jobs/src/handlers.ts +7 -1
- package/template/packages/jobs/src/types.ts +4 -0
- package/template/packages/modules/package.json +2 -2
- package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
- package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
- package/template/packages/modules/src/ai-operation-composition.ts +59 -1
- package/template/packages/modules/src/manifest.ts +25 -1
- package/template/packages/modules/src/tool-composition.ts +18 -0
- package/template/packages/server-sdk/index.ts +50 -0
- package/template/packages/server-sdk/package.json +10 -4
- package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
- package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
- package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
- package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
- package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
- package/template/packages/server-sdk/src/durable-handler.ts +29 -0
- package/template/packages/server-sdk/src/jobs-port.ts +69 -0
- package/template/packages/server-sdk/src/module-host.ts +16 -2
- package/template/packages/server-sdk/src/request-context.ts +6 -2
- package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
- package/template/packages/server-sdk/src/tool-registration.ts +2 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/rbac.ts +30 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
randomBytes,
|
|
6
|
+
} from "node:crypto";
|
|
7
|
+
import type { ModuleResourceFileRequest } from "@southwind-ai/server-sdk";
|
|
8
|
+
|
|
9
|
+
export interface AiArtifactClaims {
|
|
10
|
+
readonly version: 1;
|
|
11
|
+
readonly moduleName: string;
|
|
12
|
+
readonly actorId: string;
|
|
13
|
+
readonly source: ModuleResourceFileRequest;
|
|
14
|
+
readonly mediaType: string;
|
|
15
|
+
readonly byteSize: number;
|
|
16
|
+
readonly sha256: string;
|
|
17
|
+
readonly issuedAt: string;
|
|
18
|
+
readonly expiresAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const aad = Buffer.from("southwind-ai-artifact-v1");
|
|
22
|
+
|
|
23
|
+
export class AiArtifactCodec {
|
|
24
|
+
readonly #key: Buffer;
|
|
25
|
+
|
|
26
|
+
constructor(secret: string) {
|
|
27
|
+
if (secret.trim().length < 32) {
|
|
28
|
+
throw new Error("AI Artifact 加密 Secret 至少需要 32 个字符");
|
|
29
|
+
}
|
|
30
|
+
this.#key = createHash("sha256").update(secret).digest();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
encode(claims: AiArtifactClaims): string {
|
|
34
|
+
const iv = randomBytes(12);
|
|
35
|
+
const cipher = createCipheriv("aes-256-gcm", this.#key, iv);
|
|
36
|
+
cipher.setAAD(aad);
|
|
37
|
+
const encrypted = Buffer.concat([
|
|
38
|
+
cipher.update(JSON.stringify(claims), "utf8"),
|
|
39
|
+
cipher.final(),
|
|
40
|
+
]);
|
|
41
|
+
return [
|
|
42
|
+
"v1",
|
|
43
|
+
iv.toString("base64url"),
|
|
44
|
+
encrypted.toString("base64url"),
|
|
45
|
+
cipher.getAuthTag().toString("base64url"),
|
|
46
|
+
].join(".");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
decode(token: string): AiArtifactClaims {
|
|
50
|
+
try {
|
|
51
|
+
if (token.length > 4_096) throw new Error("token too large");
|
|
52
|
+
const [version, ivValue, encryptedValue, tagValue, extra] =
|
|
53
|
+
token.split(".");
|
|
54
|
+
if (
|
|
55
|
+
version !== "v1" ||
|
|
56
|
+
!ivValue ||
|
|
57
|
+
!encryptedValue ||
|
|
58
|
+
!tagValue ||
|
|
59
|
+
extra
|
|
60
|
+
) {
|
|
61
|
+
throw new Error("token shape");
|
|
62
|
+
}
|
|
63
|
+
const decipher = createDecipheriv(
|
|
64
|
+
"aes-256-gcm",
|
|
65
|
+
this.#key,
|
|
66
|
+
decodeCanonicalBase64url(ivValue, 12),
|
|
67
|
+
);
|
|
68
|
+
decipher.setAAD(aad);
|
|
69
|
+
decipher.setAuthTag(decodeCanonicalBase64url(tagValue, 16));
|
|
70
|
+
const plaintext = Buffer.concat([
|
|
71
|
+
decipher.update(decodeCanonicalBase64url(encryptedValue)),
|
|
72
|
+
decipher.final(),
|
|
73
|
+
]);
|
|
74
|
+
return parseClaims(JSON.parse(plaintext.toString("utf8")));
|
|
75
|
+
} catch {
|
|
76
|
+
throw new AiArtifactCodecError();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class AiArtifactCodecError extends Error {
|
|
82
|
+
constructor() {
|
|
83
|
+
super("AI Artifact 引用无效");
|
|
84
|
+
this.name = "AiArtifactCodecError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function decodeCanonicalBase64url(value: string, byteSize?: number): Buffer {
|
|
89
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
90
|
+
throw new Error("base64url invalid");
|
|
91
|
+
}
|
|
92
|
+
const decoded = Buffer.from(value, "base64url");
|
|
93
|
+
if (
|
|
94
|
+
decoded.toString("base64url") !== value ||
|
|
95
|
+
(byteSize !== undefined && decoded.byteLength !== byteSize)
|
|
96
|
+
) {
|
|
97
|
+
throw new Error("base64url non-canonical");
|
|
98
|
+
}
|
|
99
|
+
return decoded;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseClaims(value: unknown): AiArtifactClaims {
|
|
103
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.source)) {
|
|
104
|
+
throw new Error("claims invalid");
|
|
105
|
+
}
|
|
106
|
+
const claims = value as unknown as AiArtifactClaims;
|
|
107
|
+
for (const field of [
|
|
108
|
+
claims.moduleName,
|
|
109
|
+
claims.actorId,
|
|
110
|
+
claims.source.fileId,
|
|
111
|
+
claims.source.purpose,
|
|
112
|
+
claims.source.resourceType,
|
|
113
|
+
claims.source.resourceId,
|
|
114
|
+
claims.mediaType,
|
|
115
|
+
claims.sha256,
|
|
116
|
+
claims.issuedAt,
|
|
117
|
+
claims.expiresAt,
|
|
118
|
+
]) {
|
|
119
|
+
if (typeof field !== "string" || !field.trim()) {
|
|
120
|
+
throw new Error("claims field invalid");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
!Number.isSafeInteger(claims.byteSize) ||
|
|
125
|
+
claims.byteSize < 0 ||
|
|
126
|
+
!/^[a-f0-9]{64}$/.test(claims.sha256) ||
|
|
127
|
+
!Number.isFinite(Date.parse(claims.issuedAt)) ||
|
|
128
|
+
!Number.isFinite(Date.parse(claims.expiresAt))
|
|
129
|
+
) {
|
|
130
|
+
throw new Error("claims metadata invalid");
|
|
131
|
+
}
|
|
132
|
+
return claims;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
136
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
137
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RouteApp } from "../system/route-types.js";
|
|
2
|
+
import { validInternalToken } from "../agent/run-routes.js";
|
|
3
|
+
import type { AiArtifactRuntime } from "./runtime.js";
|
|
4
|
+
|
|
5
|
+
interface RouteContext {
|
|
6
|
+
readonly request: Request;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerAiArtifactRoutes(
|
|
10
|
+
app: RouteApp,
|
|
11
|
+
artifacts: AiArtifactRuntime,
|
|
12
|
+
internalToken: string,
|
|
13
|
+
) {
|
|
14
|
+
app.get("/internal/ai/artifacts", async (raw) => {
|
|
15
|
+
const context = raw as RouteContext;
|
|
16
|
+
if (!validInternalToken(context.request, internalToken)) {
|
|
17
|
+
return Response.json({ code: "FORBIDDEN" }, { status: 403 });
|
|
18
|
+
}
|
|
19
|
+
const token = context.request.headers.get("x-ai-artifact-ref");
|
|
20
|
+
if (!token) {
|
|
21
|
+
return Response.json({ code: "ARTIFACT_UNAVAILABLE" }, { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const opened = await artifacts.openToken(token, context.request.signal);
|
|
25
|
+
return new Response(opened.body, {
|
|
26
|
+
headers: {
|
|
27
|
+
"cache-control": "private, no-store",
|
|
28
|
+
"content-type": opened.mimeType,
|
|
29
|
+
"content-length": String(opened.byteSize),
|
|
30
|
+
"x-content-sha256": opened.sha256,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
return Response.json({ code: "ARTIFACT_UNAVAILABLE" }, { status: 404 });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ModuleAiArtifactError,
|
|
3
|
+
type ModuleAiArtifactPort,
|
|
4
|
+
type ModuleAiArtifactRef,
|
|
5
|
+
type ModuleResourceFileRequest,
|
|
6
|
+
} from "@southwind-ai/server-sdk";
|
|
7
|
+
import type { FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
8
|
+
import type { RequestActor, RequestGuardContext } from "../guards.js";
|
|
9
|
+
import type { InitializedModuleHosts } from "../module-host.js";
|
|
10
|
+
import {
|
|
11
|
+
createModuleStoragePort,
|
|
12
|
+
type ModuleStorageServices,
|
|
13
|
+
} from "../module-host/storage-port.js";
|
|
14
|
+
import { createModuleStorageScope } from "../module-host/storage-scope.js";
|
|
15
|
+
import type { createServerRuntime } from "../runtime.js";
|
|
16
|
+
import { AiArtifactCodec, type AiArtifactClaims } from "./codec.js";
|
|
17
|
+
|
|
18
|
+
export class AiArtifactRuntime {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly codec: AiArtifactCodec,
|
|
21
|
+
private readonly hosts: InitializedModuleHosts,
|
|
22
|
+
private readonly runtime: ReturnType<typeof createServerRuntime>,
|
|
23
|
+
private readonly features: readonly FeatureFlagDefinition[],
|
|
24
|
+
private readonly storage: ModuleStorageServices,
|
|
25
|
+
private readonly resolveActor: (
|
|
26
|
+
actorId: string,
|
|
27
|
+
) => Promise<RequestActor | undefined>,
|
|
28
|
+
private readonly now: () => Date = () => new Date(),
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
createModulePort(
|
|
32
|
+
moduleName: string,
|
|
33
|
+
guardContext: RequestGuardContext,
|
|
34
|
+
signal: AbortSignal,
|
|
35
|
+
): ModuleAiArtifactPort {
|
|
36
|
+
return {
|
|
37
|
+
issue: (source, options) =>
|
|
38
|
+
this.issue(
|
|
39
|
+
moduleName,
|
|
40
|
+
guardContext,
|
|
41
|
+
source,
|
|
42
|
+
options?.ttlSeconds,
|
|
43
|
+
signal,
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async rotate(
|
|
49
|
+
token: string,
|
|
50
|
+
actorId: string,
|
|
51
|
+
moduleName: string,
|
|
52
|
+
ttlSeconds: number,
|
|
53
|
+
signal: AbortSignal,
|
|
54
|
+
): Promise<ModuleAiArtifactRef> {
|
|
55
|
+
const claims = this.requireClaims(token, actorId, moduleName);
|
|
56
|
+
const opened = await this.open(claims, signal);
|
|
57
|
+
await opened.body.cancel().catch(() => undefined);
|
|
58
|
+
return this.reference(claims, opened, boundedTtl(ttlSeconds, 60, 3_600));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async openToken(token: string, signal: AbortSignal) {
|
|
62
|
+
return this.open(this.requireClaims(token), signal);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async issue(
|
|
66
|
+
moduleName: string,
|
|
67
|
+
guardContext: RequestGuardContext,
|
|
68
|
+
source: ModuleResourceFileRequest,
|
|
69
|
+
ttlSeconds = 300,
|
|
70
|
+
signal: AbortSignal,
|
|
71
|
+
) {
|
|
72
|
+
const port = this.storagePort(moduleName, guardContext, signal);
|
|
73
|
+
try {
|
|
74
|
+
const opened = await port.openResourceFile(source);
|
|
75
|
+
await opened.body.cancel().catch(() => undefined);
|
|
76
|
+
return this.reference(
|
|
77
|
+
{
|
|
78
|
+
version: 1,
|
|
79
|
+
moduleName,
|
|
80
|
+
actorId: guardContext.actor.id,
|
|
81
|
+
source,
|
|
82
|
+
mediaType: opened.mimeType,
|
|
83
|
+
byteSize: opened.byteSize,
|
|
84
|
+
sha256: opened.sha256,
|
|
85
|
+
issuedAt: this.now().toISOString(),
|
|
86
|
+
expiresAt: this.now().toISOString(),
|
|
87
|
+
},
|
|
88
|
+
opened,
|
|
89
|
+
boundedTtl(ttlSeconds, 60, 900),
|
|
90
|
+
);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof ModuleAiArtifactError) throw error;
|
|
93
|
+
throw new ModuleAiArtifactError(
|
|
94
|
+
"AI_ARTIFACT_FORBIDDEN",
|
|
95
|
+
"业务资源不能转换为 AI Artifact",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private reference(
|
|
101
|
+
claims: AiArtifactClaims,
|
|
102
|
+
opened: {
|
|
103
|
+
mimeType: string;
|
|
104
|
+
byteSize: number;
|
|
105
|
+
sha256: string;
|
|
106
|
+
},
|
|
107
|
+
ttlSeconds: number,
|
|
108
|
+
): ModuleAiArtifactRef {
|
|
109
|
+
const issuedAt = this.now();
|
|
110
|
+
const expiresAt = new Date(
|
|
111
|
+
issuedAt.getTime() + ttlSeconds * 1_000,
|
|
112
|
+
).toISOString();
|
|
113
|
+
const refreshed: AiArtifactClaims = {
|
|
114
|
+
...claims,
|
|
115
|
+
mediaType: opened.mimeType,
|
|
116
|
+
byteSize: opened.byteSize,
|
|
117
|
+
sha256: opened.sha256,
|
|
118
|
+
issuedAt: issuedAt.toISOString(),
|
|
119
|
+
expiresAt,
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
value: this.codec.encode(refreshed),
|
|
123
|
+
mediaType: refreshed.mediaType,
|
|
124
|
+
byteSize: refreshed.byteSize,
|
|
125
|
+
sha256: refreshed.sha256,
|
|
126
|
+
expiresAt,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private requireClaims(token: string, actorId?: string, moduleName?: string) {
|
|
131
|
+
const claims = this.codec.decode(token);
|
|
132
|
+
if (
|
|
133
|
+
claims.expiresAt <= this.now().toISOString() ||
|
|
134
|
+
(actorId && claims.actorId !== actorId) ||
|
|
135
|
+
(moduleName && claims.moduleName !== moduleName)
|
|
136
|
+
) {
|
|
137
|
+
throw new ModuleAiArtifactError(
|
|
138
|
+
"AI_ARTIFACT_FORBIDDEN",
|
|
139
|
+
"AI Artifact 已过期或不属于当前调用方",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return claims;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async open(claims: AiArtifactClaims, signal: AbortSignal) {
|
|
146
|
+
const actor = await this.resolveActor(claims.actorId);
|
|
147
|
+
if (!actor) throw forbidden();
|
|
148
|
+
const request = new Request("http://ai-artifact.internal/open", { signal });
|
|
149
|
+
const guardContext = await this.runtime.createGuardContextForActor(
|
|
150
|
+
request,
|
|
151
|
+
actor,
|
|
152
|
+
);
|
|
153
|
+
const opened = await this.storagePort(
|
|
154
|
+
claims.moduleName,
|
|
155
|
+
guardContext,
|
|
156
|
+
signal,
|
|
157
|
+
).openResourceFile(claims.source);
|
|
158
|
+
if (
|
|
159
|
+
opened.mimeType !== claims.mediaType ||
|
|
160
|
+
opened.byteSize !== claims.byteSize ||
|
|
161
|
+
opened.sha256 !== claims.sha256
|
|
162
|
+
) {
|
|
163
|
+
await opened.body.cancel().catch(() => undefined);
|
|
164
|
+
throw forbidden();
|
|
165
|
+
}
|
|
166
|
+
return opened;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private storagePort(
|
|
170
|
+
moduleName: string,
|
|
171
|
+
guardContext: RequestGuardContext,
|
|
172
|
+
signal: AbortSignal,
|
|
173
|
+
) {
|
|
174
|
+
return createModuleStoragePort(
|
|
175
|
+
this.storage,
|
|
176
|
+
createModuleStorageScope({
|
|
177
|
+
moduleName,
|
|
178
|
+
purposes: this.hosts.uploadPurposesOf(moduleName),
|
|
179
|
+
resourceFileAccessPolicies:
|
|
180
|
+
this.hosts.resourceFileAccessPoliciesOf(moduleName),
|
|
181
|
+
guardContext,
|
|
182
|
+
runtime: this.runtime,
|
|
183
|
+
features: this.features,
|
|
184
|
+
signal,
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function boundedTtl(value: number, minimum: number, maximum: number) {
|
|
191
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
192
|
+
throw new ModuleAiArtifactError(
|
|
193
|
+
"AI_ARTIFACT_INVALID",
|
|
194
|
+
`AI Artifact TTL 必须在 ${minimum}–${maximum} 秒之间`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function forbidden() {
|
|
201
|
+
return new ModuleAiArtifactError(
|
|
202
|
+
"AI_ARTIFACT_FORBIDDEN",
|
|
203
|
+
"AI Artifact 当前授权已失效",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -5,7 +5,10 @@ import { GovernedExportJobRuntime } from "./data-governance/export/job-runtime.j
|
|
|
5
5
|
import { SystemGovernedExportAuthorization } from "./data-governance/export/authorization-port.js";
|
|
6
6
|
import { InMemoryGovernedExportRepository } from "./data-governance/export/repository.js";
|
|
7
7
|
import { GovernedExportService } from "./data-governance/export/service.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
GovernedUserExportSource,
|
|
10
|
+
ModuleGovernedExportSource,
|
|
11
|
+
} from "./data-governance/export/source.js";
|
|
9
12
|
import { DrizzleBulkDataRepository } from "./bulk-data/drizzle-repository.js";
|
|
10
13
|
import { InMemoryBulkDataRepository } from "./bulk-data/repository.js";
|
|
11
14
|
import type { createFoundationSnapshot } from "./foundation.js";
|
|
@@ -45,6 +48,12 @@ import {
|
|
|
45
48
|
import { SystemNotificationRecipientDirectory } from "./notification/recipient-directory.js";
|
|
46
49
|
import { NotificationService } from "./notification/service.js";
|
|
47
50
|
import { SmtpNotificationChannelAdapter } from "./notification/smtp-adapter.js";
|
|
51
|
+
import type { InitializedModuleHosts } from "./module-host.js";
|
|
52
|
+
import {
|
|
53
|
+
createModuleDurableContextualizer,
|
|
54
|
+
type ModuleDurableContextServices,
|
|
55
|
+
} from "./module-host/durable-context.js";
|
|
56
|
+
import { resolveCurrentSystemActor } from "./system/actor.js";
|
|
48
57
|
|
|
49
58
|
interface ApplicationServicesInput {
|
|
50
59
|
persistence: ReturnType<typeof createServerPersistence>;
|
|
@@ -56,6 +65,10 @@ interface ApplicationServicesInput {
|
|
|
56
65
|
bulkRetentionDays: number;
|
|
57
66
|
mailConfig?: PlatformConfig["mail"];
|
|
58
67
|
installModuleDurableHandlers?: (registry: DurableHandlerRegistry) => void;
|
|
68
|
+
moduleHosts: InitializedModuleHosts;
|
|
69
|
+
// @windy-module system.agent begin
|
|
70
|
+
backgroundAi?: ModuleDurableContextServices["backgroundAi"];
|
|
71
|
+
// @windy-module system.agent end
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
export async function createApplicationServices(
|
|
@@ -155,6 +168,17 @@ export async function createApplicationServices(
|
|
|
155
168
|
runtime: input.runtime,
|
|
156
169
|
workflow,
|
|
157
170
|
installModuleDurableHandlers: input.installModuleDurableHandlers,
|
|
171
|
+
durableContextualize: createModuleDurableContextualizer({
|
|
172
|
+
hosts: input.moduleHosts,
|
|
173
|
+
runtime: input.runtime,
|
|
174
|
+
features: input.foundation.features,
|
|
175
|
+
storage: input.storage,
|
|
176
|
+
// @windy-module system.agent begin
|
|
177
|
+
backgroundAi: input.backgroundAi,
|
|
178
|
+
// @windy-module system.agent end
|
|
179
|
+
resolveActor: (actorId) =>
|
|
180
|
+
resolveCurrentSystemActor(input.systemRepositories, actorId),
|
|
181
|
+
}),
|
|
158
182
|
});
|
|
159
183
|
const governedExportRequests = persistence
|
|
160
184
|
? new DrizzleGovernedExportRepository(persistence.db)
|
|
@@ -162,6 +186,33 @@ export async function createApplicationServices(
|
|
|
162
186
|
const governedExportApprovals = new WorkflowGovernedExportApproval(workflow, [
|
|
163
187
|
{ code: "super-admin", name: "超级管理员" },
|
|
164
188
|
]);
|
|
189
|
+
const moduleGovernedExportSources = input.moduleHosts.governedResources
|
|
190
|
+
.filter(({ registration }) => registration.export)
|
|
191
|
+
.map(({ moduleName, registration }) => {
|
|
192
|
+
const policy = input.moduleHosts
|
|
193
|
+
.manifestOf(moduleName)
|
|
194
|
+
.dataPolicies?.find(({ key }) => key === registration.policyKey);
|
|
195
|
+
if (!policy) {
|
|
196
|
+
throw new Error(`模块数据治理策略不存在:${registration.policyKey}`);
|
|
197
|
+
}
|
|
198
|
+
return new ModuleGovernedExportSource(registration, policy, (actorId) =>
|
|
199
|
+
resolveCurrentSystemActor(input.systemRepositories, actorId),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
const moduleGovernedExportRenderers = input.moduleHosts.governedResources.flatMap(
|
|
203
|
+
({ moduleName, registration }) => {
|
|
204
|
+
const policy = input.moduleHosts
|
|
205
|
+
.manifestOf(moduleName)
|
|
206
|
+
.dataPolicies?.find(({ key }) => key === registration.policyKey);
|
|
207
|
+
if (!policy) {
|
|
208
|
+
throw new Error(`模块数据治理策略不存在:${registration.policyKey}`);
|
|
209
|
+
}
|
|
210
|
+
return (registration.export?.renderers ?? []).map((renderer) => ({
|
|
211
|
+
resourceType: policy.resourceType,
|
|
212
|
+
renderer,
|
|
213
|
+
}));
|
|
214
|
+
},
|
|
215
|
+
);
|
|
165
216
|
const governedExports = new GovernedExportJobRuntime(
|
|
166
217
|
governedExportRequests,
|
|
167
218
|
services.durable.service,
|
|
@@ -177,9 +228,18 @@ export async function createApplicationServices(
|
|
|
177
228
|
persistence?.authRepository,
|
|
178
229
|
).users,
|
|
179
230
|
),
|
|
231
|
+
...moduleGovernedExportSources,
|
|
180
232
|
],
|
|
181
233
|
(request) => governedExportApprovals.isApproved(request),
|
|
182
|
-
new SystemGovernedExportAuthorization(input.systemRepositories
|
|
234
|
+
new SystemGovernedExportAuthorization(input.systemRepositories, {
|
|
235
|
+
policies: input.moduleHosts.manifests.flatMap(
|
|
236
|
+
(manifest) => manifest.dataPolicies ?? [],
|
|
237
|
+
),
|
|
238
|
+
features: input.foundation.features,
|
|
239
|
+
runtime: input.runtime,
|
|
240
|
+
}),
|
|
241
|
+
undefined,
|
|
242
|
+
moduleGovernedExportRenderers,
|
|
183
243
|
);
|
|
184
244
|
const governedExportService = new GovernedExportService(
|
|
185
245
|
governedExportRequests,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { InMemoryAuthRepository } from "./repository.js";
|
|
3
|
+
import { ReauthenticationService } from "./reauthentication.js";
|
|
4
|
+
|
|
5
|
+
describe("AuthService reauthenticate", () => {
|
|
6
|
+
test("只接受当前启用用户的正确密码", async () => {
|
|
7
|
+
const repository = new InMemoryAuthRepository([
|
|
8
|
+
{
|
|
9
|
+
id: "user-active",
|
|
10
|
+
username: "active",
|
|
11
|
+
displayName: "启用用户",
|
|
12
|
+
passwordHash: "stored-hash",
|
|
13
|
+
credentialState: "active",
|
|
14
|
+
status: "active",
|
|
15
|
+
roleCodes: [],
|
|
16
|
+
permissionKeys: [],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "user-disabled",
|
|
20
|
+
username: "disabled",
|
|
21
|
+
displayName: "停用用户",
|
|
22
|
+
passwordHash: "stored-hash",
|
|
23
|
+
credentialState: "active",
|
|
24
|
+
status: "disabled",
|
|
25
|
+
roleCodes: [],
|
|
26
|
+
permissionKeys: [],
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
const service = new ReauthenticationService(repository, {
|
|
30
|
+
hash: async () => "stored-hash",
|
|
31
|
+
verify: async (hash, password) =>
|
|
32
|
+
hash === "stored-hash" && password === "correct",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(await service.verify("user-active", "correct")).toBe(true);
|
|
36
|
+
expect(await service.verify("user-active", "wrong")).toBe(false);
|
|
37
|
+
expect(await service.verify("user-disabled", "correct")).toBe(false);
|
|
38
|
+
expect(await service.verify("missing", "correct")).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PasswordHasher } from "./password.js";
|
|
2
|
+
import type { AuthRepository } from "./repository.js";
|
|
3
|
+
|
|
4
|
+
export class ReauthenticationService {
|
|
5
|
+
constructor(
|
|
6
|
+
private readonly repository: AuthRepository,
|
|
7
|
+
private readonly passwordHasher: PasswordHasher,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
async verify(actorId: string, password: string): Promise<boolean> {
|
|
11
|
+
if (!actorId.trim() || !password || password.length > 512) return false;
|
|
12
|
+
const user = await this.repository.findUserByReference(actorId);
|
|
13
|
+
return Boolean(
|
|
14
|
+
user?.status === "active" &&
|
|
15
|
+
user.passwordHash &&
|
|
16
|
+
(await this.passwordHasher.verify(user.passwordHash, password)),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -2,6 +2,8 @@ import type { AuditEventReader, AuditEventWriter } from "../runtime.js";
|
|
|
2
2
|
import type { PasswordPolicy } from "@southwind-ai/shared";
|
|
3
3
|
import { InMemoryAuthRepository, type AuthRepository } from "./repository.js";
|
|
4
4
|
import { AuthService } from "./service.js";
|
|
5
|
+
import { createArgon2PasswordHasher } from "./password.js";
|
|
6
|
+
import { ReauthenticationService } from "./reauthentication.js";
|
|
5
7
|
|
|
6
8
|
interface AuthRuntimeInput {
|
|
7
9
|
repository?: AuthRepository;
|
|
@@ -13,11 +15,17 @@ interface AuthRuntimeInput {
|
|
|
13
15
|
|
|
14
16
|
export function createAuthRuntime(input: AuthRuntimeInput) {
|
|
15
17
|
const authRepository = input.repository ?? new InMemoryAuthRepository();
|
|
18
|
+
const passwordHasher = createArgon2PasswordHasher(input.passwordPolicy);
|
|
16
19
|
const authService = new AuthService(authRepository, {
|
|
17
20
|
sessionTtlSeconds: input.sessionTtlSeconds,
|
|
18
21
|
passwordPolicy: input.passwordPolicy,
|
|
19
22
|
auditWriter: { write: input.auditWriter },
|
|
20
23
|
auditReader: input.auditReader,
|
|
24
|
+
passwordHasher,
|
|
21
25
|
});
|
|
22
|
-
|
|
26
|
+
const reauthentication = new ReauthenticationService(
|
|
27
|
+
authRepository,
|
|
28
|
+
passwordHasher,
|
|
29
|
+
);
|
|
30
|
+
return { authRepository, authService, reauthentication };
|
|
23
31
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AuthorizationScopeSnapshot,
|
|
3
|
+
DataScope,
|
|
4
|
+
} from "@southwind-ai/shared";
|
|
3
5
|
|
|
4
6
|
export interface DataAccessRolePolicy {
|
|
5
7
|
id: string;
|
|
@@ -96,7 +98,7 @@ export function isServerScopeContext(value: unknown): value is ScopeContext {
|
|
|
96
98
|
*/
|
|
97
99
|
export function snapshotServerScopeContext(
|
|
98
100
|
value: unknown,
|
|
99
|
-
):
|
|
101
|
+
): AuthorizationScopeSnapshot {
|
|
100
102
|
if (!isServerScopeContext(value)) {
|
|
101
103
|
throw new Error("治理任务缺少服务端 ScopeContext");
|
|
102
104
|
}
|
|
@@ -126,7 +128,7 @@ export function snapshotServerScopeContext(
|
|
|
126
128
|
|
|
127
129
|
/** Worker 只从受控 Repository 读取投影并在服务端恢复 nominal context。 */
|
|
128
130
|
export function restoreServerScopeContext(
|
|
129
|
-
snapshot:
|
|
131
|
+
snapshot: AuthorizationScopeSnapshot,
|
|
130
132
|
): ScopeContext {
|
|
131
133
|
if (
|
|
132
134
|
snapshot.tenantId !== "single-tenant" ||
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { AuthorizationScopeSnapshot } from "@southwind-ai/shared";
|
|
3
|
+
import { snapshotServerScopeContext } from "./context.js";
|
|
4
|
+
|
|
5
|
+
export function captureAuthorizationScope(value: unknown) {
|
|
6
|
+
const snapshot = snapshotServerScopeContext(value);
|
|
7
|
+
return { snapshot, hash: hashAuthorizationScope(snapshot) };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function hashAuthorizationScope(
|
|
11
|
+
snapshot: AuthorizationScopeSnapshot,
|
|
12
|
+
): string {
|
|
13
|
+
return createHash("sha256").update(stableJson(snapshot)).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stableJson(value: unknown): string {
|
|
17
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
18
|
+
if (value && typeof value === "object") {
|
|
19
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
20
|
+
.filter(([, item]) => item !== undefined)
|
|
21
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
22
|
+
return `{${entries
|
|
23
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
|
24
|
+
.join(",")}}`;
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
}
|
|
@@ -26,13 +26,16 @@ export class WorkflowGovernedExportApproval implements GovernedExportApprovalPor
|
|
|
26
26
|
async create(request: GovernedExportRequest, actor: RequestActor) {
|
|
27
27
|
const item = await this.workflow.create(
|
|
28
28
|
{
|
|
29
|
-
title:
|
|
29
|
+
title: `敏感数据导出:${request.reason}`,
|
|
30
30
|
candidateRoles: [...this.candidateRoles],
|
|
31
31
|
dueAt: request.expiresAt,
|
|
32
32
|
target: {
|
|
33
|
-
type:
|
|
33
|
+
type: `${request.resourceType}.governed-export`,
|
|
34
34
|
id: request.id,
|
|
35
|
-
route:
|
|
35
|
+
route:
|
|
36
|
+
request.resourceType === "system.user"
|
|
37
|
+
? `/system/users/exports/${request.id}`
|
|
38
|
+
: "/system/workflow",
|
|
36
39
|
},
|
|
37
40
|
idempotencyKey: `governed-export:${request.id}`,
|
|
38
41
|
},
|