@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,421 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import type { output, ZodType } from "zod";
|
|
3
|
+
import type {
|
|
4
|
+
BoundNotificationDefinition,
|
|
5
|
+
EmailNotificationPresentation,
|
|
6
|
+
NotificationChannelId,
|
|
7
|
+
NotificationPresentation,
|
|
8
|
+
NotificationRecipientKind,
|
|
9
|
+
NotificationSendInput,
|
|
10
|
+
} from "../../contracts/notification-types";
|
|
11
|
+
import { validateNotificationTargetHref } from "../../contracts/notification-types";
|
|
12
|
+
import { normalizeLocale } from "../../shared/locale";
|
|
13
|
+
import { logger } from "../logging";
|
|
14
|
+
import { encryptSecret } from "../secrets";
|
|
15
|
+
import { ensureNotificationDefinition } from "./catalog";
|
|
16
|
+
import { getNotificationChannel, type NotificationDestination, type ResolvedNotificationRecipient } from "./channels";
|
|
17
|
+
import { processNotificationDelivery } from "./dispatcher";
|
|
18
|
+
import { notificationLive } from "./live";
|
|
19
|
+
import { enqueueNotificationDeliveries, enqueueNotificationDelivery } from "./runtime";
|
|
20
|
+
|
|
21
|
+
export type TypedNotificationDeliveryStatus = "deferred" | "pending" | "sending" | "delivered" | "suppressed" | "failed";
|
|
22
|
+
|
|
23
|
+
export type TypedNotificationSendResult = {
|
|
24
|
+
id: string;
|
|
25
|
+
created: boolean;
|
|
26
|
+
status: "queued" | "delivered" | "suppressed" | "error";
|
|
27
|
+
deliveries: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
channel: string;
|
|
30
|
+
required: boolean;
|
|
31
|
+
status: TypedNotificationDeliveryStatus;
|
|
32
|
+
errorCode: string | null;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type PreparedDelivery = {
|
|
37
|
+
channel: string;
|
|
38
|
+
endpointId: string | null;
|
|
39
|
+
destinationKey: string;
|
|
40
|
+
destinationLabel: string;
|
|
41
|
+
payloadEncrypted: string | null;
|
|
42
|
+
required: boolean;
|
|
43
|
+
routePriority: number | null;
|
|
44
|
+
status: "deferred" | "pending" | "suppressed" | "failed";
|
|
45
|
+
errorCode: string | null;
|
|
46
|
+
errorMessage: string | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type DeliveryListRow = {
|
|
50
|
+
id: string;
|
|
51
|
+
channel: string;
|
|
52
|
+
required: boolean;
|
|
53
|
+
status: TypedNotificationDeliveryStatus;
|
|
54
|
+
error_code: string | null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const unique = <T>(values: readonly T[]): T[] => [...new Set(values)];
|
|
58
|
+
const log = logger("notifications:platform");
|
|
59
|
+
|
|
60
|
+
const preparationFailure = (input: { channel: string; required: boolean; routePriority: number | null }): PreparedDelivery => ({
|
|
61
|
+
channel: input.channel,
|
|
62
|
+
endpointId: null,
|
|
63
|
+
destinationKey: "preparation-failed",
|
|
64
|
+
destinationLabel: input.channel,
|
|
65
|
+
payloadEncrypted: null,
|
|
66
|
+
required: input.required,
|
|
67
|
+
routePriority: input.routePriority,
|
|
68
|
+
status: "failed",
|
|
69
|
+
errorCode: "preparation_failed",
|
|
70
|
+
errorMessage: "Notification delivery could not be prepared.",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const validatePresentation = (presentation: NotificationPresentation): NotificationPresentation => {
|
|
74
|
+
const title = presentation.title.trim();
|
|
75
|
+
const body = presentation.body?.trim();
|
|
76
|
+
if (!title) throw new Error("Notification title is required");
|
|
77
|
+
if (title.length > 200) throw new Error("Notification title must not exceed 200 characters");
|
|
78
|
+
if (body && body.length > 4_000) throw new Error("Notification body must not exceed 4000 characters");
|
|
79
|
+
const targetHref = presentation.targetHref ? validateNotificationTargetHref(presentation.targetHref) : undefined;
|
|
80
|
+
return { title, ...(body ? { body } : {}), ...(targetHref ? { targetHref } : {}) };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const resolveRecipient = async (
|
|
84
|
+
recipient: { userId: string } | { email: string },
|
|
85
|
+
): Promise<{
|
|
86
|
+
recipient: ResolvedNotificationRecipient;
|
|
87
|
+
recipientKey: string;
|
|
88
|
+
}> => {
|
|
89
|
+
if ("userId" in recipient) {
|
|
90
|
+
const rows = await sql<{ id: string; mail: string | null }[]>`
|
|
91
|
+
SELECT id, btrim(mail) AS mail FROM auth.users WHERE id = ${recipient.userId}::uuid LIMIT 1
|
|
92
|
+
`;
|
|
93
|
+
const user = rows[0];
|
|
94
|
+
if (!user) throw new Error("Notification recipient user was not found");
|
|
95
|
+
return {
|
|
96
|
+
recipient: { userId: user.id, email: user.mail?.trim().toLowerCase() || null },
|
|
97
|
+
recipientKey: `user:${user.id}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const email = recipient.email.trim().toLowerCase();
|
|
102
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw new Error("Notification recipient email is invalid");
|
|
103
|
+
return { recipient: { userId: null, email }, recipientKey: `email:${email}` };
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const preferredChannels = async (
|
|
107
|
+
definitionId: string,
|
|
108
|
+
recipient: ResolvedNotificationRecipient,
|
|
109
|
+
recommended: readonly NotificationChannelId[],
|
|
110
|
+
): Promise<{ channels: string[]; customized: boolean }> => {
|
|
111
|
+
if (!recipient.userId) return { channels: [], customized: false };
|
|
112
|
+
const rows = await sql<{ channels: string[] }[]>`
|
|
113
|
+
SELECT channels FROM notifications.preferences
|
|
114
|
+
WHERE user_id = ${recipient.userId}::uuid AND definition_id = ${definitionId}
|
|
115
|
+
LIMIT 1
|
|
116
|
+
`;
|
|
117
|
+
return rows[0] ? { channels: unique(rows[0].channels), customized: true } : { channels: unique(recommended), customized: false };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const prepareChannel = async (input: {
|
|
121
|
+
channel: string;
|
|
122
|
+
recipient: ResolvedNotificationRecipient;
|
|
123
|
+
presentation: NotificationPresentation;
|
|
124
|
+
emailPresentation?: () => Promise<EmailNotificationPresentation | undefined>;
|
|
125
|
+
required: boolean;
|
|
126
|
+
routePriority: number | null;
|
|
127
|
+
event: { id: string; definitionId: string };
|
|
128
|
+
}): Promise<PreparedDelivery[]> => {
|
|
129
|
+
const driver = getNotificationChannel(input.channel);
|
|
130
|
+
if (!driver) {
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
channel: input.channel,
|
|
134
|
+
endpointId: null,
|
|
135
|
+
destinationKey: "channel-unavailable",
|
|
136
|
+
destinationLabel: input.channel,
|
|
137
|
+
payloadEncrypted: null,
|
|
138
|
+
required: input.required,
|
|
139
|
+
routePriority: input.routePriority,
|
|
140
|
+
status: "suppressed",
|
|
141
|
+
errorCode: "channel_unavailable",
|
|
142
|
+
errorMessage: `Notification channel "${input.channel}" is unavailable.`,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let destinations: NotificationDestination[];
|
|
148
|
+
try {
|
|
149
|
+
destinations = await driver.resolveDestinations(input.recipient);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log.error("Notification destination resolution failed", {
|
|
152
|
+
channel: input.channel,
|
|
153
|
+
error: error instanceof Error ? error.message : "Notification destination resolution failed",
|
|
154
|
+
});
|
|
155
|
+
return [preparationFailure(input)];
|
|
156
|
+
}
|
|
157
|
+
if (destinations.length === 0) {
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
channel: input.channel,
|
|
161
|
+
endpointId: null,
|
|
162
|
+
destinationKey: "no-endpoint",
|
|
163
|
+
destinationLabel: input.channel,
|
|
164
|
+
payloadEncrypted: null,
|
|
165
|
+
required: input.required,
|
|
166
|
+
routePriority: input.routePriority,
|
|
167
|
+
status: "suppressed",
|
|
168
|
+
errorCode: "no_endpoint",
|
|
169
|
+
errorMessage: `No configured ${input.channel} destination is available.`,
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const emailPresentation = input.channel === "email" ? await input.emailPresentation?.() : undefined;
|
|
176
|
+
return await Promise.all(
|
|
177
|
+
destinations.map(async (destination) => ({
|
|
178
|
+
channel: input.channel,
|
|
179
|
+
endpointId: destination.endpointId ?? null,
|
|
180
|
+
destinationKey: destination.key,
|
|
181
|
+
destinationLabel: destination.label,
|
|
182
|
+
payloadEncrypted: await encryptSecret(
|
|
183
|
+
driver.createPayload({ presentation: input.presentation, email: emailPresentation, destination, event: input.event }),
|
|
184
|
+
),
|
|
185
|
+
required: input.required,
|
|
186
|
+
routePriority: input.routePriority,
|
|
187
|
+
status: "deferred" as const,
|
|
188
|
+
errorCode: null,
|
|
189
|
+
errorMessage: null,
|
|
190
|
+
})),
|
|
191
|
+
);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
log.error("Notification payload preparation failed", {
|
|
194
|
+
channel: input.channel,
|
|
195
|
+
error: error instanceof Error ? error.message : "Notification payload preparation failed",
|
|
196
|
+
});
|
|
197
|
+
return [preparationFailure(input)];
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const listDeliveries = async (eventId: string): Promise<TypedNotificationSendResult["deliveries"]> => {
|
|
202
|
+
const rows = await sql<DeliveryListRow[]>`
|
|
203
|
+
SELECT id, channel, required, status, error_code
|
|
204
|
+
FROM notifications.deliveries
|
|
205
|
+
WHERE event_id = ${eventId}::uuid
|
|
206
|
+
ORDER BY required DESC, route_priority NULLS FIRST, created_at, id
|
|
207
|
+
`;
|
|
208
|
+
return rows.map((row) => ({
|
|
209
|
+
id: row.id,
|
|
210
|
+
channel: row.channel,
|
|
211
|
+
required: row.required,
|
|
212
|
+
status: row.status,
|
|
213
|
+
errorCode: row.error_code,
|
|
214
|
+
}));
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const summarize = (id: string, created: boolean, deliveries: TypedNotificationSendResult["deliveries"]): TypedNotificationSendResult => {
|
|
218
|
+
const requiredFailures = deliveries.some(
|
|
219
|
+
(delivery) => delivery.required && (delivery.status === "failed" || delivery.status === "suppressed" || delivery.errorCode !== null),
|
|
220
|
+
);
|
|
221
|
+
const delivered = deliveries.some((delivery) => delivery.status === "delivered");
|
|
222
|
+
const queued = deliveries.some((delivery) => delivery.status === "pending" || delivery.status === "sending");
|
|
223
|
+
return {
|
|
224
|
+
id,
|
|
225
|
+
created,
|
|
226
|
+
status: requiredFailures ? "error" : delivered ? "delivered" : queued ? "queued" : "suppressed",
|
|
227
|
+
deliveries,
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const sendTypedNotification = async <
|
|
232
|
+
AppId extends string,
|
|
233
|
+
Key extends string,
|
|
234
|
+
R extends NotificationRecipientKind,
|
|
235
|
+
S extends ZodType,
|
|
236
|
+
>(
|
|
237
|
+
definition: BoundNotificationDefinition<AppId, Key, R, S>,
|
|
238
|
+
input: NotificationSendInput<BoundNotificationDefinition<AppId, Key, R, S>>,
|
|
239
|
+
): Promise<TypedNotificationSendResult> => {
|
|
240
|
+
const idempotencyKey = input.idempotencyKey.trim();
|
|
241
|
+
if (!idempotencyKey || idempotencyKey.length > 300) throw new Error("Notification idempotencyKey must contain 1 to 300 characters");
|
|
242
|
+
|
|
243
|
+
const data: output<S> = definition.data.parse(input.data);
|
|
244
|
+
const renderContext = { locale: normalizeLocale(input.locale) };
|
|
245
|
+
const presentation = validatePresentation(await definition.render(data, renderContext));
|
|
246
|
+
const resolved = await resolveRecipient(input.recipient);
|
|
247
|
+
const candidateEventId = crypto.randomUUID();
|
|
248
|
+
await ensureNotificationDefinition(definition);
|
|
249
|
+
|
|
250
|
+
const requiredChannels = unique([...(definition.delivery?.required ?? [])]);
|
|
251
|
+
const requiredChannelSet = new Set<string>(requiredChannels);
|
|
252
|
+
const preference = await preferredChannels(definition.id, resolved.recipient, definition.delivery?.recommended ?? []);
|
|
253
|
+
const preferred = preference.channels.filter((channel) => !requiredChannelSet.has(channel));
|
|
254
|
+
|
|
255
|
+
const identity = await sql.begin(async (tx) => {
|
|
256
|
+
const eventRows = await tx<{ id: string }[]>`
|
|
257
|
+
INSERT INTO notifications.events (
|
|
258
|
+
id, definition_id, recipient_user_id, recipient_email, recipient_key,
|
|
259
|
+
idempotency_key, title, target_href, sent_by
|
|
260
|
+
) VALUES (
|
|
261
|
+
${candidateEventId}::uuid, ${definition.id}, ${resolved.recipient.userId}, ${resolved.recipient.userId ? null : resolved.recipient.email},
|
|
262
|
+
${resolved.recipientKey}, ${idempotencyKey}, ${presentation.title}, ${presentation.targetHref ?? null}, ${input.sentBy ?? null}
|
|
263
|
+
)
|
|
264
|
+
ON CONFLICT (definition_id, recipient_key, idempotency_key) DO NOTHING
|
|
265
|
+
RETURNING id
|
|
266
|
+
`;
|
|
267
|
+
const createdId = eventRows[0]?.id;
|
|
268
|
+
if (createdId) return { id: createdId, created: true, needsPreparation: true };
|
|
269
|
+
|
|
270
|
+
const existing = await tx<{ id: string }[]>`
|
|
271
|
+
SELECT id FROM notifications.events
|
|
272
|
+
WHERE definition_id = ${definition.id} AND recipient_key = ${resolved.recipientKey}
|
|
273
|
+
AND idempotency_key = ${idempotencyKey}
|
|
274
|
+
FOR UPDATE
|
|
275
|
+
LIMIT 1
|
|
276
|
+
`;
|
|
277
|
+
const eventId = existing[0]?.id;
|
|
278
|
+
if (!eventId) throw new Error("Notification idempotency lookup failed");
|
|
279
|
+
const [state] = await tx<{ delivery_count: number; preparation_failure_count: number }[]>`
|
|
280
|
+
SELECT COUNT(*)::int AS delivery_count,
|
|
281
|
+
COUNT(*) FILTER (WHERE status = 'failed' AND error_code = 'preparation_failed')::int AS preparation_failure_count
|
|
282
|
+
FROM notifications.deliveries
|
|
283
|
+
WHERE event_id = ${eventId}::uuid
|
|
284
|
+
`;
|
|
285
|
+
return {
|
|
286
|
+
id: eventId,
|
|
287
|
+
created: false,
|
|
288
|
+
needsPreparation: Number(state?.delivery_count ?? 0) === 0 || Number(state?.preparation_failure_count ?? 0) > 0,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!identity.needsPreparation) {
|
|
293
|
+
return summarize(identity.id, identity.created, await listDeliveries(identity.id));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const event = { id: identity.id, definitionId: definition.id };
|
|
297
|
+
let emailPresentationPromise: Promise<EmailNotificationPresentation | undefined> | undefined;
|
|
298
|
+
const emailPresentation = (): Promise<EmailNotificationPresentation | undefined> => {
|
|
299
|
+
emailPresentationPromise ??= definition.email ? Promise.resolve(definition.email(data, renderContext)) : Promise.resolve(undefined);
|
|
300
|
+
return emailPresentationPromise;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const requiredPlans = (
|
|
304
|
+
await Promise.all(
|
|
305
|
+
requiredChannels.map((channel) =>
|
|
306
|
+
prepareChannel({
|
|
307
|
+
channel,
|
|
308
|
+
recipient: resolved.recipient,
|
|
309
|
+
presentation,
|
|
310
|
+
emailPresentation,
|
|
311
|
+
required: true,
|
|
312
|
+
routePriority: null,
|
|
313
|
+
event,
|
|
314
|
+
}),
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
).flat();
|
|
318
|
+
const preferredPlans = (
|
|
319
|
+
await Promise.all(
|
|
320
|
+
preferred.map((channel, routePriority) =>
|
|
321
|
+
prepareChannel({
|
|
322
|
+
channel,
|
|
323
|
+
recipient: resolved.recipient,
|
|
324
|
+
presentation,
|
|
325
|
+
emailPresentation,
|
|
326
|
+
required: false,
|
|
327
|
+
routePriority,
|
|
328
|
+
event,
|
|
329
|
+
}),
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
).flat();
|
|
333
|
+
|
|
334
|
+
if (requiredPlans.length === 0 && preferredPlans.length === 0) {
|
|
335
|
+
preferredPlans.push({
|
|
336
|
+
channel: "none",
|
|
337
|
+
endpointId: null,
|
|
338
|
+
destinationKey: "no-preferred-channel",
|
|
339
|
+
destinationLabel: "No delivery channel",
|
|
340
|
+
payloadEncrypted: null,
|
|
341
|
+
required: false,
|
|
342
|
+
routePriority: null,
|
|
343
|
+
status: "suppressed",
|
|
344
|
+
errorCode: preference.customized ? "disabled_by_user" : "no_preferred_channel",
|
|
345
|
+
errorMessage: preference.customized
|
|
346
|
+
? "The user disabled delivery for this notification."
|
|
347
|
+
: "No preferred delivery channel is configured.",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const firstPreferredPriority = preferredPlans.find((delivery) => delivery.status === "deferred")?.routePriority;
|
|
352
|
+
const plans = [
|
|
353
|
+
...requiredPlans.map((delivery) => ({ ...delivery, status: delivery.status === "deferred" ? ("pending" as const) : delivery.status })),
|
|
354
|
+
...preferredPlans.map((delivery) => ({
|
|
355
|
+
...delivery,
|
|
356
|
+
status: delivery.status === "deferred" && delivery.routePriority === firstPreferredPriority ? ("pending" as const) : delivery.status,
|
|
357
|
+
})),
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
const persisted = await sql.begin(async (tx) => {
|
|
361
|
+
await tx`SELECT id FROM notifications.events WHERE id = ${identity.id}::uuid FOR UPDATE`;
|
|
362
|
+
const [state] = await tx<{ delivery_count: number; preparation_failure_count: number }[]>`
|
|
363
|
+
SELECT COUNT(*)::int AS delivery_count,
|
|
364
|
+
COUNT(*) FILTER (WHERE status = 'failed' AND error_code = 'preparation_failed')::int AS preparation_failure_count
|
|
365
|
+
FROM notifications.deliveries
|
|
366
|
+
WHERE event_id = ${identity.id}::uuid
|
|
367
|
+
`;
|
|
368
|
+
const shouldPrepare = Number(state?.delivery_count ?? 0) === 0 || Number(state?.preparation_failure_count ?? 0) > 0;
|
|
369
|
+
if (!shouldPrepare) {
|
|
370
|
+
return { prepared: false, pendingIds: [] as string[], requiredIds: [] as string[] };
|
|
371
|
+
}
|
|
372
|
+
await tx`
|
|
373
|
+
DELETE FROM notifications.deliveries
|
|
374
|
+
WHERE event_id = ${identity.id}::uuid AND status = 'failed' AND error_code = 'preparation_failed'
|
|
375
|
+
`;
|
|
376
|
+
await tx`
|
|
377
|
+
UPDATE notifications.events
|
|
378
|
+
SET title = ${presentation.title}, target_href = ${presentation.targetHref ?? null}
|
|
379
|
+
WHERE id = ${identity.id}::uuid
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
const pendingIds: string[] = [];
|
|
383
|
+
const requiredIds: string[] = [];
|
|
384
|
+
for (const delivery of plans) {
|
|
385
|
+
const rows = await tx<{ id: string }[]>`
|
|
386
|
+
INSERT INTO notifications.deliveries (
|
|
387
|
+
event_id, channel, endpoint_id, destination_key, destination_label,
|
|
388
|
+
payload_encrypted, required, route_priority, status, next_attempt_at,
|
|
389
|
+
error_code, error_message
|
|
390
|
+
) VALUES (
|
|
391
|
+
${identity.id}::uuid, ${delivery.channel}, ${delivery.endpointId}::uuid, ${delivery.destinationKey},
|
|
392
|
+
${delivery.destinationLabel}, ${delivery.payloadEncrypted}, ${delivery.required}, ${delivery.routePriority},
|
|
393
|
+
${delivery.status}, CASE WHEN ${delivery.status} = 'pending' THEN now() ELSE NULL END,
|
|
394
|
+
${delivery.errorCode}, ${delivery.errorMessage}
|
|
395
|
+
)
|
|
396
|
+
ON CONFLICT (event_id, channel, destination_key) DO NOTHING
|
|
397
|
+
RETURNING id
|
|
398
|
+
`;
|
|
399
|
+
const deliveryId = rows[0]?.id;
|
|
400
|
+
if (deliveryId && delivery.status === "pending") {
|
|
401
|
+
if (delivery.required) requiredIds.push(deliveryId);
|
|
402
|
+
else pendingIds.push(deliveryId);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { prepared: true, pendingIds, requiredIds };
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (persisted.prepared) {
|
|
409
|
+
if (resolved.recipient.userId && (requiredChannelSet.has("browser") || preferred.includes("browser"))) {
|
|
410
|
+
await notificationLive.publish({ userId: resolved.recipient.userId, eventId: identity.id, presentation });
|
|
411
|
+
}
|
|
412
|
+
for (const deliveryId of persisted.requiredIds) {
|
|
413
|
+
const result = await processNotificationDelivery(deliveryId);
|
|
414
|
+
if (result.status === "retry") await enqueueNotificationDelivery(deliveryId, result.retryAfterMs);
|
|
415
|
+
if (result.activatedIds?.length) await enqueueNotificationDeliveries(result.activatedIds);
|
|
416
|
+
}
|
|
417
|
+
await enqueueNotificationDeliveries(persisted.pendingIds);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return summarize(identity.id, identity.created, await listDeliveries(identity.id));
|
|
421
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { DeliveryConfig, JobContext, Worker } from "@k2b/sync";
|
|
2
|
+
import { lazySync } from "../../_internal/process-sync";
|
|
3
|
+
import { logger, trace } from "../logging";
|
|
4
|
+
import { createRuntimeLifecycle, createRuntimeTaskTracker, stopRuntimeJobs } from "../runtime-lifecycle";
|
|
5
|
+
|
|
6
|
+
import { processNotificationDelivery, recoverNotificationDeliveries } from "./dispatcher";
|
|
7
|
+
|
|
8
|
+
type DeliveryMessage = { deliveryId: string };
|
|
9
|
+
|
|
10
|
+
const log = logger("notifications:delivery");
|
|
11
|
+
const ENQUEUE_DEDUPLICATION_WINDOW_MS = 60_000;
|
|
12
|
+
// Postgres owns delivery retries: provider failures are persisted by the
|
|
13
|
+
// dispatcher, and the recovery scan re-enqueues pending rows. A handler throw
|
|
14
|
+
// therefore means the database itself was unreachable; a few spaced attempts
|
|
15
|
+
// cover a blip, and a longer outage dead-letters once while recovery restores
|
|
16
|
+
// the row after the outage anyway.
|
|
17
|
+
const HANDLER_RETRY = { maxAttempts: 3, backoffMs: [5_000, 30_000] } satisfies Partial<DeliveryConfig>;
|
|
18
|
+
const deliveryJob = lazySync((sync) => {
|
|
19
|
+
const handle = sync.job<DeliveryMessage>({
|
|
20
|
+
id: "cloud-notification-deliveries",
|
|
21
|
+
owner: "core",
|
|
22
|
+
delivery: { ackWaitMs: 60_000, ...HANDLER_RETRY },
|
|
23
|
+
dedupeWindowMs: ENQUEUE_DEDUPLICATION_WINDOW_MS * 2,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return handle;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const enqueueNotificationDelivery = async (deliveryId: string, delayMs?: number): Promise<void> => {
|
|
30
|
+
const bucket = Math.floor(Date.now() / ENQUEUE_DEDUPLICATION_WINDOW_MS);
|
|
31
|
+
await deliveryJob().submit({
|
|
32
|
+
input: { deliveryId },
|
|
33
|
+
...(delayMs ? { delayMs } : {}),
|
|
34
|
+
// Recovery scans are intentionally repetitive. A time bucket suppresses
|
|
35
|
+
// duplicate wakeups without permanently blocking a later recovery attempt.
|
|
36
|
+
key: `delivery:${deliveryId}:${bucket}`,
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const enqueueNotificationDeliveries = async (deliveryIds: readonly string[]): Promise<void> => {
|
|
41
|
+
const bucket = Math.floor(Date.now() / ENQUEUE_DEDUPLICATION_WINDOW_MS);
|
|
42
|
+
await deliveryJob().submitMany(
|
|
43
|
+
deliveryIds.map((deliveryId) => ({
|
|
44
|
+
key: `delivery:${deliveryId}:${bucket}`,
|
|
45
|
+
input: { deliveryId },
|
|
46
|
+
})),
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleDelivery = async (context: JobContext<DeliveryMessage>): Promise<void> => {
|
|
51
|
+
const result = await trace.withSpan(
|
|
52
|
+
{
|
|
53
|
+
name: "Notification delivery",
|
|
54
|
+
source: "notifications:delivery",
|
|
55
|
+
appId: "core",
|
|
56
|
+
category: "job",
|
|
57
|
+
kind: "consumer",
|
|
58
|
+
spanKey: trace.syncSpanKey("job", "cloud-notification-deliveries", context.jobId),
|
|
59
|
+
attributes: { "cloud.notification.delivery_id": context.input.deliveryId },
|
|
60
|
+
},
|
|
61
|
+
() => processNotificationDelivery(context.input.deliveryId),
|
|
62
|
+
);
|
|
63
|
+
if (result.activatedIds?.length) await enqueueNotificationDeliveries(result.activatedIds);
|
|
64
|
+
if (result.status === "retry") context.resubmit({ delayMs: result.retryAfterMs });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type RuntimeOptions = { concurrency?: number; recoveryIntervalMs?: number };
|
|
68
|
+
let runtimeOptions: RuntimeOptions = {};
|
|
69
|
+
let worker: Worker | undefined;
|
|
70
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
71
|
+
let recovery: Promise<void> | undefined;
|
|
72
|
+
const recoveryTasks = createRuntimeTaskTracker();
|
|
73
|
+
|
|
74
|
+
const recover = (): Promise<void> => {
|
|
75
|
+
recovery ??= (
|
|
76
|
+
recoveryTasks.run(async () => {
|
|
77
|
+
try {
|
|
78
|
+
await enqueueNotificationDeliveries(await recoverNotificationDeliveries());
|
|
79
|
+
} catch (error) {
|
|
80
|
+
log.error("Delivery recovery failed", { error: error instanceof Error ? error.message : String(error) });
|
|
81
|
+
}
|
|
82
|
+
}) ?? Promise.resolve()
|
|
83
|
+
).finally(() => {
|
|
84
|
+
recovery = undefined;
|
|
85
|
+
});
|
|
86
|
+
return recovery;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const lifecycle = createRuntimeLifecycle({
|
|
90
|
+
start: async () => {
|
|
91
|
+
recoveryTasks.open();
|
|
92
|
+
const concurrency = Math.min(Math.max(Math.floor(runtimeOptions.concurrency ?? 4), 1), 16);
|
|
93
|
+
const recoveryIntervalMs = Math.max(runtimeOptions.recoveryIntervalMs ?? 30_000, 5_000);
|
|
94
|
+
worker = await deliveryJob().process(
|
|
95
|
+
{
|
|
96
|
+
concurrency,
|
|
97
|
+
onError: ({ context, error }) => {
|
|
98
|
+
log.error("Delivery worker failed", { deliveryId: context.input.deliveryId, attempt: context.attempt, error: error.message });
|
|
99
|
+
return { action: "retry" };
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
handleDelivery,
|
|
103
|
+
);
|
|
104
|
+
await recover();
|
|
105
|
+
timer = setInterval(() => void recover(), recoveryIntervalMs);
|
|
106
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
107
|
+
},
|
|
108
|
+
stop: async () => {
|
|
109
|
+
clearInterval(timer);
|
|
110
|
+
timer = undefined;
|
|
111
|
+
await stopRuntimeJobs(recoveryTasks, worker ? [worker] : []);
|
|
112
|
+
worker = undefined;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const startNotificationRuntime = async (input: RuntimeOptions = {}): Promise<void> => {
|
|
117
|
+
runtimeOptions = input;
|
|
118
|
+
await lifecycle.start();
|
|
119
|
+
};
|
|
120
|
+
export const stopNotificationRuntime = lifecycle.stop;
|