@k2b/cloud 0.6.0-rc.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/README.md +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API for platform-wide runtime settings.
|
|
3
|
+
*
|
|
4
|
+
* This route lives in cloud-lib because `/admin/settings` is a platform page
|
|
5
|
+
* and needs a typed client without depending on the core app package. The
|
|
6
|
+
* core app mounts it under `/api/admin/core/settings`.
|
|
7
|
+
*/
|
|
8
|
+
import { sql } from "bun";
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
import { describeRoute } from "hono-openapi";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { listApps } from "../_internal/registry";
|
|
13
|
+
import { listAiCredentialProfileIds, pruneAiCredentials, setAiCredential, splitAiProfileCredentials } from "../ai/credentials";
|
|
14
|
+
import { enrichDirtyAiConversations } from "../ai/enrich";
|
|
15
|
+
import {
|
|
16
|
+
type AiModelAccessChange,
|
|
17
|
+
AiModelAccessConflict,
|
|
18
|
+
AiModelAccessInvalid,
|
|
19
|
+
aiModelAccess,
|
|
20
|
+
splitAiModelAccess,
|
|
21
|
+
} from "../ai/model-access";
|
|
22
|
+
import { parseAiModelProfiles, planAiProfileCredentials, validateAiSettingsConfiguration } from "../ai/settings";
|
|
23
|
+
import { type AuthContext, auth, jsonResponse, requiresAdmin, v } from "../server";
|
|
24
|
+
import { settingsDeleteLegacyKeys, settingsListLegacyKeys } from "../services";
|
|
25
|
+
import { readAccountCategoryPolicy } from "../services/account-category-policy";
|
|
26
|
+
import { audit } from "../services/audit";
|
|
27
|
+
import { validateFreeIpaCaCert } from "../services/freeipa-config";
|
|
28
|
+
import { testFreeIpaConnection } from "../services/ipa/connection";
|
|
29
|
+
import { sendEmail } from "../services/notifications/email";
|
|
30
|
+
import { GotenbergRenderError, testGotenberg } from "../services/pdf";
|
|
31
|
+
import * as settings from "../services/settings";
|
|
32
|
+
import { SETTINGS_MAP, validateSettingValue } from "../services/settings/defaults";
|
|
33
|
+
|
|
34
|
+
const BulkSaveSchema = z.union([
|
|
35
|
+
z
|
|
36
|
+
.object({
|
|
37
|
+
updates: z.record(z.string(), z.unknown()).optional().default({}),
|
|
38
|
+
resets: z.array(z.string()).optional().default([]),
|
|
39
|
+
})
|
|
40
|
+
.strict(),
|
|
41
|
+
z.record(z.string(), z.unknown()).transform((updates) => ({ updates, resets: [] as string[] })),
|
|
42
|
+
]);
|
|
43
|
+
const TestEmailSchema = z.object({
|
|
44
|
+
recipient: z.email(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const LEGAL_DOCUMENTS = [
|
|
48
|
+
{ kind: "terms", path: "/legal/terms" },
|
|
49
|
+
{ kind: "privacy", path: "/legal/privacy" },
|
|
50
|
+
{ kind: "imprint", path: "/impressum" },
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
type FieldErrors = Record<string, string>;
|
|
54
|
+
|
|
55
|
+
const isKnownSetting = (key: string): boolean => SETTINGS_MAP.has(key);
|
|
56
|
+
|
|
57
|
+
const AI_PROFILES_KEY = "ai.model_profiles_json";
|
|
58
|
+
const AI_ENABLED_KEY = "ai.enabled";
|
|
59
|
+
const AI_DEFAULT_MODEL_KEY = "ai.default_model_id";
|
|
60
|
+
const AI_BACKGROUND_MODEL_KEY = "ai.background_model_id";
|
|
61
|
+
const AI_VISION_MODEL_KEY = "ai.vision_model_id";
|
|
62
|
+
const AI_WORKFLOW_MODEL_KEY = "ai.workflow_model_id";
|
|
63
|
+
const AI_CONFIGURATION_KEYS = new Set([
|
|
64
|
+
AI_ENABLED_KEY,
|
|
65
|
+
AI_DEFAULT_MODEL_KEY,
|
|
66
|
+
AI_BACKGROUND_MODEL_KEY,
|
|
67
|
+
AI_VISION_MODEL_KEY,
|
|
68
|
+
AI_WORKFLOW_MODEL_KEY,
|
|
69
|
+
AI_PROFILES_KEY,
|
|
70
|
+
]);
|
|
71
|
+
const INTEGER_FREEIPA_SETTINGS = new Set(["freeipa.sync_guard.max_user_changes", "freeipa.sync_guard.max_group_deletions"]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Move provider keys out of the model-profiles value before it is stored.
|
|
75
|
+
*
|
|
76
|
+
* The admin form posts profiles as one value, and a newly typed key rides along
|
|
77
|
+
* on its profile. Keys must not land in the setting — it is a `text` setting and
|
|
78
|
+
* would be handed back to the browser in full — so they go to
|
|
79
|
+
* `ai.model_credentials` here and are stripped from what gets saved. A profile
|
|
80
|
+
* whose key field was left empty keeps whatever is already stored.
|
|
81
|
+
*
|
|
82
|
+
* Doing this in the route rather than the settings service keeps the generic
|
|
83
|
+
* store free of AI knowledge, and keeps the admin page at a single save request.
|
|
84
|
+
*/
|
|
85
|
+
const storeAiCredentials = async (
|
|
86
|
+
split: NonNullable<ReturnType<typeof splitAiProfileCredentials>>,
|
|
87
|
+
keepCredentialProfileIds: readonly string[],
|
|
88
|
+
db: typeof sql,
|
|
89
|
+
): Promise<void> => {
|
|
90
|
+
for (const { profileId, secret } of split.credentials) await setAiCredential(profileId, secret, db);
|
|
91
|
+
await pruneAiCredentials(keepCredentialProfileIds, db);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type AiSettingsMutationPlan = {
|
|
95
|
+
errors: FieldErrors;
|
|
96
|
+
keepCredentialProfileIds?: string[];
|
|
97
|
+
modelProfileIds?: string[];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const valueAfterMutation = <T>(key: string, current: T, updates: Record<string, unknown>, resets: readonly string[]): T => {
|
|
101
|
+
if (key in updates) return updates[key] as T;
|
|
102
|
+
if (resets.includes(key)) return SETTINGS_MAP.get(key)?.default as T;
|
|
103
|
+
return current;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const prepareAiSettingsMutation = async (
|
|
107
|
+
updates: Record<string, unknown>,
|
|
108
|
+
resets: readonly string[],
|
|
109
|
+
aiSplit: ReturnType<typeof splitAiProfileCredentials>,
|
|
110
|
+
): Promise<AiSettingsMutationPlan> => {
|
|
111
|
+
const keys = [...Object.keys(updates), ...resets];
|
|
112
|
+
if (!keys.some((key) => AI_CONFIGURATION_KEYS.has(key))) return { errors: {} };
|
|
113
|
+
|
|
114
|
+
const [
|
|
115
|
+
currentEnabled,
|
|
116
|
+
currentDefaultModelId,
|
|
117
|
+
currentBackgroundModelId,
|
|
118
|
+
currentVisionModelId,
|
|
119
|
+
currentWorkflowModelId,
|
|
120
|
+
currentProfilesJson,
|
|
121
|
+
] = await Promise.all([
|
|
122
|
+
settings.get<boolean>(AI_ENABLED_KEY),
|
|
123
|
+
settings.get<string>(AI_DEFAULT_MODEL_KEY),
|
|
124
|
+
settings.get<string>(AI_BACKGROUND_MODEL_KEY),
|
|
125
|
+
settings.get<string>(AI_VISION_MODEL_KEY),
|
|
126
|
+
settings.get<string>(AI_WORKFLOW_MODEL_KEY),
|
|
127
|
+
settings.get<string>(AI_PROFILES_KEY),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
const currentParsed = parseAiModelProfiles(currentProfilesJson ?? "[]");
|
|
131
|
+
const profilesUpdated = AI_PROFILES_KEY in updates;
|
|
132
|
+
const profilesReset = resets.includes(AI_PROFILES_KEY);
|
|
133
|
+
const nextParsed: ReturnType<typeof parseAiModelProfiles> = profilesUpdated
|
|
134
|
+
? parseAiModelProfiles(aiSplit?.profilesJson ?? "[]")
|
|
135
|
+
: profilesReset
|
|
136
|
+
? { profiles: [] }
|
|
137
|
+
: currentParsed;
|
|
138
|
+
if (nextParsed.error) return { errors: nextParsed.error.fields ?? { [AI_PROFILES_KEY]: nextParsed.error.message } };
|
|
139
|
+
|
|
140
|
+
const nextEnabled = Boolean(valueAfterMutation(AI_ENABLED_KEY, currentEnabled, updates, resets));
|
|
141
|
+
const existingCredentialProfileIds = profilesUpdated || (!profilesReset && nextEnabled) ? await listAiCredentialProfileIds() : [];
|
|
142
|
+
|
|
143
|
+
let keepCredentialProfileIds: string[] | undefined;
|
|
144
|
+
if (profilesUpdated || profilesReset) {
|
|
145
|
+
const credentialPlan = planAiProfileCredentials({
|
|
146
|
+
currentProfiles: currentParsed.profiles,
|
|
147
|
+
nextProfiles: nextParsed.profiles,
|
|
148
|
+
existingCredentialProfileIds,
|
|
149
|
+
submittedCredentialProfileIds: aiSplit?.credentials.map(({ profileId }) => profileId) ?? [],
|
|
150
|
+
});
|
|
151
|
+
if (credentialPlan.unsupportedCredentialProfileId) {
|
|
152
|
+
return {
|
|
153
|
+
errors: {
|
|
154
|
+
[AI_PROFILES_KEY]: `Profile "${credentialPlan.unsupportedCredentialProfileId}" does not support provider credentials.`,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
keepCredentialProfileIds = credentialPlan.keepCredentialProfileIds;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const errors = validateAiSettingsConfiguration({
|
|
162
|
+
enabled: nextEnabled,
|
|
163
|
+
defaultModelId: String(valueAfterMutation(AI_DEFAULT_MODEL_KEY, currentDefaultModelId ?? "", updates, resets)),
|
|
164
|
+
backgroundModelId: String(valueAfterMutation(AI_BACKGROUND_MODEL_KEY, currentBackgroundModelId ?? "", updates, resets)),
|
|
165
|
+
visionModelId: String(valueAfterMutation(AI_VISION_MODEL_KEY, currentVisionModelId ?? "", updates, resets)),
|
|
166
|
+
workflowModelId: String(valueAfterMutation(AI_WORKFLOW_MODEL_KEY, currentWorkflowModelId ?? "", updates, resets)),
|
|
167
|
+
profiles: nextParsed.profiles,
|
|
168
|
+
credentialProfileIds: keepCredentialProfileIds ?? existingCredentialProfileIds,
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
errors,
|
|
172
|
+
keepCredentialProfileIds,
|
|
173
|
+
modelProfileIds: profilesUpdated || profilesReset ? nextParsed.profiles.map((profile) => profile.id) : undefined,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const invalidateCommittedSettings = async (keys: readonly string[]): Promise<void> => {
|
|
178
|
+
try {
|
|
179
|
+
await settings.invalidateSettingsCache(keys);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error("[settings] committed values but failed to invalidate the shared cache; TTL recovery remains active", error);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const liveSettingKeys = async () => (await listApps()).flatMap((app) => [...(app.settingKeys ?? [])]);
|
|
185
|
+
|
|
186
|
+
const app = new Hono<AuthContext>()
|
|
187
|
+
.get(
|
|
188
|
+
"/documentation",
|
|
189
|
+
auth.requireRole("admin"),
|
|
190
|
+
describeRoute({
|
|
191
|
+
tags: ["Administration"],
|
|
192
|
+
summary: "Read the documentation website configuration",
|
|
193
|
+
...requiresAdmin,
|
|
194
|
+
responses: { 200: jsonResponse(z.object({ url: z.string() }), "Documentation base URL") },
|
|
195
|
+
}),
|
|
196
|
+
async (c) => {
|
|
197
|
+
c.header("Cache-Control", "no-store");
|
|
198
|
+
return c.json({ url: await settings.get<string>("app.documentation_url") });
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
.get(
|
|
202
|
+
"/app-sign-in",
|
|
203
|
+
auth.requireRole("admin"),
|
|
204
|
+
describeRoute({
|
|
205
|
+
tags: ["Administration"],
|
|
206
|
+
summary: "Read app sign-in configuration",
|
|
207
|
+
...requiresAdmin,
|
|
208
|
+
responses: {
|
|
209
|
+
200: jsonResponse(z.object({ enabled: z.boolean(), origin: z.string(), adminPairing: z.boolean() }), "App sign-in configuration"),
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
async (c) =>
|
|
213
|
+
c.json({
|
|
214
|
+
enabled: await settings.get<boolean>("user.app_approval.enabled"),
|
|
215
|
+
origin: await settings.get<string>("user.app_approval.origin"),
|
|
216
|
+
adminPairing: await settings.get<boolean>("user.app_approval.admin_pairing"),
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
.get("/account-categories", auth.requireRole("admin"), async (c) => c.json(await readAccountCategoryPolicy()))
|
|
220
|
+
.get(
|
|
221
|
+
"/account-administration",
|
|
222
|
+
auth.requireRole("admin"),
|
|
223
|
+
describeRoute({
|
|
224
|
+
tags: ["Administration"],
|
|
225
|
+
summary: "Read account-request policy and follow-up notice configuration",
|
|
226
|
+
...requiresAdmin,
|
|
227
|
+
responses: {
|
|
228
|
+
200: jsonResponse(z.object({ requestsEnabled: z.boolean(), actionNotice: z.string() }), "Current account administration options"),
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
async (c) => {
|
|
232
|
+
const [requestsEnabled, actionNotice] = await Promise.all([
|
|
233
|
+
settings.get<boolean>("user.account_requests.enabled"),
|
|
234
|
+
settings.get<string>("user.action_notice"),
|
|
235
|
+
]);
|
|
236
|
+
c.header("Cache-Control", "no-store");
|
|
237
|
+
return c.json({ requestsEnabled, actionNotice });
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
.get("/legal", auth.requireRole("admin"), async (c) => {
|
|
241
|
+
const items = await Promise.all(
|
|
242
|
+
LEGAL_DOCUMENTS.map(async ({ kind, path }) => {
|
|
243
|
+
const [rawMode, content, url] = await Promise.all([
|
|
244
|
+
settings.get<string>(`legal.${kind}.mode`),
|
|
245
|
+
settings.get<string>(`legal.${kind}.content`),
|
|
246
|
+
settings.get<string>(`legal.${kind}.url`),
|
|
247
|
+
]);
|
|
248
|
+
return {
|
|
249
|
+
kind,
|
|
250
|
+
path,
|
|
251
|
+
mode: rawMode === "external" ? "external" : "local",
|
|
252
|
+
content: content ?? "",
|
|
253
|
+
url: url ?? "",
|
|
254
|
+
};
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
return c.json({ items });
|
|
258
|
+
})
|
|
259
|
+
.get("/legacy", auth.requireRole("admin"), async (c) => {
|
|
260
|
+
return c.json(await settingsListLegacyKeys(await liveSettingKeys()));
|
|
261
|
+
})
|
|
262
|
+
.delete("/legacy", auth.requireRole("admin"), async (c) => {
|
|
263
|
+
return c.json(await settingsDeleteLegacyKeys(await liveSettingKeys()));
|
|
264
|
+
})
|
|
265
|
+
.post("/test-email", auth.requireRole("admin"), v("json", TestEmailSchema), async (c) => {
|
|
266
|
+
const { recipient } = c.req.valid("json");
|
|
267
|
+
const sentAt = new Date().toISOString();
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await sendEmail(recipient, "Cloud test email", {
|
|
271
|
+
rawHtml: `
|
|
272
|
+
<p>This is a test email from Cloud.</p>
|
|
273
|
+
<p>If you received this message, SMTP delivery is configured correctly.</p>
|
|
274
|
+
<p style="margin-top:24px;color:#71717a;font-size:12px;">Sent at ${sentAt}</p>
|
|
275
|
+
`,
|
|
276
|
+
});
|
|
277
|
+
return c.json({ ok: true });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const message = error instanceof Error ? error.message : "Failed to send test email";
|
|
280
|
+
return c.json({ message }, 500);
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
.post("/run-ai-enrichment", auth.requireRole("admin"), async (c) => {
|
|
284
|
+
try {
|
|
285
|
+
// Small manual batch — the cron handles bulk; this is "kick it now".
|
|
286
|
+
const summary = await enrichDirtyAiConversations({ limit: 25 });
|
|
287
|
+
return c.json({ ok: true, summary });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return c.json({ message: error instanceof Error ? error.message : "AI enrichment run failed" }, 500);
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
.post("/test-pdf", auth.requireRole("admin"), async (c) => {
|
|
293
|
+
try {
|
|
294
|
+
const result = await testGotenberg();
|
|
295
|
+
return c.json({ ok: true, bytes: result.bytes, contentType: result.contentType });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof GotenbergRenderError) {
|
|
298
|
+
return c.json({ message: error.message, code: error.code, status: error.status }, error.code === "not_configured" ? 400 : 502);
|
|
299
|
+
}
|
|
300
|
+
return c.json({ message: "Failed to test Gotenberg PDF rendering" }, 500);
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
.post("/test-freeipa", auth.requireRole("admin"), async (c) => {
|
|
304
|
+
const result = await testFreeIpaConnection();
|
|
305
|
+
if (result.ok) return c.json({ ok: true });
|
|
306
|
+
return c.json({ message: result.error, kind: result.kind }, result.status);
|
|
307
|
+
})
|
|
308
|
+
.put("/", auth.requireRole("admin"), v("json", BulkSaveSchema), async (c) => {
|
|
309
|
+
const { updates, resets } = c.req.valid("json");
|
|
310
|
+
const updateKeys = Object.keys(updates);
|
|
311
|
+
const keys = [...updateKeys, ...resets];
|
|
312
|
+
|
|
313
|
+
if (keys.length === 0) {
|
|
314
|
+
return c.body(null, 204);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ownership: FieldErrors = {};
|
|
318
|
+
for (const key of keys) {
|
|
319
|
+
if (!isKnownSetting(key)) {
|
|
320
|
+
ownership[key] = `Unknown setting "${key}"`;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const key of resets) {
|
|
324
|
+
if (key in updates) {
|
|
325
|
+
ownership[key] = `Setting "${key}" cannot be updated and reset in the same save`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (Object.keys(ownership).length > 0) {
|
|
329
|
+
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Strip AI provider keys before anything is validated or written, so the
|
|
333
|
+
// value that gets checked is the value that gets stored.
|
|
334
|
+
const aiSplit = AI_PROFILES_KEY in updates ? splitAiProfileCredentials(updates[AI_PROFILES_KEY]) : null;
|
|
335
|
+
if (AI_PROFILES_KEY in updates && !aiSplit) {
|
|
336
|
+
// Unsplittable means the keys could not be separated out, and storing the
|
|
337
|
+
// value as posted would put them back in a setting the browser reads.
|
|
338
|
+
return c.json({ message: "Invalid values", errors: { [AI_PROFILES_KEY]: "Model profiles must be a JSON array" } }, 400);
|
|
339
|
+
}
|
|
340
|
+
let accessChanges: AiModelAccessChange[] = [];
|
|
341
|
+
if (aiSplit) {
|
|
342
|
+
try {
|
|
343
|
+
const split = splitAiModelAccess(aiSplit.profilesJson);
|
|
344
|
+
aiSplit.profilesJson = split.profilesJson;
|
|
345
|
+
accessChanges = split.changes;
|
|
346
|
+
} catch {
|
|
347
|
+
return c.json(
|
|
348
|
+
{
|
|
349
|
+
message: "Invalid model permissions",
|
|
350
|
+
errors: { [AI_PROFILES_KEY]: "Invalid Assistant model permissions. Reload the settings and try again." },
|
|
351
|
+
},
|
|
352
|
+
400,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const finalValues: Record<string, unknown> = aiSplit ? { ...updates, [AI_PROFILES_KEY]: aiSplit.profilesJson } : updates;
|
|
357
|
+
|
|
358
|
+
// Validate everything up front. settings.set validates as it writes, so a
|
|
359
|
+
// single bad field used to leave the earlier keys already saved — with the
|
|
360
|
+
// AI profiles among them, whose save also prunes credentials.
|
|
361
|
+
const invalid: FieldErrors = {};
|
|
362
|
+
const validatedValues: Record<string, unknown> = {};
|
|
363
|
+
for (const [key, value] of Object.entries(finalValues)) {
|
|
364
|
+
const def = SETTINGS_MAP.get(key);
|
|
365
|
+
if (!def) continue;
|
|
366
|
+
const validated = validateSettingValue(def, value);
|
|
367
|
+
if (validated.ok) validatedValues[key] = validated.value;
|
|
368
|
+
else invalid[key] = validated.error;
|
|
369
|
+
if (key === "freeipa.ca_cert") {
|
|
370
|
+
const certificate = validateFreeIpaCaCert(value);
|
|
371
|
+
if (!certificate.ok) invalid[key] = certificate.error;
|
|
372
|
+
}
|
|
373
|
+
if (INTEGER_FREEIPA_SETTINGS.has(key) && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
374
|
+
invalid[key] = "Value must be a whole number";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (Object.keys(invalid).length > 0) {
|
|
378
|
+
return c.json({ message: "Invalid values", errors: invalid }, 400);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let aiPlan: AiSettingsMutationPlan;
|
|
382
|
+
try {
|
|
383
|
+
aiPlan = await prepareAiSettingsMutation(validatedValues, resets, aiSplit);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
386
|
+
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
387
|
+
}
|
|
388
|
+
if (Object.keys(aiPlan.errors).length > 0) {
|
|
389
|
+
return c.json({ message: "Invalid AI configuration", errors: aiPlan.errors }, 400);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const fieldErrors: FieldErrors = {};
|
|
393
|
+
try {
|
|
394
|
+
// Every settings and AI credential write runs on the transaction's own
|
|
395
|
+
// connection, so a failure rolls the whole save back.
|
|
396
|
+
await sql.begin(async (tx) => {
|
|
397
|
+
for (const [key, value] of Object.entries(validatedValues)) {
|
|
398
|
+
try {
|
|
399
|
+
await settings.set(key, value, tx);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
fieldErrors[key] = `Failed to update ${key}`;
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const key of resets) {
|
|
406
|
+
try {
|
|
407
|
+
await settings.remove(key, tx);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
fieldErrors[key] = `Failed to reset ${key}`;
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const categoryKeys = keys.filter((key) => key.startsWith("user.category."));
|
|
414
|
+
if (categoryKeys.length > 0)
|
|
415
|
+
await audit.record(
|
|
416
|
+
{
|
|
417
|
+
action: "accounts.categories.configure",
|
|
418
|
+
outcome: "allowed",
|
|
419
|
+
actor: { userId: c.get("user").id },
|
|
420
|
+
target: { type: "settings", id: "user.category" },
|
|
421
|
+
metadata: {
|
|
422
|
+
keys: categoryKeys,
|
|
423
|
+
updates: Object.fromEntries(categoryKeys.filter((key) => key in validatedValues).map((key) => [key, validatedValues[key]])),
|
|
424
|
+
resets: resets.filter((key) => categoryKeys.includes(key)),
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
tx,
|
|
428
|
+
);
|
|
429
|
+
if (aiSplit && aiPlan.keepCredentialProfileIds) {
|
|
430
|
+
await storeAiCredentials(aiSplit, aiPlan.keepCredentialProfileIds, tx);
|
|
431
|
+
} else if (aiPlan.keepCredentialProfileIds) {
|
|
432
|
+
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
433
|
+
}
|
|
434
|
+
if (aiPlan.modelProfileIds) {
|
|
435
|
+
await aiModelAccess.syncProfiles(aiPlan.modelProfileIds, accessChanges, tx);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (error instanceof AiModelAccessConflict || error instanceof AiModelAccessInvalid) {
|
|
440
|
+
return c.json(
|
|
441
|
+
{ message: error.message, errors: { [AI_PROFILES_KEY]: error.message } },
|
|
442
|
+
error instanceof AiModelAccessConflict ? 409 : 400,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
console.error("[settings] failed to commit settings update", error);
|
|
446
|
+
return c.json(
|
|
447
|
+
{
|
|
448
|
+
message: "Save failed",
|
|
449
|
+
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : { _form: "The settings could not be saved." },
|
|
450
|
+
},
|
|
451
|
+
500,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Only now: dropping the cache before the commit would let another
|
|
456
|
+
// container miss, read the pre-commit row and cache it for the full TTL.
|
|
457
|
+
await invalidateCommittedSettings(keys);
|
|
458
|
+
|
|
459
|
+
return c.body(null, 204);
|
|
460
|
+
})
|
|
461
|
+
.delete("/:key{.+}", auth.requireRole("admin"), async (c) => {
|
|
462
|
+
const key = c.req.param("key");
|
|
463
|
+
if (!isKnownSetting(key)) {
|
|
464
|
+
return c.json({ message: `Unknown setting "${key}"` }, 400);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let aiPlan: AiSettingsMutationPlan;
|
|
468
|
+
try {
|
|
469
|
+
aiPlan = await prepareAiSettingsMutation({}, [key], null);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
472
|
+
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
473
|
+
}
|
|
474
|
+
if (Object.keys(aiPlan.errors).length > 0) {
|
|
475
|
+
return c.json({ message: "Invalid AI configuration", errors: aiPlan.errors }, 400);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
await sql.begin(async (tx) => {
|
|
480
|
+
await settings.remove(key, tx);
|
|
481
|
+
if (key.startsWith("user.category."))
|
|
482
|
+
await audit.record(
|
|
483
|
+
{
|
|
484
|
+
action: "accounts.categories.configure",
|
|
485
|
+
outcome: "allowed",
|
|
486
|
+
actor: { userId: c.get("user").id },
|
|
487
|
+
target: { type: "setting", id: key },
|
|
488
|
+
metadata: { reset: true },
|
|
489
|
+
},
|
|
490
|
+
tx,
|
|
491
|
+
);
|
|
492
|
+
if (aiPlan.keepCredentialProfileIds) {
|
|
493
|
+
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
494
|
+
}
|
|
495
|
+
if (aiPlan.modelProfileIds) {
|
|
496
|
+
await aiModelAccess.syncProfiles(aiPlan.modelProfileIds, [], tx);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error(`[settings] failed to reset "${key}"`, error);
|
|
501
|
+
return c.json({ message: "Reset failed" }, 500);
|
|
502
|
+
}
|
|
503
|
+
await invalidateCommittedSettings([key]);
|
|
504
|
+
return c.body(null, 204);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
export default app;
|
|
508
|
+
export type ApiType = typeof app;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { fail, ok } from "@k2b/stdlib";
|
|
2
|
+
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { CapabilityAppIdSchema } from "../contracts/capabilities";
|
|
6
|
+
import { type AuthContext, auth, err, respond, v } from "../server";
|
|
7
|
+
import { getIdentitySigningKeyStatus, identityMetrics, revokeIdentitySigningKey, rewrapIdentitySigningKeys } from "../services/identity";
|
|
8
|
+
import { WORKLOAD_SCOPES } from "../services/identity/workload-auth";
|
|
9
|
+
import { mandateMetrics, mandates } from "../services/mandates";
|
|
10
|
+
import { serviceAccountCredentials } from "../services/service-account-credentials";
|
|
11
|
+
import { serviceAccounts } from "../services/service-accounts";
|
|
12
|
+
|
|
13
|
+
const WorkloadCredentialInputSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
name: z.string().trim().min(1).max(120),
|
|
16
|
+
scopes: z
|
|
17
|
+
.array(z.enum(WORKLOAD_SCOPES))
|
|
18
|
+
.min(1)
|
|
19
|
+
.max(WORKLOAD_SCOPES.length)
|
|
20
|
+
.refine((scopes) => new Set(scopes).size === scopes.length, "Scopes must be unique"),
|
|
21
|
+
expiresAt: z.iso.datetime().nullable().optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
const WorkloadAppParamsSchema = z.object({ appId: CapabilityAppIdSchema }).strict();
|
|
26
|
+
const WorkloadCredentialParamsSchema = WorkloadAppParamsSchema.extend({ credentialId: z.uuid() }).strict();
|
|
27
|
+
const WorkloadCredentialListQuerySchema = z
|
|
28
|
+
.object({
|
|
29
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
30
|
+
perPage: z.coerce.number().int().min(1).max(500).default(100),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
const MandateListQuerySchema = z
|
|
34
|
+
.object({
|
|
35
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
36
|
+
perPage: z.coerce.number().int().min(1).max(100).default(50),
|
|
37
|
+
ownerAppId: CapabilityAppIdSchema.optional(),
|
|
38
|
+
state: z.enum(["active", "paused", "revoked"]).optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
|
|
42
|
+
type IdentityAdminDependencies = {
|
|
43
|
+
status: typeof getIdentitySigningKeyStatus;
|
|
44
|
+
rewrap: typeof rewrapIdentitySigningKeys;
|
|
45
|
+
revoke: typeof revokeIdentitySigningKey;
|
|
46
|
+
serviceAccounts: Pick<typeof serviceAccounts, "getOrCreateResourceBound">;
|
|
47
|
+
credentials: Pick<typeof serviceAccountCredentials, "createResourceApiToken" | "getOverview" | "listOverview" | "revoke">;
|
|
48
|
+
mandates: Pick<typeof mandates, "list">;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const dependencies: IdentityAdminDependencies = {
|
|
52
|
+
status: getIdentitySigningKeyStatus,
|
|
53
|
+
rewrap: rewrapIdentitySigningKeys,
|
|
54
|
+
revoke: revokeIdentitySigningKey,
|
|
55
|
+
serviceAccounts,
|
|
56
|
+
credentials: serviceAccountCredentials,
|
|
57
|
+
mandates,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const createAdminIdentityRoutes = (
|
|
61
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
62
|
+
service: IdentityAdminDependencies = dependencies,
|
|
63
|
+
) =>
|
|
64
|
+
new Hono<AuthContext>()
|
|
65
|
+
.use(authenticate)
|
|
66
|
+
.post("/workloads/:appId/credentials", v("param", WorkloadAppParamsSchema), v("json", WorkloadCredentialInputSchema), async (c) =>
|
|
67
|
+
respond(
|
|
68
|
+
c,
|
|
69
|
+
async () => {
|
|
70
|
+
const { appId } = c.req.valid("param");
|
|
71
|
+
const input = c.req.valid("json");
|
|
72
|
+
const actor = c.get("user");
|
|
73
|
+
const account = await service.serviceAccounts.getOrCreateResourceBound({
|
|
74
|
+
name: `${appId} workload`,
|
|
75
|
+
appId,
|
|
76
|
+
resourceType: "cloud.app",
|
|
77
|
+
resourceId: appId,
|
|
78
|
+
createdBy: actor.id,
|
|
79
|
+
});
|
|
80
|
+
if (!account.ok) return fail(account.error);
|
|
81
|
+
|
|
82
|
+
// The raw token intentionally appears only in this one create response.
|
|
83
|
+
return service.credentials.createResourceApiToken({
|
|
84
|
+
serviceAccountId: account.data.id,
|
|
85
|
+
actor,
|
|
86
|
+
name: input.name,
|
|
87
|
+
scopes: input.scopes,
|
|
88
|
+
expiresAt: input.expiresAt,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
201,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
.get("/workloads/:appId/credentials", v("param", WorkloadAppParamsSchema), v("query", WorkloadCredentialListQuerySchema), async (c) => {
|
|
95
|
+
const { appId } = c.req.valid("param");
|
|
96
|
+
return c.json(
|
|
97
|
+
await service.credentials.listOverview({
|
|
98
|
+
pagination: c.req.valid("query"),
|
|
99
|
+
filter: { serviceAccountKind: "resource_bound", appId, resourceType: "cloud.app", resourceId: appId },
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
})
|
|
103
|
+
.delete("/workloads/:appId/credentials/:credentialId", v("param", WorkloadCredentialParamsSchema), async (c) =>
|
|
104
|
+
respond(c, async () => {
|
|
105
|
+
const { appId, credentialId } = c.req.valid("param");
|
|
106
|
+
const credential = await service.credentials.getOverview({ id: credentialId });
|
|
107
|
+
if (
|
|
108
|
+
!credential ||
|
|
109
|
+
credential.owner.type !== "resource" ||
|
|
110
|
+
credential.owner.appId !== appId ||
|
|
111
|
+
credential.owner.resourceType !== "cloud.app" ||
|
|
112
|
+
credential.owner.resourceId !== appId
|
|
113
|
+
) {
|
|
114
|
+
return fail(err.notFound("Workload credential"));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const revoked = await service.credentials.revoke({ credentialId, actor: c.get("user") });
|
|
118
|
+
return revoked.ok ? ok({ revoked: true as const }) : revoked;
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
.get("/mandates", v("query", MandateListQuerySchema), async (c) => {
|
|
122
|
+
const query = c.req.valid("query");
|
|
123
|
+
return c.json(
|
|
124
|
+
await service.mandates.list({
|
|
125
|
+
scope: { kind: "admin" },
|
|
126
|
+
pagination: { page: query.page, perPage: query.perPage },
|
|
127
|
+
filter: { ownerAppId: query.ownerAppId, state: query.state },
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
})
|
|
131
|
+
.get("/keys", async (c) =>
|
|
132
|
+
c.json({ keys: await service.status(), metrics: identityMetrics.snapshot(), mandateMetrics: mandateMetrics.snapshot() }),
|
|
133
|
+
)
|
|
134
|
+
.post("/keys/rewrap", async (c) => c.json({ rewrapped: await service.rewrap() }))
|
|
135
|
+
.post(
|
|
136
|
+
"/keys/:kid/revoke",
|
|
137
|
+
v("param", z.object({ kid: z.uuid() })),
|
|
138
|
+
v("json", z.object({ reason: z.string().trim().min(1).max(500) })),
|
|
139
|
+
async (c) => {
|
|
140
|
+
const revoked = await service.revoke({
|
|
141
|
+
kid: c.req.valid("param").kid,
|
|
142
|
+
reason: c.req.valid("json").reason,
|
|
143
|
+
});
|
|
144
|
+
return revoked ? c.json({ revoked: true }) : c.json({ message: "Signing key not found" }, 404);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
export default createAdminIdentityRoutes();
|