@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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption helpers for settings at-rest.
|
|
3
|
+
*
|
|
4
|
+
* Settings DB rows are encrypted via stdlib's `crypto.symmetric.encrypt`
|
|
5
|
+
* (AES-256-GCM with HKDF key derivation). The key material comes from
|
|
6
|
+
* `APP_SECRET` — see `getAppSecret()` for the hex/passphrase handling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { env } from "../../config/env";
|
|
11
|
+
import { crypto } from "../../server/services";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the encryption key passed to stdlib's `crypto.symmetric.encrypt`.
|
|
15
|
+
*
|
|
16
|
+
* Stdlib derives keys via HKDF and requires the input to be a hex string
|
|
17
|
+
* (it calls `fromHex(key)` internally). Two cases:
|
|
18
|
+
*
|
|
19
|
+
* 1. APP_SECRET is already a valid hex string (any even length).
|
|
20
|
+
* Pass it through unchanged. THIS IS THE PRODUCTION PATH — existing
|
|
21
|
+
* deployments use hex secrets and have data encrypted with the secret
|
|
22
|
+
* verbatim. Any transformation here would change the derived HKDF key
|
|
23
|
+
* and make every previously encrypted settings row unreadable.
|
|
24
|
+
*
|
|
25
|
+
* 2. APP_SECRET is not valid hex (e.g. a passphrase like "supersecret").
|
|
26
|
+
* Deterministically derive a 32-byte hex key via SHA-256. Same input
|
|
27
|
+
* always produces the same hex output, so settings encrypted with a
|
|
28
|
+
* non-hex secret stay decryptable across restarts.
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ DO NOT change the hex-passthrough branch. Hashing a hex secret would
|
|
31
|
+
* silently break every prod instance: encrypted settings could no longer
|
|
32
|
+
* be decrypted because the HKDF input would differ from what was used at
|
|
33
|
+
* write time. The branch exists specifically to preserve backward compat
|
|
34
|
+
* with deployments that have always used hex-format APP_SECRET.
|
|
35
|
+
*/
|
|
36
|
+
export const getAppSecret = (): string => {
|
|
37
|
+
const raw = env.APP_SECRET.trim();
|
|
38
|
+
if (!raw) {
|
|
39
|
+
throw new Error("APP_SECRET is required to read or write encrypted settings");
|
|
40
|
+
}
|
|
41
|
+
// Case 1: already hex — pass through (backward-compat, see doc above).
|
|
42
|
+
if (raw.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(raw)) {
|
|
43
|
+
return raw;
|
|
44
|
+
}
|
|
45
|
+
// Case 2: non-hex passphrase — deterministically derive hex via SHA-256.
|
|
46
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const encryptValue = async (value: unknown): Promise<string> =>
|
|
50
|
+
crypto.symmetric.encrypt({
|
|
51
|
+
payload: JSON.stringify(value),
|
|
52
|
+
key: getAppSecret(),
|
|
53
|
+
stretched: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const decryptValue = async (value: string): Promise<unknown> => {
|
|
57
|
+
const decrypted = await crypto.symmetric.decrypt({
|
|
58
|
+
payload: value,
|
|
59
|
+
key: getAppSecret(),
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(decrypted);
|
|
63
|
+
} catch {
|
|
64
|
+
// Decrypt succeeded but the plaintext isn't JSON. Either pre-JSON-encoding
|
|
65
|
+
// legacy data or external write — return the raw string so callers can
|
|
66
|
+
// decide. Throwing here would corrupt the read path entirely.
|
|
67
|
+
return decrypted;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central settings registry.
|
|
3
|
+
* Single source of truth for configurable settings, their value kinds, defaults,
|
|
4
|
+
* validation, UI metadata, and temporary env bootstrap behavior.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order: DB value -> env fallback -> code default.
|
|
7
|
+
*
|
|
8
|
+
* `SettingKind` and `SettingOption` are re-exported from `contracts/shared` to
|
|
9
|
+
* keep a single source of truth (browser-safe types live there).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AppSettingPresentationTranslation } from "../../contracts/settings-types";
|
|
13
|
+
import type { SettingKind, SettingOption } from "../../contracts/shared";
|
|
14
|
+
import { ACCOUNT_ACTION_NOTICE_SAMPLE, renderAccountActionNotice } from "../../shared/account-action-notice";
|
|
15
|
+
import { canonicalLocale, localeFallbackChain, normalizeLocale } from "../../shared/locale";
|
|
16
|
+
import { migrateLegacyMustacheTemplate, validateLiquidTemplate } from "../../shared/template-rendering";
|
|
17
|
+
import { CORE_SETTINGS } from "./core-settings";
|
|
18
|
+
|
|
19
|
+
export type { SettingKind, SettingOption };
|
|
20
|
+
|
|
21
|
+
type SettingEnvResolver = () => unknown;
|
|
22
|
+
|
|
23
|
+
type SettingCommon = {
|
|
24
|
+
key: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
description: string;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
presentation?: {
|
|
29
|
+
baseLocale: string;
|
|
30
|
+
translations: Readonly<Record<string, AppSettingPresentationTranslation>>;
|
|
31
|
+
};
|
|
32
|
+
group: string;
|
|
33
|
+
envFallback?: SettingEnvResolver;
|
|
34
|
+
envBootstrap?: SettingEnvResolver;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type SettingStringLikeKind = "string" | "text" | "email" | "url" | "secret" | "image" | "cron" | "timezone" | "template";
|
|
38
|
+
|
|
39
|
+
type StringLikeSettingDef = SettingCommon & {
|
|
40
|
+
kind: SettingStringLikeKind;
|
|
41
|
+
default: string;
|
|
42
|
+
templateVars?: string[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type BooleanSettingDef = SettingCommon & {
|
|
46
|
+
kind: "boolean";
|
|
47
|
+
default: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type NumberSettingDef = SettingCommon & {
|
|
51
|
+
kind: "number";
|
|
52
|
+
default: number;
|
|
53
|
+
min?: number;
|
|
54
|
+
max?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type EnumSettingDef = SettingCommon & {
|
|
58
|
+
kind: "enum";
|
|
59
|
+
default: string;
|
|
60
|
+
options: SettingOption[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type StringListSettingDef = SettingCommon & {
|
|
64
|
+
kind: "string_list";
|
|
65
|
+
default: string[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type NumberListSettingDef = SettingCommon & {
|
|
69
|
+
kind: "number_list";
|
|
70
|
+
default: number[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type SettingDef =
|
|
74
|
+
| StringLikeSettingDef
|
|
75
|
+
| BooleanSettingDef
|
|
76
|
+
| NumberSettingDef
|
|
77
|
+
| EnumSettingDef
|
|
78
|
+
| StringListSettingDef
|
|
79
|
+
| NumberListSettingDef;
|
|
80
|
+
|
|
81
|
+
export type SettingValidationResult = { ok: true; value: SettingDef["default"] } | { ok: false; error: string };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Convert a `defineApp({ settings })` map into the legacy array shape.
|
|
85
|
+
*
|
|
86
|
+
* `group` is derived from the dotted prefix: the bespoke admin UIs ignore it,
|
|
87
|
+
* but legacy paths still group by it.
|
|
88
|
+
*/
|
|
89
|
+
const compilePresentation = (value: unknown, baseLocale: string, key: string): SettingCommon["presentation"] => {
|
|
90
|
+
if (!value) return undefined;
|
|
91
|
+
const catalog = value as { translations?: Readonly<Record<string, AppSettingPresentationTranslation>> };
|
|
92
|
+
const translations: Record<string, AppSettingPresentationTranslation> = {};
|
|
93
|
+
for (const [locale, translation] of Object.entries(catalog.translations ?? {})) {
|
|
94
|
+
const canonical = canonicalLocale(locale);
|
|
95
|
+
if (!canonical) throw new Error(`Setting "${key}" presentation locale ${JSON.stringify(locale)} is invalid`);
|
|
96
|
+
if (canonical === baseLocale) throw new Error(`Setting "${key}" presentation must not repeat base locale ${baseLocale}`);
|
|
97
|
+
if (translations[canonical]) throw new Error(`Setting "${key}" presentation locale ${canonical} is duplicated`);
|
|
98
|
+
translations[canonical] = Object.freeze({ ...translation });
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({ baseLocale, translations: Object.freeze(translations) });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const toLegacySettingDefs = (settings: Record<string, unknown>, requestedBaseLocale = "en"): SettingDef[] => {
|
|
104
|
+
const baseLocale = normalizeLocale(requestedBaseLocale);
|
|
105
|
+
return Object.entries(settings).map(([key, def]) => {
|
|
106
|
+
const d = def as Record<string, unknown>;
|
|
107
|
+
return {
|
|
108
|
+
key,
|
|
109
|
+
group: key.split(".")[0] ?? "app",
|
|
110
|
+
kind: d.kind as SettingDef["kind"],
|
|
111
|
+
// The cast loses the per-kind discriminated default type but the data is
|
|
112
|
+
// correct; validateSettingValue re-validates against kind anyway.
|
|
113
|
+
default: d.default as never,
|
|
114
|
+
label: d.label as string | undefined,
|
|
115
|
+
description: (d.description as string | undefined) ?? "",
|
|
116
|
+
placeholder: d.placeholder as string | undefined,
|
|
117
|
+
presentation: compilePresentation(d.presentation, baseLocale, key),
|
|
118
|
+
envFallback: d.envFallback as (() => unknown) | undefined,
|
|
119
|
+
envBootstrap: d.envBootstrap as (() => unknown) | undefined,
|
|
120
|
+
templateVars: d.templateVars as readonly string[] | undefined,
|
|
121
|
+
options: d.options as ReadonlyArray<{ value: string; label: string }> | undefined,
|
|
122
|
+
min: d.min as number | undefined,
|
|
123
|
+
max: d.max as number | undefined,
|
|
124
|
+
} as SettingDef;
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const resolveSettingPresentation = (
|
|
129
|
+
def: SettingDef,
|
|
130
|
+
requestedLocale?: string,
|
|
131
|
+
): { label: string; description: string; placeholder?: string; options?: SettingOption[] } => {
|
|
132
|
+
const base = {
|
|
133
|
+
label: getSettingLabel(def),
|
|
134
|
+
description: def.description,
|
|
135
|
+
placeholder: def.placeholder,
|
|
136
|
+
options: "options" in def ? def.options : undefined,
|
|
137
|
+
};
|
|
138
|
+
if (!requestedLocale || !def.presentation) return base;
|
|
139
|
+
const overlays = new Map(Object.entries(def.presentation.translations));
|
|
140
|
+
const resolved = localeFallbackChain(requestedLocale, def.presentation.baseLocale)
|
|
141
|
+
.filter((locale) => locale !== def.presentation!.baseLocale)
|
|
142
|
+
.reverse()
|
|
143
|
+
.reduce((value, locale) => ({ ...value, ...overlays.get(locale) }), {} as AppSettingPresentationTranslation);
|
|
144
|
+
return {
|
|
145
|
+
label: resolved.label ?? base.label,
|
|
146
|
+
description: resolved.description ?? base.description,
|
|
147
|
+
placeholder: resolved.placeholder ?? base.placeholder,
|
|
148
|
+
options: base.options?.map((option) => ({ ...option, label: resolved.options?.[option.value] ?? option.label })),
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const SETTINGS: SettingDef[] = [
|
|
153
|
+
// Platform settings — app.*, ai.*, gotenberg.*, freeipa.*, user.*,
|
|
154
|
+
// notifications.*, mail.*, security.*, legal.* — are declared once in
|
|
155
|
+
// core-settings.ts and derived here so every container registers them, not
|
|
156
|
+
// just the one that renders their admin UI.
|
|
157
|
+
//
|
|
158
|
+
// App-scoped settings do NOT belong here: an app declares them in its own
|
|
159
|
+
// defineApp({ settings }) and renders its own admin form.
|
|
160
|
+
...toLegacySettingDefs(CORE_SETTINGS),
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
164
|
+
|
|
165
|
+
const toStringValue = (value: unknown): string | null => {
|
|
166
|
+
if (typeof value === "string") return value;
|
|
167
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const parseStringList = (value: unknown): string[] | null => {
|
|
172
|
+
const rawValues = Array.isArray(value)
|
|
173
|
+
? value.flatMap((entry) => (typeof entry === "string" ? entry.split(/[,\n]/) : typeof entry === "number" ? [String(entry)] : []))
|
|
174
|
+
: typeof value === "string"
|
|
175
|
+
? value.split(/[,\n]/)
|
|
176
|
+
: null;
|
|
177
|
+
|
|
178
|
+
if (!rawValues) return null;
|
|
179
|
+
|
|
180
|
+
return [...new Set(rawValues.map((entry) => entry.trim()).filter(Boolean))];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const parseNumberList = (value: unknown): number[] | null => {
|
|
184
|
+
const rawValues = Array.isArray(value) ? value : typeof value === "string" ? value.split(/[,\n]/).map((entry) => entry.trim()) : null;
|
|
185
|
+
|
|
186
|
+
if (!rawValues) return null;
|
|
187
|
+
|
|
188
|
+
const parsed = rawValues
|
|
189
|
+
.map((entry) => (typeof entry === "number" ? entry : Number(entry)))
|
|
190
|
+
.filter((entry) => Number.isInteger(entry) && entry > 0);
|
|
191
|
+
|
|
192
|
+
return [...new Set(parsed)].sort((a, b) => b - a);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const isValidCron = (value: string): boolean => {
|
|
196
|
+
const trimmed = value.trim();
|
|
197
|
+
if (!trimmed) return false;
|
|
198
|
+
return trimmed.split(/\s+/).length === 5;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const isValidTimezone = (value: string): boolean => {
|
|
202
|
+
try {
|
|
203
|
+
new Intl.DateTimeFormat(undefined, { timeZone: value });
|
|
204
|
+
return true;
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const isNonEmptyStringKind = (kind: SettingKind): kind is Exclude<SettingKind, "boolean" | "number" | "string_list" | "number_list"> =>
|
|
211
|
+
kind !== "boolean" && kind !== "number" && kind !== "string_list" && kind !== "number_list";
|
|
212
|
+
|
|
213
|
+
export const getSettingLabel = (def: SettingDef): string => {
|
|
214
|
+
if (def.label) return def.label;
|
|
215
|
+
return def.key
|
|
216
|
+
.split(".")
|
|
217
|
+
.slice(1)
|
|
218
|
+
.join(" ")
|
|
219
|
+
.replaceAll("_", " ")
|
|
220
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const normalizeSettingValue = (def: SettingDef, raw: unknown): unknown => {
|
|
224
|
+
switch (def.kind) {
|
|
225
|
+
case "boolean":
|
|
226
|
+
if (typeof raw === "boolean") return raw;
|
|
227
|
+
if (typeof raw === "string") {
|
|
228
|
+
const trimmed = raw.trim().toLowerCase();
|
|
229
|
+
if (trimmed === "true") return true;
|
|
230
|
+
if (trimmed === "false") return false;
|
|
231
|
+
}
|
|
232
|
+
return raw;
|
|
233
|
+
case "number":
|
|
234
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
235
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
236
|
+
const parsed = Number(raw.trim());
|
|
237
|
+
return Number.isFinite(parsed) ? parsed : raw;
|
|
238
|
+
}
|
|
239
|
+
return raw;
|
|
240
|
+
case "string_list":
|
|
241
|
+
return parseStringList(raw) ?? raw;
|
|
242
|
+
case "number_list":
|
|
243
|
+
return parseNumberList(raw) ?? raw;
|
|
244
|
+
case "enum": {
|
|
245
|
+
const value = toStringValue(raw);
|
|
246
|
+
return value === null ? raw : value.trim();
|
|
247
|
+
}
|
|
248
|
+
default: {
|
|
249
|
+
const value = toStringValue(raw);
|
|
250
|
+
if (value === null) return raw;
|
|
251
|
+
return def.kind === "text" || def.kind === "template" ? value : value.trim();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const validateSettingValue = (def: SettingDef, raw: unknown): SettingValidationResult => {
|
|
257
|
+
const value = normalizeSettingValue(def, raw);
|
|
258
|
+
|
|
259
|
+
if (def.key === "app.documentation_url") {
|
|
260
|
+
try {
|
|
261
|
+
if (typeof value !== "string" || !/^https?:\/\//i.test(value)) throw new Error("Invalid URL");
|
|
262
|
+
const url = new URL(value);
|
|
263
|
+
if (url.username || url.password || url.search || url.hash) throw new Error("Invalid base URL");
|
|
264
|
+
return { ok: true, value: url.href.replace(/\/+$/, "") };
|
|
265
|
+
} catch {
|
|
266
|
+
return { ok: false, error: "Documentation website must be an absolute HTTP(S) base URL without credentials, query or fragment" };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
switch (def.kind) {
|
|
271
|
+
case "boolean":
|
|
272
|
+
return typeof value === "boolean" ? { ok: true, value } : { ok: false, error: `${getSettingLabel(def)} must be true or false` };
|
|
273
|
+
case "number":
|
|
274
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
275
|
+
return { ok: false, error: `${getSettingLabel(def)} must be a valid number` };
|
|
276
|
+
}
|
|
277
|
+
if (def.min !== undefined && value < def.min) {
|
|
278
|
+
return { ok: false, error: `${getSettingLabel(def)} must be at least ${def.min}` };
|
|
279
|
+
}
|
|
280
|
+
if (def.max !== undefined && value > def.max) {
|
|
281
|
+
return { ok: false, error: `${getSettingLabel(def)} must be at most ${def.max}` };
|
|
282
|
+
}
|
|
283
|
+
return { ok: true, value };
|
|
284
|
+
case "string_list":
|
|
285
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
286
|
+
? { ok: true, value }
|
|
287
|
+
: { ok: false, error: `${getSettingLabel(def)} must be a list of strings` };
|
|
288
|
+
case "number_list":
|
|
289
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "number" && Number.isInteger(entry) && entry > 0)
|
|
290
|
+
? { ok: true, value }
|
|
291
|
+
: { ok: false, error: `${getSettingLabel(def)} must be a list of positive whole numbers` };
|
|
292
|
+
case "enum":
|
|
293
|
+
if (typeof value !== "string") {
|
|
294
|
+
return { ok: false, error: `${getSettingLabel(def)} must be a valid option` };
|
|
295
|
+
}
|
|
296
|
+
return def.options.some((option) => option.value === value)
|
|
297
|
+
? { ok: true, value }
|
|
298
|
+
: { ok: false, error: `${getSettingLabel(def)} must be one of: ${def.options.map((option) => option.value).join(", ")}` };
|
|
299
|
+
case "email":
|
|
300
|
+
if (typeof value !== "string") return { ok: false, error: `${getSettingLabel(def)} must be a valid email address` };
|
|
301
|
+
return value.length === 0 || EMAIL_RE.test(value)
|
|
302
|
+
? { ok: true, value }
|
|
303
|
+
: { ok: false, error: `${getSettingLabel(def)} must be a valid email address` };
|
|
304
|
+
case "url":
|
|
305
|
+
case "image":
|
|
306
|
+
if (typeof value !== "string") return { ok: false, error: `${getSettingLabel(def)} must be a valid URL` };
|
|
307
|
+
if (!value.length) return { ok: true, value };
|
|
308
|
+
try {
|
|
309
|
+
new URL(value);
|
|
310
|
+
return { ok: true, value };
|
|
311
|
+
} catch {
|
|
312
|
+
return { ok: false, error: `${getSettingLabel(def)} must be a valid URL` };
|
|
313
|
+
}
|
|
314
|
+
case "cron":
|
|
315
|
+
return typeof value === "string" && isValidCron(value)
|
|
316
|
+
? { ok: true, value }
|
|
317
|
+
: { ok: false, error: `${getSettingLabel(def)} must be a valid five-field cron expression` };
|
|
318
|
+
case "timezone":
|
|
319
|
+
if (typeof value !== "string") return { ok: false, error: `${getSettingLabel(def)} must be a valid IANA timezone` };
|
|
320
|
+
return value.length === 0 || isValidTimezone(value)
|
|
321
|
+
? { ok: true, value }
|
|
322
|
+
: { ok: false, error: `${getSettingLabel(def)} must be a valid IANA timezone` };
|
|
323
|
+
case "template": {
|
|
324
|
+
if (typeof value !== "string") return { ok: false, error: `${getSettingLabel(def)} must be text` };
|
|
325
|
+
const migrated = migrateLegacyMustacheTemplate(value);
|
|
326
|
+
const valid = validateLiquidTemplate(migrated);
|
|
327
|
+
if (valid.ok && def.key === "user.action_notice") {
|
|
328
|
+
try {
|
|
329
|
+
renderAccountActionNotice(migrated, ACCOUNT_ACTION_NOTICE_SAMPLE);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return { ok: false, error: error instanceof Error ? error.message : "Invalid account action notice" };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return valid.ok ? { ok: true, value: migrated } : { ok: false, error: `${getSettingLabel(def)} ${valid.error}` };
|
|
335
|
+
}
|
|
336
|
+
default:
|
|
337
|
+
if (!isNonEmptyStringKind(def.kind)) {
|
|
338
|
+
return { ok: false, error: `${getSettingLabel(def)} is invalid` };
|
|
339
|
+
}
|
|
340
|
+
return typeof value === "string" ? { ok: true, value } : { ok: false, error: `${getSettingLabel(def)} must be text` };
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/** Lookup map for quick access by key */
|
|
345
|
+
export const SETTINGS_MAP = new Map(SETTINGS.map((setting) => [setting.key, setting] as const));
|
|
346
|
+
|
|
347
|
+
/** Fields whose divergence changes behaviour rather than just wording. */
|
|
348
|
+
const CONFLICT_FIELDS = ["kind", "default", "min", "max"] as const;
|
|
349
|
+
|
|
350
|
+
export function registerSettings(defs: SettingDef[]): void {
|
|
351
|
+
for (const def of defs) {
|
|
352
|
+
const existingIndex = SETTINGS.findIndex((setting) => setting.key === def.key);
|
|
353
|
+
if (existingIndex >= 0) {
|
|
354
|
+
// Re-registering the same definition is normal: core passes CORE_SETTINGS
|
|
355
|
+
// to defineApp, which re-registers what SETTINGS already derived. A
|
|
356
|
+
// *conflicting* re-registration means two declarations have drifted, and
|
|
357
|
+
// the winner would then depend on which container loaded which module.
|
|
358
|
+
const existing = SETTINGS[existingIndex] as Record<string, unknown>;
|
|
359
|
+
const incoming = def as unknown as Record<string, unknown>;
|
|
360
|
+
const conflicts = CONFLICT_FIELDS.filter((field) => JSON.stringify(existing[field]) !== JSON.stringify(incoming[field]));
|
|
361
|
+
if (conflicts.length > 0) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Setting "${def.key}" is registered twice with different ${conflicts.join(", ")}. ` +
|
|
364
|
+
"Declare it once — platform settings belong in services/settings/core-settings.ts, " +
|
|
365
|
+
"app settings in that app's defineApp({ settings }).",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
SETTINGS[existingIndex] = def;
|
|
369
|
+
} else {
|
|
370
|
+
SETTINGS.push(def);
|
|
371
|
+
}
|
|
372
|
+
SETTINGS_MAP.set(def.key, def);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings service — runtime-configurable settings backed by Postgres.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order: DB value -> env fallback -> code default.
|
|
5
|
+
*
|
|
6
|
+
* Custom DB values are encrypted at rest using APP_SECRET. All read/write
|
|
7
|
+
* goes through the Redis cache-aside layer in `./store.ts` (5-minute TTL).
|
|
8
|
+
* `loadCache()` runs once at boot to normalize legacy rows and bootstrap
|
|
9
|
+
* env-backed entries into Postgres.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { redis, sql } from "bun";
|
|
13
|
+
import { decryptValue, encryptValue, getAppSecret } from "./crypto";
|
|
14
|
+
import { SETTINGS, SETTINGS_MAP, resolveSettingPresentation, type SettingDef, validateSettingValue } from "./defaults";
|
|
15
|
+
import { bulkRead, deleteKey, invalidateSettingsCache, readKey, writeKey } from "./store";
|
|
16
|
+
|
|
17
|
+
type SqlClient = typeof sql;
|
|
18
|
+
|
|
19
|
+
type StoredRow = { key: string; value: string };
|
|
20
|
+
type PendingRow = { key: string; value: unknown; rewrite: boolean; existed: boolean };
|
|
21
|
+
type NormalizationStats = {
|
|
22
|
+
encryptedLoaded: number;
|
|
23
|
+
normalizedUpdated: number;
|
|
24
|
+
envBootstrapped: number;
|
|
25
|
+
invalidSkipped: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const REDIS_KEY = (key: string) => `settings:${key}`;
|
|
29
|
+
|
|
30
|
+
const isEqual = (left: unknown, right: unknown): boolean => JSON.stringify(left) === JSON.stringify(right);
|
|
31
|
+
|
|
32
|
+
const resolveEnvValue = (def: SettingDef | undefined, mode: "fallback" | "bootstrap"): unknown => {
|
|
33
|
+
const raw = mode === "fallback" ? def?.envFallback?.() : def?.envBootstrap?.();
|
|
34
|
+
if (!def || raw === undefined) return undefined;
|
|
35
|
+
|
|
36
|
+
const validated = validateSettingValue(def, raw);
|
|
37
|
+
if (!validated.ok) {
|
|
38
|
+
console.warn(`[settings] ignoring invalid ${mode} value for "${def.key}": ${validated.error}`);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return validated.value;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const shouldBootstrapValue = (def: SettingDef, value: unknown): boolean => {
|
|
46
|
+
if (value === undefined || value === null) return false;
|
|
47
|
+
|
|
48
|
+
switch (def.kind) {
|
|
49
|
+
case "boolean":
|
|
50
|
+
return value === true;
|
|
51
|
+
case "number":
|
|
52
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
53
|
+
case "string_list":
|
|
54
|
+
case "number_list":
|
|
55
|
+
return Array.isArray(value) && value.length > 0;
|
|
56
|
+
default:
|
|
57
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const upsertEncryptedRow = async (key: string, value: unknown): Promise<void> => {
|
|
62
|
+
const encryptedValue = await encryptValue(value);
|
|
63
|
+
await sql`
|
|
64
|
+
INSERT INTO settings.entries (key, value, updated_at)
|
|
65
|
+
VALUES (${key}, ${encryptedValue}, now())
|
|
66
|
+
ON CONFLICT (key)
|
|
67
|
+
DO UPDATE SET value = ${encryptedValue}, updated_at = now()
|
|
68
|
+
`;
|
|
69
|
+
await redis.del(REDIS_KEY(key));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const normalizeStoredEntries = async (): Promise<{ rows: Array<{ key: string; value: unknown }>; stats: NormalizationStats }> => {
|
|
73
|
+
const rows = await sql<StoredRow[]>`SELECT key, value FROM settings.entries`;
|
|
74
|
+
const stats: NormalizationStats = {
|
|
75
|
+
encryptedLoaded: 0,
|
|
76
|
+
normalizedUpdated: 0,
|
|
77
|
+
envBootstrapped: 0,
|
|
78
|
+
invalidSkipped: 0,
|
|
79
|
+
};
|
|
80
|
+
const pendingRows = new Map<string, PendingRow>();
|
|
81
|
+
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
try {
|
|
84
|
+
const value = await decryptValue(row.value);
|
|
85
|
+
pendingRows.set(row.key, { key: row.key, value, rewrite: false, existed: true });
|
|
86
|
+
stats.encryptedLoaded += 1;
|
|
87
|
+
} catch {
|
|
88
|
+
console.warn(`[settings] skipping invalid stored value for "${row.key}"`);
|
|
89
|
+
stats.invalidSkipped += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const normalized: Array<{ key: string; value: unknown }> = [];
|
|
94
|
+
|
|
95
|
+
for (const [key, row] of pendingRows.entries()) {
|
|
96
|
+
const def = SETTINGS_MAP.get(key);
|
|
97
|
+
if (!def) {
|
|
98
|
+
console.warn(`[settings] skipping unknown stored key "${key}"`);
|
|
99
|
+
stats.invalidSkipped += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const validated = validateSettingValue(def, row.value);
|
|
104
|
+
if (!validated.ok) {
|
|
105
|
+
console.warn(`[settings] skipping invalid value for "${key}": ${validated.error}`);
|
|
106
|
+
stats.invalidSkipped += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
normalized.push({ key, value: validated.value });
|
|
111
|
+
|
|
112
|
+
if (row.rewrite || !row.existed || !isEqual(row.value, validated.value)) {
|
|
113
|
+
await upsertEncryptedRow(key, validated.value);
|
|
114
|
+
stats.normalizedUpdated += 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { rows: normalized, stats };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const bootstrapEnvBackedEntries = async (config: {
|
|
122
|
+
rows: Array<{ key: string; value: unknown }>;
|
|
123
|
+
stats: NormalizationStats;
|
|
124
|
+
}): Promise<Array<{ key: string; value: unknown }>> => {
|
|
125
|
+
const rowsByKey = new Map(config.rows.map((row) => [row.key, row.value]));
|
|
126
|
+
|
|
127
|
+
for (const def of SETTINGS) {
|
|
128
|
+
if (rowsByKey.has(def.key)) continue;
|
|
129
|
+
|
|
130
|
+
const envValue = resolveEnvValue(def, "bootstrap");
|
|
131
|
+
if (!shouldBootstrapValue(def, envValue)) continue;
|
|
132
|
+
|
|
133
|
+
await upsertEncryptedRow(def.key, envValue);
|
|
134
|
+
rowsByKey.set(def.key, envValue);
|
|
135
|
+
config.stats.envBootstrapped += 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [...rowsByKey.entries()].map(([key, value]) => ({ key, value }));
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Boot-time normalization + env-bootstrap. Runs once at startup. Does NOT
|
|
143
|
+
* populate any in-process cache — every read goes through Redis cache-aside
|
|
144
|
+
* (`store.ts`) at request time.
|
|
145
|
+
*/
|
|
146
|
+
export async function loadCache(): Promise<void> {
|
|
147
|
+
getAppSecret();
|
|
148
|
+
const { rows, stats } = await normalizeStoredEntries();
|
|
149
|
+
const bootstrappedRows = await bootstrapEnvBackedEntries({ rows, stats });
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
`[settings] loaded ${bootstrappedRows.length} custom setting(s) (${stats.encryptedLoaded} encrypted, ${stats.normalizedUpdated} normalized, ${stats.envBootstrapped} env-bootstrapped, ${stats.invalidSkipped} skipped)`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Read a setting via the Redis cache-aside layer (always within Redis-TTL
|
|
158
|
+
* fresh). Resolution: Redis cache → Postgres → env-fallback → code default.
|
|
159
|
+
*/
|
|
160
|
+
export async function get<T = unknown>(key: string): Promise<T> {
|
|
161
|
+
return readKey(key) as Promise<T>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Write a setting. Pass `db` to run inside a caller's transaction — then the
|
|
166
|
+
* caller must also call `invalidateSettingsCache` once it commits.
|
|
167
|
+
*/
|
|
168
|
+
export async function set(key: string, value: unknown, db?: SqlClient): Promise<void> {
|
|
169
|
+
await writeKey(key, value, db);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function remove(key: string, db?: SqlClient): Promise<void> {
|
|
173
|
+
await deleteKey(key, db);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
import type { SettingEntry } from "../../contracts/shared";
|
|
177
|
+
|
|
178
|
+
export { invalidateSettingsCache } from "./store";
|
|
179
|
+
export type { SettingEntry } from "../../contracts/shared";
|
|
180
|
+
|
|
181
|
+
export async function getAll(locale?: string): Promise<SettingEntry[]> {
|
|
182
|
+
// Determine which keys have a custom row in Postgres (vs. falling back to
|
|
183
|
+
// env / code default). One indexed scan, then bulk-read all values.
|
|
184
|
+
const customRows = await sql<{ key: string }[]>`SELECT key FROM settings.entries`;
|
|
185
|
+
const customKeys = new Set(customRows.map((row) => row.key));
|
|
186
|
+
|
|
187
|
+
const allKeys = SETTINGS.map((def) => def.key);
|
|
188
|
+
const values = await bulkRead(allKeys);
|
|
189
|
+
|
|
190
|
+
return SETTINGS.map((def) => {
|
|
191
|
+
const presentation = resolveSettingPresentation(def, locale);
|
|
192
|
+
const isCustom = customKeys.has(def.key);
|
|
193
|
+
const envFallback = resolveEnvValue(def, "fallback");
|
|
194
|
+
const resetValue = envFallback ?? def.default;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
key: def.key,
|
|
198
|
+
label: presentation.label,
|
|
199
|
+
kind: def.kind,
|
|
200
|
+
description: presentation.description ?? "",
|
|
201
|
+
placeholder: presentation.placeholder,
|
|
202
|
+
group: def.group,
|
|
203
|
+
value: values.get(def.key),
|
|
204
|
+
default: def.default,
|
|
205
|
+
resetValue: def.kind === "secret" ? "" : resetValue,
|
|
206
|
+
valueSource: isCustom ? "custom" : envFallback === undefined ? "default" : "env",
|
|
207
|
+
resetValueSource: envFallback === undefined ? "default" : "env",
|
|
208
|
+
isCustom,
|
|
209
|
+
templateVars: "templateVars" in def ? def.templateVars : undefined,
|
|
210
|
+
options: presentation.options,
|
|
211
|
+
min: "min" in def ? def.min : undefined,
|
|
212
|
+
max: "max" in def ? def.max : undefined,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|