@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,45 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { backfillWeatherLocationShortIds } from "./short-id";
|
|
3
|
+
|
|
4
|
+
export const migrate = async (): Promise<void> => {
|
|
5
|
+
await sql`
|
|
6
|
+
CREATE TABLE IF NOT EXISTS weather_locations (
|
|
7
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
8
|
+
short_id TEXT,
|
|
9
|
+
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
state TEXT,
|
|
12
|
+
lat DOUBLE PRECISION NOT NULL,
|
|
13
|
+
lon DOUBLE PRECISION NOT NULL,
|
|
14
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
15
|
+
)
|
|
16
|
+
`.simple();
|
|
17
|
+
console.log(" ✓ weather_locations table");
|
|
18
|
+
|
|
19
|
+
await sql`ALTER TABLE weather_locations ADD COLUMN IF NOT EXISTS short_id TEXT`.simple();
|
|
20
|
+
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_weather_locations_short_id ON weather_locations(short_id)`.simple();
|
|
21
|
+
|
|
22
|
+
await sql.begin(async (tx) => {
|
|
23
|
+
await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.weather.location-short-id-backfill'))`;
|
|
24
|
+
const filled = await backfillWeatherLocationShortIds(tx);
|
|
25
|
+
if (filled > 0) console.log(` ✓ weather location short_id backfill: ${filled}`);
|
|
26
|
+
await tx`ALTER TABLE weather_locations ALTER COLUMN short_id SET NOT NULL`;
|
|
27
|
+
await tx`ALTER TABLE weather_locations DROP CONSTRAINT IF EXISTS weather_locations_short_id_format`;
|
|
28
|
+
await tx`
|
|
29
|
+
ALTER TABLE weather_locations
|
|
30
|
+
ADD CONSTRAINT weather_locations_short_id_format CHECK (short_id ~ '^[0-9A-Za-z]{6}$')
|
|
31
|
+
`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await sql`
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_weather_locations_user
|
|
36
|
+
ON weather_locations(user_id)
|
|
37
|
+
`.simple();
|
|
38
|
+
console.log(" ✓ weather_locations index");
|
|
39
|
+
|
|
40
|
+
await sql`
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_weather_locations_user_created
|
|
42
|
+
ON weather_locations(user_id, created_at, id)
|
|
43
|
+
`.simple();
|
|
44
|
+
console.log(" ✓ weather_locations pagination index");
|
|
45
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const WEATHER_SETTINGS = {
|
|
2
|
+
"weather.default_lat": {
|
|
3
|
+
kind: "string",
|
|
4
|
+
label: "Default Latitude",
|
|
5
|
+
default: "",
|
|
6
|
+
description: "Default latitude shown in weather widgets",
|
|
7
|
+
placeholder: "e.g. 48.401082 (Ulm)",
|
|
8
|
+
presentation: {
|
|
9
|
+
translations: {
|
|
10
|
+
de: {
|
|
11
|
+
label: "Standardbreitengrad",
|
|
12
|
+
description: "In Wetter-Widgets angezeigter Standardbreitengrad",
|
|
13
|
+
placeholder: "z. B. 48.401082 (Ulm)",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
"weather.default_lon": {
|
|
19
|
+
kind: "string",
|
|
20
|
+
label: "Default Longitude",
|
|
21
|
+
default: "",
|
|
22
|
+
description: "Default longitude shown in weather widgets",
|
|
23
|
+
placeholder: "e.g. 9.987608 (Ulm)",
|
|
24
|
+
presentation: {
|
|
25
|
+
translations: {
|
|
26
|
+
de: {
|
|
27
|
+
label: "Standardlängengrad",
|
|
28
|
+
description: "In Wetter-Widgets angezeigter Standardlängengrad",
|
|
29
|
+
placeholder: "z. B. 9.987608 (Ulm)",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"weather.cache_minutes": {
|
|
35
|
+
kind: "number",
|
|
36
|
+
label: "Cache TTL (minutes)",
|
|
37
|
+
default: 30,
|
|
38
|
+
min: 1,
|
|
39
|
+
max: 1440,
|
|
40
|
+
description: "How long weather data is cached before fetching fresh data (in minutes)",
|
|
41
|
+
presentation: {
|
|
42
|
+
translations: {
|
|
43
|
+
de: {
|
|
44
|
+
label: "Cache-TTL (Minuten)",
|
|
45
|
+
description: "Dauer in Minuten, für die Wetterdaten vor dem nächsten Abruf zwischengespeichert werden",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
"weather.geo_url": {
|
|
51
|
+
kind: "url",
|
|
52
|
+
label: "Geo API URL",
|
|
53
|
+
default: "",
|
|
54
|
+
description: "Geocoding API URL for the location search feature",
|
|
55
|
+
placeholder: "e.g. https://geocoding.example.com/search",
|
|
56
|
+
presentation: {
|
|
57
|
+
translations: {
|
|
58
|
+
de: {
|
|
59
|
+
label: "Geo-API-URL",
|
|
60
|
+
description: "Geocoding-API-URL für die Ortssuche",
|
|
61
|
+
placeholder: "z. B. https://geocoding.example.com/search",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
} as const;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { crypto } from "@k2b/stdlib";
|
|
2
|
+
import { sql } from "bun";
|
|
3
|
+
import { isUniqueViolation } from "../postgres";
|
|
4
|
+
|
|
5
|
+
export const WEATHER_LOCATION_SHORT_ID_PATTERN = /^[0-9A-Za-z]{6}$/;
|
|
6
|
+
|
|
7
|
+
const SHORT_ID_LENGTH = 6;
|
|
8
|
+
const MAX_ATTEMPTS = 10;
|
|
9
|
+
const BACKFILL_BATCH_SIZE = 500;
|
|
10
|
+
const UNIQUE_CONSTRAINT = "idx_weather_locations_short_id";
|
|
11
|
+
|
|
12
|
+
type SqlExecutor = typeof sql;
|
|
13
|
+
|
|
14
|
+
export const newWeatherLocationShortId = (): string => crypto.common.readableId(SHORT_ID_LENGTH);
|
|
15
|
+
|
|
16
|
+
export const withWeatherLocationShortId = async <T>(write: (shortId: string) => Promise<T | null>): Promise<T> => {
|
|
17
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
const result = await write(newWeatherLocationShortId());
|
|
20
|
+
if (result !== null) return result;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (!isUniqueViolation(error, UNIQUE_CONSTRAINT)) throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw new Error("Failed to allocate a Weather location short ID");
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const backfillWeatherLocationShortIds = async (db: SqlExecutor = sql): Promise<number> => {
|
|
29
|
+
let filled = 0;
|
|
30
|
+
for (;;) {
|
|
31
|
+
const rows = await db<{ id: string }[]>`
|
|
32
|
+
SELECT id
|
|
33
|
+
FROM weather_locations
|
|
34
|
+
WHERE short_id IS NULL
|
|
35
|
+
ORDER BY id
|
|
36
|
+
LIMIT ${BACKFILL_BATCH_SIZE}
|
|
37
|
+
FOR UPDATE
|
|
38
|
+
`;
|
|
39
|
+
if (rows.length === 0) return filled;
|
|
40
|
+
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
await withWeatherLocationShortId(async (shortId) => {
|
|
43
|
+
const updated = await db<{ id: string }[]>`
|
|
44
|
+
UPDATE weather_locations
|
|
45
|
+
SET short_id = ${shortId}
|
|
46
|
+
WHERE id = ${row.id}::uuid
|
|
47
|
+
AND short_id IS NULL
|
|
48
|
+
AND NOT EXISTS (SELECT 1 FROM weather_locations WHERE short_id = ${shortId})
|
|
49
|
+
RETURNING id
|
|
50
|
+
`;
|
|
51
|
+
if (updated.length === 0) return null;
|
|
52
|
+
filled++;
|
|
53
|
+
return updated[0]!;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Weather icon codes from Brightsky API */
|
|
2
|
+
export type WeatherIcon =
|
|
3
|
+
| "clear-day"
|
|
4
|
+
| "clear-night"
|
|
5
|
+
| "partly-cloudy-day"
|
|
6
|
+
| "partly-cloudy-night"
|
|
7
|
+
| "cloudy"
|
|
8
|
+
| "fog"
|
|
9
|
+
| "wind"
|
|
10
|
+
| "rain"
|
|
11
|
+
| "sleet"
|
|
12
|
+
| "snow"
|
|
13
|
+
| "hail"
|
|
14
|
+
| "thunderstorm";
|
|
15
|
+
|
|
16
|
+
/** Current weather data */
|
|
17
|
+
export type CurrentWeather = {
|
|
18
|
+
temperature: number; // deg C
|
|
19
|
+
icon: WeatherIcon;
|
|
20
|
+
cloudCover: number; // 0-100%
|
|
21
|
+
windSpeed: number; // km/h
|
|
22
|
+
windGust: number | null; // km/h
|
|
23
|
+
windDirection: number | null; // degrees
|
|
24
|
+
humidity: number | null; // 0-100%
|
|
25
|
+
precipitation: number; // mm in last hour
|
|
26
|
+
pressure: number | null; // hPa
|
|
27
|
+
visibility: number | null; // meters
|
|
28
|
+
dewPoint: number | null; // deg C
|
|
29
|
+
sunshine: number | null; // minutes in last hour
|
|
30
|
+
stationName: string;
|
|
31
|
+
timestamp: string; // ISO timestamp
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Hourly forecast entry */
|
|
35
|
+
export type HourlyForecast = {
|
|
36
|
+
timestamp: string;
|
|
37
|
+
temperature: number;
|
|
38
|
+
icon: WeatherIcon;
|
|
39
|
+
precipitation: number;
|
|
40
|
+
precipitationProbability: number | null; // 0-100%
|
|
41
|
+
windSpeed: number;
|
|
42
|
+
cloudCover: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Daily forecast summary */
|
|
46
|
+
export type DailyForecast = {
|
|
47
|
+
date: string; // YYYY-MM-DD
|
|
48
|
+
tempMin: number;
|
|
49
|
+
tempMax: number;
|
|
50
|
+
icon: WeatherIcon;
|
|
51
|
+
precipitation: number; // total mm
|
|
52
|
+
precipitationProbability: number | null; // max probability for the day
|
|
53
|
+
sunshine: number; // total minutes
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Full weather data with forecasts */
|
|
57
|
+
export type WeatherData = {
|
|
58
|
+
current: CurrentWeather;
|
|
59
|
+
hourly: HourlyForecast[];
|
|
60
|
+
daily: DailyForecast[];
|
|
61
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { WeatherIcon } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Map Brightsky icon to Tabler icon name. */
|
|
4
|
+
const getTablerIcon = (icon: WeatherIcon): string => {
|
|
5
|
+
const iconMap: Record<WeatherIcon, string> = {
|
|
6
|
+
"clear-day": "sun",
|
|
7
|
+
"clear-night": "moon",
|
|
8
|
+
"partly-cloudy-day": "sun-moon",
|
|
9
|
+
"partly-cloudy-night": "sun-moon",
|
|
10
|
+
cloudy: "cloud",
|
|
11
|
+
fog: "mist",
|
|
12
|
+
wind: "wind",
|
|
13
|
+
rain: "cloud-rain",
|
|
14
|
+
sleet: "cloud-snow",
|
|
15
|
+
snow: "snowflake",
|
|
16
|
+
hail: "cloud-snow",
|
|
17
|
+
thunderstorm: "cloud-storm",
|
|
18
|
+
};
|
|
19
|
+
return iconMap[icon] ?? "cloud";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Get Tailwind color class for temperature. */
|
|
23
|
+
const getTempColorClass = (temp: number): string => {
|
|
24
|
+
if (temp <= 0) return "text-blue-400";
|
|
25
|
+
if (temp <= 10) return "text-cyan-500";
|
|
26
|
+
if (temp <= 20) return "text-emerald-500";
|
|
27
|
+
if (temp <= 25) return "text-amber-500";
|
|
28
|
+
return "text-red-500";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Get temperature color class for an average of min/max. */
|
|
32
|
+
const getAvgTempColorClass = (tempMin: number, tempMax: number): string => {
|
|
33
|
+
return getTempColorClass((tempMin + tempMax) / 2);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Format temperature with degree symbol. */
|
|
37
|
+
const formatTemp = (temp: number): string => `${temp}°`;
|
|
38
|
+
|
|
39
|
+
/** Format temperature range (e.g., "12° / 5°"). */
|
|
40
|
+
const formatTempRange = (tempMax: number, tempMin: number): string => `${tempMax}° / ${tempMin}°`;
|
|
41
|
+
|
|
42
|
+
export const weatherUiService = {
|
|
43
|
+
getTablerIcon,
|
|
44
|
+
getTempColorClass,
|
|
45
|
+
getAvgTempColorClass,
|
|
46
|
+
formatTemp,
|
|
47
|
+
formatTempRange,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type WeatherUiService = typeof weatherUiService;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuthenticationResponseJSON,
|
|
3
|
+
type AuthenticatorTransportFuture,
|
|
4
|
+
generateAuthenticationOptions,
|
|
5
|
+
generateRegistrationOptions,
|
|
6
|
+
type PublicKeyCredentialCreationOptionsJSON,
|
|
7
|
+
type PublicKeyCredentialRequestOptionsJSON,
|
|
8
|
+
type RegistrationResponseJSON,
|
|
9
|
+
verifyAuthenticationResponse,
|
|
10
|
+
verifyRegistrationResponse,
|
|
11
|
+
type WebAuthnCredential,
|
|
12
|
+
} from "@simplewebauthn/server";
|
|
13
|
+
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
14
|
+
import { redis, sql } from "bun";
|
|
15
|
+
import type { User, WebAuthnPasskey } from "../contracts/shared";
|
|
16
|
+
import { accounts } from "./accounts";
|
|
17
|
+
import { audit } from "./audit";
|
|
18
|
+
import { logger } from "./logging";
|
|
19
|
+
import { isUniqueViolation, toPgTextArray } from "./postgres";
|
|
20
|
+
import { coreSettings } from "./settings/api";
|
|
21
|
+
|
|
22
|
+
const CHALLENGE_TTL_SECONDS = 300;
|
|
23
|
+
const REGISTRATION_CHALLENGE_PREFIX = "webauthn:registration:";
|
|
24
|
+
const AUTHENTICATION_CHALLENGE_PREFIX = "webauthn:authentication:";
|
|
25
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i;
|
|
26
|
+
const log = logger("auth:webauthn");
|
|
27
|
+
|
|
28
|
+
type DbPasskeyRow = {
|
|
29
|
+
id: string;
|
|
30
|
+
user_id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
credential_id: string;
|
|
33
|
+
public_key: Uint8Array;
|
|
34
|
+
counter: string | number | bigint;
|
|
35
|
+
transports: string[];
|
|
36
|
+
device_type: string | null;
|
|
37
|
+
backed_up: boolean;
|
|
38
|
+
created_at: Date;
|
|
39
|
+
last_used_at: Date | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type DbPasskeyWithUserRow = DbPasskeyRow & {
|
|
43
|
+
user_account_expires: Date | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type StoredWebAuthnPasskey = WebAuthnPasskey & {
|
|
47
|
+
credentialId: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type WebAuthnRp = {
|
|
51
|
+
rpName: string;
|
|
52
|
+
rpID: string;
|
|
53
|
+
origin: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const resolveWebAuthnRp = (config: { appUrl: string; appName: string }): WebAuthnRp => {
|
|
57
|
+
const rawAppUrl = config.appUrl.trim();
|
|
58
|
+
const hasProtocol = /^https?:\/\//i.test(rawAppUrl);
|
|
59
|
+
const parseUrl = (value: string) => new URL(value);
|
|
60
|
+
const hostname = hasProtocol ? parseUrl(rawAppUrl).hostname : parseUrl(`https://${rawAppUrl}`).hostname;
|
|
61
|
+
const isBareLocalhost =
|
|
62
|
+
!hasProtocol && (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]");
|
|
63
|
+
const withProtocol = hasProtocol ? rawAppUrl : `${isBareLocalhost ? "http" : "https"}://${rawAppUrl}`;
|
|
64
|
+
const url = new URL(withProtocol);
|
|
65
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
66
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocalhost)) {
|
|
67
|
+
throw new Error("WebAuthn requires an HTTPS app.url, except for localhost development.");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
rpName: config.appName.trim() || "Cloud",
|
|
71
|
+
rpID: url.hostname,
|
|
72
|
+
origin: url.origin,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const loadRp = async (): Promise<WebAuthnRp> =>
|
|
77
|
+
resolveWebAuthnRp({
|
|
78
|
+
appUrl: await coreSettings.get<string>("app.url"),
|
|
79
|
+
appName: await coreSettings.get<string>("app.name"),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const loadRpResult = async (): Promise<Result<WebAuthnRp>> => {
|
|
83
|
+
try {
|
|
84
|
+
return ok(await loadRp());
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return fail(err.badInput(error instanceof Error ? error.message : "Invalid WebAuthn configuration."));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const mapStoredPasskey = (row: DbPasskeyRow): StoredWebAuthnPasskey => ({
|
|
91
|
+
id: row.id,
|
|
92
|
+
userId: row.user_id,
|
|
93
|
+
name: row.name,
|
|
94
|
+
credentialId: row.credential_id,
|
|
95
|
+
transports: row.transports ?? [],
|
|
96
|
+
deviceType: row.device_type,
|
|
97
|
+
backedUp: row.backed_up,
|
|
98
|
+
createdAt: row.created_at.toISOString(),
|
|
99
|
+
lastUsedAt: row.last_used_at?.toISOString() ?? null,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const mapPasskey = (row: DbPasskeyRow): WebAuthnPasskey => {
|
|
103
|
+
const { credentialId: _, ...passkey } = mapStoredPasskey(row);
|
|
104
|
+
return passkey;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const actorForUser = (user: Pick<User, "id" | "uid" | "provider" | "roles">) => ({
|
|
108
|
+
userId: user.id,
|
|
109
|
+
uid: user.uid,
|
|
110
|
+
provider: user.provider,
|
|
111
|
+
roles: user.roles,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const challengeKey = (prefix: string, id: string) => `${prefix}${id}`;
|
|
115
|
+
|
|
116
|
+
const storeRegistrationChallenge = async (userId: string, challenge: string): Promise<void> => {
|
|
117
|
+
await redis.set(challengeKey(REGISTRATION_CHALLENGE_PREFIX, userId), challenge, "EX", CHALLENGE_TTL_SECONDS);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const consumeRegistrationChallenge = async (userId: string): Promise<string | null> => {
|
|
121
|
+
return redis.getdel(challengeKey(REGISTRATION_CHALLENGE_PREFIX, userId));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const storeAuthenticationChallenge = async (challenge: string): Promise<void> => {
|
|
125
|
+
await redis.set(challengeKey(AUTHENTICATION_CHALLENGE_PREFIX, challenge), "1", "EX", CHALLENGE_TTL_SECONDS);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const consumeAuthenticationChallenge = async (challenge: string): Promise<boolean> => {
|
|
129
|
+
const value = await redis.getdel(challengeKey(AUTHENTICATION_CHALLENGE_PREFIX, challenge));
|
|
130
|
+
return value === "1";
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const userIdToWebAuthnBytes = (userId: string): WebAuthnCredential["publicKey"] =>
|
|
134
|
+
new TextEncoder().encode(userId) as WebAuthnCredential["publicKey"];
|
|
135
|
+
|
|
136
|
+
const toCredential = (row: DbPasskeyRow): WebAuthnCredential => ({
|
|
137
|
+
id: row.credential_id,
|
|
138
|
+
publicKey: new Uint8Array(row.public_key) as WebAuthnCredential["publicKey"],
|
|
139
|
+
counter: Number(row.counter),
|
|
140
|
+
transports: (row.transports ?? []) as AuthenticatorTransportFuture[],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const isExpired = (date: Date | null): boolean => Boolean(date && date.getTime() <= Date.now());
|
|
144
|
+
|
|
145
|
+
const listStoredForUser = async (params: { userId: string }): Promise<StoredWebAuthnPasskey[]> => {
|
|
146
|
+
const rows = await sql<DbPasskeyRow[]>`
|
|
147
|
+
SELECT id, user_id, name, credential_id, public_key, counter, transports, device_type,
|
|
148
|
+
backed_up, created_at, last_used_at
|
|
149
|
+
FROM auth.webauthn_credentials
|
|
150
|
+
WHERE user_id = ${params.userId}::uuid
|
|
151
|
+
ORDER BY created_at DESC
|
|
152
|
+
`;
|
|
153
|
+
return rows.map(mapStoredPasskey);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const listForUser = async (params: { userId: string }): Promise<WebAuthnPasskey[]> => {
|
|
157
|
+
const rows = await listStoredForUser(params);
|
|
158
|
+
return rows.map(({ credentialId: _, ...passkey }) => passkey);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const beginRegistration = async (params: { user: User }): Promise<Result<PublicKeyCredentialCreationOptionsJSON>> => {
|
|
162
|
+
const rpResult = await loadRpResult();
|
|
163
|
+
if (!rpResult.ok) return rpResult;
|
|
164
|
+
const rp = rpResult.data;
|
|
165
|
+
const existing = await listStoredForUser({ userId: params.user.id });
|
|
166
|
+
const options = await generateRegistrationOptions({
|
|
167
|
+
rpName: rp.rpName,
|
|
168
|
+
rpID: rp.rpID,
|
|
169
|
+
userID: userIdToWebAuthnBytes(params.user.id),
|
|
170
|
+
userName: params.user.mail ?? params.user.uid,
|
|
171
|
+
userDisplayName: params.user.displayName || params.user.uid,
|
|
172
|
+
attestationType: "none",
|
|
173
|
+
authenticatorSelection: {
|
|
174
|
+
residentKey: "required",
|
|
175
|
+
userVerification: "required",
|
|
176
|
+
},
|
|
177
|
+
excludeCredentials: existing.map((credential) => ({
|
|
178
|
+
id: credential.credentialId,
|
|
179
|
+
transports: credential.transports as AuthenticatorTransportFuture[],
|
|
180
|
+
})),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await storeRegistrationChallenge(params.user.id, options.challenge);
|
|
184
|
+
return ok(options);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const finishRegistration = async (params: {
|
|
188
|
+
user: User;
|
|
189
|
+
name: string;
|
|
190
|
+
response: RegistrationResponseJSON;
|
|
191
|
+
}): Promise<Result<WebAuthnPasskey>> => {
|
|
192
|
+
const name = params.name.trim();
|
|
193
|
+
if (!name) return fail(err.badInput("Passkey name is required."));
|
|
194
|
+
if (name.length > 120) return fail(err.badInput("Passkey name must be 120 characters or fewer."));
|
|
195
|
+
|
|
196
|
+
const expectedChallenge = await consumeRegistrationChallenge(params.user.id);
|
|
197
|
+
if (!expectedChallenge) return fail(err.badInput("Passkey registration expired. Please try again."));
|
|
198
|
+
|
|
199
|
+
const rpResult = await loadRpResult();
|
|
200
|
+
if (!rpResult.ok) return rpResult;
|
|
201
|
+
const rp = rpResult.data;
|
|
202
|
+
const verification = await verifyRegistrationResponse({
|
|
203
|
+
response: params.response,
|
|
204
|
+
expectedChallenge,
|
|
205
|
+
expectedOrigin: rp.origin,
|
|
206
|
+
expectedRPID: rp.rpID,
|
|
207
|
+
requireUserVerification: true,
|
|
208
|
+
}).catch((error) => {
|
|
209
|
+
log.warn("Passkey registration verification failed", {
|
|
210
|
+
userId: params.user.id,
|
|
211
|
+
rpID: rp.rpID,
|
|
212
|
+
origin: rp.origin,
|
|
213
|
+
error: error instanceof Error ? error.message : String(error),
|
|
214
|
+
});
|
|
215
|
+
return null;
|
|
216
|
+
});
|
|
217
|
+
if (!verification) return fail(err.badInput("Passkey registration could not be verified."));
|
|
218
|
+
if (!verification.verified) return fail(err.badInput("Passkey registration could not be verified."));
|
|
219
|
+
|
|
220
|
+
const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
|
221
|
+
const transports = params.response.response.transports ?? [];
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
return sql.begin(async (tx) => {
|
|
225
|
+
const [row] = await tx<DbPasskeyRow[]>`
|
|
226
|
+
INSERT INTO auth.webauthn_credentials (
|
|
227
|
+
user_id,
|
|
228
|
+
name,
|
|
229
|
+
credential_id,
|
|
230
|
+
public_key,
|
|
231
|
+
counter,
|
|
232
|
+
transports,
|
|
233
|
+
device_type,
|
|
234
|
+
backed_up
|
|
235
|
+
)
|
|
236
|
+
VALUES (
|
|
237
|
+
${params.user.id}::uuid,
|
|
238
|
+
${name},
|
|
239
|
+
${credential.id},
|
|
240
|
+
${Buffer.from(credential.publicKey)},
|
|
241
|
+
${credential.counter},
|
|
242
|
+
${toPgTextArray(transports)}::text[],
|
|
243
|
+
${credentialDeviceType},
|
|
244
|
+
${credentialBackedUp}
|
|
245
|
+
)
|
|
246
|
+
RETURNING id, user_id, name, credential_id, public_key, counter, transports, device_type,
|
|
247
|
+
backed_up, created_at, last_used_at
|
|
248
|
+
`;
|
|
249
|
+
const result = row ? ok(mapPasskey(row)) : fail(err.badInput("Passkey could not be saved."));
|
|
250
|
+
return audit.recordResult({
|
|
251
|
+
action: "webauthn_credential.create",
|
|
252
|
+
actor: actorForUser(params.user),
|
|
253
|
+
target: { type: "webauthn_credential", id: row?.id ?? null, label: name },
|
|
254
|
+
metadata: {
|
|
255
|
+
deviceType: credentialDeviceType,
|
|
256
|
+
backedUp: credentialBackedUp,
|
|
257
|
+
transports,
|
|
258
|
+
},
|
|
259
|
+
result,
|
|
260
|
+
db: tx,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (isUniqueViolation(error)) return fail(err.conflict("Passkey"));
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const beginAuthentication = async (): Promise<Result<PublicKeyCredentialRequestOptionsJSON>> => {
|
|
270
|
+
const rpResult = await loadRpResult();
|
|
271
|
+
if (!rpResult.ok) return rpResult;
|
|
272
|
+
const rp = rpResult.data;
|
|
273
|
+
const options = await generateAuthenticationOptions({
|
|
274
|
+
rpID: rp.rpID,
|
|
275
|
+
userVerification: "required",
|
|
276
|
+
});
|
|
277
|
+
await storeAuthenticationChallenge(options.challenge);
|
|
278
|
+
return ok(options);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const findCredentialForAuthentication = async (credentialId: string): Promise<DbPasskeyWithUserRow | null> => {
|
|
282
|
+
const [row] = await sql<DbPasskeyWithUserRow[]>`
|
|
283
|
+
SELECT c.id, c.user_id, c.name, c.credential_id, c.public_key, c.counter, c.transports,
|
|
284
|
+
c.device_type, c.backed_up, c.created_at, c.last_used_at, u.account_expires AS user_account_expires
|
|
285
|
+
FROM auth.webauthn_credentials c
|
|
286
|
+
JOIN auth.users u ON u.id = c.user_id
|
|
287
|
+
WHERE c.credential_id = ${credentialId}
|
|
288
|
+
LIMIT 1
|
|
289
|
+
`;
|
|
290
|
+
return row ?? null;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const finishAuthentication = async (params: {
|
|
294
|
+
response: AuthenticationResponseJSON;
|
|
295
|
+
}): Promise<Result<{ user: User; passkey: WebAuthnPasskey }>> => {
|
|
296
|
+
const row = await findCredentialForAuthentication(params.response.id);
|
|
297
|
+
if (!row) return fail(err.unauthenticated("Passkey could not be verified."));
|
|
298
|
+
if (isExpired(row.user_account_expires)) return fail(err.forbidden("Your account has expired. Contact an administrator."));
|
|
299
|
+
|
|
300
|
+
const rpResult = await loadRpResult();
|
|
301
|
+
if (!rpResult.ok) return rpResult;
|
|
302
|
+
const rp = rpResult.data;
|
|
303
|
+
const verification = await verifyAuthenticationResponse({
|
|
304
|
+
response: params.response,
|
|
305
|
+
expectedChallenge: consumeAuthenticationChallenge,
|
|
306
|
+
expectedOrigin: rp.origin,
|
|
307
|
+
expectedRPID: rp.rpID,
|
|
308
|
+
credential: toCredential(row),
|
|
309
|
+
requireUserVerification: true,
|
|
310
|
+
}).catch((error) => {
|
|
311
|
+
log.warn("Passkey authentication verification failed", {
|
|
312
|
+
credentialId: row.id,
|
|
313
|
+
userId: row.user_id,
|
|
314
|
+
rpID: rp.rpID,
|
|
315
|
+
origin: rp.origin,
|
|
316
|
+
error: error instanceof Error ? error.message : String(error),
|
|
317
|
+
});
|
|
318
|
+
return null;
|
|
319
|
+
});
|
|
320
|
+
if (!verification) return fail(err.unauthenticated("Passkey could not be verified."));
|
|
321
|
+
if (!verification.verified) return fail(err.unauthenticated("Passkey could not be verified."));
|
|
322
|
+
|
|
323
|
+
const user = await accounts.users.get({ id: row.user_id });
|
|
324
|
+
if (!user) return fail(err.unauthenticated("Passkey user not found."));
|
|
325
|
+
if (user.accountExpires && new Date(user.accountExpires).getTime() <= Date.now()) {
|
|
326
|
+
return fail(err.forbidden("Your account has expired. Contact an administrator."));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return sql.begin(async (tx) => {
|
|
330
|
+
const [updated] = await tx<DbPasskeyRow[]>`
|
|
331
|
+
UPDATE auth.webauthn_credentials
|
|
332
|
+
SET counter = ${verification.authenticationInfo.newCounter},
|
|
333
|
+
device_type = ${verification.authenticationInfo.credentialDeviceType},
|
|
334
|
+
backed_up = ${verification.authenticationInfo.credentialBackedUp},
|
|
335
|
+
last_used_at = now()
|
|
336
|
+
WHERE id = ${row.id}::uuid
|
|
337
|
+
RETURNING id, user_id, name, credential_id, public_key, counter, transports, device_type,
|
|
338
|
+
backed_up, created_at, last_used_at
|
|
339
|
+
`;
|
|
340
|
+
const passkey = mapPasskey(updated ?? row);
|
|
341
|
+
const result = ok({ user, passkey });
|
|
342
|
+
return audit.recordResult({
|
|
343
|
+
action: "webauthn_credential.authenticate",
|
|
344
|
+
actor: actorForUser(user),
|
|
345
|
+
target: { type: "webauthn_credential", id: row.id, label: row.name },
|
|
346
|
+
metadata: {
|
|
347
|
+
deviceType: verification.authenticationInfo.credentialDeviceType,
|
|
348
|
+
backedUp: verification.authenticationInfo.credentialBackedUp,
|
|
349
|
+
},
|
|
350
|
+
result,
|
|
351
|
+
db: tx,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export const deleteForUser = async (params: { user: User; id: string }): Promise<Result<void>> => {
|
|
357
|
+
if (!UUID_PATTERN.test(params.id)) return fail(err.notFound("Passkey"));
|
|
358
|
+
|
|
359
|
+
return sql.begin(async (tx) => {
|
|
360
|
+
const [row] = await tx<Pick<DbPasskeyRow, "id" | "name">[]>`
|
|
361
|
+
DELETE FROM auth.webauthn_credentials
|
|
362
|
+
WHERE id = ${params.id}::uuid
|
|
363
|
+
AND user_id = ${params.user.id}::uuid
|
|
364
|
+
RETURNING id, name
|
|
365
|
+
`;
|
|
366
|
+
const result = row ? ok() : fail(err.notFound("Passkey"));
|
|
367
|
+
return audit.recordResult({
|
|
368
|
+
action: "webauthn_credential.delete",
|
|
369
|
+
actor: actorForUser(params.user),
|
|
370
|
+
target: { type: "webauthn_credential", id: params.id, label: row?.name ?? null },
|
|
371
|
+
result,
|
|
372
|
+
db: tx,
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const webauthn = {
|
|
378
|
+
resolveWebAuthnRp,
|
|
379
|
+
listForUser,
|
|
380
|
+
beginRegistration,
|
|
381
|
+
finishRegistration,
|
|
382
|
+
beginAuthentication,
|
|
383
|
+
finishAuthentication,
|
|
384
|
+
deleteForUser,
|
|
385
|
+
};
|