@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,268 @@
|
|
|
1
|
+
export type LiveWebSocketActivity = "always" | "visible";
|
|
2
|
+
export type LiveWebSocketStatus = "connecting" | "open" | "reconnecting" | "paused" | "closed";
|
|
3
|
+
|
|
4
|
+
export type LiveWebSocketError = {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type LiveWebSocketClose = {
|
|
10
|
+
code: number;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type LiveWebSocketControls = {
|
|
15
|
+
markApplied: (cursor: string | null | undefined) => void;
|
|
16
|
+
/** Forget an expired cursor before resubscribing from a fresh snapshot. */
|
|
17
|
+
resetCursor: () => void;
|
|
18
|
+
send: (message: unknown) => boolean;
|
|
19
|
+
terminate: (error: LiveWebSocketError, close?: LiveWebSocketClose) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type LiveWebSocketOptions<TMessage> = {
|
|
23
|
+
url: string | (() => string);
|
|
24
|
+
initialCursor?: string | null;
|
|
25
|
+
activity?: LiveWebSocketActivity;
|
|
26
|
+
subscribe: (cursor: string | null) => unknown;
|
|
27
|
+
parse: (raw: string) => TMessage | null;
|
|
28
|
+
onOpen?: (controls: LiveWebSocketControls) => void;
|
|
29
|
+
onMessage: (message: TMessage, controls: LiveWebSocketControls) => void;
|
|
30
|
+
onStatus?: (status: LiveWebSocketStatus) => void;
|
|
31
|
+
onFatal?: (error: LiveWebSocketError) => void;
|
|
32
|
+
classifyClose?: (close: LiveWebSocketClose) => LiveWebSocketError | null;
|
|
33
|
+
reconnect?: Partial<{
|
|
34
|
+
baseDelayMs: number;
|
|
35
|
+
maxDelayMs: number;
|
|
36
|
+
jitterMs: number;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type LiveWebSocket = {
|
|
41
|
+
connect: () => void;
|
|
42
|
+
markApplied: (cursor: string | null | undefined) => void;
|
|
43
|
+
/** Forget an expired cursor before resubscribing from a fresh snapshot. */
|
|
44
|
+
resetCursor: () => void;
|
|
45
|
+
send: (message: unknown) => boolean;
|
|
46
|
+
dispose: () => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DEFAULT_RECONNECT = {
|
|
50
|
+
baseDelayMs: 750,
|
|
51
|
+
maxDelayMs: 10_000,
|
|
52
|
+
jitterMs: 250,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
const defaultCloseError = ({ code, reason }: LiveWebSocketClose): LiveWebSocketError | null => {
|
|
56
|
+
if (code === 1008) return { code: reason || "access_denied", message: "Live access changed or expired." };
|
|
57
|
+
if (code === 1011) return { code: reason || "internal_error", message: "Live updates failed." };
|
|
58
|
+
if (code === 1013) return { code: reason || "backpressure", message: "Live updates are overloaded." };
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const socketUrl = (raw: string): string => {
|
|
63
|
+
const url = new URL(raw, window.location.origin);
|
|
64
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
65
|
+
else if (url.protocol === "https:") url.protocol = "wss:";
|
|
66
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new Error("Live WebSocket URL must use HTTP(S) or WS(S)");
|
|
67
|
+
return url.href;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Runs a cursor-backed, server-to-browser WebSocket subscription.
|
|
72
|
+
*
|
|
73
|
+
* The app owns its wire protocol, validation, permissions, and domain updates.
|
|
74
|
+
* This helper owns only browser transport lifecycle: one socket, visibility,
|
|
75
|
+
* reconnect backoff, applied-cursor resume, terminal closes, and disposal.
|
|
76
|
+
*/
|
|
77
|
+
export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMessage>): LiveWebSocket => {
|
|
78
|
+
const activity = options.activity ?? "visible";
|
|
79
|
+
const reconnect = { ...DEFAULT_RECONNECT, ...options.reconnect };
|
|
80
|
+
const classifyClose = options.classifyClose ?? defaultCloseError;
|
|
81
|
+
|
|
82
|
+
let socket: WebSocket | null = null;
|
|
83
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
84
|
+
let reconnectAttempt = 0;
|
|
85
|
+
let lastAppliedCursor = options.initialCursor ?? null;
|
|
86
|
+
let started = false;
|
|
87
|
+
let disposed = false;
|
|
88
|
+
let terminated = false;
|
|
89
|
+
let fatalSent = false;
|
|
90
|
+
let status: LiveWebSocketStatus | null = null;
|
|
91
|
+
|
|
92
|
+
const setStatus = (next: LiveWebSocketStatus) => {
|
|
93
|
+
if (status === next) return;
|
|
94
|
+
status = next;
|
|
95
|
+
options.onStatus?.(next);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const clearReconnect = () => {
|
|
99
|
+
if (reconnectTimer !== null) clearTimeout(reconnectTimer);
|
|
100
|
+
reconnectTimer = null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const markApplied = (cursor: string | null | undefined) => {
|
|
104
|
+
if (cursor) lastAppliedCursor = cursor;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const resetCursor = () => {
|
|
108
|
+
lastAppliedCursor = null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const send = (message: unknown): boolean => {
|
|
112
|
+
if (!socket || socket.readyState !== WebSocket.OPEN || disposed || terminated) return false;
|
|
113
|
+
try {
|
|
114
|
+
const payload = JSON.stringify(message);
|
|
115
|
+
if (payload === undefined) return false;
|
|
116
|
+
socket.send(payload);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const closeSocket = (code = 1000, reason = "") => {
|
|
124
|
+
const current = socket;
|
|
125
|
+
socket = null;
|
|
126
|
+
if (!current || current.readyState > WebSocket.OPEN) return;
|
|
127
|
+
try {
|
|
128
|
+
current.close(code, reason);
|
|
129
|
+
} catch {
|
|
130
|
+
// The browser already discarded the connection.
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const fatal = (error: LiveWebSocketError, close: LiveWebSocketClose = { code: 1008, reason: error.code }) => {
|
|
135
|
+
if (terminated) return;
|
|
136
|
+
terminated = true;
|
|
137
|
+
clearReconnect();
|
|
138
|
+
closeSocket(close.code, close.reason);
|
|
139
|
+
setStatus("closed");
|
|
140
|
+
if (!fatalSent) {
|
|
141
|
+
fatalSent = true;
|
|
142
|
+
options.onFatal?.(error);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const controls: LiveWebSocketControls = {
|
|
147
|
+
markApplied,
|
|
148
|
+
resetCursor,
|
|
149
|
+
send,
|
|
150
|
+
terminate: fatal,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const browserAvailable = () => typeof window !== "undefined" && typeof document !== "undefined" && typeof WebSocket !== "undefined";
|
|
154
|
+
const isActive = () => browserAvailable() && (activity === "always" || document.visibilityState === "visible");
|
|
155
|
+
|
|
156
|
+
const scheduleReconnect = () => {
|
|
157
|
+
if (disposed || terminated || !started || !isActive() || reconnectTimer) return;
|
|
158
|
+
setStatus("reconnecting");
|
|
159
|
+
const exponential = Math.min(reconnect.maxDelayMs, reconnect.baseDelayMs * 2 ** reconnectAttempt);
|
|
160
|
+
const delay = exponential + Math.floor(Math.random() * reconnect.jitterMs);
|
|
161
|
+
reconnectAttempt = Math.min(reconnectAttempt + 1, 5);
|
|
162
|
+
reconnectTimer = setTimeout(() => {
|
|
163
|
+
reconnectTimer = null;
|
|
164
|
+
openSocket();
|
|
165
|
+
}, delay);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const openSocket = () => {
|
|
169
|
+
if (disposed || terminated || !started || socket) return;
|
|
170
|
+
if (!browserAvailable()) {
|
|
171
|
+
fatal({ code: "unsupported", message: "Live WebSockets are not available in this browser" });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!isActive()) return;
|
|
175
|
+
|
|
176
|
+
let next: WebSocket;
|
|
177
|
+
setStatus(reconnectAttempt > 0 ? "reconnecting" : "connecting");
|
|
178
|
+
try {
|
|
179
|
+
next = new WebSocket(socketUrl(typeof options.url === "function" ? options.url() : options.url));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
fatal({ code: "invalid_url", message: error instanceof Error ? error.message : "Invalid live WebSocket URL" });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
socket = next;
|
|
185
|
+
|
|
186
|
+
next.onopen = () => {
|
|
187
|
+
if (socket !== next || disposed || terminated) return;
|
|
188
|
+
try {
|
|
189
|
+
if (!send(options.subscribe(lastAppliedCursor))) throw new Error("Live WebSocket subscription could not be sent");
|
|
190
|
+
setStatus("open");
|
|
191
|
+
options.onOpen?.(controls);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
fatal({ code: "subscribe_failed", message: error instanceof Error ? error.message : "Live subscription failed" });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
next.onmessage = (event) => {
|
|
198
|
+
if (socket !== next || typeof event.data !== "string" || disposed || terminated) return;
|
|
199
|
+
try {
|
|
200
|
+
const message = options.parse(event.data);
|
|
201
|
+
if (message) {
|
|
202
|
+
options.onMessage(message, controls);
|
|
203
|
+
reconnectAttempt = 0;
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
fatal(
|
|
207
|
+
{ code: "client_handler_failed", message: error instanceof Error ? error.message : "Live update handling failed" },
|
|
208
|
+
{ code: 1011, reason: "client_handler_failed" },
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
next.onclose = (event) => {
|
|
214
|
+
if (socket !== next) return;
|
|
215
|
+
socket = null;
|
|
216
|
+
if (disposed || terminated) return;
|
|
217
|
+
const closeError = classifyClose({ code: event.code, reason: event.reason.trim() });
|
|
218
|
+
if (closeError) fatal(closeError, { code: event.code, reason: event.reason });
|
|
219
|
+
else scheduleReconnect();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
next.onerror = () => {
|
|
223
|
+
try {
|
|
224
|
+
next.close();
|
|
225
|
+
} catch {
|
|
226
|
+
if (socket === next) {
|
|
227
|
+
socket = null;
|
|
228
|
+
scheduleReconnect();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const syncVisibility = () => {
|
|
235
|
+
if (isActive()) openSocket();
|
|
236
|
+
else {
|
|
237
|
+
clearReconnect();
|
|
238
|
+
closeSocket(1000, "inactive");
|
|
239
|
+
setStatus("paused");
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
connect: () => {
|
|
245
|
+
if (started || disposed || terminated) return;
|
|
246
|
+
started = true;
|
|
247
|
+
if (!browserAvailable()) {
|
|
248
|
+
fatal({ code: "unsupported", message: "Live WebSockets are not available in this browser" });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (activity === "visible") document.addEventListener("visibilitychange", syncVisibility);
|
|
252
|
+
openSocket();
|
|
253
|
+
},
|
|
254
|
+
markApplied,
|
|
255
|
+
resetCursor,
|
|
256
|
+
send,
|
|
257
|
+
dispose: () => {
|
|
258
|
+
if (disposed) return;
|
|
259
|
+
disposed = true;
|
|
260
|
+
clearReconnect();
|
|
261
|
+
if (activity === "visible" && typeof document !== "undefined") {
|
|
262
|
+
document.removeEventListener("visibilitychange", syncVisibility);
|
|
263
|
+
}
|
|
264
|
+
closeSocket();
|
|
265
|
+
setStatus("closed");
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cookies } from "@k2b/stdlib/browser";
|
|
2
|
+
import { canonicalLocale, LOCALE_COOKIE } from "../shared/locale";
|
|
3
|
+
|
|
4
|
+
export const PROFILE_PREFERENCE_LOCALES = ["en", "de"] as const;
|
|
5
|
+
export type ProfilePreferenceLocale = (typeof PROFILE_PREFERENCE_LOCALES)[number];
|
|
6
|
+
|
|
7
|
+
export const profilePreferenceLocale = (locale: string): ProfilePreferenceLocale =>
|
|
8
|
+
canonicalLocale(locale)?.split("-")[0] === "de" ? "de" : "en";
|
|
9
|
+
|
|
10
|
+
type LocalePreferenceRuntime = {
|
|
11
|
+
writeCookie: (name: string, value: string) => void;
|
|
12
|
+
reload: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const setLocalePreference = (
|
|
16
|
+
locale: ProfilePreferenceLocale,
|
|
17
|
+
runtime: LocalePreferenceRuntime = {
|
|
18
|
+
writeCookie: cookies.writeCookie,
|
|
19
|
+
reload: () => window.location.reload(),
|
|
20
|
+
},
|
|
21
|
+
): ProfilePreferenceLocale => {
|
|
22
|
+
runtime.writeCookie(LOCALE_COOKIE, locale);
|
|
23
|
+
runtime.reload();
|
|
24
|
+
return locale;
|
|
25
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const countParams = (params: URLSearchParams): Map<string, number> => {
|
|
2
|
+
const counts = new Map<string, number>();
|
|
3
|
+
for (const [key, value] of params) {
|
|
4
|
+
const token = `${key}\u0000${value}`;
|
|
5
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
6
|
+
}
|
|
7
|
+
return counts;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** A deep link already points at the visible view when all its params are present. */
|
|
11
|
+
export const notificationTargetMatchesLocation = (targetHref: string, currentHref: string): boolean => {
|
|
12
|
+
try {
|
|
13
|
+
const current = new URL(currentHref);
|
|
14
|
+
const target = new URL(targetHref, current.origin);
|
|
15
|
+
if (target.origin !== current.origin || target.pathname !== current.pathname) return false;
|
|
16
|
+
if (target.hash && target.hash !== current.hash) return false;
|
|
17
|
+
|
|
18
|
+
const targetParams = countParams(target.searchParams);
|
|
19
|
+
const currentParams = countParams(current.searchParams);
|
|
20
|
+
for (const [token, count] of targetParams) {
|
|
21
|
+
if ((currentParams.get(token) ?? 0) < count) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const withNotificationTimeout = <T>(operation: Promise<T>, timeoutMs: number, message: string): Promise<T> =>
|
|
2
|
+
new Promise<T>((resolve, reject) => {
|
|
3
|
+
const timer = globalThis.setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
4
|
+
operation.then(
|
|
5
|
+
(value) => {
|
|
6
|
+
globalThis.clearTimeout(timer);
|
|
7
|
+
resolve(value);
|
|
8
|
+
},
|
|
9
|
+
(error) => {
|
|
10
|
+
globalThis.clearTimeout(timer);
|
|
11
|
+
reject(error);
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { notifications as nativeNotifications } from "@k2b/stdlib/browser";
|
|
2
|
+
import { apiClient } from "../clients/core";
|
|
3
|
+
import type { BrowserPushSubscription } from "../contracts";
|
|
4
|
+
import { withNotificationTimeout } from "./notification-timeout";
|
|
5
|
+
|
|
6
|
+
const SERVICE_WORKER_PATH = "/service-worker.js";
|
|
7
|
+
const SERVICE_WORKER_SCOPE = "/";
|
|
8
|
+
const STATE_CHECK_TIMEOUT_MS = 5_000;
|
|
9
|
+
|
|
10
|
+
export type BrowserNotificationState = {
|
|
11
|
+
supported: boolean;
|
|
12
|
+
permission: NotificationPermission;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isIos = (): boolean =>
|
|
18
|
+
/iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
|
19
|
+
|
|
20
|
+
const isStandalone = (): boolean =>
|
|
21
|
+
window.matchMedia("(display-mode: standalone)").matches || (navigator as Navigator & { standalone?: boolean }).standalone === true;
|
|
22
|
+
|
|
23
|
+
const support = (): { supported: boolean; reason?: string } => {
|
|
24
|
+
if (
|
|
25
|
+
typeof window === "undefined" ||
|
|
26
|
+
!window.isSecureContext ||
|
|
27
|
+
!("serviceWorker" in navigator) ||
|
|
28
|
+
!("PushManager" in window) ||
|
|
29
|
+
!nativeNotifications.isSupported()
|
|
30
|
+
) {
|
|
31
|
+
return { supported: false, reason: "Browser notifications are not supported in this browser." };
|
|
32
|
+
}
|
|
33
|
+
if (isIos() && !isStandalone()) {
|
|
34
|
+
return {
|
|
35
|
+
supported: false,
|
|
36
|
+
reason: "On iPhone and iPad, add Cloud to your Home Screen before enabling browser notifications.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { supported: true };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ensureServiceWorker = (): Promise<ServiceWorkerRegistration> =>
|
|
43
|
+
navigator.serviceWorker.register(SERVICE_WORKER_PATH, { scope: SERVICE_WORKER_SCOPE });
|
|
44
|
+
|
|
45
|
+
const subscriptionPayload = (subscription: PushSubscription): BrowserPushSubscription => {
|
|
46
|
+
const serialized = subscription.toJSON();
|
|
47
|
+
const p256dh = serialized.keys?.p256dh;
|
|
48
|
+
const auth = serialized.keys?.auth;
|
|
49
|
+
if (!serialized.endpoint || !p256dh || !auth) {
|
|
50
|
+
throw new Error("The browser returned an incomplete push subscription.");
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
endpoint: serialized.endpoint,
|
|
54
|
+
expirationTime: subscription.expirationTime,
|
|
55
|
+
keys: { p256dh, auth },
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const responseError = async (response: Response, fallback: string): Promise<Error> => {
|
|
60
|
+
try {
|
|
61
|
+
const payload = (await response.json()) as { message?: unknown };
|
|
62
|
+
if (typeof payload.message === "string" && payload.message.trim()) return new Error(payload.message);
|
|
63
|
+
} catch {
|
|
64
|
+
// The endpoint may return an empty or non-JSON infrastructure error.
|
|
65
|
+
}
|
|
66
|
+
return new Error(fallback);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const registerEndpoint = async (subscription: PushSubscription): Promise<void> => {
|
|
70
|
+
const response = await apiClient.me.notifications.browser.endpoints.$post({
|
|
71
|
+
json: {
|
|
72
|
+
subscription: subscriptionPayload(subscription),
|
|
73
|
+
label: navigator.platform ? `Browser on ${navigator.platform}` : "This browser",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) throw await responseError(response, "Failed to register this browser for notifications.");
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const decodeApplicationServerKey = (value: string): Uint8Array<ArrayBuffer> => {
|
|
80
|
+
const padded = value
|
|
81
|
+
.replace(/-/g, "+")
|
|
82
|
+
.replace(/_/g, "/")
|
|
83
|
+
.padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
84
|
+
const binary = window.atob(padded);
|
|
85
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
86
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
87
|
+
return bytes;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const pushSubscriptionError = (cause: unknown): Error => {
|
|
91
|
+
const name = cause instanceof DOMException ? cause.name : "";
|
|
92
|
+
const message = cause instanceof Error ? cause.message.toLowerCase() : "";
|
|
93
|
+
if (name === "NotAllowedError" || message.includes("permission") || message.includes("incognito")) {
|
|
94
|
+
return new Error("This browser refused push registration. Check its notification permissions or private browsing mode.");
|
|
95
|
+
}
|
|
96
|
+
if (name === "AbortError") return new Error("Push registration was interrupted. Try again.");
|
|
97
|
+
return new Error("This browser could not create a push subscription.");
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const currentState = async (registration?: ServiceWorkerRegistration): Promise<BrowserNotificationState> => {
|
|
101
|
+
const capability = support();
|
|
102
|
+
if (!capability.supported) {
|
|
103
|
+
return { supported: false, permission: "denied", enabled: false, reason: capability.reason };
|
|
104
|
+
}
|
|
105
|
+
return withNotificationTimeout(
|
|
106
|
+
(async () => {
|
|
107
|
+
const activeRegistration = registration ?? (await navigator.serviceWorker.getRegistration(SERVICE_WORKER_SCOPE));
|
|
108
|
+
const subscription = await activeRegistration?.pushManager.getSubscription();
|
|
109
|
+
return {
|
|
110
|
+
supported: true,
|
|
111
|
+
permission: Notification.permission,
|
|
112
|
+
enabled: Notification.permission === "granted" && !!subscription,
|
|
113
|
+
};
|
|
114
|
+
})(),
|
|
115
|
+
STATE_CHECK_TIMEOUT_MS,
|
|
116
|
+
"Browser notification status check timed out.",
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const browserNotificationClient = {
|
|
121
|
+
state: currentState,
|
|
122
|
+
|
|
123
|
+
/** Register the worker and rebind an existing subscription to the signed-in user. Never prompts. */
|
|
124
|
+
refreshExisting: async (): Promise<BrowserNotificationState> => {
|
|
125
|
+
const capability = support();
|
|
126
|
+
if (!capability.supported) return currentState();
|
|
127
|
+
const registration = await ensureServiceWorker();
|
|
128
|
+
if (Notification.permission !== "granted") return currentState(registration);
|
|
129
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
130
|
+
if (subscription) await registerEndpoint(subscription);
|
|
131
|
+
return currentState(registration);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** Must only be called from an explicit user action because it may show a permission prompt. */
|
|
135
|
+
enable: async (): Promise<BrowserNotificationState> => {
|
|
136
|
+
const capability = support();
|
|
137
|
+
if (!capability.supported) return currentState();
|
|
138
|
+
if (Notification.permission !== "granted" && !(await nativeNotifications.requestPermission())) return currentState();
|
|
139
|
+
|
|
140
|
+
const registration = await ensureServiceWorker();
|
|
141
|
+
let subscription = await registration.pushManager.getSubscription();
|
|
142
|
+
if (!subscription) {
|
|
143
|
+
const configResponse = await apiClient.me.notifications.browser.configuration.$get();
|
|
144
|
+
if (!configResponse.ok) {
|
|
145
|
+
throw await responseError(configResponse, "Failed to load browser notification configuration.");
|
|
146
|
+
}
|
|
147
|
+
const config = await configResponse.json();
|
|
148
|
+
try {
|
|
149
|
+
subscription = await registration.pushManager.subscribe({
|
|
150
|
+
userVisibleOnly: true,
|
|
151
|
+
applicationServerKey: decodeApplicationServerKey(config.publicKey),
|
|
152
|
+
});
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
throw pushSubscriptionError(cause);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await registerEndpoint(subscription);
|
|
158
|
+
return currentState(registration);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
disable: async (): Promise<BrowserNotificationState> => {
|
|
162
|
+
const capability = support();
|
|
163
|
+
if (!capability.supported) return currentState();
|
|
164
|
+
const registration = await navigator.serviceWorker.getRegistration(SERVICE_WORKER_SCOPE);
|
|
165
|
+
const subscription = await registration?.pushManager.getSubscription();
|
|
166
|
+
if (subscription) {
|
|
167
|
+
let endpointError: Error | null = null;
|
|
168
|
+
try {
|
|
169
|
+
const response = await apiClient.me.notifications.browser.endpoints.$delete({
|
|
170
|
+
json: { subscription: subscriptionPayload(subscription) },
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) endpointError = await responseError(response, "Failed to disable browser notifications.");
|
|
173
|
+
} catch (cause) {
|
|
174
|
+
endpointError = cause instanceof Error ? cause : new Error("Failed to disable browser notifications.");
|
|
175
|
+
}
|
|
176
|
+
await subscription.unsubscribe();
|
|
177
|
+
if (endpointError) throw endpointError;
|
|
178
|
+
}
|
|
179
|
+
return currentState(registration);
|
|
180
|
+
},
|
|
181
|
+
} as const;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CloudResourceRef } from "../contracts/capabilities";
|
|
2
|
+
import {
|
|
3
|
+
CLOUD_RESOURCE_CLIPBOARD_MAX_BYTES,
|
|
4
|
+
CLOUD_RESOURCE_CLIPBOARD_MIME_TYPE,
|
|
5
|
+
CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT,
|
|
6
|
+
parseCloudResourceClipboard,
|
|
7
|
+
serializeCloudResourceClipboard,
|
|
8
|
+
} from "../contracts/resource-clipboard";
|
|
9
|
+
|
|
10
|
+
export type CloudResourceClipboardWrite = {
|
|
11
|
+
/** Canonical public Cloud origin derived from the configured `app.url`. */
|
|
12
|
+
cloudUrl: string;
|
|
13
|
+
ref: CloudResourceRef;
|
|
14
|
+
fallbackText: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CloudResourceClipboardItem = Pick<ClipboardItem, "types" | "getType">;
|
|
18
|
+
|
|
19
|
+
const supportsStructuredClipboard = (): boolean => {
|
|
20
|
+
if (typeof ClipboardItem === "undefined" || typeof navigator.clipboard?.write !== "function") return false;
|
|
21
|
+
return typeof ClipboardItem.supports === "function" && ClipboardItem.supports(CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const writeCloudResourceClipboard = async ({ cloudUrl, ref, fallbackText }: CloudResourceClipboardWrite): Promise<void> => {
|
|
25
|
+
if (new TextEncoder().encode(fallbackText).byteLength > CLOUD_RESOURCE_CLIPBOARD_MAX_BYTES || fallbackText.length === 0) {
|
|
26
|
+
throw new TypeError(`fallbackText must contain between 1 and ${CLOUD_RESOURCE_CLIPBOARD_MAX_BYTES} bytes`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const payload = serializeCloudResourceClipboard({ cloudUrl, ref });
|
|
30
|
+
|
|
31
|
+
if (!supportsStructuredClipboard()) {
|
|
32
|
+
await navigator.clipboard.writeText(fallbackText);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await navigator.clipboard.write([
|
|
37
|
+
new ClipboardItem({
|
|
38
|
+
[CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT]: new Blob([payload], { type: CLOUD_RESOURCE_CLIPBOARD_MIME_TYPE }),
|
|
39
|
+
"text/plain": new Blob([fallbackText], { type: "text/plain" }),
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const readCloudResourceClipboard = async (
|
|
45
|
+
cloudUrl: string,
|
|
46
|
+
items?: readonly CloudResourceClipboardItem[],
|
|
47
|
+
): Promise<CloudResourceRef | null> => {
|
|
48
|
+
const clipboardItems = items ?? (await navigator.clipboard.read());
|
|
49
|
+
|
|
50
|
+
for (const item of clipboardItems) {
|
|
51
|
+
if (!item.types.includes(CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT)) continue;
|
|
52
|
+
|
|
53
|
+
const blob = await item.getType(CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT);
|
|
54
|
+
if (blob.size > CLOUD_RESOURCE_CLIPBOARD_MAX_BYTES) continue;
|
|
55
|
+
|
|
56
|
+
const ref = parseCloudResourceClipboard(await blob.text(), cloudUrl);
|
|
57
|
+
if (ref) return ref;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const cloudResourceClipboard = {
|
|
64
|
+
mimeType: CLOUD_RESOURCE_CLIPBOARD_MIME_TYPE,
|
|
65
|
+
webFormat: CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT,
|
|
66
|
+
parse: parseCloudResourceClipboard,
|
|
67
|
+
serialize: serializeCloudResourceClipboard,
|
|
68
|
+
write: writeCloudResourceClipboard,
|
|
69
|
+
read: readCloudResourceClipboard,
|
|
70
|
+
} as const;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { prompts } from "@k2b/ui";
|
|
2
|
+
import type { SearchItem } from "../api/search/schemas";
|
|
3
|
+
import type { CloudResourceRef } from "../contracts";
|
|
4
|
+
import CloudResourceSearch from "./CloudResourceSearch";
|
|
5
|
+
import { resourceSearchMessages } from "./resource-search-messages";
|
|
6
|
+
|
|
7
|
+
export type CloudResourcePickerItem = SearchItem;
|
|
8
|
+
|
|
9
|
+
export type CloudResourcePickerOptions = {
|
|
10
|
+
title?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
initialAppId?: string;
|
|
13
|
+
excludeRefs?: readonly CloudResourceRef[];
|
|
14
|
+
requireReader?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const openCloudResourcePicker = (options: CloudResourcePickerOptions = {}): Promise<CloudResourcePickerItem | undefined> => {
|
|
18
|
+
const locale = typeof document === "undefined" ? "en" : document.documentElement.lang || "en";
|
|
19
|
+
const t = resourceSearchMessages.resolve([locale]).t;
|
|
20
|
+
return prompts.dialog<CloudResourcePickerItem>(
|
|
21
|
+
(close) => (
|
|
22
|
+
<div class="k2b-dialog__body h-[min(36rem,var(--ui-dialog-available-height))] min-h-0 p-0">
|
|
23
|
+
<CloudResourceSearch
|
|
24
|
+
initialAppId={options.initialAppId}
|
|
25
|
+
excludeRefs={options.excludeRefs}
|
|
26
|
+
requireReader={options.requireReader}
|
|
27
|
+
placeholder={options.placeholder ?? t.searchPlaceholder}
|
|
28
|
+
onSelect={close}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
{
|
|
33
|
+
title: options.title ?? t.chooseResource,
|
|
34
|
+
icon: "ti ti-cloud-search",
|
|
35
|
+
size: "large",
|
|
36
|
+
ariaLabel: options.title ?? t.chooseResource,
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
|
|
3
|
+
export const resourceSearchMessages = i18n.define({
|
|
4
|
+
baseLocale: "en",
|
|
5
|
+
messages: {
|
|
6
|
+
en: {
|
|
7
|
+
searchFailed: "Search failed.",
|
|
8
|
+
searchPlaceholder: "Search across apps…",
|
|
9
|
+
searchCloudResources: "Search Cloud resources",
|
|
10
|
+
tagSuggestions: "Tag suggestions",
|
|
11
|
+
filterByApp: "Filter by app",
|
|
12
|
+
allApps: "All apps",
|
|
13
|
+
removeTag: ({ tag }: { tag: string }) => `Remove tag ${tag}`,
|
|
14
|
+
removeTagTitle: ({ tag }: { tag: string }) => `Remove #${tag}`,
|
|
15
|
+
resultCount: ({ count }: { count: number }) => i18n.plural(count, "en", { one: `${count} result`, other: `${count} results` }),
|
|
16
|
+
tagHelp: "tag help",
|
|
17
|
+
typeToSearchBeforeTag: "Type to search, or use a",
|
|
18
|
+
typeToSearchAfterTag: "to focus on one app.",
|
|
19
|
+
noTagsAvailable: "No tags available.",
|
|
20
|
+
addTag: ({ tag, app }: { tag: string; app: string }) => `Add #${tag} (${app})`,
|
|
21
|
+
seeAllTagDescriptions: "See all tag descriptions",
|
|
22
|
+
noAppSupports: "No app supports",
|
|
23
|
+
removeTagOrPick: "Remove the tag or pick one below.",
|
|
24
|
+
removeNamedTag: ({ tag }: { tag: string }) => `Remove #${tag}`,
|
|
25
|
+
noMatches: "No matches",
|
|
26
|
+
matchesFor: "for",
|
|
27
|
+
selectPreview: "Select a result to preview details.",
|
|
28
|
+
chooseResource: "Choose Cloud resource",
|
|
29
|
+
},
|
|
30
|
+
de: {
|
|
31
|
+
searchFailed: "Die Suche ist fehlgeschlagen.",
|
|
32
|
+
searchPlaceholder: "Apps durchsuchen…",
|
|
33
|
+
searchCloudResources: "Cloud-Inhalte durchsuchen",
|
|
34
|
+
tagSuggestions: "Tag-Vorschläge",
|
|
35
|
+
filterByApp: "Nach App filtern",
|
|
36
|
+
allApps: "Alle Apps",
|
|
37
|
+
removeTag: ({ tag }) => `Tag ${tag} entfernen`,
|
|
38
|
+
removeTagTitle: ({ tag }) => `#${tag} entfernen`,
|
|
39
|
+
resultCount: ({ count }) => i18n.plural(count, "de", { one: `${count} Ergebnis`, other: `${count} Ergebnisse` }),
|
|
40
|
+
tagHelp: "Hilfe zu Tags",
|
|
41
|
+
typeToSearchBeforeTag: "Gib einen Suchbegriff ein oder verwende einen",
|
|
42
|
+
typeToSearchAfterTag: "für die Suche in einer bestimmten App.",
|
|
43
|
+
noTagsAvailable: "Keine Tags verfügbar.",
|
|
44
|
+
addTag: ({ tag, app }) => `#${tag} hinzufügen (${app})`,
|
|
45
|
+
seeAllTagDescriptions: "Alle Tag-Beschreibungen anzeigen",
|
|
46
|
+
noAppSupports: "Keine App unterstützt",
|
|
47
|
+
removeTagOrPick: "Entferne den Tag oder wähle unten einen anderen aus.",
|
|
48
|
+
removeNamedTag: ({ tag }) => `#${tag} entfernen`,
|
|
49
|
+
noMatches: "Keine Treffer",
|
|
50
|
+
matchesFor: "für",
|
|
51
|
+
selectPreview: "Wähle ein Ergebnis aus, um Details anzuzeigen.",
|
|
52
|
+
chooseResource: "Cloud-Inhalt auswählen",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|