@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,252 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { sql } from "bun";
|
|
3
|
+
import webpush from "web-push";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { isSafeNotificationTargetHref } from "../../contracts/notification-types";
|
|
6
|
+
import { type BrowserPushSubscription, BrowserPushSubscriptionSchema } from "../../contracts/user-notifications";
|
|
7
|
+
import { decryptSecret, encryptSecret } from "../secrets";
|
|
8
|
+
import { coreSettings } from "../settings/api";
|
|
9
|
+
import { type NotificationDestination, registerNotificationChannel } from "./channels";
|
|
10
|
+
import { sendPinnedWebPush } from "./web-push-transport";
|
|
11
|
+
|
|
12
|
+
type EndpointRow = {
|
|
13
|
+
id: string;
|
|
14
|
+
endpoint_hash: string;
|
|
15
|
+
label: string;
|
|
16
|
+
secret_encrypted: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type BrowserDeliveryPayload = {
|
|
20
|
+
endpointId: string;
|
|
21
|
+
subscription: BrowserPushSubscription;
|
|
22
|
+
eventId: string;
|
|
23
|
+
title: string;
|
|
24
|
+
targetHref?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const BrowserDeliveryPayloadSchema = z.object({
|
|
28
|
+
endpointId: z.uuid(),
|
|
29
|
+
subscription: BrowserPushSubscriptionSchema,
|
|
30
|
+
eventId: z.uuid(),
|
|
31
|
+
title: z.string().min(1).max(200),
|
|
32
|
+
targetHref: z.string().max(4_000).refine(isSafeNotificationTargetHref).optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const BrowserDestinationContextSchema = z.object({
|
|
36
|
+
endpointId: z.uuid(),
|
|
37
|
+
subscription: BrowserPushSubscriptionSchema,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const endpointHash = (endpoint: string): string => createHash("sha256").update(endpoint).digest("hex");
|
|
41
|
+
|
|
42
|
+
let vapidConfigPromise: Promise<{ publicKey: string; privateKey: string }> | null = null;
|
|
43
|
+
let webPushConfigurationPromise: Promise<void> | null = null;
|
|
44
|
+
let unregisterDriver: (() => void) | null = null;
|
|
45
|
+
|
|
46
|
+
const ensureVapidConfig = (): Promise<{ publicKey: string; privateKey: string }> => {
|
|
47
|
+
if (vapidConfigPromise) return vapidConfigPromise;
|
|
48
|
+
vapidConfigPromise = sql
|
|
49
|
+
.begin(async (tx) => {
|
|
50
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended('notifications:web-push-vapid', 0))`;
|
|
51
|
+
let [publicKey, privateKey] = await Promise.all([
|
|
52
|
+
coreSettings.get<string>("notifications.web_push_public_key"),
|
|
53
|
+
coreSettings.get<string>("notifications.web_push_private_key"),
|
|
54
|
+
]);
|
|
55
|
+
if (!publicKey || !privateKey) {
|
|
56
|
+
const generated = webpush.generateVAPIDKeys();
|
|
57
|
+
publicKey = generated.publicKey;
|
|
58
|
+
privateKey = generated.privateKey;
|
|
59
|
+
await coreSettings.set("notifications.web_push_public_key", publicKey);
|
|
60
|
+
await coreSettings.set("notifications.web_push_private_key", privateKey);
|
|
61
|
+
}
|
|
62
|
+
return { publicKey, privateKey };
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
vapidConfigPromise = null;
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
return vapidConfigPromise;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const vapidSubject = async (): Promise<string> => {
|
|
72
|
+
const [configuredUrl, contactEmail] = await Promise.all([
|
|
73
|
+
coreSettings.get<string>("app.url"),
|
|
74
|
+
coreSettings.get<string>("app.contact_email"),
|
|
75
|
+
]);
|
|
76
|
+
const rawUrl = configuredUrl?.trim();
|
|
77
|
+
if (rawUrl) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = new URL(/^https?:\/\//.test(rawUrl) ? rawUrl : `https://${rawUrl}`);
|
|
80
|
+
const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
|
|
81
|
+
if (parsed.protocol === "https:" && !isLocalhost) return parsed.origin;
|
|
82
|
+
} catch {
|
|
83
|
+
// Fall through to the deployment contact below.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return contactEmail?.trim() ? `mailto:${contactEmail.trim()}` : "mailto:notifications@cloud.local";
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const ensureWebPushConfigured = (): Promise<void> => {
|
|
90
|
+
if (webPushConfigurationPromise) return webPushConfigurationPromise;
|
|
91
|
+
webPushConfigurationPromise = Promise.all([ensureVapidConfig(), vapidSubject()])
|
|
92
|
+
.then(([config, subject]) => {
|
|
93
|
+
webpush.setVapidDetails(subject, config.publicKey, config.privateKey);
|
|
94
|
+
})
|
|
95
|
+
.catch((error) => {
|
|
96
|
+
webPushConfigurationPromise = null;
|
|
97
|
+
throw error;
|
|
98
|
+
});
|
|
99
|
+
return webPushConfigurationPromise;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const disableEndpoint = async (endpointId: string): Promise<void> => {
|
|
103
|
+
await sql`
|
|
104
|
+
UPDATE notifications.endpoints
|
|
105
|
+
SET disabled_at = now(), updated_at = now()
|
|
106
|
+
WHERE id = ${endpointId}::uuid AND channel = 'browser'
|
|
107
|
+
`;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const browserDriver = {
|
|
111
|
+
id: "browser" as const,
|
|
112
|
+
resolveDestinations: async (recipient: { userId: string | null }) => {
|
|
113
|
+
if (!recipient.userId) return [];
|
|
114
|
+
const rows = await sql<EndpointRow[]>`
|
|
115
|
+
SELECT id, endpoint_hash, label, secret_encrypted
|
|
116
|
+
FROM notifications.endpoints
|
|
117
|
+
WHERE user_id = ${recipient.userId}::uuid
|
|
118
|
+
AND channel = 'browser'
|
|
119
|
+
AND verified_at IS NOT NULL
|
|
120
|
+
AND disabled_at IS NULL
|
|
121
|
+
ORDER BY last_seen_at DESC, id
|
|
122
|
+
`;
|
|
123
|
+
const destinations: NotificationDestination[] = [];
|
|
124
|
+
for (const row of rows) {
|
|
125
|
+
try {
|
|
126
|
+
const subscription = BrowserPushSubscriptionSchema.parse(await decryptSecret(row.secret_encrypted));
|
|
127
|
+
if (subscription.expirationTime && subscription.expirationTime <= Date.now()) {
|
|
128
|
+
await disableEndpoint(row.id);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
destinations.push({
|
|
132
|
+
key: row.endpoint_hash,
|
|
133
|
+
label: row.label,
|
|
134
|
+
endpointId: row.id,
|
|
135
|
+
context: { endpointId: row.id, subscription },
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
await disableEndpoint(row.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return destinations;
|
|
142
|
+
},
|
|
143
|
+
createPayload: ({ presentation, destination, event }) => {
|
|
144
|
+
const context = BrowserDestinationContextSchema.parse(destination.context);
|
|
145
|
+
return BrowserDeliveryPayloadSchema.parse({
|
|
146
|
+
endpointId: context.endpointId,
|
|
147
|
+
subscription: context.subscription,
|
|
148
|
+
eventId: event.id,
|
|
149
|
+
title: presentation.title,
|
|
150
|
+
targetHref: presentation.targetHref,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
deliver: async (value: unknown) => {
|
|
154
|
+
const payload: BrowserDeliveryPayload = BrowserDeliveryPayloadSchema.parse(value);
|
|
155
|
+
await ensureWebPushConfigured();
|
|
156
|
+
try {
|
|
157
|
+
await sendPinnedWebPush(
|
|
158
|
+
payload.subscription,
|
|
159
|
+
JSON.stringify({
|
|
160
|
+
type: "cloud-notification",
|
|
161
|
+
eventId: payload.eventId,
|
|
162
|
+
title: payload.title,
|
|
163
|
+
targetHref: payload.targetHref,
|
|
164
|
+
}),
|
|
165
|
+
{
|
|
166
|
+
TTL: 24 * 60 * 60,
|
|
167
|
+
urgency: "normal",
|
|
168
|
+
topic: createHash("sha256").update(payload.eventId).digest("base64url").slice(0, 32),
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const statusCode = error && typeof error === "object" && "statusCode" in error ? Number(error.statusCode) : null;
|
|
173
|
+
if (statusCode === 404 || statusCode === 410) {
|
|
174
|
+
await disableEndpoint(payload.endpointId);
|
|
175
|
+
throw Object.assign(new Error("Browser notification endpoint is no longer registered"), {
|
|
176
|
+
code: "endpoint_gone",
|
|
177
|
+
retryable: false,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (statusCode !== null && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429) {
|
|
181
|
+
throw Object.assign(new Error("Browser push provider rejected the delivery"), {
|
|
182
|
+
code: "provider_rejected",
|
|
183
|
+
retryable: false,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
} satisfies import("./channels").NotificationChannelDriver;
|
|
190
|
+
|
|
191
|
+
// Every app process may create deliveries; Core remains the only process that
|
|
192
|
+
// runs the durable delivery queue.
|
|
193
|
+
unregisterDriver = registerNotificationChannel(browserDriver);
|
|
194
|
+
|
|
195
|
+
export const browserNotifications = {
|
|
196
|
+
start: async (): Promise<void> => {
|
|
197
|
+
unregisterDriver ??= registerNotificationChannel(browserDriver);
|
|
198
|
+
await ensureWebPushConfigured();
|
|
199
|
+
},
|
|
200
|
+
stop: (): void => {
|
|
201
|
+
unregisterDriver?.();
|
|
202
|
+
unregisterDriver = null;
|
|
203
|
+
},
|
|
204
|
+
configuration: async (): Promise<{ publicKey: string }> => {
|
|
205
|
+
const config = await ensureVapidConfig();
|
|
206
|
+
return { publicKey: config.publicKey };
|
|
207
|
+
},
|
|
208
|
+
registerEndpoint: async (config: {
|
|
209
|
+
userId: string;
|
|
210
|
+
subscription: BrowserPushSubscription;
|
|
211
|
+
label: string;
|
|
212
|
+
}): Promise<{ id: string; label: string }> => {
|
|
213
|
+
const subscription = BrowserPushSubscriptionSchema.parse(config.subscription);
|
|
214
|
+
const hash = endpointHash(subscription.endpoint);
|
|
215
|
+
const encrypted = await encryptSecret(subscription);
|
|
216
|
+
const label = config.label.trim().slice(0, 120) || "This browser";
|
|
217
|
+
const rows = await sql.begin(async (tx) => {
|
|
218
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended(${`notifications:browser:${hash}`}, 0))`;
|
|
219
|
+
await tx`
|
|
220
|
+
UPDATE notifications.endpoints
|
|
221
|
+
SET disabled_at = now(), updated_at = now()
|
|
222
|
+
WHERE channel = 'browser' AND endpoint_hash = ${hash}
|
|
223
|
+
AND user_id <> ${config.userId}::uuid AND disabled_at IS NULL
|
|
224
|
+
`;
|
|
225
|
+
return tx<{ id: string; label: string }[]>`
|
|
226
|
+
INSERT INTO notifications.endpoints (
|
|
227
|
+
user_id, channel, endpoint_hash, label, secret_encrypted,
|
|
228
|
+
verified_at, last_seen_at, disabled_at, updated_at
|
|
229
|
+
) VALUES (
|
|
230
|
+
${config.userId}::uuid, 'browser', ${hash}, ${label}, ${encrypted},
|
|
231
|
+
now(), now(), NULL, now()
|
|
232
|
+
)
|
|
233
|
+
ON CONFLICT (user_id, channel, endpoint_hash) DO UPDATE
|
|
234
|
+
SET label = EXCLUDED.label, secret_encrypted = EXCLUDED.secret_encrypted,
|
|
235
|
+
verified_at = now(), last_seen_at = now(), disabled_at = NULL, updated_at = now()
|
|
236
|
+
RETURNING id, label
|
|
237
|
+
`;
|
|
238
|
+
});
|
|
239
|
+
return rows[0]!;
|
|
240
|
+
},
|
|
241
|
+
disableEndpoint: async (config: { userId: string; subscription: BrowserPushSubscription }): Promise<boolean> => {
|
|
242
|
+
const subscription = BrowserPushSubscriptionSchema.parse(config.subscription);
|
|
243
|
+
const rows = await sql<{ id: string }[]>`
|
|
244
|
+
UPDATE notifications.endpoints
|
|
245
|
+
SET disabled_at = now(), updated_at = now()
|
|
246
|
+
WHERE user_id = ${config.userId}::uuid AND channel = 'browser'
|
|
247
|
+
AND endpoint_hash = ${endpointHash(subscription.endpoint)} AND disabled_at IS NULL
|
|
248
|
+
RETURNING id
|
|
249
|
+
`;
|
|
250
|
+
return rows.length > 0;
|
|
251
|
+
},
|
|
252
|
+
} as const;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import type { AnyBoundNotificationDefinition } from "../../contracts/notification-types";
|
|
3
|
+
import { logger } from "../logging";
|
|
4
|
+
import { toPgTextArray } from "../postgres";
|
|
5
|
+
|
|
6
|
+
type NotificationCatalog = Readonly<Record<string, AnyBoundNotificationDefinition>>;
|
|
7
|
+
|
|
8
|
+
const RETRY_MS = 5_000;
|
|
9
|
+
const log = logger("notifications:catalog");
|
|
10
|
+
|
|
11
|
+
const isSchemaUnavailable = (error: unknown): boolean => {
|
|
12
|
+
if (!error || typeof error !== "object" || !("code" in error)) return false;
|
|
13
|
+
return error.code === "42P01" || error.code === "42703" || error.code === "3F000";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db: typeof sql = sql): Promise<void> => {
|
|
17
|
+
const recommended = toPgTextArray([...(definition.delivery?.recommended ?? [])]);
|
|
18
|
+
const required = toPgTextArray([...(definition.delivery?.required ?? [])]);
|
|
19
|
+
await db`
|
|
20
|
+
INSERT INTO notifications.definitions (
|
|
21
|
+
id, app_id, kind, label, description, presentation, recipient_kind,
|
|
22
|
+
recommended_channels, required_channels, active, last_seen_at, updated_at
|
|
23
|
+
) VALUES (
|
|
24
|
+
${definition.id}, ${definition.appId}, ${definition.key}, ${definition.label},
|
|
25
|
+
${definition.description}, (${definition.presentation ? JSON.stringify(definition.presentation) : null}::text)::jsonb,
|
|
26
|
+
${definition.recipient}, ${recommended}::text[],
|
|
27
|
+
${required}::text[], true, now(), now()
|
|
28
|
+
)
|
|
29
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
30
|
+
app_id = EXCLUDED.app_id,
|
|
31
|
+
kind = EXCLUDED.kind,
|
|
32
|
+
label = EXCLUDED.label,
|
|
33
|
+
description = EXCLUDED.description,
|
|
34
|
+
presentation = EXCLUDED.presentation,
|
|
35
|
+
recipient_kind = EXCLUDED.recipient_kind,
|
|
36
|
+
recommended_channels = EXCLUDED.recommended_channels,
|
|
37
|
+
required_channels = EXCLUDED.required_channels,
|
|
38
|
+
active = true,
|
|
39
|
+
last_seen_at = now(),
|
|
40
|
+
updated_at = now()
|
|
41
|
+
`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Persist serializable app metadata; schemas and renderers remain code-owned. */
|
|
45
|
+
export const registerNotificationDefinitions = async (appId: string, definitions: NotificationCatalog): Promise<void> => {
|
|
46
|
+
const current = Object.values(definitions);
|
|
47
|
+
if (current.some((definition) => definition.appId !== appId)) {
|
|
48
|
+
throw new Error(`Notification catalog for app "${appId}" contains a definition owned by another app`);
|
|
49
|
+
}
|
|
50
|
+
const ids = toPgTextArray(current.map((definition) => definition.id));
|
|
51
|
+
await sql.begin(async (tx) => {
|
|
52
|
+
for (const definition of current) await upsertDefinition(definition, tx);
|
|
53
|
+
await tx`
|
|
54
|
+
UPDATE notifications.definitions
|
|
55
|
+
SET active = false, updated_at = now()
|
|
56
|
+
WHERE app_id = ${appId} AND NOT (id = ANY(${ids}::text[]))
|
|
57
|
+
`;
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Keep app startup rollout-safe when Core has not expanded the schema yet. */
|
|
62
|
+
export const startNotificationDefinitionRegistration = async (
|
|
63
|
+
appId: string,
|
|
64
|
+
definitions: NotificationCatalog,
|
|
65
|
+
options: {
|
|
66
|
+
register?: (appId: string, definitions: NotificationCatalog) => Promise<void>;
|
|
67
|
+
retryMs?: number;
|
|
68
|
+
} = {},
|
|
69
|
+
): Promise<() => void> => {
|
|
70
|
+
const register = options.register ?? registerNotificationDefinitions;
|
|
71
|
+
const retryMs = options.retryMs ?? RETRY_MS;
|
|
72
|
+
let stopped = false;
|
|
73
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
74
|
+
|
|
75
|
+
const schedule = () => {
|
|
76
|
+
if (stopped || timer) return;
|
|
77
|
+
timer = setTimeout(() => {
|
|
78
|
+
timer = undefined;
|
|
79
|
+
void attempt();
|
|
80
|
+
}, retryMs);
|
|
81
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const attempt = async (): Promise<void> => {
|
|
85
|
+
if (stopped) return;
|
|
86
|
+
try {
|
|
87
|
+
await register(appId, definitions);
|
|
88
|
+
log.info("Notification definitions registered after schema became available", { appId });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
log.warn("Notification definition registration failed; retrying", {
|
|
91
|
+
appId,
|
|
92
|
+
error: error instanceof Error ? error.message : "Notification catalog registration failed",
|
|
93
|
+
});
|
|
94
|
+
schedule();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await register(appId, definitions);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!isSchemaUnavailable(error)) throw error;
|
|
102
|
+
log.warn("Notification schema is not available yet; registration will retry", { appId });
|
|
103
|
+
schedule();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
stopped = true;
|
|
108
|
+
if (timer) clearTimeout(timer);
|
|
109
|
+
timer = undefined;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const ensureNotificationDefinition = upsertDefinition;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { EmailNotificationPresentation, NotificationChannelId, NotificationPresentation } from "../../contracts/notification-types";
|
|
3
|
+
import { sendEmail } from "./email";
|
|
4
|
+
|
|
5
|
+
export type ResolvedNotificationRecipient = {
|
|
6
|
+
userId: string | null;
|
|
7
|
+
email: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type NotificationDestination = {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
endpointId?: string;
|
|
14
|
+
context: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type NotificationChannelDriver = {
|
|
18
|
+
id: NotificationChannelId | (string & {});
|
|
19
|
+
resolveDestinations: (recipient: ResolvedNotificationRecipient) => Promise<NotificationDestination[]>;
|
|
20
|
+
createPayload: (input: {
|
|
21
|
+
presentation: NotificationPresentation;
|
|
22
|
+
email?: EmailNotificationPresentation;
|
|
23
|
+
destination: NotificationDestination;
|
|
24
|
+
event: { id: string; definitionId: string };
|
|
25
|
+
}) => unknown;
|
|
26
|
+
deliver: (payload: unknown) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const drivers = new Map<string, NotificationChannelDriver>();
|
|
30
|
+
|
|
31
|
+
export const registerNotificationChannel = (driver: NotificationChannelDriver): (() => void) => {
|
|
32
|
+
if (!/^[a-z][a-z0-9_-]{0,79}$/.test(driver.id)) {
|
|
33
|
+
throw new Error("Notification channel id must be a lowercase identifier of at most 80 characters");
|
|
34
|
+
}
|
|
35
|
+
const existing = drivers.get(driver.id);
|
|
36
|
+
if (existing && existing !== driver) throw new Error(`Notification channel "${driver.id}" is already registered`);
|
|
37
|
+
drivers.set(driver.id, driver);
|
|
38
|
+
return () => {
|
|
39
|
+
if (drivers.get(driver.id) === driver) drivers.delete(driver.id);
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getNotificationChannel = (id: string): NotificationChannelDriver | undefined => drivers.get(id);
|
|
44
|
+
|
|
45
|
+
export const listNotificationChannels = (): string[] => [...drivers.keys()].sort();
|
|
46
|
+
|
|
47
|
+
const normalizeEmail = (value: string): string => value.trim().toLowerCase();
|
|
48
|
+
const emailKey = (value: string): string => createHash("sha256").update(normalizeEmail(value)).digest("hex");
|
|
49
|
+
|
|
50
|
+
const emailLabel = (value: string): string => {
|
|
51
|
+
const [local = "", domain = ""] = normalizeEmail(value).split("@", 2);
|
|
52
|
+
if (!domain) return "Email";
|
|
53
|
+
return `${local.slice(0, 1) || "*"}***@${domain}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type EmailPayload = {
|
|
57
|
+
to: string;
|
|
58
|
+
subject: string;
|
|
59
|
+
content?: string;
|
|
60
|
+
rawHtml?: string;
|
|
61
|
+
messageId?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const parseEmailPayload = (value: unknown): EmailPayload => {
|
|
65
|
+
if (!value || typeof value !== "object") throw new Error("Invalid email notification payload");
|
|
66
|
+
const payload = value as Partial<EmailPayload>;
|
|
67
|
+
if (typeof payload.to !== "string" || typeof payload.subject !== "string") {
|
|
68
|
+
throw new Error("Invalid email notification payload");
|
|
69
|
+
}
|
|
70
|
+
if (payload.content !== undefined && typeof payload.content !== "string") throw new Error("Invalid email notification payload");
|
|
71
|
+
if (payload.rawHtml !== undefined && typeof payload.rawHtml !== "string") throw new Error("Invalid email notification payload");
|
|
72
|
+
if (payload.messageId !== undefined && typeof payload.messageId !== "string") throw new Error("Invalid email notification payload");
|
|
73
|
+
return { to: payload.to, subject: payload.subject, content: payload.content, rawHtml: payload.rawHtml, messageId: payload.messageId };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const emailDriver: NotificationChannelDriver = {
|
|
77
|
+
id: "email",
|
|
78
|
+
resolveDestinations: async (recipient) => {
|
|
79
|
+
if (!recipient.email) return [];
|
|
80
|
+
const email = normalizeEmail(recipient.email);
|
|
81
|
+
return [{ key: emailKey(email), label: emailLabel(email), context: { email } }];
|
|
82
|
+
},
|
|
83
|
+
createPayload: ({ presentation, email, destination, event }) => {
|
|
84
|
+
const context = destination.context as { email?: unknown };
|
|
85
|
+
if (typeof context.email !== "string") throw new Error("Email destination is missing an address");
|
|
86
|
+
return {
|
|
87
|
+
to: context.email,
|
|
88
|
+
subject: email?.subject ?? presentation.title,
|
|
89
|
+
content: email?.content ?? presentation.body,
|
|
90
|
+
rawHtml: email?.rawHtml,
|
|
91
|
+
messageId: `<cloud-notification-${event.id}@cloud.invalid>`,
|
|
92
|
+
} satisfies EmailPayload;
|
|
93
|
+
},
|
|
94
|
+
deliver: async (value) => {
|
|
95
|
+
const payload = parseEmailPayload(value);
|
|
96
|
+
await sendEmail(payload.to, payload.subject, {
|
|
97
|
+
content: payload.content,
|
|
98
|
+
rawHtml: payload.rawHtml,
|
|
99
|
+
messageId: payload.messageId,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
registerNotificationChannel(emailDriver);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { decryptSecret } from "../secrets";
|
|
3
|
+
import { getNotificationChannel } from "./channels";
|
|
4
|
+
|
|
5
|
+
const MAX_DELIVERY_ATTEMPTS = 5;
|
|
6
|
+
const BASE_RETRY_MS = 2_000;
|
|
7
|
+
const MAX_RETRY_MS = 5 * 60_000;
|
|
8
|
+
|
|
9
|
+
type DeliveryRow = {
|
|
10
|
+
id: string;
|
|
11
|
+
event_id: string;
|
|
12
|
+
channel: string;
|
|
13
|
+
payload_encrypted: string | null;
|
|
14
|
+
required: boolean;
|
|
15
|
+
route_priority: number | null;
|
|
16
|
+
attempt_count: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DeliveryAttemptResult =
|
|
20
|
+
| { status: "skipped" | "delivered" | "failed" }
|
|
21
|
+
| { status: "retry"; retryAfterMs: number; error: string };
|
|
22
|
+
|
|
23
|
+
const retryDelay = (attempt: number): number => Math.min(BASE_RETRY_MS * 2 ** Math.max(0, attempt - 1), MAX_RETRY_MS);
|
|
24
|
+
|
|
25
|
+
const activateNextFallback = async (eventId: string): Promise<string[]> => {
|
|
26
|
+
const delivered = await sql<{ exists: boolean }[]>`
|
|
27
|
+
SELECT EXISTS (
|
|
28
|
+
SELECT 1 FROM notifications.deliveries
|
|
29
|
+
WHERE event_id = ${eventId}::uuid AND required = false AND status = 'delivered'
|
|
30
|
+
) AS exists
|
|
31
|
+
`;
|
|
32
|
+
if (delivered[0]?.exists) {
|
|
33
|
+
await sql`
|
|
34
|
+
UPDATE notifications.deliveries
|
|
35
|
+
SET status = 'suppressed', error_code = 'fallback_not_needed',
|
|
36
|
+
error_message = 'A preferred channel was delivered.', payload_encrypted = NULL, updated_at = now()
|
|
37
|
+
WHERE event_id = ${eventId}::uuid AND required = false AND status = 'deferred'
|
|
38
|
+
`;
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const active = await sql<{ exists: boolean }[]>`
|
|
43
|
+
SELECT EXISTS (
|
|
44
|
+
SELECT 1 FROM notifications.deliveries
|
|
45
|
+
WHERE event_id = ${eventId}::uuid AND required = false AND status IN ('pending', 'sending')
|
|
46
|
+
) AS exists
|
|
47
|
+
`;
|
|
48
|
+
if (active[0]?.exists) return [];
|
|
49
|
+
|
|
50
|
+
const rows = await sql<{ id: string }[]>`
|
|
51
|
+
WITH next_route AS (
|
|
52
|
+
SELECT MIN(route_priority) AS priority
|
|
53
|
+
FROM notifications.deliveries
|
|
54
|
+
WHERE event_id = ${eventId}::uuid AND required = false AND status = 'deferred'
|
|
55
|
+
)
|
|
56
|
+
UPDATE notifications.deliveries d
|
|
57
|
+
SET status = 'pending', next_attempt_at = now(), updated_at = now()
|
|
58
|
+
FROM next_route
|
|
59
|
+
WHERE d.event_id = ${eventId}::uuid
|
|
60
|
+
AND d.required = false
|
|
61
|
+
AND d.status = 'deferred'
|
|
62
|
+
AND d.route_priority = next_route.priority
|
|
63
|
+
RETURNING d.id
|
|
64
|
+
`;
|
|
65
|
+
return rows.map((row) => row.id);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const processNotificationDelivery = async (deliveryId: string): Promise<DeliveryAttemptResult & { activatedIds?: string[] }> => {
|
|
69
|
+
const rows = await sql<DeliveryRow[]>`
|
|
70
|
+
UPDATE notifications.deliveries
|
|
71
|
+
SET status = 'sending', attempt_count = attempt_count + 1,
|
|
72
|
+
last_attempt_at = now(), updated_at = now()
|
|
73
|
+
WHERE id = ${deliveryId}::uuid
|
|
74
|
+
AND status = 'pending'
|
|
75
|
+
AND (next_attempt_at IS NULL OR next_attempt_at <= now())
|
|
76
|
+
RETURNING id, event_id, channel, payload_encrypted, required, route_priority, attempt_count
|
|
77
|
+
`;
|
|
78
|
+
const delivery = rows[0];
|
|
79
|
+
if (!delivery) return { status: "skipped" };
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const driver = getNotificationChannel(delivery.channel);
|
|
83
|
+
if (!driver)
|
|
84
|
+
throw Object.assign(new Error(`Notification channel "${delivery.channel}" is unavailable`), { code: "channel_unavailable" });
|
|
85
|
+
if (!delivery.payload_encrypted) {
|
|
86
|
+
throw Object.assign(new Error("Notification delivery payload is unavailable"), { code: "payload_missing", retryable: false });
|
|
87
|
+
}
|
|
88
|
+
const payload = await decryptSecret(delivery.payload_encrypted);
|
|
89
|
+
await driver.deliver(payload);
|
|
90
|
+
await sql`
|
|
91
|
+
UPDATE notifications.deliveries
|
|
92
|
+
SET status = 'delivered', delivered_at = now(), next_attempt_at = NULL,
|
|
93
|
+
error_code = NULL, error_message = NULL, payload_encrypted = NULL, updated_at = now()
|
|
94
|
+
WHERE id = ${delivery.id}::uuid
|
|
95
|
+
`;
|
|
96
|
+
const activatedIds = delivery.required ? [] : await activateNextFallback(delivery.event_id);
|
|
97
|
+
return { status: "delivered", activatedIds };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : "Notification delivery failed";
|
|
100
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "provider_error";
|
|
101
|
+
const retryable = !(error && typeof error === "object" && "retryable" in error && error.retryable === false);
|
|
102
|
+
if (retryable && delivery.attempt_count < MAX_DELIVERY_ATTEMPTS) {
|
|
103
|
+
const retryAfterMs = retryDelay(delivery.attempt_count);
|
|
104
|
+
await sql`
|
|
105
|
+
UPDATE notifications.deliveries
|
|
106
|
+
SET status = 'pending', next_attempt_at = now() + (${retryAfterMs}::int * INTERVAL '1 millisecond'),
|
|
107
|
+
error_code = ${code}, error_message = ${message}, updated_at = now()
|
|
108
|
+
WHERE id = ${delivery.id}::uuid
|
|
109
|
+
`;
|
|
110
|
+
return { status: "retry", retryAfterMs, error: message };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await sql`
|
|
114
|
+
UPDATE notifications.deliveries
|
|
115
|
+
SET status = 'failed', next_attempt_at = NULL, error_code = ${code},
|
|
116
|
+
error_message = ${message}, payload_encrypted = NULL, updated_at = now()
|
|
117
|
+
WHERE id = ${delivery.id}::uuid
|
|
118
|
+
`;
|
|
119
|
+
const activatedIds = delivery.required ? [] : await activateNextFallback(delivery.event_id);
|
|
120
|
+
return { status: "failed", activatedIds };
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const recoverNotificationDeliveries = async (): Promise<string[]> => {
|
|
125
|
+
await sql`
|
|
126
|
+
UPDATE notifications.deliveries
|
|
127
|
+
SET status = 'pending', next_attempt_at = now(), error_code = 'lease_recovered',
|
|
128
|
+
error_message = 'Recovered an interrupted delivery attempt.', updated_at = now()
|
|
129
|
+
WHERE status = 'sending' AND last_attempt_at < now() - INTERVAL '5 minutes'
|
|
130
|
+
`;
|
|
131
|
+
const rows = await sql<{ id: string }[]>`
|
|
132
|
+
SELECT id
|
|
133
|
+
FROM notifications.deliveries
|
|
134
|
+
WHERE status = 'pending' AND (next_attempt_at IS NULL OR next_attempt_at <= now())
|
|
135
|
+
ORDER BY next_attempt_at NULLS FIRST, created_at
|
|
136
|
+
LIMIT 500
|
|
137
|
+
`;
|
|
138
|
+
return rows.map((row) => row.id);
|
|
139
|
+
};
|