@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,633 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
import type { BoundNotificationDefinition, NotificationRecipientKind, NotificationSendInput } from "../../contracts/notification-types";
|
|
4
|
+
import type { PaginationParams } from "../../contracts/shared";
|
|
5
|
+
import { logger } from "../logging";
|
|
6
|
+
import { escapeLikePattern } from "../postgres";
|
|
7
|
+
import { sendEmail } from "./email";
|
|
8
|
+
import "./browser";
|
|
9
|
+
import { notificationLive } from "./live";
|
|
10
|
+
import { notificationObservability } from "./observability";
|
|
11
|
+
import { sendTypedNotification, type TypedNotificationSendResult } from "./platform";
|
|
12
|
+
import { userNotifications } from "./user";
|
|
13
|
+
|
|
14
|
+
const log = logger("notifications");
|
|
15
|
+
|
|
16
|
+
export type NotificationType = "email";
|
|
17
|
+
export type NotificationStatus = "sent" | "pending" | "error";
|
|
18
|
+
export type NotificationStatusSummary = Record<NotificationStatus, number>;
|
|
19
|
+
export type NotificationSearchSummary = NotificationStatusSummary & {
|
|
20
|
+
total: number;
|
|
21
|
+
system: number;
|
|
22
|
+
latestCreatedAt: Date | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Computes notification delivery status from sent/error timestamps.
|
|
27
|
+
*/
|
|
28
|
+
const determineStatus = (sentAt: Date | null, error: string | null): NotificationStatus => {
|
|
29
|
+
if (sentAt) return "sent";
|
|
30
|
+
if (error) return "error";
|
|
31
|
+
return "pending";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** @deprecated Declare a typed app notification and call `notifications.send(definition, input)`. */
|
|
35
|
+
export type SendNotificationParams = {
|
|
36
|
+
type: NotificationType;
|
|
37
|
+
recipient: string;
|
|
38
|
+
subject: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
rawHtml?: string;
|
|
41
|
+
autoSend?: boolean; // default true - when false, only store in DB without sending
|
|
42
|
+
sentBy?: string; // user ID of sender
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** @deprecated Return type of the legacy email-only send API. */
|
|
46
|
+
export type SendNotificationResult = {
|
|
47
|
+
id: string;
|
|
48
|
+
status: NotificationStatus;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** @deprecated Declare a typed user notification and call `notifications.send(definition, input)`. */
|
|
53
|
+
export type SendToUserParams = {
|
|
54
|
+
userId: string;
|
|
55
|
+
subject: string;
|
|
56
|
+
content?: string;
|
|
57
|
+
rawHtml?: string;
|
|
58
|
+
sentBy?: string; // user ID of sender
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type NotificationMessage = {
|
|
62
|
+
id: string;
|
|
63
|
+
type: NotificationType;
|
|
64
|
+
recipient: string;
|
|
65
|
+
subject: string;
|
|
66
|
+
content: string;
|
|
67
|
+
sentAt: Date | null;
|
|
68
|
+
error: string | null;
|
|
69
|
+
createdAt: Date;
|
|
70
|
+
sentBy: string | null;
|
|
71
|
+
sentByName: string | null;
|
|
72
|
+
status: NotificationStatus;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const emptyStatusSummary = (): NotificationStatusSummary => ({
|
|
76
|
+
sent: 0,
|
|
77
|
+
pending: 0,
|
|
78
|
+
error: 0,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const emptySearchSummary = (): NotificationSearchSummary => ({
|
|
82
|
+
total: 0,
|
|
83
|
+
sent: 0,
|
|
84
|
+
pending: 0,
|
|
85
|
+
error: 0,
|
|
86
|
+
system: 0,
|
|
87
|
+
latestCreatedAt: null,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const warnDeprecatedSendApi = (api: "notifications.send" | "notifications.sendToUser"): void => {
|
|
91
|
+
log.warn("Deprecated notification send API used", {
|
|
92
|
+
api,
|
|
93
|
+
deprecated: true,
|
|
94
|
+
replacement: "defineApp({ notifications }) and notifications.send(definition, input)",
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type DbNotificationRow = {
|
|
99
|
+
id: string;
|
|
100
|
+
type: NotificationType;
|
|
101
|
+
recipient: string;
|
|
102
|
+
subject: string;
|
|
103
|
+
content: string;
|
|
104
|
+
sent_at: Date | null;
|
|
105
|
+
error: string | null;
|
|
106
|
+
created_at: Date;
|
|
107
|
+
sent_by: string | null;
|
|
108
|
+
sent_by_name: string | null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Send a notification. Persists to DB, attempts delivery (if autoSend=true), updates sent_at/error.
|
|
113
|
+
*/
|
|
114
|
+
const sendLegacy = async (params: SendNotificationParams): Promise<SendNotificationResult> => {
|
|
115
|
+
const { type, recipient, subject, content, rawHtml, autoSend = true, sentBy } = params;
|
|
116
|
+
|
|
117
|
+
// Persist to DB
|
|
118
|
+
const dbContent = rawHtml ?? content ?? "";
|
|
119
|
+
const rows = await sql`
|
|
120
|
+
INSERT INTO notifications.messages (type, recipient, subject, content, sent_by)
|
|
121
|
+
VALUES (${type}, ${recipient}, ${subject}, ${dbContent}, ${sentBy ?? null})
|
|
122
|
+
RETURNING id
|
|
123
|
+
`;
|
|
124
|
+
const id = rows[0]!.id as string;
|
|
125
|
+
|
|
126
|
+
// Skip delivery if autoSend is false
|
|
127
|
+
if (!autoSend) {
|
|
128
|
+
log.info("Stored notification", { type, recipient });
|
|
129
|
+
return { id, status: "pending" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Attempt delivery
|
|
133
|
+
try {
|
|
134
|
+
if (type === "email") {
|
|
135
|
+
await sendEmail(recipient, subject, { content, rawHtml });
|
|
136
|
+
}
|
|
137
|
+
await sql`UPDATE notifications.messages SET sent_at = now(), error = NULL WHERE id = ${id}`;
|
|
138
|
+
return { id, status: "sent" };
|
|
139
|
+
} catch (e) {
|
|
140
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
141
|
+
log.error("Failed to send", { type, recipient, error });
|
|
142
|
+
await sql`UPDATE notifications.messages SET error = ${error} WHERE id = ${id}`;
|
|
143
|
+
return { id, status: "error", error };
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function send<AppId extends string, Key extends string, R extends NotificationRecipientKind, S extends ZodType>(
|
|
148
|
+
definition: BoundNotificationDefinition<AppId, Key, R, S>,
|
|
149
|
+
input: NotificationSendInput<BoundNotificationDefinition<AppId, Key, R, S>>,
|
|
150
|
+
): Promise<TypedNotificationSendResult>;
|
|
151
|
+
/** @deprecated Declare a typed app notification and call the definition overload. */
|
|
152
|
+
export function send(params: SendNotificationParams): Promise<SendNotificationResult>;
|
|
153
|
+
export function send<AppId extends string, Key extends string, R extends NotificationRecipientKind, S extends ZodType>(
|
|
154
|
+
definitionOrParams: SendNotificationParams | BoundNotificationDefinition<AppId, Key, R, S>,
|
|
155
|
+
input?: NotificationSendInput<BoundNotificationDefinition<AppId, Key, R, S>>,
|
|
156
|
+
): Promise<SendNotificationResult | TypedNotificationSendResult> {
|
|
157
|
+
if ("type" in definitionOrParams) {
|
|
158
|
+
warnDeprecatedSendApi("notifications.send");
|
|
159
|
+
return sendLegacy(definitionOrParams);
|
|
160
|
+
}
|
|
161
|
+
if (!input) throw new Error("Typed notification send input is required");
|
|
162
|
+
return sendTypedNotification(definitionOrParams, input);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Send a notification to a user by their database ID.
|
|
167
|
+
* Looks up the user's preferred notification method (currently email only).
|
|
168
|
+
*/
|
|
169
|
+
export const sendToUser = async (params: SendToUserParams): Promise<{ ok: true; id: string } | { ok: false; error: string }> => {
|
|
170
|
+
warnDeprecatedSendApi("notifications.sendToUser");
|
|
171
|
+
const { userId, subject, content, rawHtml, sentBy } = params;
|
|
172
|
+
|
|
173
|
+
// Get user's email from database
|
|
174
|
+
const rows = await sql`SELECT mail FROM auth.users WHERE id = ${userId}`;
|
|
175
|
+
if (rows.length === 0) {
|
|
176
|
+
return { ok: false, error: "User not found" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const email = rows[0]!.mail as string | null;
|
|
180
|
+
if (!email) {
|
|
181
|
+
return { ok: false, error: "User has no email address" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// For now, always use email. Later this can be extended to support other notification types
|
|
185
|
+
// based on user preferences stored in the database.
|
|
186
|
+
const result = await sendLegacy({
|
|
187
|
+
type: "email",
|
|
188
|
+
recipient: email,
|
|
189
|
+
subject,
|
|
190
|
+
content,
|
|
191
|
+
rawHtml,
|
|
192
|
+
sentBy,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (result.status === "error") {
|
|
196
|
+
return { ok: false, error: result.error ?? "Email delivery failed" };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { ok: true, id: result.id };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* List notifications with pagination and optional search.
|
|
204
|
+
* Admins see all, regular users see only their own sent notifications.
|
|
205
|
+
*/
|
|
206
|
+
export const list = async (
|
|
207
|
+
pagination: PaginationParams,
|
|
208
|
+
options?: { sentBy?: string; isAdmin?: boolean; search?: string; status?: NotificationStatus },
|
|
209
|
+
): Promise<{ notifications: NotificationMessage[]; total: number }> => {
|
|
210
|
+
const { offset, perPage } = pagination;
|
|
211
|
+
const { sentBy, isAdmin, search, status } = options ?? {};
|
|
212
|
+
|
|
213
|
+
// Build query based on permissions
|
|
214
|
+
let countRows: Array<{ count: number | string }> = [];
|
|
215
|
+
let dataRows: DbNotificationRow[] = [];
|
|
216
|
+
|
|
217
|
+
const searchPattern = search ? `%${escapeLikePattern(search)}%` : null;
|
|
218
|
+
const statusFilter = status ?? null;
|
|
219
|
+
|
|
220
|
+
if (isAdmin) {
|
|
221
|
+
// Admins see all notifications
|
|
222
|
+
if (searchPattern) {
|
|
223
|
+
countRows = await sql`
|
|
224
|
+
SELECT COUNT(*)::int as count FROM notifications.messages
|
|
225
|
+
WHERE
|
|
226
|
+
(${statusFilter}::text IS NULL OR CASE WHEN sent_at IS NOT NULL THEN 'sent' WHEN error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter})
|
|
227
|
+
AND (subject ILIKE ${searchPattern} ESCAPE '\' OR content ILIKE ${searchPattern} ESCAPE '\' OR recipient ILIKE ${searchPattern} ESCAPE '\')
|
|
228
|
+
`;
|
|
229
|
+
dataRows = await sql`
|
|
230
|
+
SELECT
|
|
231
|
+
m.id, m.type, m.recipient, m.subject, m.content,
|
|
232
|
+
m.sent_at, m.error, m.created_at, m.sent_by,
|
|
233
|
+
u.display_name as sent_by_name
|
|
234
|
+
FROM notifications.messages m
|
|
235
|
+
LEFT JOIN auth.users u ON m.sent_by = u.id
|
|
236
|
+
WHERE
|
|
237
|
+
(${statusFilter}::text IS NULL OR CASE WHEN m.sent_at IS NOT NULL THEN 'sent' WHEN m.error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter})
|
|
238
|
+
AND (m.subject ILIKE ${searchPattern} ESCAPE '\' OR m.content ILIKE ${searchPattern} ESCAPE '\' OR m.recipient ILIKE ${searchPattern} ESCAPE '\')
|
|
239
|
+
ORDER BY m.created_at DESC
|
|
240
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
241
|
+
`;
|
|
242
|
+
} else {
|
|
243
|
+
countRows = await sql`
|
|
244
|
+
SELECT COUNT(*)::int as count FROM notifications.messages
|
|
245
|
+
WHERE ${statusFilter}::text IS NULL OR CASE WHEN sent_at IS NOT NULL THEN 'sent' WHEN error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter}
|
|
246
|
+
`;
|
|
247
|
+
dataRows = await sql`
|
|
248
|
+
SELECT
|
|
249
|
+
m.id, m.type, m.recipient, m.subject, m.content,
|
|
250
|
+
m.sent_at, m.error, m.created_at, m.sent_by,
|
|
251
|
+
u.display_name as sent_by_name
|
|
252
|
+
FROM notifications.messages m
|
|
253
|
+
LEFT JOIN auth.users u ON m.sent_by = u.id
|
|
254
|
+
WHERE ${statusFilter}::text IS NULL OR CASE WHEN m.sent_at IS NOT NULL THEN 'sent' WHEN m.error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter}
|
|
255
|
+
ORDER BY m.created_at DESC
|
|
256
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
} else if (sentBy) {
|
|
260
|
+
// Regular users see only their own sent notifications
|
|
261
|
+
if (searchPattern) {
|
|
262
|
+
countRows = await sql`
|
|
263
|
+
SELECT COUNT(*)::int as count FROM notifications.messages
|
|
264
|
+
WHERE
|
|
265
|
+
sent_by = ${sentBy}
|
|
266
|
+
AND (${statusFilter}::text IS NULL OR CASE WHEN sent_at IS NOT NULL THEN 'sent' WHEN error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter})
|
|
267
|
+
AND (subject ILIKE ${searchPattern} ESCAPE '\' OR content ILIKE ${searchPattern} ESCAPE '\' OR recipient ILIKE ${searchPattern} ESCAPE '\')
|
|
268
|
+
`;
|
|
269
|
+
dataRows = await sql`
|
|
270
|
+
SELECT
|
|
271
|
+
m.id, m.type, m.recipient, m.subject, m.content,
|
|
272
|
+
m.sent_at, m.error, m.created_at, m.sent_by,
|
|
273
|
+
u.display_name as sent_by_name
|
|
274
|
+
FROM notifications.messages m
|
|
275
|
+
LEFT JOIN auth.users u ON m.sent_by = u.id
|
|
276
|
+
WHERE
|
|
277
|
+
m.sent_by = ${sentBy}
|
|
278
|
+
AND (${statusFilter}::text IS NULL OR CASE WHEN m.sent_at IS NOT NULL THEN 'sent' WHEN m.error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter})
|
|
279
|
+
AND (m.subject ILIKE ${searchPattern} ESCAPE '\' OR m.content ILIKE ${searchPattern} ESCAPE '\' OR m.recipient ILIKE ${searchPattern} ESCAPE '\')
|
|
280
|
+
ORDER BY m.created_at DESC
|
|
281
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
282
|
+
`;
|
|
283
|
+
} else {
|
|
284
|
+
countRows = await sql`
|
|
285
|
+
SELECT COUNT(*)::int as count FROM notifications.messages
|
|
286
|
+
WHERE
|
|
287
|
+
sent_by = ${sentBy}
|
|
288
|
+
AND (${statusFilter}::text IS NULL OR CASE WHEN sent_at IS NOT NULL THEN 'sent' WHEN error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter})
|
|
289
|
+
`;
|
|
290
|
+
dataRows = await sql`
|
|
291
|
+
SELECT
|
|
292
|
+
m.id, m.type, m.recipient, m.subject, m.content,
|
|
293
|
+
m.sent_at, m.error, m.created_at, m.sent_by,
|
|
294
|
+
u.display_name as sent_by_name
|
|
295
|
+
FROM notifications.messages m
|
|
296
|
+
LEFT JOIN auth.users u ON m.sent_by = u.id
|
|
297
|
+
WHERE
|
|
298
|
+
m.sent_by = ${sentBy}
|
|
299
|
+
AND (${statusFilter}::text IS NULL OR CASE WHEN m.sent_at IS NOT NULL THEN 'sent' WHEN m.error IS NOT NULL THEN 'error' ELSE 'pending' END = ${statusFilter})
|
|
300
|
+
ORDER BY m.created_at DESC
|
|
301
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
return { notifications: [], total: 0 };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const rawTotal = countRows[0]?.count ?? 0;
|
|
309
|
+
const total = typeof rawTotal === "string" ? Number.parseInt(rawTotal, 10) : rawTotal;
|
|
310
|
+
|
|
311
|
+
const notifications: NotificationMessage[] = dataRows.map((row: DbNotificationRow) => {
|
|
312
|
+
const sentAt = row.sent_at as Date | null;
|
|
313
|
+
const error = row.error as string | null;
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
id: row.id,
|
|
317
|
+
type: row.type,
|
|
318
|
+
recipient: row.recipient,
|
|
319
|
+
subject: row.subject,
|
|
320
|
+
content: row.content,
|
|
321
|
+
sentAt,
|
|
322
|
+
error,
|
|
323
|
+
createdAt: row.created_at,
|
|
324
|
+
sentBy: row.sent_by,
|
|
325
|
+
sentByName: row.sent_by_name,
|
|
326
|
+
status: determineStatus(sentAt, error),
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return { notifications, total };
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Count current notification statuses for recent entries.
|
|
335
|
+
*/
|
|
336
|
+
export const getStatusSummary = async (options?: {
|
|
337
|
+
sentBy?: string;
|
|
338
|
+
isAdmin?: boolean;
|
|
339
|
+
days?: number;
|
|
340
|
+
}): Promise<NotificationStatusSummary> => {
|
|
341
|
+
const { sentBy, isAdmin, days = 7 } = options ?? {};
|
|
342
|
+
const windowDays = Math.max(1, Math.floor(days));
|
|
343
|
+
const summary = emptyStatusSummary();
|
|
344
|
+
|
|
345
|
+
let rows: Array<{ status: NotificationStatus; count: number | string }> = [];
|
|
346
|
+
if (isAdmin) {
|
|
347
|
+
rows = await sql`
|
|
348
|
+
SELECT
|
|
349
|
+
CASE WHEN sent_at IS NOT NULL THEN 'sent' WHEN error IS NOT NULL THEN 'error' ELSE 'pending' END as status,
|
|
350
|
+
COUNT(*)::int as count
|
|
351
|
+
FROM notifications.messages
|
|
352
|
+
WHERE created_at >= now() - (${windowDays}::int * interval '1 day')
|
|
353
|
+
GROUP BY status
|
|
354
|
+
`;
|
|
355
|
+
} else if (sentBy) {
|
|
356
|
+
rows = await sql`
|
|
357
|
+
SELECT
|
|
358
|
+
CASE WHEN sent_at IS NOT NULL THEN 'sent' WHEN error IS NOT NULL THEN 'error' ELSE 'pending' END as status,
|
|
359
|
+
COUNT(*)::int as count
|
|
360
|
+
FROM notifications.messages
|
|
361
|
+
WHERE sent_by = ${sentBy} AND created_at >= now() - (${windowDays}::int * interval '1 day')
|
|
362
|
+
GROUP BY status
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const row of rows) {
|
|
367
|
+
const count = typeof row.count === "string" ? Number.parseInt(row.count, 10) : row.count;
|
|
368
|
+
summary[row.status] = Number.isFinite(count) ? count : 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return summary;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Count statuses across all entries matching a search term in the current access scope.
|
|
376
|
+
*/
|
|
377
|
+
export const getSearchSummary = async (options: {
|
|
378
|
+
search: string;
|
|
379
|
+
sentBy?: string;
|
|
380
|
+
isAdmin?: boolean;
|
|
381
|
+
}): Promise<NotificationSearchSummary> => {
|
|
382
|
+
const { sentBy, isAdmin } = options;
|
|
383
|
+
const search = options.search.trim();
|
|
384
|
+
if (!search) return emptySearchSummary();
|
|
385
|
+
|
|
386
|
+
const searchPattern = `%${escapeLikePattern(search)}%`;
|
|
387
|
+
let rows: Array<{
|
|
388
|
+
total: number | string;
|
|
389
|
+
sent: number | string;
|
|
390
|
+
pending: number | string;
|
|
391
|
+
error: number | string;
|
|
392
|
+
system: number | string;
|
|
393
|
+
latest_created_at: Date | null;
|
|
394
|
+
}> = [];
|
|
395
|
+
|
|
396
|
+
if (isAdmin) {
|
|
397
|
+
rows = await sql`
|
|
398
|
+
SELECT
|
|
399
|
+
COUNT(*)::int AS total,
|
|
400
|
+
COUNT(*) FILTER (WHERE sent_at IS NOT NULL)::int AS sent,
|
|
401
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NULL)::int AS pending,
|
|
402
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NOT NULL)::int AS error,
|
|
403
|
+
COUNT(*) FILTER (WHERE sent_by IS NULL)::int AS system,
|
|
404
|
+
MAX(created_at) AS latest_created_at
|
|
405
|
+
FROM notifications.messages
|
|
406
|
+
WHERE subject ILIKE ${searchPattern} ESCAPE '\'
|
|
407
|
+
OR content ILIKE ${searchPattern} ESCAPE '\'
|
|
408
|
+
OR recipient ILIKE ${searchPattern} ESCAPE '\'
|
|
409
|
+
`;
|
|
410
|
+
} else if (sentBy) {
|
|
411
|
+
rows = await sql`
|
|
412
|
+
SELECT
|
|
413
|
+
COUNT(*)::int AS total,
|
|
414
|
+
COUNT(*) FILTER (WHERE sent_at IS NOT NULL)::int AS sent,
|
|
415
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NULL)::int AS pending,
|
|
416
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NOT NULL)::int AS error,
|
|
417
|
+
COUNT(*) FILTER (WHERE sent_by IS NULL)::int AS system,
|
|
418
|
+
MAX(created_at) AS latest_created_at
|
|
419
|
+
FROM notifications.messages
|
|
420
|
+
WHERE sent_by = ${sentBy}
|
|
421
|
+
AND (
|
|
422
|
+
subject ILIKE ${searchPattern} ESCAPE '\'
|
|
423
|
+
OR content ILIKE ${searchPattern} ESCAPE '\'
|
|
424
|
+
OR recipient ILIKE ${searchPattern} ESCAPE '\'
|
|
425
|
+
)
|
|
426
|
+
`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const row = rows[0];
|
|
430
|
+
if (!row) return emptySearchSummary();
|
|
431
|
+
|
|
432
|
+
const toNumber = (value: number | string) => {
|
|
433
|
+
const parsed = typeof value === "string" ? Number.parseInt(value, 10) : value;
|
|
434
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
total: toNumber(row.total),
|
|
439
|
+
sent: toNumber(row.sent),
|
|
440
|
+
pending: toNumber(row.pending),
|
|
441
|
+
error: toNumber(row.error),
|
|
442
|
+
system: toNumber(row.system),
|
|
443
|
+
latestCreatedAt: row.latest_created_at,
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get a single notification by ID.
|
|
449
|
+
*/
|
|
450
|
+
export const getById = async (id: string): Promise<NotificationMessage | null> => {
|
|
451
|
+
const rows = await sql`
|
|
452
|
+
SELECT
|
|
453
|
+
m.id, m.type, m.recipient, m.subject, m.content,
|
|
454
|
+
m.sent_at, m.error, m.created_at, m.sent_by,
|
|
455
|
+
u.display_name as sent_by_name
|
|
456
|
+
FROM notifications.messages m
|
|
457
|
+
LEFT JOIN auth.users u ON m.sent_by = u.id
|
|
458
|
+
WHERE m.id = ${id}
|
|
459
|
+
`;
|
|
460
|
+
|
|
461
|
+
if (rows.length === 0) return null;
|
|
462
|
+
|
|
463
|
+
const row = rows[0]!;
|
|
464
|
+
const sentAt = row.sent_at as Date | null;
|
|
465
|
+
const error = row.error as string | null;
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
id: row.id as string,
|
|
469
|
+
type: row.type as NotificationType,
|
|
470
|
+
recipient: row.recipient as string,
|
|
471
|
+
subject: row.subject as string,
|
|
472
|
+
content: row.content as string,
|
|
473
|
+
sentAt,
|
|
474
|
+
error,
|
|
475
|
+
createdAt: row.created_at as Date,
|
|
476
|
+
sentBy: row.sent_by as string | null,
|
|
477
|
+
sentByName: row.sent_by_name as string | null,
|
|
478
|
+
status: determineStatus(sentAt, error),
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Resend a notification (retry delivery).
|
|
484
|
+
*/
|
|
485
|
+
export const resend = async (id: string): Promise<{ ok: true } | { ok: false; code: "not_found" | "delivery_failed"; error: string }> => {
|
|
486
|
+
const notification = await getById(id);
|
|
487
|
+
if (!notification) {
|
|
488
|
+
return { ok: false, code: "not_found", error: "Notification not found" };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
if (notification.type === "email") {
|
|
493
|
+
await sendEmail(notification.recipient, notification.subject, {
|
|
494
|
+
rawHtml: notification.content,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
await sql`UPDATE notifications.messages SET sent_at = now(), error = NULL WHERE id = ${id}`;
|
|
498
|
+
return { ok: true };
|
|
499
|
+
} catch (e) {
|
|
500
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
501
|
+
await sql`UPDATE notifications.messages SET error = ${error} WHERE id = ${id}`;
|
|
502
|
+
return { ok: false, code: "delivery_failed", error };
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Update a notification.
|
|
508
|
+
* Non-admins can only edit pending/error notifications.
|
|
509
|
+
* Admins can edit any notification (including sent ones).
|
|
510
|
+
*/
|
|
511
|
+
export const update = async (
|
|
512
|
+
id: string,
|
|
513
|
+
data: { subject?: string; content?: string; recipient?: string },
|
|
514
|
+
options?: { isAdmin?: boolean },
|
|
515
|
+
): Promise<{ ok: true } | { ok: false; code: "not_found" | "invalid_state"; error: string }> => {
|
|
516
|
+
const notification = await getById(id);
|
|
517
|
+
if (!notification) {
|
|
518
|
+
return { ok: false, code: "not_found", error: "Notification not found" };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Non-admins cannot edit sent notifications
|
|
522
|
+
if (!options?.isAdmin && notification.status === "sent") {
|
|
523
|
+
return { ok: false, code: "invalid_state", error: "Cannot edit a sent notification" };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (data.subject === undefined && data.content === undefined && data.recipient === undefined) {
|
|
527
|
+
return { ok: true };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Clear error when editing (only for non-sent notifications)
|
|
531
|
+
const clearError = notification.status !== "sent";
|
|
532
|
+
|
|
533
|
+
await sql`
|
|
534
|
+
UPDATE notifications.messages
|
|
535
|
+
SET
|
|
536
|
+
subject = COALESCE(${data.subject ?? null}, subject),
|
|
537
|
+
content = COALESCE(${data.content ?? null}, content),
|
|
538
|
+
recipient = COALESCE(${data.recipient ?? null}, recipient),
|
|
539
|
+
error = CASE WHEN ${clearError} THEN NULL ELSE error END
|
|
540
|
+
WHERE id = ${id}
|
|
541
|
+
`;
|
|
542
|
+
|
|
543
|
+
return { ok: true };
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Get count of pending system notifications (sent_by IS NULL).
|
|
548
|
+
*/
|
|
549
|
+
export const getPendingSystemCount = async (): Promise<number> => {
|
|
550
|
+
const rows = await sql`
|
|
551
|
+
SELECT COUNT(*)::int as count FROM notifications.messages
|
|
552
|
+
WHERE sent_at IS NULL AND error IS NULL AND sent_by IS NULL
|
|
553
|
+
`;
|
|
554
|
+
return rows[0]?.count ?? 0;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Send all pending system notifications (sent_by IS NULL).
|
|
559
|
+
* Returns the count of successfully sent and failed notifications.
|
|
560
|
+
*/
|
|
561
|
+
export const sendAllPendingSystem = async (): Promise<{
|
|
562
|
+
sent: number;
|
|
563
|
+
failed: number;
|
|
564
|
+
errors: { id: string; recipient: string; error: string }[];
|
|
565
|
+
}> => {
|
|
566
|
+
// Get all pending system notifications
|
|
567
|
+
const rows = await sql`
|
|
568
|
+
SELECT id, type, recipient, subject, content
|
|
569
|
+
FROM notifications.messages
|
|
570
|
+
WHERE sent_at IS NULL AND error IS NULL AND sent_by IS NULL
|
|
571
|
+
ORDER BY created_at ASC
|
|
572
|
+
`;
|
|
573
|
+
|
|
574
|
+
let sent = 0;
|
|
575
|
+
let failed = 0;
|
|
576
|
+
const errors: { id: string; recipient: string; error: string }[] = [];
|
|
577
|
+
|
|
578
|
+
for (const row of rows) {
|
|
579
|
+
const id = row.id as string;
|
|
580
|
+
const type = row.type as NotificationType;
|
|
581
|
+
const recipient = row.recipient as string;
|
|
582
|
+
const subject = row.subject as string;
|
|
583
|
+
const content = row.content as string;
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
if (type === "email") {
|
|
587
|
+
await sendEmail(recipient, subject, { rawHtml: content });
|
|
588
|
+
}
|
|
589
|
+
await sql`UPDATE notifications.messages SET sent_at = now(), error = NULL WHERE id = ${id}`;
|
|
590
|
+
sent++;
|
|
591
|
+
} catch (e) {
|
|
592
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
593
|
+
await sql`UPDATE notifications.messages SET error = ${error} WHERE id = ${id}`;
|
|
594
|
+
failed++;
|
|
595
|
+
errors.push({ id, recipient, error });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return { sent, failed, errors };
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
export const notifications = {
|
|
603
|
+
send,
|
|
604
|
+
sendToUser,
|
|
605
|
+
list,
|
|
606
|
+
getById,
|
|
607
|
+
resend,
|
|
608
|
+
update,
|
|
609
|
+
getPendingSystemCount,
|
|
610
|
+
sendAllPendingSystem,
|
|
611
|
+
getStatusSummary,
|
|
612
|
+
getSearchSummary,
|
|
613
|
+
live: notificationLive,
|
|
614
|
+
observability: notificationObservability,
|
|
615
|
+
user: userNotifications,
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
export type {
|
|
619
|
+
NotificationBatch,
|
|
620
|
+
NotificationBatchPreview,
|
|
621
|
+
NotificationBatchRecipient,
|
|
622
|
+
NotificationBatchRecipientStatus,
|
|
623
|
+
NotificationBatchSelection,
|
|
624
|
+
NotificationBatchStatus,
|
|
625
|
+
} from "./batches";
|
|
626
|
+
export { notificationBatches } from "./batches";
|
|
627
|
+
export {
|
|
628
|
+
type NotificationDefinitionObservabilityItem,
|
|
629
|
+
type NotificationDeliveryObservabilityItem,
|
|
630
|
+
type NotificationDeliveryTimeseriesPoint,
|
|
631
|
+
notificationObservability,
|
|
632
|
+
} from "./observability";
|
|
633
|
+
export { userNotifications } from "./user";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { lazySync } from "../../_internal/process-sync";
|
|
2
|
+
import type { NotificationLiveEvent } from "../../contracts/notification-live";
|
|
3
|
+
import type { NotificationPresentation } from "../../contracts/notification-types";
|
|
4
|
+
import { logger } from "../logging";
|
|
5
|
+
import { latestTopicCursor } from "../topic-cursor";
|
|
6
|
+
|
|
7
|
+
const LIVE_TOPIC_ID = "cloud:notifications:live";
|
|
8
|
+
const LIVE_RETENTION_MS = 60 * 60 * 1_000;
|
|
9
|
+
|
|
10
|
+
const log = logger("notifications:live");
|
|
11
|
+
|
|
12
|
+
const liveTopic = lazySync((sync) =>
|
|
13
|
+
sync.topic<NotificationLiveEvent>({
|
|
14
|
+
id: LIVE_TOPIC_ID,
|
|
15
|
+
owner: "core",
|
|
16
|
+
// One hour of foreground alerts fits the shared event-log storage budget.
|
|
17
|
+
retention: { maxAgeMs: LIVE_RETENTION_MS, maxBytes: 64 * 1024 * 1024 },
|
|
18
|
+
maxPayloadBytes: 9_000,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const publish = async (input: { userId: string; eventId: string; presentation: NotificationPresentation }): Promise<void> => {
|
|
23
|
+
const event: NotificationLiveEvent = {
|
|
24
|
+
type: "cloud-notification",
|
|
25
|
+
eventId: input.eventId,
|
|
26
|
+
title: input.presentation.title,
|
|
27
|
+
...(input.presentation.targetHref ? { targetHref: input.presentation.targetHref } : {}),
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
await liveTopic().publish({
|
|
31
|
+
tenantId: input.userId,
|
|
32
|
+
orderingKey: input.userId,
|
|
33
|
+
idempotencyKey: `event:${input.eventId}`,
|
|
34
|
+
data: event,
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
log.warn("Failed to publish foreground notification", {
|
|
38
|
+
eventId: input.eventId,
|
|
39
|
+
error: error instanceof Error ? error.message : "Foreground notification publish failed",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const notificationLive = {
|
|
45
|
+
publish,
|
|
46
|
+
latestCursor: (userId: string): Promise<string> => latestTopicCursor({ topic: liveTopic(), resourceId: LIVE_TOPIC_ID, tenantId: userId }),
|
|
47
|
+
emptyCursor: (): string => liveTopic().cursorAt(0),
|
|
48
|
+
events: (input: { userId: string; after?: string; signal?: AbortSignal }) =>
|
|
49
|
+
liveTopic().hub({ tenantId: input.userId }).subscribe({ after: input.after, signal: input.signal }),
|
|
50
|
+
} as const;
|